Harden reconciliation conflict handling

This commit is contained in:
2026-05-23 18:18:44 +02:00
parent 7831673820
commit 706b360736
8 changed files with 445 additions and 22 deletions

View File

@@ -647,3 +647,110 @@ class TestReconciliationEndpoints:
messages = r.json()
assert len(messages) == 1
assert "blocking reason" in messages[0]["body"]
async def test_apply_workstream_stale_expected_status_creates_conflict_message(self, client, tmp_path):
await _create_domain(client)
repo_root = tmp_path / "repo"
workplans = repo_root / "workplans"
workplans.mkdir(parents=True)
repo = await _create_repo(client, local_path=repo_root)
topic = await _create_topic(client)
ws = await _create_workstream(client, topic["id"], repo_id=repo["id"])
wp = workplans / "STATE-WP-9999-demo.md"
wp.write_text(
"---\n"
"id: STATE-WP-9999\n"
"type: workplan\n"
"title: Demo\n"
"domain: custodian\n"
"repo: state-hub\n"
"status: active\n"
f"state_hub_workstream_id: \"{ws['id']}\"\n"
"---\n",
encoding="utf-8",
)
await client.patch(f"/workstreams/{ws['id']}", json={"status": "ready"})
r = await client.post("/reconciliation/state-change", json={
"target_type": "workstream",
"target_id": ws["id"],
"target_status": "backlog",
"expected_current_status": "active",
"apply": True,
})
assert r.status_code == 200, r.text
body = r.json()
assert body["conflict"] is True
assert body["write_through_result"] == "not_applicable"
assert body["current_status"] == "ready"
assert "expected" in body["reason"]
assert "status: active" in wp.read_text(encoding="utf-8")
r = await client.get(f"/workstreams/{ws['id']}")
assert r.json()["status"] == "ready"
async def test_apply_workstream_file_status_drift_creates_conflict_message(self, client, tmp_path):
await _create_domain(client)
repo_root = tmp_path / "repo"
workplans = repo_root / "workplans"
workplans.mkdir(parents=True)
repo = await _create_repo(client, local_path=repo_root)
topic = await _create_topic(client)
ws = await _create_workstream(client, topic["id"], repo_id=repo["id"])
wp = workplans / "STATE-WP-9999-demo.md"
wp.write_text(
"---\n"
"id: STATE-WP-9999\n"
"type: workplan\n"
"title: Demo\n"
"domain: custodian\n"
"repo: state-hub\n"
"status: ready\n"
f"state_hub_workstream_id: \"{ws['id']}\"\n"
"---\n",
encoding="utf-8",
)
r = await client.post("/reconciliation/state-change", json={
"target_type": "workstream",
"target_id": ws["id"],
"target_status": "backlog",
"expected_current_status": "active",
"apply": True,
})
assert r.status_code == 200, r.text
body = r.json()
assert body["conflict"] is True
assert body["write_through_result"] == "not_applicable"
assert "differs from cached DB status" in body["reason"]
assert "status: ready" in wp.read_text(encoding="utf-8")
r = await client.get(f"/workstreams/{ws['id']}")
assert r.json()["status"] == "active"
async def test_apply_task_unavailable_host_path_creates_conflict_message(self, client, tmp_path):
await _create_domain(client)
repo = await _create_repo(client, local_path=tmp_path / "missing-repo")
topic = await _create_topic(client)
ws = await _create_workstream(client, topic["id"], repo_id=repo["id"])
task = await _create_task(client, ws["id"])
r = await client.post("/reconciliation/state-change", json={
"target_type": "task",
"target_id": task["id"],
"target_status": "in_progress",
"expected_current_status": "todo",
"apply": True,
})
assert r.status_code == 200, r.text
body = r.json()
assert body["conflict"] is True
assert body["write_through_result"] == "not_applicable"
assert "host path" in body["reason"]
assert body["reconciliation_record_id"]
r = await client.get(f"/tasks/{task['id']}")
assert r.json()["status"] == "todo"