366 lines
14 KiB
Python
366 lines
14 KiB
Python
"""Regression tests for structured WorkItem <-> runtime Task links."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from opc.core.models import DelegationWorkItem, Phase, Task, TaskStatus
|
|
from opc.database.store import OPCStore
|
|
from opc.layer2_organization import phase_hooks # noqa: F401
|
|
from opc.layer2_organization.work_item_identity import mark_work_item_projection
|
|
from opc.layer2_organization.work_item_runtime import mark_work_item_runtime
|
|
from opc.layer2_organization.work_item_transition import transition_work_item_from_task
|
|
|
|
|
|
def _work_item(
|
|
work_item_id: str = "wi-1",
|
|
*,
|
|
projection_id: str = "",
|
|
metadata: dict | None = None,
|
|
) -> DelegationWorkItem:
|
|
return DelegationWorkItem(
|
|
work_item_id=work_item_id,
|
|
run_id="run-1",
|
|
cell_id="cell-1",
|
|
team_instance_id="team-instance-1",
|
|
team_id="team-1",
|
|
role_id="worker",
|
|
seat_id="seat-1",
|
|
title=f"Work {work_item_id}",
|
|
phase=Phase.READY,
|
|
projection_id=projection_id,
|
|
metadata=dict(metadata or {}),
|
|
)
|
|
|
|
|
|
def _task(
|
|
task_id: str = "task-1",
|
|
*,
|
|
status: TaskStatus = TaskStatus.PENDING,
|
|
metadata: dict | None = None,
|
|
created_at: datetime | None = None,
|
|
project_id: str = "default",
|
|
session_id: str | None = None,
|
|
parent_session_id: str | None = None,
|
|
) -> Task:
|
|
return Task(
|
|
id=task_id,
|
|
title=f"Task {task_id}",
|
|
assigned_to="worker",
|
|
status=status,
|
|
project_id=project_id,
|
|
session_id=session_id,
|
|
parent_session_id=parent_session_id,
|
|
metadata=dict(metadata or {}),
|
|
created_at=created_at or datetime.now(),
|
|
)
|
|
|
|
|
|
def _runtime_task(
|
|
task_id: str,
|
|
*,
|
|
projection_id: str,
|
|
run_id: str = "run-1",
|
|
status: TaskStatus = TaskStatus.PENDING,
|
|
member_session_id: str = "",
|
|
created_at: datetime | None = None,
|
|
project_id: str = "default",
|
|
session_id: str | None = None,
|
|
parent_session_id: str | None = None,
|
|
) -> Task:
|
|
metadata = mark_work_item_projection(
|
|
mark_work_item_runtime(
|
|
{
|
|
"delegation_run_id": run_id,
|
|
"work_item_role_id": "worker",
|
|
"delegation_seat_id": "seat-1",
|
|
}
|
|
),
|
|
projection_id=projection_id,
|
|
turn_type="execute",
|
|
)
|
|
if member_session_id:
|
|
metadata["member_session_id"] = member_session_id
|
|
return _task(
|
|
task_id,
|
|
status=status,
|
|
metadata=metadata,
|
|
created_at=created_at,
|
|
project_id=project_id,
|
|
session_id=session_id,
|
|
parent_session_id=parent_session_id,
|
|
)
|
|
|
|
|
|
class WorkItemRuntimeLinkTests(unittest.IsolatedAsyncioTestCase):
|
|
async def asyncSetUp(self) -> None:
|
|
self._tmpdir = tempfile.TemporaryDirectory()
|
|
self.store = OPCStore(db_path=Path(self._tmpdir.name) / "store.db")
|
|
await self.store.initialize()
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
await self.store.close()
|
|
self._tmpdir.cleanup()
|
|
|
|
async def test_link_api_hydrates_tasks_and_resolves_both_directions(self) -> None:
|
|
item = _work_item("wi-linked")
|
|
task = _task("task-linked")
|
|
await self.store.save_delegation_work_item(item)
|
|
await self.store.save_task(task)
|
|
|
|
linked = await self.store.link_work_item_runtime_task(item.work_item_id, task.id)
|
|
|
|
self.assertTrue(linked)
|
|
fetched_task = await self.store.get_task(task.id)
|
|
self.assertEqual(fetched_task.linked_work_item_id, item.work_item_id)
|
|
self.assertEqual(
|
|
(await self.store.get_runtime_task_for_work_item(item.work_item_id)).id,
|
|
task.id,
|
|
)
|
|
self.assertEqual(
|
|
(await self.store.get_work_item_for_runtime_task(task.id)).work_item_id,
|
|
item.work_item_id,
|
|
)
|
|
self.assertEqual(
|
|
await self.store.get_runtime_links_for_work_items([item.work_item_id]),
|
|
{item.work_item_id: task.id},
|
|
)
|
|
|
|
async def test_ensure_runtime_task_reuses_exact_existing_task_and_writes_link(self) -> None:
|
|
projection_id = "corporate::execute::reuse"
|
|
item = _work_item("wi-ensure-reuse", projection_id=projection_id)
|
|
existing = _runtime_task(
|
|
"task-existing-runtime",
|
|
projection_id=projection_id,
|
|
project_id="project-1",
|
|
session_id="root-session:wi-ensure-reuse",
|
|
parent_session_id="root-session",
|
|
)
|
|
candidate = _runtime_task(
|
|
"task-new-candidate",
|
|
projection_id=projection_id,
|
|
project_id="project-1",
|
|
session_id="root-session:wi-ensure-reuse",
|
|
parent_session_id="root-session",
|
|
)
|
|
await self.store.save_delegation_work_item(item)
|
|
await self.store.save_task(existing)
|
|
|
|
ensured = await self.store.ensure_runtime_task_for_work_item(item, lambda: candidate)
|
|
|
|
self.assertEqual(ensured.id, existing.id)
|
|
self.assertEqual(
|
|
await self.store.get_runtime_links_for_work_items([item.work_item_id]),
|
|
{item.work_item_id: existing.id},
|
|
)
|
|
self.assertIsNone(await self.store.get_task(candidate.id))
|
|
|
|
def fail_factory() -> Task:
|
|
raise AssertionError("factory should not run once link exists")
|
|
|
|
ensured_again = await self.store.ensure_runtime_task_for_work_item(item, fail_factory)
|
|
self.assertEqual(ensured_again.id, existing.id)
|
|
|
|
async def test_ensure_runtime_task_creates_task_and_writes_structured_link(self) -> None:
|
|
projection_id = "corporate::execute::create"
|
|
item = _work_item("wi-ensure-create", projection_id=projection_id)
|
|
candidate = _runtime_task(
|
|
"task-created-runtime",
|
|
projection_id=projection_id,
|
|
project_id="project-1",
|
|
session_id="root-session:wi-ensure-create",
|
|
parent_session_id="root-session",
|
|
)
|
|
await self.store.save_delegation_work_item(item)
|
|
|
|
ensured = await self.store.ensure_runtime_task_for_work_item(item, lambda: candidate)
|
|
fetched = await self.store.get_task(ensured.id)
|
|
|
|
self.assertEqual(ensured.id, candidate.id)
|
|
self.assertEqual(fetched.linked_work_item_id, item.work_item_id)
|
|
|
|
async def test_ensure_runtime_task_accepts_canonical_delivery_turn_kind(self) -> None:
|
|
projection_id = "corporate::delivery::create"
|
|
metadata = mark_work_item_projection(
|
|
mark_work_item_runtime({
|
|
"runtime_model": "multi_team_org",
|
|
"work_kind": "delivery",
|
|
"delegation_turn_kind": "delivery",
|
|
"seat_id": "seat-1",
|
|
}),
|
|
projection_id=projection_id,
|
|
turn_type="deliver",
|
|
)
|
|
item = DelegationWorkItem(
|
|
work_item_id="wi-delivery-create",
|
|
run_id="run-1",
|
|
cell_id="cell-1",
|
|
team_instance_id="team-instance-1",
|
|
team_id="team-1",
|
|
role_id="worker",
|
|
seat_id="seat-1",
|
|
title="Deliver",
|
|
kind="delivery",
|
|
projection_id=projection_id,
|
|
phase=Phase.READY,
|
|
metadata=metadata,
|
|
)
|
|
candidate = _runtime_task(
|
|
"task-delivery-runtime",
|
|
projection_id=projection_id,
|
|
project_id="project-1",
|
|
session_id="root-session:wi-delivery-create",
|
|
parent_session_id="root-session",
|
|
)
|
|
# Regression for new35: materialization must stamp the runtime turn as
|
|
# deliver when the WorkItem business kind is delivery.
|
|
candidate.metadata["work_item_turn_type"] = "deliver"
|
|
candidate.metadata["work_kind"] = "delivery"
|
|
candidate.metadata["delegation_turn_kind"] = "delivery"
|
|
await self.store.save_delegation_work_item(item)
|
|
|
|
ensured = await self.store.ensure_runtime_task_for_work_item(item, lambda: candidate)
|
|
|
|
self.assertEqual(ensured.id, candidate.id)
|
|
self.assertEqual(
|
|
await self.store.get_runtime_links_for_work_items([item.work_item_id]),
|
|
{item.work_item_id: candidate.id},
|
|
)
|
|
|
|
async def test_ensure_runtime_task_rejects_stale_execute_turn_for_delivery(self) -> None:
|
|
projection_id = "corporate::delivery::stale"
|
|
metadata = mark_work_item_projection(
|
|
mark_work_item_runtime({
|
|
"runtime_model": "multi_team_org",
|
|
"work_kind": "delivery",
|
|
"delegation_turn_kind": "delivery",
|
|
"seat_id": "seat-1",
|
|
}),
|
|
projection_id=projection_id,
|
|
turn_type="deliver",
|
|
)
|
|
item = DelegationWorkItem(
|
|
work_item_id="wi-delivery-stale",
|
|
run_id="run-1",
|
|
cell_id="cell-1",
|
|
team_instance_id="team-instance-1",
|
|
team_id="team-1",
|
|
role_id="worker",
|
|
seat_id="seat-1",
|
|
title="Deliver",
|
|
kind="delivery",
|
|
projection_id=projection_id,
|
|
phase=Phase.READY,
|
|
metadata=metadata,
|
|
)
|
|
candidate = _runtime_task(
|
|
"task-delivery-stale",
|
|
projection_id=projection_id,
|
|
project_id="project-1",
|
|
session_id="root-session:wi-delivery-stale",
|
|
parent_session_id="root-session",
|
|
)
|
|
candidate.metadata["work_item_turn_type"] = "execute"
|
|
candidate.metadata["work_kind"] = "delivery"
|
|
candidate.metadata["delegation_turn_kind"] = "delivery"
|
|
await self.store.save_delegation_work_item(item)
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "work_kind_mismatch"):
|
|
await self.store.ensure_runtime_task_for_work_item(item, lambda: candidate)
|
|
|
|
async def test_ensure_runtime_task_rejects_projection_owner_seat_mismatch(self) -> None:
|
|
projection_id = "corporate::execute::seat-mismatch"
|
|
item = _work_item("wi-seat-mismatch", projection_id=projection_id)
|
|
candidate = _runtime_task(
|
|
"task-seat-mismatch",
|
|
projection_id=projection_id,
|
|
project_id="project-1",
|
|
session_id="root-session:wi-seat-mismatch",
|
|
parent_session_id="root-session",
|
|
)
|
|
candidate.metadata["runtime_model"] = "multi_team_org"
|
|
candidate.metadata["delegation_seat_id"] = "wrong-seat"
|
|
await self.store.save_delegation_work_item(item)
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "owner_seat_mismatch"):
|
|
await self.store.ensure_runtime_task_for_work_item(item, lambda: candidate)
|
|
|
|
self.assertIsNone(await self.store.get_task(candidate.id))
|
|
self.assertEqual(await self.store.get_runtime_links_for_work_items([item.work_item_id]), {})
|
|
|
|
async def test_hydrate_task_work_item_links_does_not_repair_missing_link(self) -> None:
|
|
projection_id = "corporate::execute::unlinked"
|
|
item = _work_item("wi-unlinked", projection_id=projection_id)
|
|
task = _runtime_task(
|
|
"task-unlinked",
|
|
projection_id=projection_id,
|
|
project_id="project-1",
|
|
session_id="root-session:wi-unlinked",
|
|
parent_session_id="root-session",
|
|
)
|
|
task.metadata["runtime_model"] = "multi_team_org"
|
|
await self.store.save_delegation_work_item(item)
|
|
await self.store.save_task(task)
|
|
|
|
fetched = await self.store.get_task(task.id)
|
|
await self.store.hydrate_task_work_item_links([task])
|
|
|
|
self.assertEqual(fetched.linked_work_item_id, "")
|
|
self.assertEqual(task.linked_work_item_id, "")
|
|
self.assertEqual(await self.store.get_runtime_links_for_work_items([item.work_item_id]), {})
|
|
|
|
async def test_link_api_does_not_overwrite_active_link_with_duplicate_by_default(self) -> None:
|
|
projection_id = "corporate::execute::stable"
|
|
item = _work_item("wi-no-overwrite", projection_id=projection_id)
|
|
canonical = _runtime_task("task-canonical", projection_id=projection_id, status=TaskStatus.RUNNING)
|
|
duplicate = _runtime_task("task-duplicate-new", projection_id=projection_id, status=TaskStatus.PENDING)
|
|
await self.store.save_delegation_work_item(item)
|
|
await self.store.save_task(canonical)
|
|
await self.store.save_task(duplicate)
|
|
self.assertTrue(await self.store.link_work_item_runtime_task(item.work_item_id, canonical.id))
|
|
|
|
self.assertFalse(await self.store.link_work_item_runtime_task(item.work_item_id, duplicate.id))
|
|
self.assertEqual(
|
|
await self.store.get_runtime_links_for_work_items([item.work_item_id]),
|
|
{item.work_item_id: canonical.id},
|
|
)
|
|
|
|
async def test_task_delete_cascades_link_and_allows_rematerialization(self) -> None:
|
|
item = _work_item("wi-cascade")
|
|
task = _task("task-cascade")
|
|
await self.store.save_delegation_work_item(item)
|
|
await self.store.save_task(task)
|
|
await self.store.link_work_item_runtime_task(item.work_item_id, task.id)
|
|
|
|
await self.store.hard_delete_task(task.id)
|
|
|
|
self.assertEqual(await self.store.get_runtime_links_for_work_items([item.work_item_id]), {})
|
|
replacement = _task("task-cascade-replacement")
|
|
await self.store.save_task(replacement)
|
|
self.assertTrue(await self.store.link_work_item_runtime_task(item.work_item_id, replacement.id))
|
|
|
|
async def test_transition_from_task_uses_structured_link_without_legacy_metadata(self) -> None:
|
|
item = _work_item("wi-transition")
|
|
task = _task("task-transition")
|
|
await self.store.save_delegation_work_item(item)
|
|
await self.store.save_task(task)
|
|
await self.store.link_work_item_runtime_task(item.work_item_id, task.id)
|
|
linked_task = await self.store.get_task(task.id)
|
|
|
|
ok = await transition_work_item_from_task(
|
|
self.store,
|
|
linked_task,
|
|
target_status_or_phase=TaskStatus.RUNNING,
|
|
reason="structured_link_test",
|
|
)
|
|
|
|
self.assertTrue(ok)
|
|
self.assertEqual((await self.store.get_delegation_work_item(item.work_item_id)).phase, Phase.RUNNING)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|