Initial commit
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user