From 42106d11e0bace17375e2d231f2244b05c418bf9 Mon Sep 17 00:00:00 2001 From: Paul Fidika Date: Fri, 7 Aug 2026 01:25:17 -0600 Subject: [PATCH] pgw#1008 (P1, catalog-blocking): the clone producer's checkpoint facts reach the publish declare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit th#1411 refuses a v2 publish into a repo whose live rows carry classification unless the request restates objective/distilled. run_clone spent the caller's facts only on apply_objective_scheduler_config (the scheduler stamp inside the produced tree) and dropped them before publish_v2 — so a clone-huggingface into ANY already-classified mirror dies at declare, before a byte moves. publish_flavors has passed both since pgw#654; this call site was the one that did not. Live cost (ie#609): master's tensorhub/wan22-t2v-a14b is missing transformer_2 because the mirror plan's third source_include leg was never run, and the repair leg — one clone-huggingface merge, correct payload, objective='flow', distilled=false — was refused twice at declare, 400 classification_required, predecessor sha256:3b57e969. Every wan-2.2 function on the master catalog stays down until this ships. Revert-turns-red: both tests in tests/convert/test_clone_classification_pgw1008.py fail with the two kwargs removed. Full tests/convert/: 74 passed, 2 skipped, 1 xfailed. --- src/gen_worker/convert/clone.py | 10 ++ .../test_clone_classification_pgw1008.py | 107 ++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 tests/convert/test_clone_classification_pgw1008.py diff --git a/src/gen_worker/convert/clone.py b/src/gen_worker/convert/clone.py index 7549897a..d4416527 100644 --- a/src/gen_worker/convert/clone.py +++ b/src/gen_worker/convert/clone.py @@ -953,6 +953,16 @@ def _dl_progress(done: int, total: Optional[int]) -> None: dtype=str(attrs.get("dtype") or spec.dtype), file_layout=str(attrs.get("file_layout") or spec.file_layout), file_type=str(attrs.get("file_type") or spec.file_type), + # pgw#1008: the caller's checkpoint facts have to reach the + # DECLARE, not just `apply_objective_scheduler_config`. th#1411 + # refuses a publish into a repo whose live rows carry + # classification unless the request restates it, so without + # these two a clone can never re-publish into a classified repo + # — which is every mirror the catalog already serves. + # `publish_flavors` has passed them since pgw#654; this call + # site was the one that did not. + objective=objective_fact, + distilled=distilled_fact, metadata=metadata, provenance=provenance, repo_spec=source.repo_spec, diff --git a/tests/convert/test_clone_classification_pgw1008.py b/tests/convert/test_clone_classification_pgw1008.py new file mode 100644 index 00000000..0951e0c8 --- /dev/null +++ b/tests/convert/test_clone_classification_pgw1008.py @@ -0,0 +1,107 @@ +"""pgw#1008: the CLONE producer restates classification on the declare. + +th#1411 refuses a v2 publish into a repo whose live rows carry classification +unless the request restates `objective` / `distilled`. `publish_flavors` has +done that since pgw#654; `run_clone`'s publish call site never did — it spent +the caller's facts only on `apply_objective_scheduler_config` (the scheduler +stamp inside the produced tree) and dropped them before the wire. + +Live cost (ie#609): master's `tensorhub/wan22-t2v-a14b` mirror is missing +`transformer_2`, and the repair — one more `clone-huggingface` leg — is +impossible, because every re-clone into that already-classified repo dies at +declare with `classification_required`. Every mirror the catalog already +serves is in the same position. + +Revert-turns-red: drop `objective=`/`distilled=` from the `publish_v2(...)` +call in `clone.py` and both assertions below fail. + + pytest tests/convert/test_clone_classification_pgw1008.py -q +""" + +from __future__ import annotations + +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest + +from gen_worker.convert.clone import run_clone +from gen_worker.convert.ingest import IngestedSource + +from fake_hub import _FakeHub + + +class _Ctx: + def __init__(self, server: Any) -> None: + self._file_api_base_url = f"http://127.0.0.1:{server.server_port}" + self._worker_capability_token = "cap-token" + self.owner = "tensorhub" + self.request_id = "req-1008" + self.destination = {"repo": "tensorhub/fallback"} + + +def _source(dest_dir: Path) -> IngestedSource: + dest_dir.mkdir(parents=True, exist_ok=True) + (dest_dir / "config.json").write_text('{"architectures": ["FakeBackbone"]}') + (dest_dir / "model.safetensors").write_bytes(b"\x00" * 64) + return IngestedSource( + provider="huggingface", source_ref="Wan-AI/Wan2.2-T2V-A14B-Diffusers", + source_revision="5be7df96", dir=dest_dir, layout="singlefile", + model_family="wan", model_family_variant="wan22", + classification=SimpleNamespace(strategy="transformers"), + attrs={"dtype": "fp32", "file_layout": "singlefile"}, + metadata={"source_provider": "huggingface"}, + repo_spec={"kind": "model", "library_name": "transformers"}, + ) + + +def _run(fake_hub: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, + **kw: Any) -> Any: + _FakeHub.state["finalize_calls"] = 1 + monkeypatch.setenv("COZY_CONVERT_WORKDIR", str(tmp_path / "work")) + src = _source(tmp_path / "source") + monkeypatch.setattr("gen_worker.convert.clone.ingest_huggingface", + lambda source_ref, dest_dir, **kwargs: src) + return run_clone( + _Ctx(fake_hub), provider="huggingface", + source_ref="Wan-AI/Wan2.2-T2V-A14B-Diffusers", + destination_repo="tensorhub/wan22-t2v-a14b", + destination_repo_tags=["prod"], + outputs=[{"dtype": "fp32", "file_layout": "diffusers", + "file_type": "safetensors"}], + **kw, + ) + + +def test_declared_facts_reach_the_publish_declare( + fake_hub: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + """The ie#609 repair shape: a merge leg into a classified mirror.""" + result = _run(fake_hub, tmp_path, monkeypatch, + objective="flow", distilled=False, overwrite_repo=False) + + assert not result.failed_flavors, result.failed_flavors + req = _FakeHub.state["publish_request"] + assert req["objective"] == "flow" + assert req["distilled"] is False + assert req["mode"] == "merge" + + +def test_undeclared_objective_stays_off_the_wire( + fake_hub: Any, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + """An unstated objective is still unstated — a clone must not author + `epsilon` (or an empty string) nobody declared, so the field is omitted and + th#1411's refusal is what tells the caller to state it. + + `distilled` is different by construction: `CloneHuggingFaceInput.distilled` + is a plain `bool` defaulting to False, so an undeclared clone DOES state + `distilled: false` — the honest reading of a straight upstream mirror, and + the same first-hand declaration `publish_flavors` makes.""" + result = _run(fake_hub, tmp_path, monkeypatch) + + assert not result.failed_flavors, result.failed_flavors + req = _FakeHub.state["publish_request"] + assert "objective" not in req + assert req["distilled"] is False