"""Follow-up tests for the round-2 fixes landed after new16/app13: - Fix 1 retry: first non-actionable reject spawns Review #N+1 with a concrete example hint; only a *second* non-actionable verdict degrades to approve-with-warning. - Fix 2 tightening: ``_role_session_id`` prefers ``task.metadata. delegation_team_instance_id`` over an empty cache, so the runtime no longer produces ``_no_team::{role}`` sentinel rows when the task knows its team. Migration collapses legacy sentinel rows into the canonical companion when both exist for a (run_id, role_id). - Fix 3 guard: the manager-dispatch guard whitelists turn kinds where "no delegate_work call" is the expected shape (delivery / synthesize / aggregate / review), so the CEO final-delivery work item is no longer marked failed when its subteam work is complete. """ from __future__ import annotations import tempfile import unittest from datetime import datetime from pathlib import Path from types import SimpleNamespace from unittest.mock import MagicMock from opc.core.models import ( CompanyMemberSession, DelegationWorkItem, Phase, Task, TaskStatus, ) from opc.database.store import OPCStore from opc.layer2_organization.company_mode import CompanyWorkItemExecutor from opc.layer2_organization.company_runtime import ( _NO_TEAM_SENTINEL, canonical_role_session_id, ) # ── Fix 1 retry path: deleted — runtime no longer does shape-based salvage / # retry / degrade. See company_mode.py:_finalize_review_work_item for the # new mechanical-apply flow with verdict-parse-retry as the only fallback. # ── Fix 5 PR1: role_session_id is role-scoped, no team slot ────────────── class RoleSessionIdIsRoleScopedTests(unittest.TestCase): """Fix 5 PR1: ``_role_session_id`` returns ``role-runtime::{run}::{role}`` regardless of any team_instance_id cache/metadata. Same role across every team context resolves to a single session — that's the whole point of PR1.""" def setUp(self) -> None: from opc.layer2_organization.company_runtime import CompanyRuntime async def _noop(*_a, **_kw): return None self.runtime = CompanyRuntime( org_engine=None, communication=None, store=None, save_runtime_session=_noop, emit_runtime_event=_noop, ) def _task(self, metadata: dict) -> Task: return Task( id="t", title="t", description="", assigned_to="cto", status=TaskStatus.RUNNING, project_id="p", session_id="s", metadata=metadata, ) def test_cache_does_not_affect_session_id(self) -> None: self.runtime._home_team_instance_by_role["cto"] = "ti-cached" task = self._task({"delegation_run_id": "run-1"}) sid = self.runtime._role_session_id(task, role_id="cto") self.assertEqual(sid, "role-runtime::run-1::cto") def test_task_metadata_team_instance_does_not_affect_session_id(self) -> None: task = self._task({ "delegation_run_id": "run-1", "delegation_team_instance_id": "ti-from-task", "team_instance_id": "ti-legacy", }) sid = self.runtime._role_session_id(task, role_id="cto") self.assertEqual(sid, "role-runtime::run-1::cto") def test_explicit_metadata_override_honoured(self) -> None: # When caller has already stamped the session id (e.g. re-used # from a prior lookup), we must respect it verbatim. task = self._task({ "delegation_run_id": "run-1", "delegation_role_session_id": "role-runtime::run-1::cto", }) sid = self.runtime._role_session_id(task, role_id="cto") self.assertEqual(sid, "role-runtime::run-1::cto") class SentinelLegacyMigrationTests(unittest.IsolatedAsyncioTestCase): """Post-Fix-5 PR2: the ``_no_team::{role}`` sentinel shape is just one more legacy form. The rich merge migration collapses it into the canonical 3-segment row and redirects every reference.""" 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 _raw_insert( self, *, role_session_id: str, run_id: str, role_id: str, team_instance_id: str = "", updated_at: str | None = None, ) -> None: ts = updated_at or datetime.now().isoformat() for table in ("role_runtime_sessions", "delegation_role_sessions"): await self.store._db.execute( f"""INSERT INTO {table} (role_session_id, run_id, project_id, team_instance_id, team_id, role_id, seat_id, seat_state_id, employee_id, focused_work_item_id, background_work_item_ids, manager_role_ids, manager_seat_ids, seat_ids, adapter_session_state, inbox_state, memory_slices_by_work_item, resume_state, current_work_item, latest_notification, manager_digest, status, metadata, created_at, updated_at) VALUES (?, ?, 'p', ?, 'team::x', ?, '', '', '', '', '[]', '[]', '[]', '[]', '{{}}', '{{}}', '{{}}', '{{}}', '{{}}', '{{}}', '{{}}', 'idle', '{{}}', ?, ?)""", (role_session_id, run_id, team_instance_id, role_id, ts, ts), ) await self.store._db.commit() async def test_sentinel_plus_legacy_full_form_merge_to_canonical(self) -> None: run_id = "run-merge" role_id = "designer" canonical = canonical_role_session_id(run_id=run_id, role_id=role_id) legacy_full = f"role-runtime::{run_id}::ti-real::{role_id}" sentinel = f"role-runtime::{run_id}::{_NO_TEAM_SENTINEL}::{role_id}" await self._raw_insert( role_session_id=legacy_full, run_id=run_id, role_id=role_id, team_instance_id="ti-real", ) await self._raw_insert( role_session_id=sentinel, run_id=run_id, role_id=role_id, ) # Work item with its claim pointing at the sentinel — the app13 # scenario where a role's live session diverged from bootstrap. wi = DelegationWorkItem( work_item_id="wi-claim", run_id=run_id, cell_id="c", team_instance_id="ti-real", team_id="team::x", role_id=role_id, seat_id="seat::team::x::designer", role_runtime_session_id=sentinel, claimed_by_role_runtime_session_id=sentinel, claimed_by_seat_id="seat::team::x::designer", manager_role_id="cmo", manager_seat_id="seat::team::x::cmo", title="t", phase=Phase.RUNNING, metadata={"assigned_role_runtime_id": sentinel}, ) await self.store.save_delegation_work_item(wi) stats = await self.store._migrate_role_sessions_merge_by_role() self.assertEqual(stats["groups"], 1) self.assertEqual(stats["canonical_written"], 1) # Both legacy rows are gone, canonical survives. async with self.store._db.execute( "SELECT COUNT(*) FROM role_runtime_sessions WHERE role_session_id=?", (sentinel,), ) as cursor: (count_sentinel,) = await cursor.fetchone() self.assertEqual(count_sentinel, 0) async with self.store._db.execute( "SELECT COUNT(*) FROM role_runtime_sessions WHERE role_session_id=?", (legacy_full,), ) as cursor: (count_full,) = await cursor.fetchone() self.assertEqual(count_full, 0) async with self.store._db.execute( "SELECT COUNT(*) FROM role_runtime_sessions WHERE role_session_id=?", (canonical,), ) as cursor: (count_canonical,) = await cursor.fetchone() self.assertEqual(count_canonical, 1) # The claim on the work item must now resolve to canonical. wi_after = await self.store.get_delegation_work_item("wi-claim") self.assertEqual(wi_after.claimed_by_role_runtime_session_id, canonical) self.assertEqual(wi_after.role_runtime_session_id, canonical) self.assertEqual(wi_after.metadata.get("assigned_role_runtime_id"), canonical) async def test_lone_sentinel_renamed_to_canonical(self) -> None: """Post-Fix-5: a lone sentinel row IS migrated to canonical (unlike the pre-PR2 behavior that left it in place). A runtime lookup via ``canonical_role_session_id`` now targets the 3-segment PK, so leaving sentinel rows untouched would orphan them.""" run_id = "run-lone" role_id = "qa_analyst" sentinel = f"role-runtime::{run_id}::{_NO_TEAM_SENTINEL}::{role_id}" canonical = canonical_role_session_id(run_id=run_id, role_id=role_id) await self._raw_insert( role_session_id=sentinel, run_id=run_id, role_id=role_id, ) stats = await self.store._migrate_role_sessions_merge_by_role() self.assertEqual(stats["groups"], 1) self.assertEqual(stats["canonical_written"], 1) async with self.store._db.execute( "SELECT COUNT(*) FROM role_runtime_sessions WHERE role_session_id=?", (sentinel,), ) as cursor: (count,) = await cursor.fetchone() self.assertEqual(count, 0) async with self.store._db.execute( "SELECT COUNT(*) FROM role_runtime_sessions WHERE role_session_id=?", (canonical,), ) as cursor: (count_canonical,) = await cursor.fetchone() self.assertEqual(count_canonical, 1) async def test_migration_idempotent_with_nothing_to_do(self) -> None: stats = await self.store._migrate_role_sessions_merge_by_role() self.assertEqual(stats["groups"], 0) self.assertEqual(stats["canonical_written"], 0) # ── Fix 3 guard whitelist ──────────────────────────────────────────────── class ManagerDispatchGuardWhitelistTests(unittest.TestCase): """The guard fires when (runtime_model=multi_team_org + turn_mode= dispatch_required + has direct reports). Fix 3 skips the whole chain for delivery / synthesize / aggregate / review work items regardless of the upstream turn-mode resolution, because those work items legitimately produce no children.""" def _session_with_reports(self) -> CompanyMemberSession: return CompanyMemberSession( member_session_id="ms-1", role_id="ceo", employee_id="e", metadata={ "direct_report_seat_ids": ["seat::team::ceo::cto"], "allowed_delegate_role_ids": ["cto", "cmo", "coo"], }, ) def _task(self, metadata: dict) -> Task: base = { "runtime_model": "multi_team_org", "current_turn_mode": "dispatch_required", "direct_report_seat_ids": ["seat::team::ceo::cto"], "allowed_delegate_role_ids": ["cto", "cmo", "coo"], } base.update(metadata) return Task( id="t", title="t", description="", assigned_to="ceo", status=TaskStatus.RUNNING, project_id="p", session_id="s", metadata=base, ) def test_dispatch_work_item_still_guarded(self) -> None: task = self._task({"work_kind": "dispatch", "work_item_turn_type": "dispatch"}) self.assertTrue( CompanyWorkItemExecutor._requires_manager_dispatch_guard( task, member_session=self._session_with_reports() ) ) def test_delivery_work_item_skips_guard(self) -> None: # The app13 smoking gun: CEO delivery with current_turn_mode # accidentally set to dispatch_required. Fix 3 short-circuits # on the work_kind signal. task = self._task({ "work_kind": "delivery", "work_item_turn_type": "execute", # not "deliver" because of the upstream resolver bug }) self.assertFalse( CompanyWorkItemExecutor._requires_manager_dispatch_guard( task, member_session=self._session_with_reports() ) ) def test_synthesize_work_item_skips_guard(self) -> None: task = self._task({"work_kind": "synthesize"}) self.assertFalse( CompanyWorkItemExecutor._requires_manager_dispatch_guard( task, member_session=self._session_with_reports() ) ) def test_aggregate_work_item_skips_guard(self) -> None: task = self._task({"work_kind": "aggregate"}) self.assertFalse( CompanyWorkItemExecutor._requires_manager_dispatch_guard( task, member_session=self._session_with_reports() ) ) def test_review_work_item_skips_guard(self) -> None: task = self._task({"work_kind": "review"}) self.assertFalse( CompanyWorkItemExecutor._requires_manager_dispatch_guard( task, member_session=self._session_with_reports() ) ) def test_turn_kind_falls_through_to_delegation_turn_kind(self) -> None: # Older work items stamp the kind under delegation_turn_kind only. task = self._task({ "work_kind": "", # modern field missing "delegation_turn_kind": "delivery", }) self.assertFalse( CompanyWorkItemExecutor._requires_manager_dispatch_guard( task, member_session=self._session_with_reports() ) ) def test_turn_kind_falls_through_to_work_item_turn_type(self) -> None: task = self._task({ "work_kind": "", "delegation_turn_kind": "", "work_item_turn_type": "deliver", }) self.assertFalse( CompanyWorkItemExecutor._requires_manager_dispatch_guard( task, member_session=self._session_with_reports() ) ) if __name__ == "__main__": unittest.main()