2078 lines
81 KiB
Python
2078 lines
81 KiB
Python
"""Tests for the automatic worker→manager review handoff in company mode.
|
|
|
|
Validates the three hooks that glue worker completion to manager review:
|
|
|
|
1. Worker completion automatically transitions the work item to ``in_review``
|
|
through the canonical done transition and preserves the ``manager_role_id`` /
|
|
``manager_seat_id`` of the reviewer even when the task metadata has lost
|
|
them (common after work-item runtime rehydration).
|
|
|
|
2. ``CompanyRuntime.complete_claim`` sends an ``APPROVAL_REQUEST`` message to
|
|
the manager with ``reply_needed=True`` and ``review_required`` metadata so
|
|
the dispatch loop wakes the manager up through the existing
|
|
attention-work-item mechanism.
|
|
|
|
3. ``_resolve_current_turn_mode`` returns ``"review_pending"`` when the manager
|
|
has direct reports with work items awaiting review, overriding
|
|
``dispatch_required`` / ``monitor_children``. The contract builder injects
|
|
a ``## Review Requirement`` block listing the pending items.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
import uuid
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock
|
|
|
|
from opc.core.models import (
|
|
AgentMessage,
|
|
CommsSemanticType,
|
|
CompanyMemberSession,
|
|
DelegationWorkItem,
|
|
Phase,
|
|
Task,
|
|
TaskResult,
|
|
TaskStatus,
|
|
)
|
|
from opc.database.store import OPCStore
|
|
from opc.layer2_organization.company_runtime import CompanyRuntime
|
|
from opc.layer2_organization.prompt_contract import make_prompt_contract
|
|
from opc.layer2_organization.work_item_transition import transition_work_item_from_task
|
|
from opc.layer2_organization.work_item_links import set_linked_work_item_id
|
|
from opc.layer3_agent.company_runtime_contract import (
|
|
build_company_work_item_contract,
|
|
build_external_company_work_item_contract,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class RecordingCommunication:
|
|
"""Communication stub that captures manager notifications."""
|
|
|
|
def __init__(self) -> None:
|
|
self.calls: list[dict[str, Any]] = []
|
|
|
|
async def send_manager_notification(
|
|
self,
|
|
*,
|
|
from_agent: str,
|
|
task: Task | None,
|
|
semantic_type: CommsSemanticType,
|
|
subject: str,
|
|
body: str,
|
|
metadata: dict[str, Any] | None = None,
|
|
reply_needed: bool = False,
|
|
requires_ack: bool = False,
|
|
) -> AgentMessage:
|
|
record = {
|
|
"from_agent": from_agent,
|
|
"task": task,
|
|
"semantic_type": semantic_type,
|
|
"subject": subject,
|
|
"body": body,
|
|
"metadata": dict(metadata or {}),
|
|
"reply_needed": reply_needed,
|
|
"requires_ack": requires_ack,
|
|
}
|
|
self.calls.append(record)
|
|
return AgentMessage(
|
|
from_agent=from_agent,
|
|
to_agents=["manager"],
|
|
subject=subject,
|
|
body=body,
|
|
semantic_type=semantic_type,
|
|
reply_needed=reply_needed,
|
|
requires_ack=requires_ack,
|
|
metadata=dict(metadata or {}),
|
|
)
|
|
|
|
async def read_inbox(self, **_: Any) -> list[dict[str, Any]]:
|
|
return []
|
|
|
|
|
|
class StubOrgEngine:
|
|
"""Minimal org engine providing reports_to resolution for tests."""
|
|
|
|
def __init__(self) -> None:
|
|
self._roles = {
|
|
"ceo": SimpleNamespace(role_id="ceo", reports_to="owner"),
|
|
"cto": SimpleNamespace(role_id="cto", reports_to="ceo"),
|
|
"engineer": SimpleNamespace(role_id="engineer", reports_to="cto"),
|
|
}
|
|
|
|
def get_agent(self, role_id: str):
|
|
return self._roles.get(role_id)
|
|
|
|
|
|
def _make_worker_session(
|
|
*,
|
|
role_id: str = "engineer",
|
|
seat_id: str = "seat::eng",
|
|
manager_role_id: str = "cto",
|
|
manager_seat_id: str = "seat::cto",
|
|
status: str = "running",
|
|
) -> CompanyMemberSession:
|
|
session = CompanyMemberSession(
|
|
member_session_id=f"member::{role_id}",
|
|
role_id=role_id,
|
|
employee_id="employee-1",
|
|
seat_id=seat_id,
|
|
status=status,
|
|
resident_status=status,
|
|
manager_role_id=manager_role_id,
|
|
manager_role_ids=[manager_role_id] if manager_role_id else [],
|
|
metadata={
|
|
"seat_id": seat_id,
|
|
"manager_role_id": manager_role_id,
|
|
"manager_seat_id": manager_seat_id,
|
|
"session_scope_id": "scope-1",
|
|
"runtime_model": "multi_team_org",
|
|
},
|
|
)
|
|
return session
|
|
|
|
|
|
def _make_manager_session(
|
|
*,
|
|
role_id: str = "cto",
|
|
seat_id: str = "seat::cto",
|
|
direct_report_seat_ids: list[str] | None = None,
|
|
direct_report_role_ids: list[str] | None = None,
|
|
manager_board_summary: dict[str, Any] | None = None,
|
|
) -> CompanyMemberSession:
|
|
session = CompanyMemberSession(
|
|
member_session_id=f"member::{role_id}",
|
|
role_id=role_id,
|
|
employee_id="mgr-employee",
|
|
seat_id=seat_id,
|
|
status="idle",
|
|
resident_status="idle",
|
|
manager_role_id="ceo",
|
|
manager_role_ids=["ceo"],
|
|
metadata={
|
|
"seat_id": seat_id,
|
|
"runtime_model": "multi_team_org",
|
|
"direct_report_seat_ids": list(direct_report_seat_ids or []),
|
|
"direct_report_role_ids": list(direct_report_role_ids or []),
|
|
"managed_team_id": "team::cto",
|
|
"manager_board_summary": dict(manager_board_summary or {}),
|
|
},
|
|
)
|
|
return session
|
|
|
|
|
|
def _make_worker_task(
|
|
*,
|
|
task_id: str = "task-engineer",
|
|
work_item_id: str = "work-item-engineer",
|
|
status: TaskStatus = TaskStatus.DONE,
|
|
manager_role_id: str = "cto",
|
|
manager_seat_id: str = "seat::cto",
|
|
) -> Task:
|
|
task = Task(
|
|
id=task_id,
|
|
title="Engineer deliverable",
|
|
assigned_to="engineer",
|
|
status=status,
|
|
project_id="proj1",
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"work_item_role_id": "engineer",
|
|
"work_item_projection_id": "engineer__execute",
|
|
"manager_role_id": manager_role_id,
|
|
"manager_seat_id": manager_seat_id,
|
|
"delegation_run_id": "run-1",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, work_item_id)
|
|
return task
|
|
|
|
|
|
async def _apply_task_transition(
|
|
executor: Any,
|
|
task: Task,
|
|
*,
|
|
status: str,
|
|
result: TaskResult | None = None,
|
|
) -> None:
|
|
if status == TaskStatus.DONE.value:
|
|
await executor._apply_done_transition(task, result=result)
|
|
return
|
|
summary = ""
|
|
if result is not None:
|
|
summary = str(result.content or "").strip()
|
|
try:
|
|
target: TaskStatus | str = TaskStatus(status)
|
|
except ValueError:
|
|
target = status
|
|
await transition_work_item_from_task(
|
|
executor.store,
|
|
task,
|
|
target_status_or_phase=target,
|
|
reason="test_task_transition",
|
|
summary=summary or None,
|
|
)
|
|
await executor._notify_kanban_changed()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# complete_claim() review-notification hook
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class CompleteClaimReviewNotificationTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_worker_done_sends_approval_request_with_reply_needed(self) -> None:
|
|
communication = RecordingCommunication()
|
|
runtime = CompanyRuntime(org_engine=StubOrgEngine(), communication=communication)
|
|
session = _make_worker_session(status="running")
|
|
runtime.member_sessions[session.member_session_id] = session
|
|
task = _make_worker_task(status=TaskStatus.DONE)
|
|
|
|
await runtime.complete_claim(
|
|
session,
|
|
task,
|
|
result=TaskResult(status=TaskStatus.DONE, content="Deliverable ready for review."),
|
|
)
|
|
|
|
# One or more notifications sent. The FIRST should be the review
|
|
# request (semantic=APPROVAL_REQUEST, reply_needed=True).
|
|
review_calls = [
|
|
call for call in communication.calls
|
|
if call["semantic_type"] == CommsSemanticType.APPROVAL_REQUEST
|
|
]
|
|
self.assertEqual(len(review_calls), 1)
|
|
review = review_calls[0]
|
|
self.assertTrue(review["reply_needed"])
|
|
self.assertIn("Review needed", review["subject"])
|
|
self.assertEqual(review["metadata"]["work_item_id"], "work-item-engineer")
|
|
self.assertTrue(review["metadata"]["review_required"])
|
|
self.assertEqual(review["metadata"]["completion_report"], "Deliverable ready for review.")
|
|
# Session state cleared.
|
|
self.assertEqual(session.status, "idle")
|
|
self.assertEqual(session.current_assignment, {})
|
|
|
|
async def test_worker_done_without_summary_still_triggers_review(self) -> None:
|
|
"""Edge case: worker finished but produced no content. The manager
|
|
still needs to be woken up to inspect the artifacts directly."""
|
|
communication = RecordingCommunication()
|
|
runtime = CompanyRuntime(org_engine=StubOrgEngine(), communication=communication)
|
|
session = _make_worker_session()
|
|
runtime.member_sessions[session.member_session_id] = session
|
|
task = _make_worker_task(status=TaskStatus.DONE)
|
|
|
|
await runtime.complete_claim(
|
|
session,
|
|
task,
|
|
result=TaskResult(status=TaskStatus.DONE, content=""),
|
|
)
|
|
|
|
review_calls = [
|
|
call for call in communication.calls
|
|
if call["semantic_type"] == CommsSemanticType.APPROVAL_REQUEST
|
|
]
|
|
self.assertEqual(len(review_calls), 1)
|
|
self.assertTrue(review_calls[0]["reply_needed"])
|
|
# Body must still contain actionable context so the manager doesn't
|
|
# receive an empty ping.
|
|
self.assertTrue(review_calls[0]["body"])
|
|
self.assertIn("work-item-engineer", review_calls[0]["body"])
|
|
|
|
async def test_worker_awaiting_manager_review_releases_role_session(self) -> None:
|
|
communication = RecordingCommunication()
|
|
runtime = CompanyRuntime(org_engine=StubOrgEngine(), communication=communication)
|
|
session = _make_worker_session(status="running")
|
|
session.focused_work_item_id = "work-item-engineer"
|
|
session.current_task_id = "task-engineer"
|
|
runtime.member_sessions[session.member_session_id] = session
|
|
task = _make_worker_task(status=TaskStatus.AWAITING_MANAGER_REVIEW)
|
|
|
|
await runtime.complete_claim(
|
|
session,
|
|
task,
|
|
result=TaskResult(
|
|
status=TaskStatus.AWAITING_MANAGER_REVIEW,
|
|
content="Submitted for manager review.",
|
|
),
|
|
)
|
|
|
|
self.assertEqual(session.status, "idle")
|
|
self.assertEqual(session.resident_status, "idle")
|
|
self.assertEqual(session.focused_work_item_id, "")
|
|
self.assertEqual(session.current_task_id, "")
|
|
blocker_calls = [
|
|
call for call in communication.calls
|
|
if call["semantic_type"] == CommsSemanticType.BLOCKER
|
|
]
|
|
self.assertEqual(blocker_calls, [])
|
|
|
|
async def test_blocked_worker_does_not_request_review(self) -> None:
|
|
communication = RecordingCommunication()
|
|
runtime = CompanyRuntime(org_engine=StubOrgEngine(), communication=communication)
|
|
session = _make_worker_session()
|
|
runtime.member_sessions[session.member_session_id] = session
|
|
task = _make_worker_task(status=TaskStatus.AWAITING_PEER)
|
|
task.metadata["peer_wait"] = {"waiting_on_agents": ["reviewer"]}
|
|
|
|
await runtime.complete_claim(
|
|
session,
|
|
task,
|
|
result=TaskResult(status=TaskStatus.AWAITING_PEER, content="Waiting for reviewer."),
|
|
)
|
|
|
|
approval_calls = [
|
|
call for call in communication.calls
|
|
if call["semantic_type"] == CommsSemanticType.APPROVAL_REQUEST
|
|
]
|
|
self.assertEqual(approval_calls, [])
|
|
# A blocker notification is still sent so the manager sees the status.
|
|
blocker_calls = [
|
|
call for call in communication.calls
|
|
if call["semantic_type"] == CommsSemanticType.BLOCKER
|
|
]
|
|
self.assertTrue(blocker_calls)
|
|
|
|
async def test_worker_without_manager_does_not_request_review(self) -> None:
|
|
"""Top-level role (CEO) completing a root task has no manager to
|
|
notify; the code path must degrade gracefully."""
|
|
communication = RecordingCommunication()
|
|
runtime = CompanyRuntime(org_engine=StubOrgEngine(), communication=communication)
|
|
session = _make_worker_session(
|
|
role_id="ceo",
|
|
manager_role_id="",
|
|
manager_seat_id="",
|
|
)
|
|
session.manager_role_id = ""
|
|
runtime.member_sessions[session.member_session_id] = session
|
|
task = _make_worker_task(manager_role_id="", manager_seat_id="")
|
|
task.metadata["manager_role_id"] = ""
|
|
task.metadata["manager_seat_id"] = ""
|
|
|
|
await runtime.complete_claim(
|
|
session,
|
|
task,
|
|
result=TaskResult(status=TaskStatus.DONE, content="All done."),
|
|
)
|
|
|
|
self.assertEqual(communication.calls, [])
|
|
|
|
async def test_non_multi_team_org_sends_work_item_result_not_approval(self) -> None:
|
|
"""Work-item runtime company mode (not multi_team_org) should keep the
|
|
legacy WORK_ITEM_RESULT semantic unchanged."""
|
|
communication = RecordingCommunication()
|
|
runtime = CompanyRuntime(org_engine=StubOrgEngine(), communication=communication)
|
|
session = _make_worker_session()
|
|
# Remove the multi_team_org marker from session metadata.
|
|
session.metadata.pop("runtime_model", None)
|
|
runtime.member_sessions[session.member_session_id] = session
|
|
task = _make_worker_task()
|
|
task.metadata.pop("runtime_model", None)
|
|
|
|
await runtime.complete_claim(
|
|
session,
|
|
task,
|
|
result=TaskResult(status=TaskStatus.DONE, content="Work item output."),
|
|
)
|
|
|
|
work_item_calls = [
|
|
call for call in communication.calls
|
|
if call["semantic_type"] == CommsSemanticType.WORK_ITEM_RESULT
|
|
]
|
|
self.assertEqual(len(work_item_calls), 1)
|
|
self.assertFalse(work_item_calls[0]["reply_needed"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _resolve_current_turn_mode review_pending detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ReviewPendingTurnModeTests(unittest.TestCase):
|
|
def _runtime(self) -> CompanyRuntime:
|
|
return CompanyRuntime(org_engine=StubOrgEngine(), communication=RecordingCommunication())
|
|
|
|
def test_pending_in_review_children_yields_review_pending(self) -> None:
|
|
runtime = self._runtime()
|
|
manager = _make_manager_session(
|
|
direct_report_seat_ids=["seat::eng"],
|
|
direct_report_role_ids=["engineer"],
|
|
manager_board_summary={
|
|
"total_children": 2,
|
|
"upstream_summary": [
|
|
{
|
|
"work_item_id": "wi-1",
|
|
"phase": "awaiting_manager_review",
|
|
"kanban_column": "in_review",
|
|
"review_owner_role_id": "cto",
|
|
"review_owner_seat_id": "seat::cto",
|
|
"title": "Slice 1",
|
|
"role_id": "engineer",
|
|
},
|
|
{
|
|
"work_item_id": "wi-2",
|
|
"phase": "running",
|
|
"kanban_column": "in_progress",
|
|
"title": "Slice 2",
|
|
"role_id": "engineer",
|
|
},
|
|
],
|
|
},
|
|
)
|
|
runtime.member_sessions[manager.member_session_id] = manager
|
|
mode = runtime._resolve_current_turn_mode(manager)
|
|
self.assertEqual(mode, "review_pending")
|
|
|
|
def test_no_pending_reviews_falls_back_to_monitor_children(self) -> None:
|
|
runtime = self._runtime()
|
|
manager = _make_manager_session(
|
|
direct_report_seat_ids=["seat::eng"],
|
|
direct_report_role_ids=["engineer"],
|
|
manager_board_summary={
|
|
"total_children": 1,
|
|
"upstream_summary": [
|
|
{
|
|
"work_item_id": "wi-1",
|
|
"phase": "running",
|
|
"kanban_column": "in_progress",
|
|
"title": "Slice 1",
|
|
"role_id": "engineer",
|
|
},
|
|
],
|
|
},
|
|
)
|
|
runtime.member_sessions[manager.member_session_id] = manager
|
|
mode = runtime._resolve_current_turn_mode(manager)
|
|
self.assertEqual(mode, "monitor_children")
|
|
|
|
def test_already_approved_item_does_not_count_as_pending(self) -> None:
|
|
runtime = self._runtime()
|
|
manager = _make_manager_session(
|
|
direct_report_seat_ids=["seat::eng"],
|
|
manager_board_summary={
|
|
"total_children": 1,
|
|
"upstream_summary": [
|
|
{
|
|
"work_item_id": "wi-1",
|
|
"phase": "approved",
|
|
"kanban_column": "done",
|
|
"review_owner_role_id": "cto",
|
|
"title": "Slice 1",
|
|
"role_id": "engineer",
|
|
},
|
|
],
|
|
},
|
|
)
|
|
runtime.member_sessions[manager.member_session_id] = manager
|
|
mode = runtime._resolve_current_turn_mode(manager)
|
|
self.assertEqual(mode, "monitor_children")
|
|
|
|
def test_pending_review_owned_by_other_role_is_ignored(self) -> None:
|
|
"""A human-gate item whose reviewer is not this manager must not
|
|
flip this manager into review_pending mode."""
|
|
runtime = self._runtime()
|
|
manager = _make_manager_session(
|
|
direct_report_seat_ids=["seat::eng"],
|
|
manager_board_summary={
|
|
"total_children": 1,
|
|
"upstream_summary": [
|
|
{
|
|
"work_item_id": "wi-human",
|
|
"phase": "awaiting_human",
|
|
"kanban_column": "in_review",
|
|
"review_owner_role_id": "owner",
|
|
"review_owner_seat_id": "seat::owner",
|
|
"title": "Needs human sign-off",
|
|
"role_id": "engineer",
|
|
},
|
|
],
|
|
},
|
|
)
|
|
runtime.member_sessions[manager.member_session_id] = manager
|
|
mode = runtime._resolve_current_turn_mode(manager)
|
|
self.assertNotEqual(mode, "review_pending")
|
|
|
|
def test_review_pending_overrides_dispatch_required(self) -> None:
|
|
"""With no children yet a manager would see ``dispatch_required``.
|
|
If a pending review exists (e.g. from a prior batch), it takes
|
|
priority so the manager clears the queue before dispatching more."""
|
|
runtime = self._runtime()
|
|
manager = _make_manager_session(
|
|
direct_report_seat_ids=["seat::eng"],
|
|
manager_board_summary={
|
|
"total_children": 1,
|
|
"upstream_summary": [
|
|
{
|
|
"work_item_id": "wi-1",
|
|
"phase": "awaiting_manager_review",
|
|
"kanban_column": "in_review",
|
|
"title": "Slice 1",
|
|
"role_id": "engineer",
|
|
},
|
|
],
|
|
},
|
|
)
|
|
runtime.member_sessions[manager.member_session_id] = manager
|
|
mode = runtime._resolve_current_turn_mode(manager)
|
|
self.assertEqual(mode, "review_pending")
|
|
|
|
def test_attention_review_wrapper_stays_review_pending(self) -> None:
|
|
"""A wake-up Review Turn wrapper is not the dedicated child review card.
|
|
|
|
It must surface the pending-review queue, not run a target-less review
|
|
against the stale original session brief.
|
|
"""
|
|
runtime = self._runtime()
|
|
manager = _make_manager_session(
|
|
direct_report_seat_ids=["seat::eng"],
|
|
direct_report_role_ids=["engineer"],
|
|
manager_board_summary={
|
|
"total_children": 1,
|
|
"upstream_summary": [
|
|
{
|
|
"work_item_id": "wi-1",
|
|
"phase": "awaiting_manager_review",
|
|
"kanban_column": "in_review",
|
|
"title": "Coral implementation",
|
|
"role_id": "engineer",
|
|
},
|
|
],
|
|
},
|
|
)
|
|
task = Task(
|
|
id="attention-review",
|
|
title="Review Turn: cto",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"work_item_turn_type": "review",
|
|
"attention_work_item": True,
|
|
},
|
|
)
|
|
|
|
mode = runtime._resolve_current_turn_mode(manager, task)
|
|
|
|
self.assertEqual(mode, "review_pending")
|
|
|
|
def test_non_multi_team_org_returns_empty(self) -> None:
|
|
"""For legacy company mode (runtime_model != multi_team_org) the
|
|
runtime returns an empty string regardless of pending reviews."""
|
|
runtime = self._runtime()
|
|
manager = _make_manager_session(
|
|
direct_report_seat_ids=["seat::eng"],
|
|
manager_board_summary={
|
|
"total_children": 1,
|
|
"upstream_summary": [
|
|
{
|
|
"work_item_id": "wi-1",
|
|
"phase": "awaiting_manager_review",
|
|
"kanban_column": "in_review",
|
|
},
|
|
],
|
|
},
|
|
)
|
|
manager.metadata["runtime_model"] = "" # pretend not multi-team-org
|
|
runtime.member_sessions[manager.member_session_id] = manager
|
|
mode = runtime._resolve_current_turn_mode(manager)
|
|
self.assertEqual(mode, "")
|
|
|
|
def test_dedicated_review_work_item_forces_review_execute(self) -> None:
|
|
runtime = self._runtime()
|
|
manager = _make_manager_session(
|
|
direct_report_seat_ids=["seat::eng"],
|
|
direct_report_role_ids=["engineer"],
|
|
manager_board_summary={
|
|
"total_children": 1,
|
|
"upstream_summary": [
|
|
{
|
|
"work_item_id": "wi-1",
|
|
"phase": "awaiting_manager_review",
|
|
"kanban_column": "in_review",
|
|
"review_owner_role_id": "cto",
|
|
"review_owner_seat_id": "seat::cto",
|
|
},
|
|
],
|
|
},
|
|
)
|
|
runtime.member_sessions[manager.member_session_id] = manager
|
|
task = Task(
|
|
id="review-turn",
|
|
title="Review: wi-1",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"work_item_turn_type": "review",
|
|
"review_execution_work_item": True,
|
|
"review_target_work_item_id": "wi-1",
|
|
"delegation_seat_id": "seat::cto",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, "review::wi-1")
|
|
mode = runtime._resolve_current_turn_mode(manager, task)
|
|
self.assertEqual(mode, "review_execute")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Contract builder: review_pending prompt injection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ReviewPendingContractTests(unittest.TestCase):
|
|
def test_native_contract_keeps_native_tool_wording_by_default(self) -> None:
|
|
task = Task(
|
|
id="plan-turn",
|
|
title="Plan turn",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={"work_item_turn_type": "plan"},
|
|
)
|
|
|
|
contract = build_company_work_item_contract(task)
|
|
|
|
self.assertIn("agent_spawn(profile='explore')", contract)
|
|
|
|
def test_external_contract_uses_external_tool_wording_across_turns(self) -> None:
|
|
cases = [
|
|
Task(
|
|
id="intake-turn",
|
|
title="Intake turn",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={"work_item_turn_type": "intake"},
|
|
),
|
|
Task(
|
|
id="execute-turn",
|
|
title="Execute turn",
|
|
project_id="proj1",
|
|
assigned_to="engineer",
|
|
status=TaskStatus.PENDING,
|
|
metadata={"work_item_turn_type": "execute"},
|
|
),
|
|
Task(
|
|
id="review-turn",
|
|
title="Review turn",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={"work_item_turn_type": "review"},
|
|
),
|
|
Task(
|
|
id="dispatch-turn",
|
|
title="Dispatch turn",
|
|
project_id="proj1",
|
|
assigned_to="ceo",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"current_turn_mode": "dispatch_required",
|
|
"direct_report_seat_ids": ["seat::team::ceo::cto"],
|
|
},
|
|
),
|
|
Task(
|
|
id="report-turn",
|
|
title="Report turn",
|
|
project_id="proj1",
|
|
assigned_to="engineer",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"current_turn_mode": "report_required",
|
|
"report_execution_work_item": True,
|
|
},
|
|
),
|
|
]
|
|
|
|
for task in cases:
|
|
with self.subTest(task=task.id):
|
|
contract = build_external_company_work_item_contract(task)
|
|
self.assertNotIn("agent_spawn(profile='explore')", contract)
|
|
self.assertNotIn("file_read", contract)
|
|
self.assertNotIn("git_*", contract)
|
|
self.assertNotIn("bash", contract)
|
|
|
|
review_contract = build_company_work_item_contract(cases[2], audience="external")
|
|
self.assertIn("external agent", review_contract)
|
|
self.assertIn('"review_verdict":"approve"', review_contract)
|
|
self.assertIn('"review_verdict":"reject"', review_contract)
|
|
|
|
def test_contract_includes_review_requirement_block(self) -> None:
|
|
task = Task(
|
|
id="mgr-turn",
|
|
title="Manager turn",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"current_turn_mode": "review_pending",
|
|
"pending_review_items": [
|
|
{
|
|
"work_item_id": "wi-1",
|
|
"title": "Slice 1",
|
|
"role_id": "engineer",
|
|
"deliverable_summary": "Implemented rollback path.",
|
|
"review_state": "pending_manager",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
|
|
contract = build_company_work_item_contract(task)
|
|
self.assertIn("Review Requirement", contract)
|
|
self.assertIn("review_verdict", contract)
|
|
self.assertIn("wi-1", contract)
|
|
self.assertIn("engineer", contract)
|
|
self.assertIn("Implemented rollback path", contract)
|
|
self.assertIn("only provide analysis/planning/search notes", contract)
|
|
self.assertIn("Previous review findings are only leads", contract)
|
|
|
|
def test_attention_review_wrapper_uses_pending_queue_contract(self) -> None:
|
|
task = Task(
|
|
id="attention-review",
|
|
title="Review Turn: cto",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"current_turn_mode": "review_pending",
|
|
"work_item_turn_type": "review",
|
|
"attention_work_item": True,
|
|
"direct_report_seat_ids": ["seat::eng"],
|
|
"pending_review_items": [
|
|
{
|
|
"work_item_id": "wi-1",
|
|
"title": "Coral implementation",
|
|
"role_id": "engineer",
|
|
"deliverable_summary": "Implemented Coral Magnet Maze.",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
|
|
contract = build_company_work_item_contract(task)
|
|
|
|
self.assertIn("Review Requirement", contract)
|
|
self.assertIn("wi-1", contract)
|
|
self.assertIn("Implemented Coral Magnet Maze", contract)
|
|
self.assertNotIn("Kanban Review Turn", contract)
|
|
|
|
def test_contract_without_pending_items_omits_review_block(self) -> None:
|
|
task = Task(
|
|
id="mgr-turn",
|
|
title="Manager turn",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"current_turn_mode": "dispatch_required",
|
|
"direct_report_seat_ids": ["seat::eng"],
|
|
},
|
|
)
|
|
contract = build_company_work_item_contract(task)
|
|
self.assertNotIn("Review Requirement", contract)
|
|
|
|
def test_review_block_header_still_emitted_when_items_payload_missing(self) -> None:
|
|
"""Even if the runtime forgot to attach ``pending_review_items`` the
|
|
header block should still fire based on the turn_mode signal so the
|
|
manager sees a coherent instruction."""
|
|
task = Task(
|
|
id="mgr-turn",
|
|
title="Manager turn",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"current_turn_mode": "review_pending",
|
|
},
|
|
)
|
|
contract = build_company_work_item_contract(task)
|
|
self.assertIn("Review Requirement", contract)
|
|
self.assertIn("review_verdict", contract)
|
|
|
|
def test_dedicated_review_turn_renders_structured_review_evidence(self) -> None:
|
|
task = Task(
|
|
id="mgr-review-turn",
|
|
title="Review: Slice 1",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"work_item_turn_type": "review",
|
|
"current_turn_mode": "review_execute",
|
|
"review_execution_work_item": True,
|
|
"review_target_work_item_id": "wi-1",
|
|
"review_target_worker_role_id": "engineer",
|
|
"review_target_title": "Slice 1",
|
|
"review_target_description": "Implement the rollback path.",
|
|
"review_target_prompt_contract": make_prompt_contract(
|
|
task_brief=(
|
|
"Implement the rollback path. "
|
|
+ "This long target brief must remain visible in full. " * 30
|
|
),
|
|
deliverables=["Rollback implementation"],
|
|
acceptance_criteria=["Rollback test passes"],
|
|
non_overlap_guard="Do not modify unrelated auth flows.",
|
|
),
|
|
"review_completion_report": "Completed and validated rollback support.",
|
|
"review_evidence": {
|
|
"artifact_manifest": [
|
|
{"kind": "file", "label": "report", "value": "reports/review.md"}
|
|
],
|
|
"changed_areas": ["src/api.py", "tests/test_api.py"],
|
|
"verification_results": {
|
|
"status": {"label": "verified", "summary": "pytest passed"},
|
|
"checks": [
|
|
{"command": "pytest -q", "status": "pass", "summary": "42 passed"}
|
|
],
|
|
},
|
|
"key_commands": ["pytest -q"],
|
|
"output_paths": ["reports/review.md"],
|
|
"open_risks": ["Needs auth review"],
|
|
},
|
|
},
|
|
)
|
|
contract = build_company_work_item_contract(task)
|
|
self.assertIn("Kanban Review Turn", contract)
|
|
# The new prompt phrases this as "Treat the original brief as
|
|
# the contract." rather than the previous "original child brief
|
|
# as the contract." wording — both convey the same idea.
|
|
self.assertIn("original brief as the contract", contract)
|
|
self.assertIn("This long target brief must remain visible in full.", contract)
|
|
self.assertIn("Rollback implementation", contract)
|
|
self.assertIn("Rollback test passes", contract)
|
|
self.assertIn("Do not modify unrelated auth flows.", contract)
|
|
self.assertNotIn("brief=`", contract)
|
|
self.assertIn("Current workspace evidence is the truth", contract)
|
|
self.assertIn("Artifact Manifest", contract)
|
|
self.assertIn("Verification", contract)
|
|
self.assertIn("Output Paths", contract)
|
|
self.assertIn("pytest -q", contract)
|
|
self.assertIn("Needs auth review", contract)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Company comms reactivation must stay scoped to the relevant work item.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class WorkItemScopedCommsTests(unittest.TestCase):
|
|
def test_actionable_unread_scope_uses_target_work_item_not_role_inbox(self) -> None:
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
self.assertTrue(
|
|
CompanyWorkItemExecutor._unread_header_matches_scope(
|
|
{
|
|
"semantic_type": "approval_request",
|
|
"target_work_item_id": "wi-target",
|
|
"task_id": "legacy-task",
|
|
},
|
|
work_item_id="wi-target",
|
|
task_id="task-other",
|
|
require_work_item_scope=True,
|
|
)
|
|
)
|
|
self.assertFalse(
|
|
CompanyWorkItemExecutor._unread_header_matches_scope(
|
|
{
|
|
"semantic_type": "approval_request",
|
|
"target_work_item_id": "wi-other",
|
|
"task_id": "legacy-task",
|
|
},
|
|
work_item_id="wi-target",
|
|
task_id="task-other",
|
|
require_work_item_scope=True,
|
|
)
|
|
)
|
|
|
|
def test_parent_work_item_context_does_not_match_child_reactivation_scope(self) -> None:
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
self.assertFalse(
|
|
CompanyWorkItemExecutor._unread_header_matches_scope(
|
|
{
|
|
"semantic_type": "approval_request",
|
|
"parent_work_item_id": "wi-parent",
|
|
"target_work_item_id": "wi-child",
|
|
},
|
|
work_item_id="wi-parent",
|
|
task_id="task-parent",
|
|
require_work_item_scope=True,
|
|
)
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pending review context snapshot injection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class PendingReviewContextInjectionTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_prepare_task_copies_pending_review_items(self) -> None:
|
|
runtime = CompanyRuntime(org_engine=StubOrgEngine(), communication=RecordingCommunication())
|
|
manager = _make_manager_session(
|
|
direct_report_seat_ids=["seat::eng"],
|
|
manager_board_summary={
|
|
"total_children": 1,
|
|
"upstream_summary": [
|
|
{
|
|
"work_item_id": "wi-1",
|
|
"phase": "awaiting_manager_review",
|
|
"kanban_column": "in_review",
|
|
"review_owner_role_id": "cto",
|
|
"review_owner_seat_id": "seat::cto",
|
|
"title": "Slice 1",
|
|
"role_id": "engineer",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
# Emulate what _refresh_manager_board_state would populate.
|
|
pending_items = runtime._pending_reviews_from_board_summary(manager)
|
|
manager.metadata["pending_review_items"] = pending_items
|
|
runtime.member_sessions[manager.member_session_id] = manager
|
|
|
|
task = Task(
|
|
id="mgr-turn",
|
|
title="CTO turn",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"work_item_role_id": "cto",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, "parent-wi")
|
|
runtime.prepare_task_for_session(manager, task)
|
|
|
|
items = task.context_snapshot.get("pending_review_items")
|
|
self.assertIsNotNone(items)
|
|
self.assertEqual(items[0]["work_item_id"], "wi-1")
|
|
self.assertEqual(task.metadata["pending_review_items"][0]["work_item_id"], "wi-1")
|
|
|
|
async def test_prepare_task_clears_pending_review_items_when_empty(self) -> None:
|
|
runtime = CompanyRuntime(org_engine=StubOrgEngine(), communication=RecordingCommunication())
|
|
manager = _make_manager_session(
|
|
direct_report_seat_ids=["seat::eng"],
|
|
manager_board_summary={"total_children": 0, "upstream_summary": []},
|
|
)
|
|
runtime.member_sessions[manager.member_session_id] = manager
|
|
|
|
task = Task(
|
|
id="mgr-turn",
|
|
title="CTO turn",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"work_item_role_id": "cto",
|
|
"pending_review_items": [{"work_item_id": "stale"}],
|
|
},
|
|
context_snapshot={"pending_review_items": [{"work_item_id": "stale"}]},
|
|
)
|
|
set_linked_work_item_id(task, "parent-wi")
|
|
runtime.prepare_task_for_session(manager, task)
|
|
|
|
self.assertNotIn("pending_review_items", task.context_snapshot)
|
|
self.assertNotIn("pending_review_items", task.metadata)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# End-to-end: task transition → summarize_parent_status →
|
|
# turn_mode transition. Uses a real OPCStore so the canonicalisation,
|
|
# upstream_summary payload, and review_owner metadata round-trip work.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TaskTransitionReviewFallbackTests(unittest.IsolatedAsyncioTestCase):
|
|
async def asyncSetUp(self) -> None:
|
|
self._tmp = Path(tempfile.mkdtemp(prefix=f"review-flow-{uuid.uuid4().hex}-"))
|
|
self.store = OPCStore(self._tmp / "tasks.db")
|
|
await self.store.initialize()
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
await self.store.close()
|
|
shutil.rmtree(self._tmp, ignore_errors=True)
|
|
|
|
async def _seed_run_and_work_item(self) -> DelegationWorkItem:
|
|
from opc.core.models import Phase
|
|
|
|
item = DelegationWorkItem(
|
|
work_item_id="wi-engineer",
|
|
run_id="run-1",
|
|
cell_id="team::cto",
|
|
team_id="team::cto",
|
|
role_id="engineer",
|
|
seat_id="seat::eng",
|
|
parent_work_item_id="wi-cto",
|
|
title="Engineer deliverable",
|
|
summary="",
|
|
kind="execute",
|
|
projection_id="engineer__execute",
|
|
phase=Phase.RUNNING,
|
|
manager_role_id="cto",
|
|
manager_seat_id="seat::cto",
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"work_item_runtime": True,
|
|
"dependency_work_item_ids": [],
|
|
},
|
|
)
|
|
await self.store.save_delegation_work_item(item)
|
|
return item
|
|
|
|
async def test_done_task_transitions_work_item_to_in_review(self) -> None:
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
await self._seed_run_and_work_item()
|
|
|
|
executor = CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=AsyncMock(),
|
|
store=self.store,
|
|
)
|
|
|
|
task = Task(
|
|
id="engineer-task",
|
|
title="Engineer work item",
|
|
project_id="proj1",
|
|
assigned_to="engineer",
|
|
status=TaskStatus.DONE,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"delegation_run_id": "run-1",
|
|
"manager_role_id": "cto",
|
|
"manager_seat_id": "seat::cto",
|
|
"work_item_projection_id": "engineer__execute",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, "wi-engineer")
|
|
|
|
await _apply_task_transition(
|
|
executor,
|
|
task,
|
|
status=TaskStatus.DONE.value,
|
|
result=TaskResult(status=TaskStatus.DONE, content="All green."),
|
|
)
|
|
|
|
refreshed = await self.store.get_delegation_work_item("wi-engineer")
|
|
self.assertIsNotNone(refreshed)
|
|
self.assertEqual(refreshed.phase, Phase.AWAITING_MANAGER_REVIEW)
|
|
self.assertEqual(refreshed.metadata.get("review_owner_role_id"), "cto")
|
|
self.assertEqual(refreshed.metadata.get("review_owner_seat_id"), "seat::cto")
|
|
self.assertEqual(refreshed.metadata.get("completion_report"), "All green.")
|
|
|
|
def test_approve_with_missing_evidence_is_treated_as_review_blocker(self) -> None:
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
reason = CompanyWorkItemExecutor._review_approval_blocker_reason({
|
|
"structured_review_verdict": {"label": "approve", "summary": "Looks good."},
|
|
"review_completion_report": "No artifacts were provided, so there is no evidence to verify.",
|
|
"review_evidence": {
|
|
"artifact_manifest": [],
|
|
"output_paths": [],
|
|
"verification_results": {
|
|
"status": {"label": "missing", "summary": "No checks were run."},
|
|
"checks": [],
|
|
},
|
|
},
|
|
})
|
|
|
|
self.assertIn("verification evidence", reason)
|
|
|
|
def test_approve_with_generic_not_verified_evidence_is_not_a_hard_blocker(self) -> None:
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
reason = CompanyWorkItemExecutor._review_approval_blocker_reason({
|
|
"structured_review_verdict": {"label": "approve", "summary": "Reviewer verified the handoff."},
|
|
"review_completion_report": "Artifacts are present; external verification was not run.",
|
|
"review_evidence": {
|
|
"artifact_manifest": [{"path": "TEST_REPORT.md", "status": "provided"}],
|
|
"output_paths": ["TEST_REPORT.md"],
|
|
"verification_results": {
|
|
"status": {"label": "not_verified", "summary": "No automated verifier was available."},
|
|
"checks": [],
|
|
},
|
|
},
|
|
})
|
|
|
|
self.assertEqual(reason, "")
|
|
|
|
async def test_failed_task_can_close_in_progress_work_item_as_terminal_done(self) -> None:
|
|
"""Terminal failures should close a claimed work item without routing
|
|
through manager review."""
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
await self._seed_run_and_work_item()
|
|
|
|
executor = CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=AsyncMock(),
|
|
store=self.store,
|
|
)
|
|
|
|
task = Task(
|
|
id="engineer-task",
|
|
title="Engineer work item",
|
|
project_id="proj1",
|
|
assigned_to="engineer",
|
|
status=TaskStatus.FAILED,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"delegation_run_id": "run-1",
|
|
"manager_role_id": "cto",
|
|
"manager_seat_id": "seat::cto",
|
|
"work_item_projection_id": "engineer__execute",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, "wi-engineer")
|
|
|
|
await _apply_task_transition(
|
|
executor,
|
|
task,
|
|
status=TaskStatus.FAILED.value,
|
|
result=TaskResult(status=TaskStatus.FAILED, content="Dispatch guard rejected the turn."),
|
|
)
|
|
|
|
refreshed = await self.store.get_delegation_work_item("wi-engineer")
|
|
self.assertIsNotNone(refreshed)
|
|
self.assertEqual(refreshed.phase, Phase.FAILED)
|
|
|
|
async def test_done_transition_falls_back_to_work_item_manager_when_task_metadata_missing(self) -> None:
|
|
"""After work-item runtime rehydration, the task may have lost its
|
|
``manager_role_id``/``manager_seat_id``. The done transition must look
|
|
up the work item itself so review_owner is still populated."""
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
await self._seed_run_and_work_item()
|
|
|
|
executor = CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=AsyncMock(),
|
|
store=self.store,
|
|
)
|
|
|
|
task = Task(
|
|
id="engineer-task",
|
|
title="Engineer work item",
|
|
project_id="proj1",
|
|
assigned_to="engineer",
|
|
status=TaskStatus.DONE,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"delegation_run_id": "run-1",
|
|
# manager_role_id / manager_seat_id deliberately absent.
|
|
"work_item_projection_id": "engineer__execute",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, "wi-engineer")
|
|
|
|
await _apply_task_transition(
|
|
executor,
|
|
task,
|
|
status=TaskStatus.DONE.value,
|
|
result=TaskResult(status=TaskStatus.DONE, content="Output."),
|
|
)
|
|
|
|
refreshed = await self.store.get_delegation_work_item("wi-engineer")
|
|
self.assertEqual(refreshed.phase, Phase.AWAITING_MANAGER_REVIEW)
|
|
self.assertEqual(refreshed.metadata.get("review_owner_role_id"), "cto")
|
|
self.assertEqual(refreshed.metadata.get("review_owner_seat_id"), "seat::cto")
|
|
|
|
async def test_done_transition_does_not_regress_approved_work_item_back_to_review(self) -> None:
|
|
"""A repeated late DONE transition must preserve an already-approved phase.
|
|
|
|
Shared role sessions can emit a second completion after a review
|
|
verdict has already finalized the worker item. That follow-up must
|
|
not try to send APPROVED -> AWAITING_MANAGER_REVIEW, which violates
|
|
the phase state machine and crashes the claimed work item.
|
|
"""
|
|
from opc.core.models import Phase
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
await self._seed_run_and_work_item()
|
|
await self.store.update_delegation_work_item("wi-engineer", phase=Phase.APPROVED)
|
|
|
|
executor = CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=AsyncMock(),
|
|
store=self.store,
|
|
)
|
|
|
|
task = Task(
|
|
id="engineer-task",
|
|
title="Engineer work item",
|
|
project_id="proj1",
|
|
assigned_to="engineer",
|
|
status=TaskStatus.DONE,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"delegation_run_id": "run-1",
|
|
"manager_role_id": "cto",
|
|
"manager_seat_id": "seat::cto",
|
|
"work_item_projection_id": "engineer__execute",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, "wi-engineer")
|
|
|
|
await _apply_task_transition(
|
|
executor,
|
|
task,
|
|
status=TaskStatus.DONE.value,
|
|
result=TaskResult(status=TaskStatus.DONE, content="Late duplicate sync."),
|
|
)
|
|
|
|
refreshed = await self.store.get_delegation_work_item("wi-engineer")
|
|
self.assertIsNotNone(refreshed)
|
|
self.assertEqual(refreshed.phase, Phase.APPROVED)
|
|
|
|
async def test_summarize_parent_status_surfaces_review_owner_fields(self) -> None:
|
|
"""The per-session upstream summary must now include review_owner_*
|
|
fields so ``_pending_reviews_from_board_summary`` can filter on them."""
|
|
# Parent work item (manager's own).
|
|
parent = DelegationWorkItem(
|
|
work_item_id="wi-cto",
|
|
run_id="run-1",
|
|
cell_id="team::ceo",
|
|
team_id="team::ceo",
|
|
role_id="cto",
|
|
seat_id="seat::cto",
|
|
parent_work_item_id="",
|
|
title="CTO slice",
|
|
kind="plan",
|
|
projection_id="cto__plan",
|
|
phase=Phase.RUNNING,
|
|
manager_role_id="ceo",
|
|
manager_seat_id="seat::ceo",
|
|
metadata={"runtime_model": "multi_team_org"},
|
|
)
|
|
await self.store.save_delegation_work_item(parent)
|
|
# Child that the CTO needs to review.
|
|
child = DelegationWorkItem(
|
|
work_item_id="wi-engineer",
|
|
run_id="run-1",
|
|
cell_id="team::cto",
|
|
team_id="team::cto",
|
|
role_id="engineer",
|
|
seat_id="seat::eng",
|
|
parent_work_item_id="wi-cto",
|
|
title="Engineer slice",
|
|
kind="execute",
|
|
projection_id="engineer__execute",
|
|
phase=Phase.AWAITING_MANAGER_REVIEW,
|
|
manager_role_id="cto",
|
|
manager_seat_id="seat::cto",
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"review_state": "pending_manager",
|
|
"review_owner_role_id": "cto",
|
|
"review_owner_seat_id": "seat::cto",
|
|
"activation_state": "awaiting_review",
|
|
"completion_report": "Done.",
|
|
},
|
|
)
|
|
await self.store.save_delegation_work_item(child)
|
|
|
|
summary = await self.store.summarize_parent_status(
|
|
"run-1",
|
|
manager_seat_id="seat::cto",
|
|
parent_work_item_id="wi-cto",
|
|
)
|
|
rows = [row for row in summary["upstream_summary"] if row["work_item_id"] == "wi-engineer"]
|
|
self.assertEqual(len(rows), 1)
|
|
self.assertEqual(rows[0]["review_owner_role_id"], "cto")
|
|
self.assertEqual(rows[0]["review_owner_seat_id"], "seat::cto")
|
|
self.assertEqual(rows[0]["completion_report"], "Done.")
|
|
|
|
async def test_end_to_end_review_pending_turn_mode_flows_from_store(self) -> None:
|
|
"""Full round-trip: seed store → refresh board state → turn mode
|
|
resolves to review_pending because a pending review exists."""
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
parent = DelegationWorkItem(
|
|
work_item_id="wi-cto",
|
|
run_id="run-1",
|
|
cell_id="team::ceo",
|
|
team_id="team::ceo",
|
|
role_id="cto",
|
|
seat_id="seat::cto",
|
|
parent_work_item_id="",
|
|
title="CTO slice",
|
|
kind="plan",
|
|
projection_id="cto__plan",
|
|
phase=Phase.RUNNING,
|
|
manager_role_id="ceo",
|
|
manager_seat_id="seat::ceo",
|
|
metadata={"runtime_model": "multi_team_org"},
|
|
)
|
|
await self.store.save_delegation_work_item(parent)
|
|
await self._seed_run_and_work_item()
|
|
|
|
# Worker completes: transition their work item to in_review first.
|
|
executor = CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=AsyncMock(),
|
|
store=self.store,
|
|
)
|
|
worker_task = Task(
|
|
id="engineer-task",
|
|
title="Engineer work item",
|
|
project_id="proj1",
|
|
assigned_to="engineer",
|
|
status=TaskStatus.DONE,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"delegation_run_id": "run-1",
|
|
"manager_role_id": "cto",
|
|
"manager_seat_id": "seat::cto",
|
|
},
|
|
)
|
|
set_linked_work_item_id(worker_task, "wi-engineer")
|
|
await _apply_task_transition(
|
|
executor,
|
|
worker_task,
|
|
status=TaskStatus.DONE.value,
|
|
result=TaskResult(status=TaskStatus.DONE, content="Done."),
|
|
)
|
|
|
|
# Now simulate the CTO manager session refreshing its board state.
|
|
runtime = CompanyRuntime(
|
|
org_engine=StubOrgEngine(),
|
|
communication=RecordingCommunication(),
|
|
store=self.store,
|
|
)
|
|
manager = _make_manager_session(
|
|
role_id="cto",
|
|
seat_id="seat::cto",
|
|
direct_report_seat_ids=["seat::eng"],
|
|
direct_report_role_ids=["engineer"],
|
|
)
|
|
manager.focused_work_item_id = "wi-cto"
|
|
manager.metadata["delegation_run_id"] = "run-1"
|
|
runtime.member_sessions[manager.member_session_id] = manager
|
|
|
|
await runtime._refresh_manager_board_state(manager)
|
|
|
|
# Pending reviews discovered from store.
|
|
pending = list(manager.metadata.get("pending_review_items", []) or [])
|
|
self.assertEqual(len(pending), 1)
|
|
self.assertEqual(pending[0]["work_item_id"], "wi-engineer")
|
|
self.assertEqual(
|
|
runtime._resolve_current_turn_mode(manager),
|
|
"review_pending",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Regression: on_kanban_changed logger must surface the real exception.
|
|
#
|
|
# The original warning was a single line ("on_kanban_changed collab_sync
|
|
# broadcast failed") because loguru silently drops ``exc_info=True`` when the
|
|
# sink was not configured with ``backtrace=True`` (see ``opc_logger.py``).
|
|
# That made the real failure on the new09 run undiagnosable from the log
|
|
# file. We now use ``logger.opt(exception=True)`` so the full traceback is
|
|
# emitted regardless of sink configuration. Guarding against regression keeps
|
|
# the diagnostic channel open the next time the transient race fires.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class OnKanbanChangedLoggerTests(unittest.TestCase):
|
|
def test_warning_uses_opt_exception_for_real_traceback(self) -> None:
|
|
"""The on_kanban_changed handler must call logger.opt(exception=True)
|
|
so the stack trace is emitted even though our file sink lacks
|
|
``backtrace=True``. Using ``logger.warning(..., exc_info=True)`` alone
|
|
would be silently swallowed."""
|
|
source = Path("opc/plugins/office_ui/ws_handler.py").read_text()
|
|
# Locate the handler body.
|
|
handler_marker = "async def on_kanban_changed(self"
|
|
handler_idx = source.find(handler_marker)
|
|
self.assertGreaterEqual(handler_idx, 0, "on_kanban_changed handler not found")
|
|
# The handler is short; take the next ~40 lines.
|
|
body = source[handler_idx:handler_idx + 2000]
|
|
self.assertIn(
|
|
"logger.opt(exception=True)",
|
|
body,
|
|
"on_kanban_changed must log via logger.opt(exception=True) so the "
|
|
"traceback survives loguru sinks without backtrace=True",
|
|
)
|
|
# And the exception type / message should be inline so the first log
|
|
# line alone is already actionable.
|
|
self.assertIn("type(exc).__name__", body)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# After the kanban-write-tool removal refactor the contract only exposes
|
|
# `delegate_work` + `manager_board_read`; runtime owns all state transitions.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class MultiTeamOrgGuidelineDifferentiationTests(unittest.TestCase):
|
|
def _contract(self) -> str:
|
|
task = Task(
|
|
id="mgr-turn",
|
|
title="Manager turn",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"direct_report_seat_ids": ["seat::eng"],
|
|
},
|
|
)
|
|
return build_company_work_item_contract(task)
|
|
|
|
def test_only_intent_side_tools_are_described(self) -> None:
|
|
contract = self._contract()
|
|
self.assertIn("Manager Runtime Contract", contract)
|
|
self.assertIn("Use `delegate_work` only to CREATE child WorkItems", contract)
|
|
self.assertIn("Use `manager_board_read` only to READ child-board state", contract)
|
|
self.assertIn("direct reports", contract)
|
|
self.assertNotIn("Dispatch Planning Contract", contract)
|
|
self.assertNotIn("Leader Delegation Planning Overlay", contract)
|
|
# Write-side kanban tools were removed; runtime advances state.
|
|
self.assertNotIn("manager_board_update", contract)
|
|
self.assertNotIn("manager_board_release", contract)
|
|
self.assertNotIn("manager_board_rollup", contract)
|
|
self.assertNotIn("Team-management toolset", contract)
|
|
|
|
def test_leaf_worker_omits_leader_delegation_overlay(self) -> None:
|
|
task = Task(
|
|
id="worker-turn",
|
|
title="Worker turn",
|
|
project_id="proj1",
|
|
assigned_to="engineer",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"current_turn_mode": "worker_execute",
|
|
},
|
|
)
|
|
|
|
contract = build_company_work_item_contract(task)
|
|
|
|
self.assertIn("Organization Runtime Contract", contract)
|
|
self.assertIn("assigned WorkItem", contract)
|
|
self.assertNotIn("Manager Runtime Contract", contract)
|
|
self.assertNotIn("Leader Delegation Planning Overlay", contract)
|
|
self.assertNotIn("Dispatch Planning Contract", contract)
|
|
self.assertNotIn("`delegate_work`", contract)
|
|
self.assertNotIn("`manager_board_read`", contract)
|
|
|
|
def test_manager_capable_execute_turn_keeps_only_manager_contract(self) -> None:
|
|
task = Task(
|
|
id="manager-execute-turn",
|
|
title="Manager execute turn",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"current_turn_mode": "worker_execute",
|
|
"runtime_topology": {
|
|
"seats": [
|
|
{
|
|
"seat_id": "seat::team::ceo::cto",
|
|
"role_id": "cto",
|
|
"direct_report_seat_ids": ["seat::team::cto::senior_engineer"],
|
|
}
|
|
]
|
|
},
|
|
"delegation_seat_id": "seat::team::ceo::cto",
|
|
},
|
|
)
|
|
|
|
contract = build_company_work_item_contract(task)
|
|
|
|
self.assertIn("Organization Runtime Contract", contract)
|
|
self.assertIn("Manager Runtime Contract", contract)
|
|
self.assertNotIn("Leader Delegation Planning Overlay", contract)
|
|
self.assertNotIn("Dispatch Planning Contract", contract)
|
|
|
|
def test_dispatch_required_contract_requires_production_briefs(self) -> None:
|
|
task = Task(
|
|
id="mgr-turn",
|
|
title="Manager turn",
|
|
project_id="proj1",
|
|
assigned_to="cmo",
|
|
status=TaskStatus.PENDING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"current_turn_mode": "dispatch_required",
|
|
"direct_report_seat_ids": ["seat::marketing-specialist"],
|
|
"allowed_delegate_role_ids": ["marketing_specialist"],
|
|
},
|
|
)
|
|
|
|
contract = build_company_work_item_contract(task)
|
|
|
|
self.assertIn("Dispatch Planning Contract", contract)
|
|
self.assertIn("Manager Runtime Contract", contract)
|
|
self.assertNotIn("Leader Delegation Planning Overlay", contract)
|
|
self.assertIn("Scope first", contract)
|
|
self.assertIn("upstream goal", contract)
|
|
self.assertIn("requested deliverable form", contract)
|
|
self.assertIn("hard dependencies", contract)
|
|
self.assertIn("startable preparation", contract)
|
|
self.assertIn("outcome-based child WorkItems", contract)
|
|
self.assertIn("must not replace requested production work", contract)
|
|
self.assertIn("dispatch or escalate the blocker", contract)
|
|
self.assertNotIn("`task_brief`", contract)
|
|
self.assertNotIn("concrete output/handoff paths", contract)
|
|
self.assertNotIn("cmo-preproduction", contract)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Regression: blocked manager sessions must be unblocked when an attention
|
|
# work item (including a review turn) is created/revived for them.
|
|
#
|
|
# Root cause found while debugging the new09 real run:
|
|
# 1. Worker completes → APPROVAL_REQUEST message lands in manager inbox ✓
|
|
# 2. Worker's work_item transitions to ``in_review`` ✓
|
|
# 3. _queue_multi_team_response_tasks sees the session's protocol_backlog
|
|
# and calls _upsert_attention_work_item → review work item created ✓
|
|
# 4. BUT the manager session was parked ``blocked`` in complete_claim,
|
|
# and claim_runnable_tasks skips "blocked" sessions ✗
|
|
# 5. The review attention item is queued but NEVER claimed → manager
|
|
# never comes back → review queue stays full forever.
|
|
#
|
|
# Fix: _upsert_attention_work_item now calls _unblock_attention_session
|
|
# after creating/reviving an attention work item, flipping the session
|
|
# back to ``idle`` so the next claim_runnable_tasks pass picks it up.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class UnblockAttentionSessionTests(unittest.IsolatedAsyncioTestCase):
|
|
async def asyncSetUp(self) -> None:
|
|
self._tmp = Path(tempfile.mkdtemp(prefix=f"unblock-attn-{uuid.uuid4().hex}-"))
|
|
self.store = OPCStore(self._tmp / "tasks.db")
|
|
await self.store.initialize()
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
await self.store.close()
|
|
shutil.rmtree(self._tmp, ignore_errors=True)
|
|
|
|
def _make_blocked_manager(self) -> CompanyMemberSession:
|
|
# A manager session that delegated and parked. This is exactly the
|
|
# state complete_claim leaves it in after task.status = BLOCKED.
|
|
session = CompanyMemberSession(
|
|
member_session_id="member::cto",
|
|
role_id="cto",
|
|
employee_id="cto-emp",
|
|
seat_id="seat::team::cto::cto",
|
|
team_id="team::cto",
|
|
status="blocked",
|
|
resident_status="blocked",
|
|
current_task_id="cto-parent-task",
|
|
focused_work_item_id="wi-cto-parent",
|
|
manager_role_id="ceo",
|
|
manager_role_ids=["ceo"],
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"seat_id": "seat::team::cto::cto",
|
|
"team_id": "team::cto",
|
|
"session_scope_id": "scope-1",
|
|
"direct_report_seat_ids": ["seat::team::cto::senior_engineer"],
|
|
"direct_report_role_ids": ["senior_engineer"],
|
|
"managed_team_id": "team::cto",
|
|
"manager_role_id": "ceo",
|
|
"manager_seat_id": "seat::team::ceo::cto",
|
|
},
|
|
)
|
|
return session
|
|
|
|
async def test_upsert_attention_work_item_unblocks_the_session(self) -> None:
|
|
"""Primary regression test for the new09 review-flow stall."""
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
# Seed CTO's parent + a child in_review awaiting CTO's review.
|
|
parent = DelegationWorkItem(
|
|
work_item_id="wi-cto-parent",
|
|
run_id="run-1",
|
|
cell_id="team::cto",
|
|
team_id="team::cto",
|
|
role_id="cto",
|
|
seat_id="seat::team::cto::cto",
|
|
parent_work_item_id="",
|
|
title="CTO oversight",
|
|
kind="plan",
|
|
projection_id="cto__plan",
|
|
phase=Phase.WAITING_FOR_CHILDREN,
|
|
manager_role_id="ceo",
|
|
manager_seat_id="seat::team::ceo::cto",
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"activation_state": "waiting_for_children",
|
|
"dependency_work_item_ids": ["wi-senior-engineer"],
|
|
},
|
|
)
|
|
child = DelegationWorkItem(
|
|
work_item_id="wi-senior-engineer",
|
|
run_id="run-1",
|
|
cell_id="team::cto",
|
|
team_id="team::cto",
|
|
role_id="senior_engineer",
|
|
seat_id="seat::team::cto::senior_engineer",
|
|
parent_work_item_id="wi-cto-parent",
|
|
title="Senior engineer deliverable",
|
|
kind="execute",
|
|
projection_id="senior_engineer__execute",
|
|
phase=Phase.AWAITING_MANAGER_REVIEW, # child is awaiting CTO's approval
|
|
manager_role_id="cto",
|
|
manager_seat_id="seat::team::cto::cto",
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"review_state": "pending_manager",
|
|
"review_owner_role_id": "cto",
|
|
"review_owner_seat_id": "seat::team::cto::cto",
|
|
"activation_state": "awaiting_review",
|
|
"completion_report": "Built the app.",
|
|
},
|
|
)
|
|
await self.store.save_delegation_work_item(parent)
|
|
await self.store.save_delegation_work_item(child)
|
|
|
|
executor = CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=AsyncMock(),
|
|
store=self.store,
|
|
)
|
|
|
|
manager = self._make_blocked_manager()
|
|
executor.runtime.member_sessions[manager.member_session_id] = manager
|
|
assert manager.status == "blocked" # precondition
|
|
|
|
# The root task the loop picks for run_id resolution. Must carry
|
|
# delegation_run_id so _upsert_attention_work_item's guard passes.
|
|
root_task = Task(
|
|
id="root",
|
|
title="CEO Intake",
|
|
project_id="proj1",
|
|
assigned_to="ceo",
|
|
status=TaskStatus.BLOCKED,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"delegation_run_id": "run-1",
|
|
"work_item_runtime": True,
|
|
},
|
|
)
|
|
|
|
source_message = {
|
|
"msg_id": "msg-review-1",
|
|
"from_agent": "senior_engineer",
|
|
"subject": "Review needed: Built the app",
|
|
"body": "Please review.",
|
|
"semantic_type": "approval_request",
|
|
"message_class": "protocol",
|
|
"metadata": {"message_class": "protocol"},
|
|
}
|
|
|
|
work_items = [parent, child]
|
|
tasks = [root_task]
|
|
updated_tasks, updated_work_items = await executor._upsert_attention_work_item(
|
|
root_task=root_task,
|
|
tasks=tasks,
|
|
work_items=work_items,
|
|
session=manager,
|
|
source_message=source_message,
|
|
)
|
|
|
|
# Core assertion: the manager session is no longer blocked and will
|
|
# be picked up by claim_runnable_tasks on the next iteration.
|
|
self.assertEqual(manager.status, "idle")
|
|
self.assertEqual(manager.resident_status, "idle")
|
|
|
|
async def test_upsert_attention_syncs_blocked_current_task_without_phase_change(self) -> None:
|
|
"""Regression for blocked task + already-runnable work item.
|
|
|
|
The helper used to define ``target_phase`` only when it actively
|
|
changed the work-item phase. If the work item was already READY but
|
|
its projected task was still BLOCKED, syncing the task status crashed
|
|
with UnboundLocalError.
|
|
"""
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
parent = DelegationWorkItem(
|
|
work_item_id="wi-cto-parent",
|
|
run_id="run-1",
|
|
cell_id="team::cto",
|
|
team_id="team::cto",
|
|
role_id="cto",
|
|
seat_id="seat::team::cto::cto",
|
|
parent_work_item_id="",
|
|
title="CTO oversight",
|
|
kind="plan",
|
|
projection_id="cto__plan",
|
|
phase=Phase.READY,
|
|
manager_role_id="ceo",
|
|
manager_seat_id="seat::team::ceo::cto",
|
|
metadata={"runtime_model": "multi_team_org"},
|
|
)
|
|
await self.store.save_delegation_work_item(parent)
|
|
|
|
current_task = Task(
|
|
id="cto-parent-task",
|
|
title="CTO oversight",
|
|
project_id="proj1",
|
|
assigned_to="cto",
|
|
status=TaskStatus.BLOCKED,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"delegation_run_id": "run-1",
|
|
"work_item_runtime": True,
|
|
},
|
|
)
|
|
set_linked_work_item_id(current_task, "wi-cto-parent")
|
|
|
|
save_task = AsyncMock()
|
|
executor = CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=save_task,
|
|
store=self.store,
|
|
)
|
|
|
|
manager = self._make_blocked_manager()
|
|
manager.current_work_item = {"work_item_id": "wi-cto-parent"}
|
|
source_message = {
|
|
"msg_id": "msg-review-2",
|
|
"from_agent": "senior_engineer",
|
|
"subject": "Review needed",
|
|
"body": "Please review.",
|
|
"semantic_type": "approval_request",
|
|
"message_class": "protocol",
|
|
"metadata": {"message_class": "protocol"},
|
|
}
|
|
|
|
await executor._upsert_attention_work_item(
|
|
root_task=current_task,
|
|
tasks=[current_task],
|
|
work_items=[parent],
|
|
session=manager,
|
|
source_message=source_message,
|
|
)
|
|
|
|
save_task.assert_awaited_once()
|
|
saved_task = save_task.await_args.args[0]
|
|
self.assertEqual(saved_task.status, TaskStatus.PENDING)
|
|
self.assertEqual(manager.status, "idle")
|
|
self.assertEqual(manager.resident_status, "idle")
|
|
|
|
async def test_upsert_attention_does_not_touch_idle_session(self) -> None:
|
|
"""If a session is already idle/cold, we must not reset its other
|
|
fields by mistake — the helper is a targeted unblock, not a blanket
|
|
status reset."""
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
executor = CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=AsyncMock(),
|
|
store=self.store,
|
|
)
|
|
idle_session = self._make_blocked_manager()
|
|
idle_session.status = "idle"
|
|
idle_session.resident_status = "idle"
|
|
idle_session.current_task_id = "some-task" # must not be cleared
|
|
prior_current_task_id = idle_session.current_task_id
|
|
|
|
executor._unblock_attention_session(idle_session)
|
|
|
|
# status stays idle, current_task_id untouched.
|
|
self.assertEqual(idle_session.status, "idle")
|
|
self.assertEqual(idle_session.current_task_id, prior_current_task_id)
|
|
|
|
async def test_unblock_attention_session_handles_draining_status(self) -> None:
|
|
"""A draining manager session should also be rescued — otherwise
|
|
the team can stall during graceful shutdown / replan windows."""
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
executor = CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=AsyncMock(),
|
|
store=self.store,
|
|
)
|
|
session = self._make_blocked_manager()
|
|
session.status = "draining"
|
|
session.resident_status = "draining"
|
|
|
|
executor._unblock_attention_session(session)
|
|
|
|
self.assertEqual(session.status, "idle")
|
|
self.assertEqual(session.resident_status, "idle")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Regression: kanban UI must refresh on every work-item transition, not
|
|
# only at the gather boundary.
|
|
#
|
|
# Symptom from a real run: "when already in_progress the kanban still
|
|
# shows Todo; when already ready for in_review it still shows In Progress."
|
|
#
|
|
# Root cause: _execute_multi_team_org only called on_kanban_changed AFTER
|
|
# asyncio.gather of all _run_claimed_work_item coroutines completed. With 7
|
|
# parallel codex workers that took minutes, the UI was stuck on the
|
|
# pre-claim snapshot until the SLOWEST worker finished and then jumped
|
|
# directly from "Todo" to "In Review", silently eating the "In Progress"
|
|
# state. Managers reviewing later saw the same lag on in_review → done.
|
|
#
|
|
# Fix: worker-start and worker-end transitions now fire
|
|
# on_kanban_changed so every transition hits the UI immediately. The
|
|
# _upsert_attention_work_item path gets the same treatment so fresh
|
|
# Review Turn / Dispatch Turn cards surface without delay.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class KanbanPerTransitionPushTests(unittest.IsolatedAsyncioTestCase):
|
|
async def asyncSetUp(self) -> None:
|
|
self._tmp = Path(tempfile.mkdtemp(prefix=f"kanban-push-{uuid.uuid4().hex}-"))
|
|
self.store = OPCStore(self._tmp / "tasks.db")
|
|
await self.store.initialize()
|
|
self.refresh_count = 0
|
|
|
|
async def on_changed() -> None:
|
|
self.refresh_count += 1
|
|
|
|
self.on_changed = on_changed
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
await self.store.close()
|
|
shutil.rmtree(self._tmp, ignore_errors=True)
|
|
|
|
def _executor(self):
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
return CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=AsyncMock(),
|
|
store=self.store,
|
|
on_kanban_changed=self.on_changed,
|
|
)
|
|
|
|
async def _seed_work_item(self, status: str) -> DelegationWorkItem:
|
|
phase_by_status = {
|
|
"ready": Phase.READY,
|
|
"in_progress": Phase.RUNNING,
|
|
"in_review": Phase.AWAITING_MANAGER_REVIEW,
|
|
"done": Phase.APPROVED,
|
|
}
|
|
item = DelegationWorkItem(
|
|
work_item_id="wi-1",
|
|
run_id="run-1",
|
|
cell_id="team::cto",
|
|
team_id="team::cto",
|
|
role_id="senior_engineer",
|
|
seat_id="seat::eng",
|
|
parent_work_item_id="wi-parent",
|
|
title="Slice",
|
|
kind="execute",
|
|
projection_id="senior_engineer__execute",
|
|
phase=phase_by_status[status],
|
|
manager_role_id="cto",
|
|
manager_seat_id="seat::cto",
|
|
metadata={"runtime_model": "multi_team_org"},
|
|
)
|
|
await self.store.save_delegation_work_item(item)
|
|
return item
|
|
|
|
async def test_running_transition_pushes_to_ui(self) -> None:
|
|
"""Worker start (status=RUNNING) must fire on_kanban_changed so the
|
|
card moves out of Todo immediately.
|
|
|
|
The push is now debounced (see ``_schedule_kanban_notification``),
|
|
so we wait out one debounce window before asserting. A single
|
|
transition still produces exactly one broadcast — the debounce
|
|
only collapses multiple rapid-fire pushes.
|
|
"""
|
|
executor = self._executor()
|
|
await self._seed_work_item("ready")
|
|
|
|
task = Task(
|
|
id="engineer-task",
|
|
title="Engineer work item",
|
|
project_id="proj1",
|
|
assigned_to="senior_engineer",
|
|
status=TaskStatus.RUNNING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"delegation_run_id": "run-1",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, "wi-1")
|
|
before = self.refresh_count
|
|
await _apply_task_transition(
|
|
executor,
|
|
task,
|
|
status=TaskStatus.RUNNING.value,
|
|
)
|
|
await asyncio.sleep(executor._kanban_debounce_sec + 0.1)
|
|
self.assertEqual(self.refresh_count, before + 1)
|
|
|
|
async def test_done_transition_pushes_to_ui(self) -> None:
|
|
"""Worker finish (status=DONE) must fire on_kanban_changed so the
|
|
card moves to In Review immediately, not after the whole parallel
|
|
batch clears. Debounced — see ``test_running_transition_pushes_to_ui``.
|
|
"""
|
|
executor = self._executor()
|
|
await self._seed_work_item("in_progress")
|
|
|
|
task = Task(
|
|
id="engineer-task",
|
|
title="Engineer work item",
|
|
project_id="proj1",
|
|
assigned_to="senior_engineer",
|
|
status=TaskStatus.DONE,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"delegation_run_id": "run-1",
|
|
"manager_role_id": "cto",
|
|
"manager_seat_id": "seat::cto",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, "wi-1")
|
|
before = self.refresh_count
|
|
await _apply_task_transition(
|
|
executor,
|
|
task,
|
|
status=TaskStatus.DONE.value,
|
|
result=TaskResult(status=TaskStatus.DONE, content="done"),
|
|
)
|
|
await asyncio.sleep(executor._kanban_debounce_sec + 0.1)
|
|
self.assertEqual(self.refresh_count, before + 1)
|
|
|
|
async def test_transition_without_callback_is_safe(self) -> None:
|
|
"""Executors constructed without on_kanban_changed must not crash
|
|
when the per-transition hook fires."""
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
executor = CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=AsyncMock(),
|
|
store=self.store,
|
|
# on_kanban_changed deliberately omitted.
|
|
)
|
|
await self._seed_work_item("ready")
|
|
task = Task(
|
|
id="engineer-task",
|
|
title="Engineer work item",
|
|
project_id="proj1",
|
|
assigned_to="senior_engineer",
|
|
status=TaskStatus.RUNNING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"delegation_run_id": "run-1",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, "wi-1")
|
|
# Should not raise.
|
|
await _apply_task_transition(
|
|
executor,
|
|
task,
|
|
status=TaskStatus.RUNNING.value,
|
|
)
|
|
|
|
async def test_callback_failure_does_not_break_transition(self) -> None:
|
|
"""If the UI push raises (e.g. transient WS race), the work-item
|
|
state machine must continue unaffected."""
|
|
from opc.layer2_organization.company_mode import CompanyWorkItemExecutor
|
|
|
|
async def bad_callback() -> None:
|
|
raise RuntimeError("UI gone")
|
|
|
|
executor = CompanyWorkItemExecutor(
|
|
org_engine=StubOrgEngine(),
|
|
communication=SimpleNamespace(),
|
|
approval_engine=SimpleNamespace(),
|
|
memory=None,
|
|
execute_task=AsyncMock(),
|
|
save_task=AsyncMock(),
|
|
store=self.store,
|
|
on_kanban_changed=bad_callback,
|
|
)
|
|
await self._seed_work_item("ready")
|
|
task = Task(
|
|
id="engineer-task",
|
|
title="Engineer work item",
|
|
project_id="proj1",
|
|
assigned_to="senior_engineer",
|
|
status=TaskStatus.RUNNING,
|
|
metadata={
|
|
"runtime_model": "multi_team_org",
|
|
"delegation_run_id": "run-1",
|
|
},
|
|
)
|
|
set_linked_work_item_id(task, "wi-1")
|
|
# Must not propagate.
|
|
await _apply_task_transition(
|
|
executor,
|
|
task,
|
|
status=TaskStatus.RUNNING.value,
|
|
)
|
|
# And the DB write DID happen despite the UI error.
|
|
refreshed = await self.store.get_delegation_work_item("wi-1")
|
|
self.assertEqual(refreshed.phase, Phase.RUNNING)
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
unittest.main()
|