feat(tasks): give task rows a canonical record identifier
Every work-record type carried a stable identifier except tasks, whose rows held only id, workplan_id, title, status and priority — nothing connecting a row to CUST-WP-0067-T01 in the file it came from. Matching was therefore by title, so a renamed heading looked like one task vanishing and another appearing, and the forge-derived reset had to refuse to touch tasks at all. Adds tasks.record_id (nullable: no migration can invent an identity for an existing row) and a backfill that reads the pairing from the repository files, where a task declares both its canonical id and its projection UUID. 5516 pairs across 121 repositories with zero conflicts; 4456 of 6073 cache task rows identified. Diff and reset now key on record_id where present, falling back to a title-prefixed key so an unidentified row stays visibly unidentified. Unknown stays unknown: a row the files do not claim keeps no identity and the reset keeps refusing to act on it, and an existing identity is never overwritten — a mismatch is recorded as a conflict rather than resolved. Refs STATE-WP-0083-T06 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
This commit is contained in:
parent
43ffe883c3
commit
8b207a991a
6 changed files with 325 additions and 6 deletions
123
tests/test_task_record_id_backfill.py
Normal file
123
tests/test_task_record_id_backfill.py
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
"""Recovering canonical identity for existing task rows (STATE-WP-0083-T06).
|
||||
|
||||
The mapping exists only in the repository files, where a task declares both its
|
||||
canonical id and the projection UUID it was registered under. Reading the pairing
|
||||
is safe; inferring it from titles is not, which is the whole reason this exists.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from api.services import task_record_id_backfill as bf
|
||||
|
||||
|
||||
def _repo(tmp_path: Path, name: str, body: str) -> Path:
|
||||
root = tmp_path / name
|
||||
(root / "workplans").mkdir(parents=True)
|
||||
(root / "workplans" / "WP.md").write_text(body, encoding="utf-8")
|
||||
return root
|
||||
|
||||
|
||||
def test_reads_the_pairing_a_file_declares(tmp_path):
|
||||
root = _repo(tmp_path, "a", (
|
||||
"---\nid: DEMO-WP-0001\ntype: workplan\n---\n\n"
|
||||
'```task\nid: DEMO-WP-0001-T01\nstate_hub_task_id: "11111111-1111-5111-8111-111111111111"\n```\n'
|
||||
))
|
||||
pairs, rep = bf.collect_pairs([root])
|
||||
assert pairs == {"11111111-1111-5111-8111-111111111111": "DEMO-WP-0001-T01"}
|
||||
assert rep.pairs_found == 1
|
||||
|
||||
|
||||
def test_task_without_a_projection_uuid_is_skipped(tmp_path):
|
||||
"""No pairing to read means no identity to assign."""
|
||||
root = _repo(tmp_path, "b", (
|
||||
"---\nid: DEMO-WP-0001\ntype: workplan\n---\n\n"
|
||||
"```task\nid: DEMO-WP-0001-T01\nstatus: todo\n```\n"
|
||||
))
|
||||
pairs, _ = bf.collect_pairs([root])
|
||||
assert pairs == {}
|
||||
|
||||
|
||||
def test_one_uuid_claimed_twice_is_a_conflict_not_a_guess(tmp_path):
|
||||
"""Duplicate registration: picking a winner would fabricate an identity."""
|
||||
dup = "22222222-2222-5222-8222-222222222222"
|
||||
a = _repo(tmp_path, "a", (
|
||||
"---\nid: A-WP-0001\ntype: workplan\n---\n\n"
|
||||
f'```task\nid: A-WP-0001-T01\nstate_hub_task_id: "{dup}"\n```\n'
|
||||
))
|
||||
b = _repo(tmp_path, "b", (
|
||||
"---\nid: B-WP-0001\ntype: workplan\n---\n\n"
|
||||
f'```task\nid: B-WP-0001-T01\nstate_hub_task_id: "{dup}"\n```\n'
|
||||
))
|
||||
pairs, rep = bf.collect_pairs([a, b])
|
||||
assert len(rep.conflicts) == 1
|
||||
assert pairs[dup] == "A-WP-0001-T01" # first wins, second recorded not applied
|
||||
|
||||
|
||||
class _Row:
|
||||
def __init__(self, tid, record_id=None):
|
||||
self.id = tid
|
||||
self.record_id = record_id
|
||||
|
||||
|
||||
class _Session:
|
||||
def __init__(self, rows):
|
||||
self.rows = rows
|
||||
|
||||
async def execute(self, *_a, **_k):
|
||||
rows = self.rows
|
||||
|
||||
class R:
|
||||
def scalars(self_inner):
|
||||
return iter(rows)
|
||||
|
||||
return R()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dry_run_changes_nothing(tmp_path):
|
||||
root = _repo(tmp_path, "a", (
|
||||
"---\nid: DEMO-WP-0001\ntype: workplan\n---\n\n"
|
||||
'```task\nid: DEMO-WP-0001-T01\nstate_hub_task_id: "11111111-1111-5111-8111-111111111111"\n```\n'
|
||||
))
|
||||
row = _Row("11111111-1111-5111-8111-111111111111")
|
||||
rep = await bf.backfill_task_record_ids(_Session([row]), [root], dry_run=True)
|
||||
assert rep.updated == 1 and row.record_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_applies_the_identity_when_not_dry_run(tmp_path):
|
||||
root = _repo(tmp_path, "a", (
|
||||
"---\nid: DEMO-WP-0001\ntype: workplan\n---\n\n"
|
||||
'```task\nid: DEMO-WP-0001-T01\nstate_hub_task_id: "11111111-1111-5111-8111-111111111111"\n```\n'
|
||||
))
|
||||
row = _Row("11111111-1111-5111-8111-111111111111")
|
||||
rep = await bf.backfill_task_record_ids(_Session([row]), [root], dry_run=False)
|
||||
assert rep.updated == 1 and row.record_id == "DEMO-WP-0001-T01"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_row_the_files_do_not_claim_keeps_no_identity(tmp_path):
|
||||
"""Unknown must stay unknown; the reset then keeps refusing to act on it."""
|
||||
root = _repo(tmp_path, "a", (
|
||||
"---\nid: DEMO-WP-0001\ntype: workplan\n---\n\n"
|
||||
'```task\nid: DEMO-WP-0001-T01\nstate_hub_task_id: "11111111-1111-5111-8111-111111111111"\n```\n'
|
||||
))
|
||||
orphan = _Row("99999999-9999-4999-8999-999999999999")
|
||||
await bf.backfill_task_record_ids(_Session([orphan]), [root], dry_run=False)
|
||||
assert orphan.record_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_existing_identity_is_never_overwritten(tmp_path):
|
||||
root = _repo(tmp_path, "a", (
|
||||
"---\nid: DEMO-WP-0001\ntype: workplan\n---\n\n"
|
||||
'```task\nid: DEMO-WP-0001-T01\nstate_hub_task_id: "11111111-1111-5111-8111-111111111111"\n```\n'
|
||||
))
|
||||
row = _Row("11111111-1111-5111-8111-111111111111", record_id="OTHER-WP-0001-T09")
|
||||
rep = await bf.backfill_task_record_ids(_Session([row]), [root], dry_run=False)
|
||||
assert row.record_id == "OTHER-WP-0001-T09"
|
||||
assert rep.conflicts and rep.updated == 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue