diff_against_hub accepts hub_edges/edge_targets and reports edges to create, remove, retain, unresolved and external, using the same pure planner that receiving now calls (plan_source_edges + classify_source_edges). The classifier works on a row list, fixing a collapse where an external duplicate of an owned edge key could hide the owned row from removal. 7 new pure tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 692292@bnt-lap001 Assistant-Session: 89c7d8e8-e1b3-47eb-9411-a0ab227fca2f
157 lines
7.5 KiB
Python
157 lines
7.5 KiB
Python
"""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")]
|