"""Dry-run preview of source-owned dependency edges (STATE-WP-0094-T05 follow-up). ``diff_against_hub`` is pure, so these tests need no database: hub state is passed as data. They pin that the preview uses the same rules as receiving. """ from pathlib import Path from api.services import forge_projection as fp from tests.test_forge_dependency_edges import _source, _write TGT_WP = "11111111-1111-1111-1111-111111111111" TGT_T01 = "22222222-2222-2222-2222-222222222222" OWNER = "forge:edge-source" def _derive(tmp_path: Path, frontmatter_deps: str, **kw): _write(tmp_path, _source(frontmatter_deps, **kw)) return fp.derive_from_checkout(tmp_path, "edge-source", "b" * 40) def _hub(derived): """Hub already holds every derived workplan and task under derived UUIDs.""" workplans = [{"id": w.uuid, "slug": w.record_id.lower(), "status": w.status, "flavor": w.flavor, "backing_relative_path": w.relative_path} for w in derived.workplans] tasks = {w.uuid: [{"id": t.uuid, "record_id": t.record_id, "title": t.title, "status": t.status} for t in w.tasks] for w in derived.workplans} return workplans, tasks def _ids(derived): wp = {w.record_id: w.uuid for w in derived.workplans} tasks = {t.record_id: t.uuid for w in derived.workplans for t in w.tasks} return wp, tasks def test_preview_absent_without_hub_edges(tmp_path): derived = _derive(tmp_path, "depends_on: [TGT-WP-0001]\n") workplans, tasks = _hub(derived) diff = fp.diff_against_hub(derived, workplans, tasks) assert diff.edges is None assert "edges" not in diff.to_dict() def test_preview_lists_create_unresolved_and_external(tmp_path): derived = _derive(tmp_path, "depends_on: [TGT-WP-0001, TGT-WP-0001-T01]\n") workplans, tasks = _hub(derived) wp, tk = _ids(derived) # The sibling edge already exists as an externally authored (C-20/API) row. external = {"id": "e1", "from_workplan_id": wp["SRC-WP-0001"], "from_task_id": tk["SRC-WP-0001-T01"], "to_workplan_id": wp["SRC-WP-0002"], "to_task_id": None, "relationship_type": "blocks", "source_owner": None} diff = fp.diff_against_hub( derived, workplans, tasks, hub_edges=[external], edge_targets={"TGT-WP-0001": [TGT_WP], "TGT-WP-0001-T01": [TGT_T01], "SRC-WP-0002": [wp["SRC-WP-0002"]]}, ) e = diff.edges created = {(c["from"], c["target"], c["relationship_type"]) for c in e["create"]} assert created == {("SRC-WP-0001", "TGT-WP-0001", "blocks"), ("SRC-WP-0001", "TGT-WP-0001-T01", "starts_after")} assert [(x["from"], x["target"]) for x in e["external"]] == [("SRC-WP-0001-T01", "SRC-WP-0002")] # T02 declares a missing target; the closed T03 declares nothing. assert [(u["from"], u["reason"]) for u in e["unresolved"]] == [("SRC-WP-0001-T02", "missing")] assert e["remove"] == [] and e["retain"] == [] assert not diff.clean out = diff.to_dict() assert out["edge_counts"] == {"create": 2, "remove": 0, "retain": 0, "unresolved": 1, "external": 1} def test_preview_removes_only_undeclared_owned_edges(tmp_path): derived = _derive(tmp_path, "", t02_target="SRC-WP-0002") workplans, tasks = _hub(derived) wp, tk = _ids(derived) targets = {"SRC-WP-0002": [wp["SRC-WP-0002"]]} owned_live = {"id": "o1", "from_workplan_id": wp["SRC-WP-0001"], "from_task_id": tk["SRC-WP-0001-T01"], "to_workplan_id": wp["SRC-WP-0002"], "to_task_id": None, "relationship_type": "blocks", "source_owner": OWNER} owned_live_t02 = {**owned_live, "id": "o2", "from_task_id": tk["SRC-WP-0001-T02"]} owned_gone = {"id": "o3", "from_workplan_id": wp["SRC-WP-0001"], "from_task_id": None, "to_workplan_id": TGT_WP, "to_task_id": None, "relationship_type": "blocks", "source_owner": OWNER} foreign_gone = {**owned_gone, "id": "x1", "source_owner": "forge:other"} null_gone = {**owned_gone, "id": "x2", "source_owner": None} diff = fp.diff_against_hub( derived, workplans, tasks, hub_edges=[owned_live, owned_live_t02, owned_gone, foreign_gone, null_gone], edge_targets=targets, ) e = diff.edges assert [r["id"] for r in e["remove"]] == ["o3"] assert e["create"] == [] and e["external"] == [] and e["unresolved"] == [] assert not diff.clean def test_preview_holds_owned_edges_of_unresolved_side_and_is_clean_when_settled(tmp_path): derived = _derive(tmp_path, "depends_on: [TGT-WP-0001]\n") workplans, tasks = _hub(derived) wp, tk = _ids(derived) owned = [ {"id": "o1", "from_workplan_id": wp["SRC-WP-0001"], "from_task_id": None, "to_workplan_id": TGT_WP, "to_task_id": None, "relationship_type": "blocks", "source_owner": OWNER}, {"id": "o2", "from_workplan_id": wp["SRC-WP-0001"], "from_task_id": tk["SRC-WP-0001-T01"], "to_workplan_id": wp["SRC-WP-0002"], "to_task_id": None, "relationship_type": "blocks", "source_owner": OWNER}, # Owned edge of T02, whose declared target is now ambiguous: retained. {"id": "o3", "from_workplan_id": wp["SRC-WP-0001"], "from_task_id": tk["SRC-WP-0001-T02"], "to_workplan_id": TGT_WP, "to_task_id": None, "relationship_type": "blocks", "source_owner": OWNER}, ] diff = fp.diff_against_hub( derived, workplans, tasks, hub_edges=owned, edge_targets={"TGT-WP-0001": [TGT_WP], "SRC-WP-0002": [wp["SRC-WP-0002"]], "MISSING-WP-0009": ["a", "b"]}, ) e = diff.edges assert e["create"] == [] and e["remove"] == [] assert [r["id"] for r in e["retain"]] == ["o3"] assert e["unresolved"][0]["reason"] == "ambiguous" assert e["unresolved"][0]["candidates"] == ["a", "b"] assert diff.clean # unresolved/retained/external alone do not change the hub def test_preview_resolves_targets_created_in_the_same_receipt(tmp_path): derived = _derive(tmp_path, "") workplans, tasks = _hub(derived) wp, tk = _ids(derived) # The hub lacks SRC-WP-0002 entirely; the receipt would create it. workplans = [w for w in workplans if w["slug"] != "src-wp-0002"] diff = fp.diff_against_hub(derived, workplans, tasks, hub_edges=[], edge_targets={}) created = [(c["from"], c["to_workplan_id"]) for c in diff.edges["create"]] assert created == [("SRC-WP-0001-T01", wp["SRC-WP-0002"])] assert any(m["record_id"] == "SRC-WP-0002" for m in diff.missing) def test_preview_flags_self_edge(tmp_path): derived = _derive(tmp_path, "depends_on: [SRC-WP-0001]\n") workplans, tasks = _hub(derived) wp, _ = _ids(derived) diff = fp.diff_against_hub( derived, workplans, tasks, hub_edges=[], edge_targets={"SRC-WP-0001": [wp["SRC-WP-0001"]], "SRC-WP-0002": [wp["SRC-WP-0002"]]}, ) assert ("SRC-WP-0001", "self") in {(u["from"], u["reason"]) for u in diff.edges["unresolved"]} def test_classifier_never_collapses_duplicate_keys(): key = ("wp", None, "tgt", None, "blocks") rows = [(key, "owned", OWNER), (key, "api", None)] # Declared: the owned row satisfies it; the API duplicate is not reported. plan = fp.classify_source_edges({key: {}}, set(), rows, OWNER) assert plan == {"create": [], "external": [], "remove": [], "retain": []} # Undeclared: only the owned row is removed, whatever the row order. for ordered in (rows, rows[::-1]): plan = fp.classify_source_edges({}, set(), ordered, OWNER) assert plan["remove"] == [(key, "owned")]