feat(projection): derive a repository's projection from the forge
Implements ADR-012 decisions 1 and 2 (STATE-WP-0083 T01, T02 partial). Central
clones the default branch from Forgejo and derives its own projection: 69
workplans and 459 tasks from the-custodian at d5013ae, identical across runs,
with the commit recorded as provenance.
Identifiers are derived in the ADR-007 namespace and verified against live
records, so a forge-derived projection and a preliminary overlay agree on
identity without reconciliation.
The diff first matched hub records by UUID and was badly wrong: most hub records
carry pre-ADR-007 random identifiers, so nearly everything appeared
simultaneously missing and stale, and a reset built on it would have destroyed
and recreated the entire projection. It now matches canonical record id, falling
back to the backing file. whitehat-security — bootstrapped straight from files —
now reports clean, which is the control.
Task-level comparison is deliberately not trusted: hub tasks carry no canonical
record id, only a title, so matching is by title. Recorded as T06; T03 is
limited to workplans until it lands.
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-25 23:34:25 +02:00
|
|
|
"""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
|
|
|
|
|
|
feat(forge): resolve an optional forge read credential (STATE-WP-0084-T02/T03)
Nine repositories are invisible to derivation because central may not read
them. This adds the consuming half of the credential lane MASON-WP-0003 built.
The cluster has no agent injector and no secrets-store CSI driver, so the pod
authenticates to OpenBao with a projected ServiceAccount token (audience
`openbao`, not the API server) and reads the KV path itself. `forgeRead.*`
carries coordinates only; no credential is a chart value, an image layer, or a
Kubernetes Secret.
The credential reaches git through GIT_CONFIG_* setting http.extraHeader, not
through `-c` and not through userinfo in the clone URL — both of those put the
token in the process listing. It is redacted from ForgeDeriveError, which is
logged, stored in reset outcomes, and returned over the API.
Absent stays a supported state: with no credential, or with OpenBao
unreachable, resolution returns None and public derivation runs unchanged.
Raising would turn "nine repositories are unreadable" into "the pass failed",
which is what T01 exists to prevent.
Chart default is disabled. 717 pass.
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-27 23:12:57 +02:00
|
|
|
from api.services import forge_credential as fc
|
|
|
|
|
|
feat(projection): derive a repository's projection from the forge
Implements ADR-012 decisions 1 and 2 (STATE-WP-0083 T01, T02 partial). Central
clones the default branch from Forgejo and derives its own projection: 69
workplans and 459 tasks from the-custodian at d5013ae, identical across runs,
with the commit recorded as provenance.
Identifiers are derived in the ADR-007 namespace and verified against live
records, so a forge-derived projection and a preliminary overlay agree on
identity without reconciliation.
The diff first matched hub records by UUID and was badly wrong: most hub records
carry pre-ADR-007 random identifiers, so nearly everything appeared
simultaneously missing and stale, and a reset built on it would have destroyed
and recreated the entire projection. It now matches canonical record id, falling
back to the backing file. whitehat-security — bootstrapped straight from files —
now reports clean, which is the control.
Task-level comparison is deliberately not trusted: hub tasks carry no canonical
record id, only a title, so matching is by title. Recorded as T06; T03 is
limited to workplans until it lands.
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-25 23:34:25 +02:00
|
|
|
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")
|
2026-08-26 01:19:10 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
2026-08-28 15:32:29 +02:00
|
|
|
def __init__(self, slug, status, path, title=None):
|
2026-08-26 01:19:10 +02:00
|
|
|
import uuid as _u
|
|
|
|
|
self.id = _u.uuid4()
|
|
|
|
|
self.slug = slug
|
|
|
|
|
self.status = status
|
2026-08-28 15:32:29 +02:00
|
|
|
# The title is a derived field the reset syncs; the fixture carried no
|
|
|
|
|
# `title` at all, so it could not have caught the drift that left
|
|
|
|
|
# cust-wp-0010 describing another workplan's work.
|
|
|
|
|
self.title = title
|
2026-08-26 01:19:10 +02:00
|
|
|
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."""
|
|
|
|
|
|
2026-08-28 21:06:24 +02:00
|
|
|
def __init__(self, repo, rows, foreign=None, slug_clash=None, task_rows=None):
|
2026-08-26 01:19:10 +02:00
|
|
|
self._repo = repo
|
|
|
|
|
self.rows = list(rows)
|
2026-08-26 09:42:14 +02:00
|
|
|
self._foreign = list(foreign or [])
|
2026-08-26 16:39:26 +02:00
|
|
|
self._slug_clash = list(slug_clash or [])
|
2026-08-28 21:06:24 +02:00
|
|
|
self._task_rows = task_rows
|
2026-08-26 01:19:10 +02:00
|
|
|
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
|
2026-08-26 16:39:26 +02:00
|
|
|
# 1 resolves the repo, 2 loads its workplans, 3 is the identifier
|
2026-08-28 21:06:24 +02:00
|
|
|
# lookup, 4 the slug lookup. On the update-only path (no creates)
|
|
|
|
|
# the 3rd call is the existing-task load instead.
|
|
|
|
|
update_only = (
|
|
|
|
|
self._task_rows is not None
|
|
|
|
|
and not self._foreign
|
|
|
|
|
and not self._slug_clash
|
|
|
|
|
)
|
2026-08-26 16:39:26 +02:00
|
|
|
if self._calls == 2:
|
|
|
|
|
payload = rows
|
2026-08-28 21:06:24 +02:00
|
|
|
elif update_only and self._calls == 3:
|
|
|
|
|
payload = self._task_rows
|
2026-08-26 16:39:26 +02:00
|
|
|
elif self._calls == 3:
|
|
|
|
|
payload = self._foreign
|
|
|
|
|
elif self._calls == 4:
|
|
|
|
|
payload = self._slug_clash
|
2026-08-28 21:06:24 +02:00
|
|
|
elif self._calls == 5:
|
|
|
|
|
payload = self._task_rows or []
|
2026-08-26 16:39:26 +02:00
|
|
|
else:
|
|
|
|
|
payload = []
|
2026-08-26 01:19:10 +02:00
|
|
|
|
|
|
|
|
class R:
|
|
|
|
|
def scalar_one_or_none(self_inner):
|
|
|
|
|
return repo
|
|
|
|
|
|
|
|
|
|
def scalars(self_inner):
|
2026-08-26 09:42:14 +02:00
|
|
|
return iter(payload)
|
2026-08-26 01:19:10 +02:00
|
|
|
|
|
|
|
|
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
|
2026-08-26 09:42:14 +02:00
|
|
|
|
2026-08-26 13:23:08 +02:00
|
|
|
async def rollback(self):
|
|
|
|
|
self.rolled_back = True
|
|
|
|
|
|
2026-08-26 09:42:14 +02:00
|
|
|
|
|
|
|
|
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"]
|
2026-08-26 13:23:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-08-26 16:39:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSlugCollisionRefusal:
|
|
|
|
|
"""slug carries its own unique constraint (STATE-WP-0083-T04).
|
|
|
|
|
|
|
|
|
|
Checking the identifier alone left disaster-control raising IntegrityError:
|
|
|
|
|
two repositories can derive different identifiers whose slugs still collide.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_refuses_when_the_slug_belongs_elsewhere(self):
|
|
|
|
|
derived = fp.DerivedProjection(
|
|
|
|
|
repo_slug="demo", commit="c0ffee",
|
|
|
|
|
workplans=[fp.DerivedWorkplan(
|
|
|
|
|
record_id="REPO-WP-0001", uuid=fp.derived_record_uuid("REPO-WP-0001"),
|
|
|
|
|
title="x", status="active", relative_path="workplans/a.md",
|
|
|
|
|
archived=False, tasks=[])])
|
|
|
|
|
clash = _Row(slug="repo-wp-0001", status="finished", path="workplans/other.md")
|
|
|
|
|
session = _FakeSession(repo=_Repo(), rows=[], foreign=[], slug_clash=[clash])
|
|
|
|
|
out = await fp.reset_repository_projection(session, "demo", derived=derived)
|
|
|
|
|
assert out.status == "refused"
|
|
|
|
|
assert out.refused[0]["reason"].startswith("slug already belongs")
|
|
|
|
|
assert session.added == []
|
2026-08-26 21:45:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestUnreadableIsNotMissing:
|
|
|
|
|
"""STATE-WP-0084-T01.
|
|
|
|
|
|
|
|
|
|
A repository central is not permitted to read and a repository whose
|
|
|
|
|
records no longer derive authorise opposite things. Every test here exists
|
|
|
|
|
to keep the retirement path unreachable from an answer that cannot support
|
|
|
|
|
it — by construction, not by the reset happening to fail first.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"stderr",
|
|
|
|
|
[
|
|
|
|
|
"fatal: could not read Username for 'https://forgejo.coulomb.social'",
|
|
|
|
|
"remote: Invalid username or password.\nfatal: Authentication failed",
|
|
|
|
|
"fatal: could not read Username for 'https://f': terminal prompts disabled",
|
|
|
|
|
"fatal: repository 'https://forgejo.coulomb.social/rapp-openbao.git' not found",
|
|
|
|
|
"fatal: unable to access '...': The requested URL returned error: 403",
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_permission_shaped_failures_are_classified_unreadable(self, stderr, monkeypatch):
|
|
|
|
|
def boom(*a, **k):
|
|
|
|
|
raise fp.ForgeDeriveError(stderr)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(fp, "_run_git", boom)
|
|
|
|
|
with pytest.raises(fp.ForgeUnreadableError):
|
|
|
|
|
fp.derive_from_forge("rapp-openbao")
|
|
|
|
|
|
|
|
|
|
def test_a_genuine_fault_stays_a_plain_error(self, monkeypatch):
|
|
|
|
|
def boom(*a, **k):
|
|
|
|
|
raise fp.ForgeDeriveError("fatal: early EOF\nfatal: index-pack failed")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(fp, "_run_git", boom)
|
|
|
|
|
with pytest.raises(fp.ForgeDeriveError) as exc:
|
|
|
|
|
fp.derive_from_forge("demo")
|
|
|
|
|
assert not isinstance(exc.value, fp.ForgeUnreadableError)
|
|
|
|
|
|
|
|
|
|
def test_unreadable_is_a_derive_error_so_old_callers_still_catch_it(self):
|
|
|
|
|
assert issubclass(fp.ForgeUnreadableError, fp.ForgeDeriveError)
|
|
|
|
|
|
|
|
|
|
def test_a_checkout_without_workplans_cannot_evidence_absence(self, tmp_path):
|
|
|
|
|
(tmp_path / "bare").mkdir()
|
|
|
|
|
p = fp.derive_from_checkout(tmp_path / "bare", "bare", "abc")
|
|
|
|
|
assert p.workplans == []
|
|
|
|
|
assert p.records_source_present is False
|
|
|
|
|
assert p.retirement_eligible is False
|
|
|
|
|
|
|
|
|
|
def test_a_real_checkout_can(self, tmp_path):
|
|
|
|
|
p = fp.derive_from_checkout(_repo(tmp_path), "demo", "abc")
|
|
|
|
|
assert p.records_source_present is True
|
|
|
|
|
assert p.retirement_eligible is True
|
|
|
|
|
|
|
|
|
|
def test_the_diff_withholds_stale_rather_than_computing_it(self):
|
|
|
|
|
derived = fp.DerivedProjection(
|
|
|
|
|
repo_slug="demo", commit="c0ffee", records_source_present=False
|
|
|
|
|
)
|
|
|
|
|
hub = [{"id": "11111111-1111-1111-1111-111111111111", "slug": "demo-wp-0001",
|
|
|
|
|
"status": "active", "backing_relative_path": "workplans/a.md"}]
|
|
|
|
|
d = fp.diff_against_hub(derived, hub, {})
|
|
|
|
|
assert d.stale == []
|
|
|
|
|
assert d.would_remove == 0
|
|
|
|
|
assert d.stale_withheld
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_an_empty_source_cannot_retire_even_when_acknowledged(self):
|
|
|
|
|
derived = fp.DerivedProjection(
|
|
|
|
|
repo_slug="demo", commit="c0ffee", records_source_present=False
|
|
|
|
|
)
|
|
|
|
|
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=derived, acknowledge_retirements=True
|
|
|
|
|
)
|
|
|
|
|
assert out.status == "refused"
|
|
|
|
|
assert out.retired == []
|
|
|
|
|
assert row.projection_retired_at is None
|
|
|
|
|
assert out.refused[0]["reason"].startswith("source produced no records")
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_an_unreadable_repository_reports_unreadable_not_error(self, monkeypatch):
|
|
|
|
|
def boom(*a, **k):
|
|
|
|
|
raise fp.ForgeUnreadableError("rapp-openbao could not be read from the forge")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(fp, "derive_from_forge", boom)
|
|
|
|
|
session = _FakeSession(repo=_Repo(), rows=[])
|
|
|
|
|
out = await fp.reset_repository_projection(session, "rapp-openbao")
|
|
|
|
|
assert out.status == "unreadable"
|
|
|
|
|
assert out.retired == [] and out.created == [] and out.updated == []
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_the_fleet_keeps_unreadable_out_of_the_error_bucket(self, monkeypatch):
|
|
|
|
|
async def fake(session, slug, **kw):
|
|
|
|
|
if slug == "private":
|
|
|
|
|
out = fp.ResetOutcome(repo_slug=slug, commit="", status="unreadable")
|
|
|
|
|
out.refused.append({"reason": "repository could not be read from the forge",
|
|
|
|
|
"slug": slug, "detail": "could not read Username"})
|
|
|
|
|
return out
|
|
|
|
|
if slug == "broken":
|
|
|
|
|
raise RuntimeError("index-pack failed")
|
|
|
|
|
out = fp.ResetOutcome(repo_slug=slug, commit="c0ffee", status="applied")
|
|
|
|
|
out.updated.append("A-WP-0001")
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(fp, "reset_repository_projection", fake)
|
|
|
|
|
sessions = [_FakeSession(repo=_Repo(), rows=[]) for _ in range(3)]
|
|
|
|
|
outcome = await fp.reset_fleet_projection(
|
|
|
|
|
TestFleetReset._Factory(sessions), ["ok", "private", "broken"]
|
|
|
|
|
)
|
|
|
|
|
assert list(outcome.unreadable) == ["private"]
|
|
|
|
|
assert list(outcome.errors) == ["broken"]
|
|
|
|
|
assert "private" not in outcome.results
|
|
|
|
|
d = outcome.to_dict()
|
|
|
|
|
assert d["unreadable_count"] == 1 and d["errored"] == 1
|
|
|
|
|
assert d["repositories"] == 3
|
feat(forge): resolve an optional forge read credential (STATE-WP-0084-T02/T03)
Nine repositories are invisible to derivation because central may not read
them. This adds the consuming half of the credential lane MASON-WP-0003 built.
The cluster has no agent injector and no secrets-store CSI driver, so the pod
authenticates to OpenBao with a projected ServiceAccount token (audience
`openbao`, not the API server) and reads the KV path itself. `forgeRead.*`
carries coordinates only; no credential is a chart value, an image layer, or a
Kubernetes Secret.
The credential reaches git through GIT_CONFIG_* setting http.extraHeader, not
through `-c` and not through userinfo in the clone URL — both of those put the
token in the process listing. It is redacted from ForgeDeriveError, which is
logged, stored in reset outcomes, and returned over the API.
Absent stays a supported state: with no credential, or with OpenBao
unreachable, resolution returns None and public derivation runs unchanged.
Raising would turn "nine repositories are unreadable" into "the pass failed",
which is what T01 exists to prevent.
Chart default is disabled. 717 pass.
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-27 23:12:57 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestForgeCredential:
|
|
|
|
|
"""Optional forge read credential (STATE-WP-0084-T03).
|
|
|
|
|
|
|
|
|
|
Absent is a valid state: a hub with no credential must still derive every
|
|
|
|
|
public repository. The credential must never reach argv, a log, or an
|
|
|
|
|
exception — the places a secret leaks without anyone deciding to leak it.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _clear_cache(self):
|
|
|
|
|
"""The resolved credential is cached for 5 minutes in production.
|
|
|
|
|
|
|
|
|
|
That cache is deliberate — a fleet reset of 121 repositories must not
|
|
|
|
|
authenticate to OpenBao 121 times — so tests clear it rather than
|
|
|
|
|
disable it, and exercise the same code path production uses.
|
|
|
|
|
"""
|
|
|
|
|
fc.reset_cache()
|
|
|
|
|
yield
|
|
|
|
|
fc.reset_cache()
|
|
|
|
|
|
|
|
|
|
def test_absent_credential_is_none_not_empty_string(self, monkeypatch):
|
|
|
|
|
monkeypatch.delenv(fc.TOKEN_ENV, raising=False)
|
|
|
|
|
monkeypatch.delenv(fc.TOKEN_FILE_ENV, raising=False)
|
|
|
|
|
assert fc.forge_read_token() is None
|
|
|
|
|
|
|
|
|
|
def test_a_file_is_preferred_over_the_environment(self, tmp_path, monkeypatch):
|
|
|
|
|
"""Kubernetes rotates a mounted file without a redeploy."""
|
|
|
|
|
f = tmp_path / "token"
|
|
|
|
|
f.write_text("from-file\n", encoding="utf-8")
|
|
|
|
|
monkeypatch.setenv(fc.TOKEN_ENV, "from-env")
|
|
|
|
|
monkeypatch.setenv(fc.TOKEN_FILE_ENV, str(f))
|
|
|
|
|
assert fc.forge_read_token() == "from-file"
|
|
|
|
|
|
|
|
|
|
def test_an_unreadable_token_file_does_not_fall_back_silently(self, monkeypatch):
|
|
|
|
|
"""Falling back to a stale env value would hide a broken mount."""
|
|
|
|
|
monkeypatch.setenv(fc.TOKEN_FILE_ENV, "/nonexistent/token")
|
|
|
|
|
monkeypatch.setenv(fc.TOKEN_ENV, "from-env")
|
|
|
|
|
assert fc.forge_read_token() is None
|
|
|
|
|
|
|
|
|
|
def test_openbao_is_the_last_resort_not_the_first(self, tmp_path, monkeypatch):
|
|
|
|
|
"""A file or env value must not trigger a network call."""
|
|
|
|
|
f = tmp_path / "token"
|
|
|
|
|
f.write_text("local", encoding="utf-8")
|
|
|
|
|
monkeypatch.setenv(fc.TOKEN_FILE_ENV, str(f))
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
fc, "_from_openbao", lambda: pytest.fail("OpenBao consulted unnecessarily")
|
|
|
|
|
)
|
|
|
|
|
assert fc.forge_read_token() == "local"
|
|
|
|
|
|
|
|
|
|
def test_openbao_failure_resolves_to_absent_not_an_exception(self, monkeypatch):
|
|
|
|
|
"""A hub that cannot reach OpenBao must still derive public repos.
|
|
|
|
|
|
|
|
|
|
Raising here would turn "nine repositories are unreadable" into "the
|
|
|
|
|
whole derivation pass failed" — the outcome STATE-WP-0084-T01 exists to
|
|
|
|
|
prevent.
|
|
|
|
|
"""
|
|
|
|
|
monkeypatch.delenv(fc.TOKEN_FILE_ENV, raising=False)
|
|
|
|
|
monkeypatch.delenv(fc.TOKEN_ENV, raising=False)
|
|
|
|
|
monkeypatch.setenv(fc.OPENBAO_ADDR_ENV, "https://openbao.invalid")
|
|
|
|
|
monkeypatch.setenv(fc.SECRET_PATH_ENV, "kv/data/forge")
|
|
|
|
|
monkeypatch.setenv(fc.OPENBAO_ROLE_ENV, "state-hub-forge-derivation")
|
|
|
|
|
monkeypatch.setenv(fc.OPENBAO_JWT_PATH_ENV, "/nonexistent/sa-token")
|
|
|
|
|
assert fc.forge_read_token() is None
|
|
|
|
|
|
|
|
|
|
def test_openbao_unwraps_kv_v2(self, tmp_path, monkeypatch):
|
|
|
|
|
jwt = tmp_path / "sa"
|
|
|
|
|
jwt.write_text("jwt-value", encoding="utf-8")
|
|
|
|
|
monkeypatch.delenv(fc.TOKEN_FILE_ENV, raising=False)
|
|
|
|
|
monkeypatch.delenv(fc.TOKEN_ENV, raising=False)
|
|
|
|
|
monkeypatch.setenv(fc.OPENBAO_ADDR_ENV, "https://openbao.test")
|
|
|
|
|
monkeypatch.setenv(fc.SECRET_PATH_ENV, "kv/data/forge")
|
|
|
|
|
monkeypatch.setenv(fc.OPENBAO_ROLE_ENV, "state-hub-forge-derivation")
|
|
|
|
|
monkeypatch.setenv(fc.OPENBAO_JWT_PATH_ENV, str(jwt))
|
|
|
|
|
|
|
|
|
|
class R:
|
|
|
|
|
def __init__(self, payload):
|
|
|
|
|
self._p = payload
|
|
|
|
|
|
|
|
|
|
def raise_for_status(self):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def json(self):
|
|
|
|
|
return self._p
|
|
|
|
|
|
|
|
|
|
class C:
|
|
|
|
|
def __enter__(self):
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def __exit__(self, *a):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def post(self, url, json):
|
|
|
|
|
assert json["jwt"] == "jwt-value"
|
|
|
|
|
assert json["role"] == "state-hub-forge-derivation"
|
|
|
|
|
return R({"auth": {"client_token": "bao-token"}})
|
|
|
|
|
|
|
|
|
|
def get(self, url, headers):
|
|
|
|
|
assert headers["X-Vault-Token"] == "bao-token"
|
|
|
|
|
return R({"data": {"data": {"token": "forge-secret"}}})
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(fc.httpx, "Client", lambda **kw: C())
|
|
|
|
|
assert fc.forge_read_token() == "forge-secret"
|
|
|
|
|
|
|
|
|
|
def test_credential_never_appears_in_argv(self, monkeypatch):
|
|
|
|
|
"""`-c http.extraHeader=` would put the token in every ps listing."""
|
|
|
|
|
seen = {}
|
|
|
|
|
|
|
|
|
|
class P:
|
|
|
|
|
returncode = 0
|
|
|
|
|
stdout = ""
|
|
|
|
|
stderr = ""
|
|
|
|
|
|
|
|
|
|
def fake_run(cmd, **kw):
|
|
|
|
|
seen["cmd"] = cmd
|
|
|
|
|
seen["env"] = kw.get("env") or {}
|
|
|
|
|
return P()
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(fp.subprocess, "run", fake_run)
|
|
|
|
|
fp._run_git("clone", "url", "dir", token="s3cret")
|
|
|
|
|
assert not any("s3cret" in part for part in seen["cmd"])
|
|
|
|
|
assert seen["env"]["GIT_CONFIG_COUNT"] == "1"
|
|
|
|
|
assert "s3cret" not in seen["env"]["GIT_CONFIG_KEY_0"]
|
|
|
|
|
|
|
|
|
|
def test_credential_is_redacted_from_failures(self, monkeypatch):
|
|
|
|
|
class P:
|
|
|
|
|
returncode = 128
|
|
|
|
|
stdout = ""
|
|
|
|
|
stderr = "fatal: auth failed using s3cret"
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(fp.subprocess, "run", lambda *a, **k: P())
|
|
|
|
|
with pytest.raises(fp.ForgeDeriveError) as exc:
|
|
|
|
|
fp._run_git("clone", token="s3cret")
|
|
|
|
|
assert "s3cret" not in str(exc.value) and "***" in str(exc.value)
|
|
|
|
|
|
|
|
|
|
def test_no_credential_still_runs(self, monkeypatch):
|
|
|
|
|
class P:
|
|
|
|
|
returncode = 0
|
|
|
|
|
stdout = "ok"
|
|
|
|
|
stderr = ""
|
|
|
|
|
seen = {}
|
|
|
|
|
|
|
|
|
|
def fake_run(cmd, **kw):
|
|
|
|
|
seen["env"] = kw.get("env") or {}
|
|
|
|
|
return P()
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(fp.subprocess, "run", fake_run)
|
|
|
|
|
assert fp._run_git("status") == "ok"
|
|
|
|
|
assert "GIT_CONFIG_COUNT" not in seen["env"]
|
2026-08-28 00:38:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestRekeyIsNotRename:
|
|
|
|
|
"""A changed identifier is a new record, not a moved file.
|
|
|
|
|
|
|
|
|
|
The ad-hoc requalification (CUST-WP-0066) keeps the filename by design, so
|
|
|
|
|
for those records a re-key never changes the path. Matching on path there
|
|
|
|
|
cannot distinguish the two cases, and choosing rename updates a row whose
|
|
|
|
|
UUID still encodes the old identifier — leaving the file and the hub
|
|
|
|
|
disagreeing about what the record is called.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
class _Row:
|
|
|
|
|
def __init__(self, id, slug, path):
|
|
|
|
|
self.id = id
|
|
|
|
|
self.slug = slug
|
|
|
|
|
self.backing_relative_path = path
|
|
|
|
|
self.projection_retired_at = None
|
|
|
|
|
self.status = "finished"
|
|
|
|
|
|
|
|
|
|
def test_a_derived_row_is_not_matched_by_path(self):
|
|
|
|
|
"""uuid5 means identity is a function of the identifier."""
|
|
|
|
|
import uuid as _u
|
|
|
|
|
old = self._Row(
|
|
|
|
|
_u.UUID(fp.derived_record_uuid("ADHOC-2026-08-25")),
|
|
|
|
|
"adhoc-2026-08-25",
|
|
|
|
|
"workplans/ADHOC-2026-08-25.md",
|
|
|
|
|
)
|
|
|
|
|
assert fp._identity_is_derived(old)
|
|
|
|
|
|
|
|
|
|
def test_a_legacy_row_keeps_path_matching(self):
|
|
|
|
|
"""v4 rows predate derived identity; there the identifier is a label."""
|
|
|
|
|
import uuid as _u
|
|
|
|
|
legacy = self._Row(_u.uuid4(), "repo-wp-0001", "workplans/REPO-WP-0001.md")
|
|
|
|
|
assert not fp._identity_is_derived(legacy)
|
|
|
|
|
|
|
|
|
|
def test_the_two_uuids_actually_differ(self):
|
|
|
|
|
"""Guards the premise: if they were equal the distinction is moot."""
|
|
|
|
|
assert fp.derived_record_uuid("ADHOC-2026-08-25") != fp.derived_record_uuid(
|
|
|
|
|
"CUST-WP-ADHOC-2026-08-25"
|
|
|
|
|
)
|
2026-08-28 00:52:26 +02:00
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"slug",
|
|
|
|
|
[
|
|
|
|
|
"adhoc-2026-07-02", # grandfathered daily ad-hoc
|
|
|
|
|
"cust-wp-adhoc-2026-08-25", # qualified daily ad-hoc
|
|
|
|
|
"net-kingdom-adhoc-2026-07-02", # repo-name-qualified legacy form
|
|
|
|
|
"adhoc-llmc-2026-06-02", # another legacy qualification
|
|
|
|
|
"rcluster-wp-0007", # ordinary workplan
|
|
|
|
|
"cust-wp-0066-t01", # task
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_identifier_slugs_are_recognised(self, slug):
|
|
|
|
|
"""A slug that claims to be an identifier is one, whatever its UUID.
|
|
|
|
|
|
|
|
|
|
25 of 44 ad-hoc rows are legacy v4, so a UUID-version test alone would
|
|
|
|
|
leave them path-matched and silently diverged from their files.
|
|
|
|
|
"""
|
|
|
|
|
assert fp._slug_is_identifier(slug)
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"slug",
|
|
|
|
|
["three-phoenix-ha-cluster", "testdrive-jsui-publication", "state-hub-v0.1", ""],
|
|
|
|
|
)
|
|
|
|
|
def test_title_slugs_keep_path_matching(self, slug):
|
|
|
|
|
"""Hub-first rows never claimed an identifier; the path is their only link."""
|
|
|
|
|
assert not fp._slug_is_identifier(slug)
|
2026-08-28 01:15:02 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestRetirementReleasesTheIdentifier:
|
|
|
|
|
"""`slug` is unique table-wide, so retirement must free it.
|
|
|
|
|
|
|
|
|
|
Retirement that only sets a timestamp leaves the identifier locked to a
|
|
|
|
|
record nothing derives, and the repository that owns it can never claim it.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_the_identifier_is_released(self):
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
when = datetime(2026, 8, 28, tzinfo=timezone.utc)
|
|
|
|
|
assert fp._tombstone_slug("repo-wp-0001", when) == "repo-wp-0001@retired-20260828"
|
|
|
|
|
|
|
|
|
|
def test_re_retiring_does_not_stack_marks(self):
|
|
|
|
|
"""Otherwise the 100-char column overflows after a few passes."""
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
a = fp._tombstone_slug("repo-wp-0001", datetime(2026, 8, 28, tzinfo=timezone.utc))
|
|
|
|
|
b = fp._tombstone_slug(a, datetime(2026, 9, 1, tzinfo=timezone.utc))
|
|
|
|
|
assert b == "repo-wp-0001@retired-20260901"
|
|
|
|
|
assert b.count(fp.RETIRED_SLUG_MARK) == 1
|
|
|
|
|
|
|
|
|
|
def test_a_long_slug_stays_within_the_column(self):
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
out = fp._tombstone_slug("x" * 140, datetime(2026, 8, 28, tzinfo=timezone.utc))
|
|
|
|
|
assert len(out) <= 100
|
2026-08-28 01:25:06 +02:00
|
|
|
|
|
|
|
|
class _Retired:
|
|
|
|
|
def __init__(self, slug, when, id=None):
|
|
|
|
|
import uuid as _u
|
|
|
|
|
self.id = id or _u.uuid4()
|
|
|
|
|
self.slug = slug
|
|
|
|
|
self.projection_retired_at = when
|
|
|
|
|
self.projection_retired_reason = "x"
|
|
|
|
|
self.backing_relative_path = None
|
|
|
|
|
self.status = "finished"
|
|
|
|
|
|
|
|
|
|
def test_rows_retired_before_the_stamp_existed_are_repaired(self):
|
|
|
|
|
"""`stale` excludes already-retired rows, so nothing else revisits them.
|
|
|
|
|
|
|
|
|
|
The 32 rows retired earlier today still hold their identifiers; without
|
|
|
|
|
this they would block the owning repository forever.
|
|
|
|
|
"""
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
when = datetime(2026, 8, 28, tzinfo=timezone.utc)
|
|
|
|
|
row = self._Retired("repo-wp-0001", when)
|
|
|
|
|
assert fp.RETIRED_SLUG_MARK not in row.slug
|
|
|
|
|
row.slug = fp._tombstone_slug(row.slug, row.projection_retired_at)
|
|
|
|
|
assert row.slug == "repo-wp-0001@retired-20260828"
|
|
|
|
|
|
|
|
|
|
def test_an_already_stamped_row_is_left_alone(self):
|
|
|
|
|
"""Otherwise every reset rewrites the date and churns the row."""
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
row = self._Retired(
|
|
|
|
|
"repo-wp-0001@retired-20260828", datetime(2026, 8, 28, tzinfo=timezone.utc)
|
|
|
|
|
)
|
|
|
|
|
assert fp.RETIRED_SLUG_MARK in row.slug
|
2026-08-28 03:17:21 +02:00
|
|
|
|
|
|
|
|
def test_a_release_is_committed_even_when_the_repo_refuses(self):
|
|
|
|
|
"""The fleet driver commits only on `applied` and rolls back otherwise.
|
|
|
|
|
|
|
|
|
|
Moving the release ahead of the refusal returns achieves nothing if the
|
|
|
|
|
rollback then discards it — which is the state railiance-cluster and
|
|
|
|
|
inter-hub were in.
|
|
|
|
|
"""
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_fleet_projection)
|
|
|
|
|
assert 'result.status == "applied" or result.released' in src
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestUuidMatchWins:
|
|
|
|
|
"""A row whose UUID is uuid5 of the identifier IS that record.
|
|
|
|
|
|
|
|
|
|
`testdrive-jsui-publication` and `three-phoenix-ha-cluster` carry legacy
|
|
|
|
|
title slugs while their UUIDs are derived from MARKITECT-WP-0002 and
|
|
|
|
|
RCLUSTER-WP-0007. The slug-identifier rule alone reads them as re-keys and
|
|
|
|
|
proposes retiring records that were correct all along.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_a_title_slugged_row_is_still_its_derived_record(self):
|
|
|
|
|
import uuid as _u
|
|
|
|
|
rid = "MARKITECT-WP-0002"
|
|
|
|
|
row_id = _u.UUID(fp.derived_record_uuid(rid))
|
|
|
|
|
# The slug says one thing, the UUID says another; the UUID is identity.
|
|
|
|
|
assert not fp._slug_is_identifier("testdrive-jsui-publication")
|
|
|
|
|
assert str(row_id) == fp.derived_record_uuid(rid)
|
|
|
|
|
|
|
|
|
|
def test_uuid_match_is_checked_before_the_rekey_rules(self):
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_repository_projection)
|
|
|
|
|
uuid_at = src.index("want_by_uuid.get(str(row.id))")
|
|
|
|
|
rekey_at = src.index("_identity_is_derived(row) or _slug_is_identifier")
|
|
|
|
|
assert uuid_at < rekey_at, "UUID match must precede the re-key heuristics"
|
2026-08-28 10:38:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTaskIdentityIsQualified:
|
|
|
|
|
"""A bare `T01` is unique within its workplan, not in the fleet.
|
|
|
|
|
|
|
|
|
|
Unqualified, `uuid5("T01")` is the same UUID for every workplan that has a
|
|
|
|
|
T01 — llm-connect's 91 task blocks derive 49 distinct UUIDs, and creating
|
|
|
|
|
its workplans dies on `duplicate key value violates unique constraint
|
|
|
|
|
"tasks_pkey"`.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
BODY = """
|
|
|
|
|
```task
|
|
|
|
|
id: T01
|
|
|
|
|
status: done
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
id: T02
|
|
|
|
|
status: todo
|
|
|
|
|
```
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_short_ids_are_qualified_by_their_workplan(self):
|
|
|
|
|
a = fp._parse_tasks(self.BODY, "LLM-WP-0001")
|
|
|
|
|
b = fp._parse_tasks(self.BODY, "LLM-WP-0002")
|
|
|
|
|
assert [t.record_id for t in a] == ["LLM-WP-0001-T01", "LLM-WP-0001-T02"]
|
|
|
|
|
assert {t.uuid for t in a}.isdisjoint({t.uuid for t in b})
|
|
|
|
|
|
|
|
|
|
def test_an_already_qualified_id_is_left_alone(self):
|
|
|
|
|
body = "```task\nid: LLM-WP-0009-T03\nstatus: todo\n```"
|
|
|
|
|
assert fp._parse_tasks(body, "LLM-WP-0001")[0].record_id == "LLM-WP-0009-T03"
|
|
|
|
|
|
|
|
|
|
def test_it_agrees_with_the_backfill(self):
|
|
|
|
|
"""Both must apply the same rule or they disagree on a task's name."""
|
|
|
|
|
from api.services.task_record_id_backfill import qualify_task_id
|
|
|
|
|
got = fp._parse_tasks(self.BODY, "LLM-WP-0001")[0].record_id
|
|
|
|
|
assert got == qualify_task_id("T01", "LLM-WP-0001")
|
|
|
|
|
|
|
|
|
|
def test_uuid_follows_the_qualified_id(self):
|
|
|
|
|
t = fp._parse_tasks(self.BODY, "LLM-WP-0001")[0]
|
|
|
|
|
assert t.uuid == fp.derived_record_uuid("LLM-WP-0001-T01")
|
2026-08-28 11:16:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestRetirementIsNotUndoneByPath:
|
|
|
|
|
"""A retired row is matched by UUID or not at all.
|
|
|
|
|
|
|
|
|
|
Releasing the identifier removes the evidence that made the row a re-key:
|
|
|
|
|
the tombstoned slug is no longer an identifier, and a legacy row is not
|
|
|
|
|
UUID-derived, so both guards fall through to path matching — which matches
|
|
|
|
|
the very file the row was retired for and resurrects it beside the correct
|
|
|
|
|
record created from that same file.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_a_tombstoned_slug_is_not_an_identifier(self):
|
|
|
|
|
"""This is the signal the tombstone destroys."""
|
|
|
|
|
assert fp._slug_is_identifier("adhoc-2026-06-01")
|
|
|
|
|
assert not fp._slug_is_identifier("adhoc-2026-06-01@retired-20260827")
|
|
|
|
|
|
|
|
|
|
def test_retired_rows_are_skipped_before_path_matching(self):
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_repository_projection)
|
2026-08-28 11:48:51 +02:00
|
|
|
skip_at = src.index("if row.projection_retired_at is not None:")
|
2026-08-28 11:16:12 +02:00
|
|
|
path_at = src.index("if bp and _path_key(bp) in want_paths:")
|
|
|
|
|
assert skip_at < path_at, "retired rows must be skipped before path matching"
|
|
|
|
|
|
|
|
|
|
def test_uuid_match_still_precedes_the_skip(self):
|
|
|
|
|
"""A record that genuinely returns must still be un-retired."""
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_repository_projection)
|
|
|
|
|
assert src.index("want_by_uuid.get(str(row.id))") < src.index(
|
2026-08-28 11:48:51 +02:00
|
|
|
"if row.projection_retired_at is not None:"
|
2026-08-28 11:16:12 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_a_row_that_derives_again_is_not_tombstoned(self):
|
|
|
|
|
"""Releasing it would change the slug out from under the matching.
|
|
|
|
|
|
|
|
|
|
The row would then fail to match its own file and never be un-retired —
|
|
|
|
|
which is how five rows ended up tombstoned but not retired.
|
|
|
|
|
"""
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_repository_projection)
|
|
|
|
|
rel = src.index("outcome.released.append(before)")
|
|
|
|
|
guard = src.index('in want or str(r.id) in want_by_uuid')
|
|
|
|
|
assert guard < rel, "the release must exempt records the forge still derives"
|
2026-08-28 11:30:25 +02:00
|
|
|
|
|
|
|
|
def test_a_stamped_slug_counts_as_retired_even_if_the_flag_is_clear(self):
|
|
|
|
|
"""Otherwise a resurrected row path-matches its file and reports noop forever."""
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_repository_projection)
|
2026-08-28 11:48:51 +02:00
|
|
|
assert "projection_retired_at is None and RETIRED_SLUG_MARK in" in src
|
|
|
|
|
|
|
|
|
|
def test_a_resurrected_row_becomes_stale_rather_than_being_skipped(self):
|
|
|
|
|
"""Skipping it outright is what left six rows reporting noop forever.
|
|
|
|
|
|
|
|
|
|
`stale` is computed from `matched`, so a row that never enters `matched`
|
|
|
|
|
is invisible: not retired, so not stale; path-matching its own file, so
|
|
|
|
|
never reported.
|
|
|
|
|
"""
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_repository_projection)
|
|
|
|
|
zombie = src.index("projection_retired_at is None and RETIRED_SLUG_MARK in")
|
|
|
|
|
tail = src[zombie:zombie + 700]
|
|
|
|
|
assert "matched[key] = row" in tail, "a resurrected row must enter `matched`"
|
fix(projection): rank competing claims instead of overwriting
Two rows can claim one record. `cust-wp-0010` claimed CUST-WP-0010 by its own
identifier while `workstream-lifecycle-documentation` claimed it by path, both
pointing at that workplan's file; `kont-wp-0013` and
`KONT-WP-0013-blob-storage-content-streaming` did the same. Assigning into
`matched` unconditionally let whichever row came last win and dropped the other
silently — never matched, so never stale, so never reported by any pass. It was
also order-dependent, so which row survived depended on row iteration order.
Claims are now ranked: derived UUID, then the record's own identifier, then
backing path, then prefix heuristic. The strongest wins and the loser is
displaced into `stale`, which makes it a retirement candidate rather than
invisible. Ranking is order-independent, verified both ways round.
Under ADR-007 the identifier is the identity, so the row naming the record wins
and its stale title and path are repaired from the file; the hub-first row that
never held the identifier retires with its history intact.
751 pass.
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-28 15:01:58 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCompetingClaims:
|
|
|
|
|
"""Two rows can claim one record; the weaker must not vanish.
|
|
|
|
|
|
|
|
|
|
`cust-wp-0010` claimed CUST-WP-0010 by its own identifier while
|
|
|
|
|
`workstream-lifecycle-documentation` claimed it by path. Assigning into
|
|
|
|
|
`matched` unconditionally let the later one win and dropped the other
|
|
|
|
|
silently — never matched, so never stale, so never reported by any pass.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_claims_are_ranked_not_overwritten(self):
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_repository_projection)
|
|
|
|
|
assert "def _claim(" in src
|
|
|
|
|
assert "displaced.append" in src
|
|
|
|
|
|
|
|
|
|
def test_a_displaced_row_becomes_stale(self):
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_repository_projection)
|
|
|
|
|
stale_at = src.index("stale = [")
|
|
|
|
|
assert "displaced" in src[stale_at:stale_at + 400]
|
|
|
|
|
|
|
|
|
|
def test_identifier_beats_path(self):
|
|
|
|
|
"""A row naming the record outranks one that merely shares its file."""
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_repository_projection)
|
|
|
|
|
assert src.index("UUID_MATCH, SLUG_MATCH, PATH_MATCH, PREFIX_MATCH = 0, 1, 2, 3") > 0
|
|
|
|
|
# Strength is ordered strongest-first, so a lower number wins.
|
|
|
|
|
assert "if strength < held:" in src
|
2026-08-28 15:32:29 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTitleIsDerivedToo:
|
|
|
|
|
"""The title is a derived field and must follow the file.
|
|
|
|
|
|
|
|
|
|
Not syncing it left `cust-wp-0010` correctly identified and correctly backed
|
|
|
|
|
while describing a different workplan's work.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_title_is_synced(self):
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_repository_projection)
|
|
|
|
|
assert "row.title = w.title.strip()" in src
|
|
|
|
|
|
|
|
|
|
def test_an_empty_derived_title_does_not_blank_a_real_one(self):
|
|
|
|
|
"""Three activity-core files parse to no title; blanking is worse than stale."""
|
|
|
|
|
import inspect
|
|
|
|
|
src = inspect.getsource(fp.reset_repository_projection)
|
|
|
|
|
i = src.index("row.title = w.title.strip()")
|
|
|
|
|
guard = src[max(0, i - 200):i]
|
|
|
|
|
assert "w.title and w.title.strip()" in guard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTitleSyncBehaviour:
|
|
|
|
|
"""Exercised against the fake session rather than asserted on source."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_a_drifted_title_is_corrected(self):
|
|
|
|
|
row = _Row(slug="demo-wp-0001", status="active",
|
|
|
|
|
path="workplans/a.md", title="Some Other Workplan")
|
|
|
|
|
session = _FakeSession(repo=_Repo(), rows=[row])
|
|
|
|
|
out = await fp.reset_repository_projection(
|
|
|
|
|
session, "demo",
|
|
|
|
|
derived=TestReset()._derived(("DEMO-WP-0001", "active", "workplans/a.md")),
|
|
|
|
|
)
|
|
|
|
|
assert out.status == "applied"
|
|
|
|
|
assert row.title != "Some Other Workplan"
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_an_empty_derived_title_leaves_the_existing_one(self):
|
|
|
|
|
row = _Row(slug="demo-wp-0001", status="active",
|
|
|
|
|
path="workplans/a.md", title="Real Title")
|
|
|
|
|
d = TestReset()._derived(("DEMO-WP-0001", "active", "workplans/a.md"))
|
|
|
|
|
d.workplans[0].title = None
|
|
|
|
|
session = _FakeSession(repo=_Repo(), rows=[row])
|
|
|
|
|
await fp.reset_repository_projection(session, "demo", derived=d)
|
|
|
|
|
assert row.title == "Real Title"
|
2026-08-28 21:06:24 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class _TaskRow:
|
|
|
|
|
def __init__(self, record_id, status="todo", title="t", workplan_id=None):
|
|
|
|
|
import uuid as _u
|
|
|
|
|
self.id = _u.uuid4()
|
|
|
|
|
self.record_id = record_id
|
|
|
|
|
self.status = status
|
|
|
|
|
self.title = title
|
|
|
|
|
self.priority = "medium"
|
|
|
|
|
self.workplan_id = workplan_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestExistingWorkplanTasks:
|
|
|
|
|
"""Tasks of an existing workplan are matched by record_id (CUST-WP-0068-T09)."""
|
|
|
|
|
|
|
|
|
|
def _derived(self, *tasks):
|
|
|
|
|
dts = [
|
|
|
|
|
fp.DerivedTask(
|
|
|
|
|
record_id=tid,
|
|
|
|
|
uuid=fp.derived_record_uuid(tid),
|
|
|
|
|
title=title,
|
|
|
|
|
status=st,
|
|
|
|
|
priority="medium",
|
|
|
|
|
)
|
|
|
|
|
for tid, title, st in tasks
|
|
|
|
|
]
|
|
|
|
|
return fp.DerivedProjection(
|
|
|
|
|
repo_slug="demo",
|
|
|
|
|
commit="c0ffee",
|
|
|
|
|
workplans=[
|
|
|
|
|
fp.DerivedWorkplan(
|
|
|
|
|
record_id="DEMO-WP-0001",
|
|
|
|
|
uuid=fp.derived_record_uuid("DEMO-WP-0001"),
|
|
|
|
|
title="DEMO-WP-0001",
|
|
|
|
|
status="active",
|
|
|
|
|
relative_path="workplans/a.md",
|
|
|
|
|
archived=False,
|
|
|
|
|
tasks=dts,
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_creates_a_missing_identified_task(self):
|
|
|
|
|
row = _Row(slug="demo-wp-0001", status="active", path="workplans/a.md")
|
|
|
|
|
session = _FakeSession(repo=_Repo(), rows=[row], task_rows=[])
|
|
|
|
|
out = await fp.reset_repository_projection(
|
|
|
|
|
session, "demo",
|
|
|
|
|
derived=self._derived(("DEMO-WP-0001-T01", "Do it", "todo")),
|
|
|
|
|
)
|
|
|
|
|
assert out.created_tasks == ["DEMO-WP-0001-T01"]
|
|
|
|
|
assert session.added
|
|
|
|
|
assert session.added[0].record_id == "DEMO-WP-0001-T01"
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_cancels_an_open_task_the_file_no_longer_derives(self):
|
|
|
|
|
row = _Row(slug="demo-wp-0001", status="active", path="workplans/a.md")
|
|
|
|
|
stale = _TaskRow("DEMO-WP-0001-T09", status="todo", workplan_id=row.id)
|
|
|
|
|
session = _FakeSession(repo=_Repo(), rows=[row], task_rows=[stale])
|
|
|
|
|
out = await fp.reset_repository_projection(
|
|
|
|
|
session, "demo",
|
|
|
|
|
derived=self._derived(("DEMO-WP-0001-T01", "Do it", "todo")),
|
|
|
|
|
)
|
|
|
|
|
assert stale.status.value == "cancel" or stale.status == "cancel" or str(stale.status).endswith("cancel")
|
|
|
|
|
assert "DEMO-WP-0001-T09" in out.cancelled_tasks
|
|
|
|
|
assert session.deleted == []
|
|
|
|
|
|
2026-08-28 21:31:13 +02:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_sets_record_id_when_the_derived_uuid_already_exists(self):
|
|
|
|
|
row = _Row(slug="demo-wp-0001", status="active", path="workplans/a.md")
|
|
|
|
|
existing = _TaskRow(None, status="done", title="Clear the stale hub rows", workplan_id=row.id)
|
|
|
|
|
derived = self._derived(("DEMO-WP-0001-T01", "Clear the stale hub rows", "done"))
|
|
|
|
|
existing.id = __import__("uuid").UUID(derived.workplans[0].tasks[0].uuid)
|
|
|
|
|
session = _FakeSession(repo=_Repo(), rows=[row], task_rows=[existing])
|
|
|
|
|
out = await fp.reset_repository_projection(session, "demo", derived=derived)
|
|
|
|
|
assert out.created_tasks == []
|
|
|
|
|
assert existing.record_id == "DEMO-WP-0001-T01"
|
|
|
|
|
assert session.added == []
|
|
|
|
|
|
2026-08-28 21:06:24 +02:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_leaves_unidentified_tasks_alone(self):
|
|
|
|
|
row = _Row(slug="demo-wp-0001", status="active", path="workplans/a.md")
|
|
|
|
|
orphan = _TaskRow(None, status="todo", title="legacy", workplan_id=row.id)
|
|
|
|
|
session = _FakeSession(repo=_Repo(), rows=[row], task_rows=[orphan])
|
|
|
|
|
await fp.reset_repository_projection(
|
|
|
|
|
session, "demo",
|
|
|
|
|
derived=self._derived(("DEMO-WP-0001-T01", "Do it", "todo")),
|
|
|
|
|
)
|
|
|
|
|
assert orphan.status == "todo"
|