fix: bound task identity ambiguity
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
This commit is contained in:
parent
14e865ab30
commit
ddce470944
4 changed files with 394 additions and 8 deletions
|
|
@ -218,12 +218,21 @@ class _Row:
|
|||
class _FakeSession:
|
||||
"""Stands in for AsyncSession: enough to prove intent without a database."""
|
||||
|
||||
def __init__(self, repo, rows, foreign=None, slug_clash=None, task_rows=None):
|
||||
def __init__(
|
||||
self,
|
||||
repo,
|
||||
rows,
|
||||
foreign=None,
|
||||
slug_clash=None,
|
||||
task_rows=None,
|
||||
task_collision_rows=None,
|
||||
):
|
||||
self._repo = repo
|
||||
self.rows = list(rows)
|
||||
self._foreign = list(foreign or [])
|
||||
self._slug_clash = list(slug_clash or [])
|
||||
self._task_rows = task_rows
|
||||
self._task_collision_rows = list(task_collision_rows or [])
|
||||
self.added = []
|
||||
self.deleted = []
|
||||
self.committed = False
|
||||
|
|
@ -244,12 +253,14 @@ class _FakeSession:
|
|||
payload = rows
|
||||
elif update_only and self._calls == 3:
|
||||
payload = self._task_rows
|
||||
elif update_only and self._calls == 4:
|
||||
payload = self._task_collision_rows
|
||||
elif self._calls == 3:
|
||||
payload = self._foreign
|
||||
elif self._calls == 4:
|
||||
payload = self._slug_clash
|
||||
elif self._calls == 5:
|
||||
payload = self._task_rows or []
|
||||
payload = self._task_collision_rows
|
||||
else:
|
||||
payload = []
|
||||
|
||||
|
|
@ -1129,3 +1140,124 @@ class TestExistingWorkplanTasks:
|
|||
derived=self._derived(("DEMO-WP-0001-T01", "Do it", "todo")),
|
||||
)
|
||||
assert orphan.status == "todo"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_duplicate_identity_inside_current_workplan_is_refused(self):
|
||||
row = _Row(slug="demo-wp-0001", status="active", path="workplans/a.md")
|
||||
first = _TaskRow("DEMO-WP-0001-T01", workplan_id=row.id)
|
||||
second = _TaskRow("demo-wp-0001-t01", workplan_id=row.id)
|
||||
session = _FakeSession(repo=_Repo(), rows=[row], task_rows=[first, second])
|
||||
out = await fp.reset_repository_projection(
|
||||
session,
|
||||
"demo",
|
||||
derived=self._derived(("DEMO-WP-0001-T01", "Do it", "todo")),
|
||||
)
|
||||
assert out.status == "refused"
|
||||
assert out.refused[0]["reason"].startswith("current workplan contains duplicate")
|
||||
assert session.added == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_derived_task_uuid_held_elsewhere_is_refused_before_insert(self):
|
||||
row = _Row(slug="demo-wp-0001", status="active", path="workplans/a.md")
|
||||
derived = self._derived(("DEMO-WP-0001-T01", "Do it", "todo"))
|
||||
holder = _TaskRow("DEMO-WP-0001-T01", workplan_id=__import__("uuid").uuid4())
|
||||
holder.id = __import__("uuid").UUID(derived.workplans[0].tasks[0].uuid)
|
||||
session = _FakeSession(
|
||||
repo=_Repo(),
|
||||
rows=[row],
|
||||
task_rows=[],
|
||||
task_collision_rows=[holder],
|
||||
)
|
||||
out = await fp.reset_repository_projection(session, "demo", derived=derived)
|
||||
assert out.status == "refused"
|
||||
assert out.refused[0]["reason"].startswith("task identity already belongs")
|
||||
assert session.added == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tasks_on_retired_copy_do_not_block_current_projection(self):
|
||||
from datetime import datetime, timezone
|
||||
|
||||
current = _Row(slug="demo-wp-0001", status="active", path="workplans/a.md")
|
||||
current.id = __import__("uuid").UUID(fp.derived_record_uuid("DEMO-WP-0001"))
|
||||
retired = _Row(
|
||||
slug="demo-wp-0001@retired-20260831",
|
||||
status="finished",
|
||||
path="workplans/a.md",
|
||||
)
|
||||
retired.projection_retired_at = datetime.now(tz=timezone.utc)
|
||||
task = _TaskRow("DEMO-WP-0001-T01", status="todo", workplan_id=current.id)
|
||||
session = _FakeSession(repo=_Repo(), rows=[current, retired], task_rows=[task])
|
||||
out = await fp.reset_repository_projection(
|
||||
session,
|
||||
"demo",
|
||||
derived=self._derived(("DEMO-WP-0001-T01", "Do it", "todo")),
|
||||
)
|
||||
assert out.status in {"applied", "noop"}
|
||||
assert out.refused == []
|
||||
|
||||
|
||||
def test_diff_reports_duplicate_current_task_identity_as_ambiguous():
|
||||
derived = TestExistingWorkplanTasks()._derived(
|
||||
("DEMO-WP-0001-T01", "Do it", "todo")
|
||||
)
|
||||
workplan_id = derived.workplans[0].uuid
|
||||
hub_workplans = [
|
||||
{
|
||||
"id": workplan_id,
|
||||
"slug": "demo-wp-0001",
|
||||
"status": "active",
|
||||
"backing_relative_path": "workplans/a.md",
|
||||
}
|
||||
]
|
||||
hub_tasks = {
|
||||
workplan_id: [
|
||||
{
|
||||
"id": "11111111-1111-4111-8111-111111111111",
|
||||
"record_id": "DEMO-WP-0001-T01",
|
||||
"title": "a",
|
||||
"status": "todo",
|
||||
},
|
||||
{
|
||||
"id": "22222222-2222-4222-8222-222222222222",
|
||||
"record_id": "demo-wp-0001-t01",
|
||||
"title": "b",
|
||||
"status": "todo",
|
||||
},
|
||||
]
|
||||
}
|
||||
diff = fp.diff_against_hub(derived, hub_workplans, hub_tasks)
|
||||
assert diff.clean is False
|
||||
assert diff.to_dict()["counts"]["ambiguous"] == 1
|
||||
assert diff.ambiguous[0]["source"] == "hub"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_refuses_duplicate_task_identity_in_forge_projection():
|
||||
first = TestExistingWorkplanTasks()._derived(
|
||||
("SHARED-WP-0001-T01", "First", "todo")
|
||||
).workplans[0]
|
||||
second = fp.DerivedWorkplan(
|
||||
record_id="DEMO-WP-0002",
|
||||
uuid=fp.derived_record_uuid("DEMO-WP-0002"),
|
||||
title="Second",
|
||||
status="active",
|
||||
relative_path="workplans/b.md",
|
||||
archived=False,
|
||||
tasks=[
|
||||
fp.DerivedTask(
|
||||
record_id="SHARED-WP-0001-T01",
|
||||
uuid=fp.derived_record_uuid("SHARED-WP-0001-T01"),
|
||||
title="Second claim",
|
||||
status="todo",
|
||||
priority="medium",
|
||||
)
|
||||
],
|
||||
)
|
||||
derived = fp.DerivedProjection(
|
||||
repo_slug="demo", commit="c0ffee", workplans=[first, second]
|
||||
)
|
||||
session = _FakeSession(repo=_Repo(), rows=[])
|
||||
out = await fp.reset_repository_projection(session, "demo", derived=derived)
|
||||
assert out.status == "refused"
|
||||
assert any(item.get("source") == "forge" for item in out.refused)
|
||||
assert session.added == []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue