state-hub/tests/test_forge_projection.py
tegwick 532583ce17
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 25s
feat(projection): add the fleet reset as a loop over the repository form
ADR-012 decision 7 requires the fleet form to share the per-repository
implementation: the rarely-run wide operation must be the frequently-run narrow
one, or the wide one is trusted on the strength of never having been exercised.

Failure behaviour is the substance. A refusal does not stop the pass — aborting
on the first refusal means one unresolved repository blocks reconstruction
everywhere, which in practice means permanently. An error does not stop it
either. Each repository gets its own session so one failure cannot roll back
another's work, and only repositories that applied are committed.

Refs STATE-WP-0083-T04

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-26 13:23:08 +02:00

386 lines
15 KiB
Python

"""Deriving a projection from the forge (STATE-WP-0083-T01).
The properties that matter are identity and determinism: a forge-derived
projection must compute the same record identities as repo-manager, and the same
commit must always yield the same projection. Without both, the reset in T03
cannot be verified against anything.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from api.services import forge_projection as fp
def test_identity_matches_the_fleet_derivation():
"""Same namespace as ADR-007, so overlay and forge agree on identity."""
assert fp.derived_record_uuid("CUST-WP-0067") == "16249302-2767-55df-aec0-d92c2751c225"
assert fp.derived_record_uuid("CUST-WP-0067-T01") == "f3608db4-20a5-58fb-a965-885eb14858af"
def _repo(tmp_path: Path) -> Path:
root = tmp_path / "demo"
(root / "workplans" / "archived").mkdir(parents=True)
(root / "workplans" / "DEMO-WP-0001-a.md").write_text(
"---\nid: DEMO-WP-0001\ntype: workplan\ntitle: \"First\"\nstatus: active\n---\n\n"
"## Do the thing\n\n```task\nid: DEMO-WP-0001-T01\nstatus: todo\npriority: high\n```\n\n"
"## Do the other\n\n```task\nid: DEMO-WP-0001-T02\nstatus: done\npriority: low\n```\n",
encoding="utf-8",
)
(root / "workplans" / "archived" / "260101-DEMO-WP-0002-b.md").write_text(
"---\nid: DEMO-WP-0002\ntype: workplan\ntitle: \"Second\"\nstatus: finished\n---\n\n# b\n",
encoding="utf-8",
)
(root / "workplans" / "NOTES.md").write_text(
"---\nid: NOT-A-WORKPLAN\ntype: note\n---\n\n# not a workplan\n", encoding="utf-8"
)
return root
def test_derives_workplans_tasks_and_archived_flag(tmp_path):
p = fp.derive_from_checkout(_repo(tmp_path), "demo", "abc123")
assert [w.record_id for w in p.workplans] == ["DEMO-WP-0001", "DEMO-WP-0002"]
first, second = p.workplans
assert first.status == "active" and first.archived is False
assert second.archived is True
assert p.task_count == 2
assert [t.record_id for t in first.tasks] == ["DEMO-WP-0001-T01", "DEMO-WP-0001-T02"]
def test_ignores_files_that_are_not_workplans(tmp_path):
"""Selection is by `type: workplan`; anything else is not this hub's business."""
p = fp.derive_from_checkout(_repo(tmp_path), "demo", "abc123")
assert all(w.record_id != "NOT-A-WORKPLAN" for w in p.workplans)
def test_task_titles_fall_back_to_the_preceding_heading(tmp_path):
p = fp.derive_from_checkout(_repo(tmp_path), "demo", "abc123")
titles = [t.title for t in p.workplans[0].tasks]
assert titles == ["Do the thing", "Do the other"]
def test_identifiers_are_derived_not_read_from_the_file(tmp_path):
"""A forge projection must not inherit whatever id a file happens to carry."""
root = _repo(tmp_path)
f = root / "workplans" / "DEMO-WP-0001-a.md"
f.write_text(
f.read_text(encoding="utf-8").replace(
"status: active",
'status: active\nstate_hub_workstream_id: "00000000-0000-4000-8000-000000000000"',
),
encoding="utf-8",
)
p = fp.derive_from_checkout(root, "demo", "abc123")
assert p.workplans[0].uuid == fp.derived_record_uuid("DEMO-WP-0001")
assert p.workplans[0].uuid != "00000000-0000-4000-8000-000000000000"
def test_same_input_yields_identical_output(tmp_path):
root = _repo(tmp_path)
assert fp.derive_from_checkout(root, "demo", "abc").to_dict() == \
fp.derive_from_checkout(root, "demo", "abc").to_dict()
def test_missing_workplans_directory_is_empty_not_an_error(tmp_path):
(tmp_path / "bare").mkdir()
p = fp.derive_from_checkout(tmp_path / "bare", "bare", "abc")
assert p.workplans == [] and p.commit == "abc"
def test_clone_failure_is_reported_not_swallowed(monkeypatch):
def boom(*a, **k):
raise fp.ForgeDeriveError("repository not found")
monkeypatch.setattr(fp, "_run_git", boom)
with pytest.raises(fp.ForgeDeriveError, match="not found"):
fp.derive_from_forge("nope")
class TestReset:
"""Applying the reset (STATE-WP-0083-T03).
The properties worth guarding are the refusals, not the happy path. A reset
that quietly retires a record someone still needs is worse than one that
does nothing.
"""
@staticmethod
def _derived(*records):
wps = []
for rid, status, path in records:
wps.append(
fp.DerivedWorkplan(
record_id=rid, uuid=fp.derived_record_uuid(rid), title=rid,
status=status, relative_path=path, archived=False, tasks=[],
)
)
return fp.DerivedProjection(repo_slug="demo", commit="c0ffee", workplans=wps)
@pytest.mark.asyncio
async def test_refuses_retirement_unless_acknowledged(self, monkeypatch):
"""A record that stops deriving may mean a deleted file — or a wrong branch."""
session = _FakeSession(
repo=_Repo(),
rows=[_Row(slug="demo-wp-0001", status="active", path="workplans/a.md")],
)
out = await fp.reset_repository_projection(
session, "demo", derived=self._derived() # forge has nothing
)
assert out.status == "refused"
assert out.refused and out.refused[0]["slug"] == "demo-wp-0001"
assert out.retired == []
assert session.committed is False
@pytest.mark.asyncio
async def test_retires_when_acknowledged(self):
row = _Row(slug="demo-wp-0001", status="active", path="workplans/a.md")
session = _FakeSession(repo=_Repo(), rows=[row])
out = await fp.reset_repository_projection(
session, "demo", derived=self._derived(), acknowledge_retirements=True
)
assert out.status == "applied" and out.retired == ["demo-wp-0001"]
assert row.projection_retired_at is not None
assert row.projection_retired_reason == fp.RETIRE_REASON
@pytest.mark.asyncio
async def test_retirement_is_not_deletion(self):
"""Hub-native records reference workplans with RESTRICT; the row survives."""
row = _Row(slug="demo-wp-0001", status="active", path="workplans/a.md")
session = _FakeSession(repo=_Repo(), rows=[row])
await fp.reset_repository_projection(
session, "demo", derived=self._derived(), acknowledge_retirements=True
)
assert row in session.rows
assert session.deleted == []
@pytest.mark.asyncio
async def test_unretires_a_record_that_derives_again(self):
from datetime import datetime, timezone
row = _Row(slug="demo-wp-0001", status="active", path="workplans/a.md")
row.projection_retired_at = datetime.now(tz=timezone.utc)
row.projection_retired_reason = fp.RETIRE_REASON
session = _FakeSession(repo=_Repo(), rows=[row])
out = await fp.reset_repository_projection(
session, "demo",
derived=self._derived(("DEMO-WP-0001", "active", "workplans/a.md")),
)
assert out.status == "applied"
assert row.projection_retired_at is None
@pytest.mark.asyncio
async def test_unregistered_repository_is_refused_not_created(self):
session = _FakeSession(repo=None, rows=[])
out = await fp.reset_repository_projection(session, "nope", derived=self._derived())
assert out.status == "refused"
assert "not registered" in out.refused[0]["reason"]
@pytest.mark.asyncio
async def test_records_the_commit_it_derived_from(self):
row = _Row(slug="demo-wp-0001", status="proposed", path="workplans/a.md")
session = _FakeSession(repo=_Repo(), rows=[row])
await fp.reset_repository_projection(
session, "demo",
derived=self._derived(("DEMO-WP-0001", "active", "workplans/a.md")),
)
assert row.derived_from_commit == "c0ffee"
assert row.status == "active"
class _Repo:
def __init__(self):
import uuid as _u
self.id = _u.uuid4()
self.topic_id = None
class _Row:
def __init__(self, slug, status, path):
import uuid as _u
self.id = _u.uuid4()
self.slug = slug
self.status = status
self.backing_relative_path = path
self.backing_filename = path.rsplit("/", 1)[-1]
self.backing_archived = False
self.projection_retired_at = None
self.projection_retired_reason = None
self.derived_from_commit = None
class _FakeSession:
"""Stands in for AsyncSession: enough to prove intent without a database."""
def __init__(self, repo, rows, foreign=None):
self._repo = repo
self.rows = list(rows)
self._foreign = list(foreign or [])
self.added = []
self.deleted = []
self.committed = False
self._calls = 0
async def execute(self, *_a, **_k):
self._calls += 1
repo, rows = self._repo, self.rows
# 1st call resolves the repo, 2nd loads its workplans, 3rd is the
# foreign-identifier lookup.
payload = rows if self._calls == 2 else self._foreign
class R:
def scalar_one_or_none(self_inner):
return repo
def scalars(self_inner):
return iter(payload)
return R()
def add(self, obj):
self.added.append(obj)
def delete(self, obj):
self.deleted.append(obj)
async def flush(self):
return None
async def commit(self):
self.committed = True
async def rollback(self):
self.rolled_back = True
class TestCollisionRefusal:
"""A colliding identifier must refuse, not raise (STATE-WP-0083-T03).
net-kingdom's ADHOC-2026-08-23 derives to an identifier another repository
already holds — the case CUST-WP-0066 documents. The reset previously failed
on a database constraint, which tells the caller nothing they can act on.
"""
@staticmethod
def _derived(rid="DEMO-WP-0001"):
return fp.DerivedProjection(
repo_slug="demo", commit="c0ffee",
workplans=[fp.DerivedWorkplan(
record_id=rid, uuid=fp.derived_record_uuid(rid), title=rid,
status="active", relative_path="workplans/a.md",
archived=False, tasks=[])],
)
@pytest.mark.asyncio
async def test_refuses_when_the_identifier_belongs_elsewhere(self):
derived = self._derived()
foreign = _Row(slug="held-by-someone-else", status="finished", path="workplans/x.md")
foreign.id = __import__("uuid").UUID(derived.workplans[0].uuid)
session = _FakeSession(repo=_Repo(), rows=[], foreign=[foreign])
out = await fp.reset_repository_projection(session, "demo", derived=derived)
assert out.status == "refused"
assert out.refused[0]["reason"].startswith("derived identifier already belongs")
assert out.refused[0]["held_by_slug"] == "held-by-someone-else"
assert out.created == [] and session.added == []
@pytest.mark.asyncio
async def test_acknowledging_retirements_does_not_authorise_a_collision(self):
"""Different decision, different authorisation."""
derived = self._derived()
foreign = _Row(slug="held-by-someone-else", status="finished", path="workplans/x.md")
foreign.id = __import__("uuid").UUID(derived.workplans[0].uuid)
session = _FakeSession(repo=_Repo(), rows=[], foreign=[foreign])
out = await fp.reset_repository_projection(
session, "demo", derived=derived, acknowledge_retirements=True
)
assert out.status == "refused"
assert session.added == []
@pytest.mark.asyncio
async def test_no_collision_still_creates(self):
derived = self._derived()
session = _FakeSession(repo=_Repo(), rows=[], foreign=[])
out = await fp.reset_repository_projection(session, "demo", derived=derived)
assert out.status == "applied" and out.created == ["DEMO-WP-0001"]
class TestFleetReset:
"""The fleet form must be a loop over the repository form (T04).
Its value is entirely in what it does with failure: a wide reset that stops
at the first refusal is one nobody can run, because there is always one
unresolved repository somewhere.
"""
class _Factory:
def __init__(self, sessions):
self._sessions = list(sessions)
def __call__(self):
session = self._sessions.pop(0)
class Ctx:
async def __aenter__(self_inner):
return session
async def __aexit__(self_inner, *a):
return False
return Ctx()
@staticmethod
def _derived(rid):
return fp.DerivedProjection(
repo_slug="x", commit="c0ffee",
workplans=[fp.DerivedWorkplan(
record_id=rid, uuid=fp.derived_record_uuid(rid), title=rid,
status="active", relative_path="workplans/a.md", archived=False, tasks=[])])
@pytest.mark.asyncio
async def test_a_refusal_does_not_stop_the_pass(self, monkeypatch):
calls = []
async def fake(session, slug, **kw):
calls.append(slug)
out = fp.ResetOutcome(repo_slug=slug, commit="c0ffee", status="applied")
if slug == "bad":
out.status = "refused"
out.refused.append({"reason": "would be retired", "slug": "x"})
else:
out.updated.append("A-WP-0001")
return out
monkeypatch.setattr(fp, "reset_repository_projection", fake)
s = [_FakeSession(repo=_Repo(), rows=[]) for _ in range(3)]
res = await fp.reset_fleet_projection(self._Factory(s), ["good", "bad", "also-good"])
assert calls == ["good", "bad", "also-good"]
d = res.to_dict()
assert d["by_status"] == {"applied": 2, "refused": 1}
assert d["totals"]["updated"] == 2
@pytest.mark.asyncio
async def test_an_error_does_not_stop_the_pass(self, monkeypatch):
async def fake(session, slug, **kw):
if slug == "boom":
raise RuntimeError("clone failed")
return fp.ResetOutcome(repo_slug=slug, commit="c", status="noop")
monkeypatch.setattr(fp, "reset_repository_projection", fake)
s = [_FakeSession(repo=_Repo(), rows=[]) for _ in range(3)]
res = await fp.reset_fleet_projection(self._Factory(s), ["a", "boom", "b"])
d = res.to_dict()
assert d["errored"] == 1 and "clone failed" in res.errors["boom"]
assert set(res.results) == {"a", "b"}
@pytest.mark.asyncio
async def test_only_applied_repositories_are_committed(self, monkeypatch):
async def fake(session, slug, **kw):
out = fp.ResetOutcome(repo_slug=slug, commit="c", status="applied")
if slug == "refuser":
out.status = "refused"
else:
out.updated.append("A-WP-0001")
return out
monkeypatch.setattr(fp, "reset_repository_projection", fake)
s = [_FakeSession(repo=_Repo(), rows=[]) for _ in range(2)]
await fp.reset_fleet_projection(self._Factory(s), ["applier", "refuser"])
assert s[0].committed is True
assert s[1].committed is False