state-hub/tests/test_task_record_id_backfill.py
tegwick 7f41d42488
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 28s
fix(backfill): qualify short task ids with their workplan
A task id written as a bare "T01" is unique only inside its own workplan.
Stored as a canonical identifier it makes every workplan's first task share one
identity: 51 such ids were assigned to 148 rows on central before this was
caught, found because identified rows outnumbered distinct identities.

Short ids are now qualified as WORKPLAN-ID-T01. A short id in a file with no
workplan id in frontmatter is left unidentified — an identity that is not unique
is worse than none, which is the same rule the rest of this module already
follows.

The 136 affected rows on central have been cleared so the corrected backfill can
reassign them; the backfill never overwrites an existing identity, so they had to
be nulled rather than re-derived over.

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
2026-08-26 02:17:46 +02:00

161 lines
5.9 KiB
Python

"""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
def test_a_bare_task_id_is_qualified_by_its_workplan(tmp_path):
"""`T01` is unique inside one workplan and meaningless outside it.
Stored raw, every workplan's first task shares one identity — 51 such ids
were assigned to 148 rows on central before this was caught.
"""
root = _repo(tmp_path, "a", (
"---\nid: LLM-WP-0001\ntype: workplan\n---\n\n"
'```task\nid: T01\nstate_hub_task_id: "11111111-1111-5111-8111-111111111111"\n```\n'
))
pairs, _ = bf.collect_pairs([root])
assert pairs == {"11111111-1111-5111-8111-111111111111": "LLM-WP-0001-T01"}
def test_two_workplans_first_tasks_do_not_collide(tmp_path):
a = _repo(tmp_path, "a", (
"---\nid: A-WP-0001\ntype: workplan\n---\n\n"
'```task\nid: T01\nstate_hub_task_id: "11111111-1111-5111-8111-111111111111"\n```\n'
))
b = _repo(tmp_path, "b", (
"---\nid: B-WP-0001\ntype: workplan\n---\n\n"
'```task\nid: T01\nstate_hub_task_id: "22222222-2222-5222-8222-222222222222"\n```\n'
))
pairs, rep = bf.collect_pairs([a, b])
assert set(pairs.values()) == {"A-WP-0001-T01", "B-WP-0001-T01"}
assert rep.conflicts == []
def test_unqualifiable_short_id_gets_no_identity(tmp_path):
"""No identity beats a non-unique one."""
root = _repo(tmp_path, "a", (
"id: not-frontmatter\n\n"
'```task\nid: T01\nstate_hub_task_id: "11111111-1111-5111-8111-111111111111"\n```\n'
))
pairs, _ = bf.collect_pairs([root])
assert pairs == {}