Files
OpenOPC/tests/test_followup_fixes.py
T
2026-07-01 17:56:31 +08:00

362 lines
14 KiB
Python

"""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()