Files
LZH-YS1998 76c530a9e5 test: repair full suite — hang fix, stale-test updates, patch hygiene, timeout backstop
Suite went from 27 failures plus one permanent hang (never finished) to
1846 passed / 0 failed in ~85s, including under FORCE_COLOR.

- office_shutdown_lifecycle: construct WSHandler via the real __init__
  (helper) instead of hand-copied __new__ stubs that drift from the
  constructor (#11 added _runtime_status_sync_task and the stubs hung);
  the formerly-hanging wait now has a 5s wait_for.
- import-time patch hygiene: company_recruiter / company_reorg /
  engine_session_defaults replaced module-level permanent
  tempfile.TemporaryDirectory monkeypatching with paired
  setUpModule/tearDownModule, fixing order-dependent sqlite failures in
  transcript_pagination during full runs.
- stale tests updated to current product semantics: resume stubs use
  status="done" (failed is deliberately non-resumable), fix4 asserts the
  native review contract through build_company_work_item_contract,
  delivery fixture carries user_visible/feedback_scope=final, ownership
  doc names progress_log, session compression calls
  maybe_compact_session(force=True) explicitly, hard delete removes the
  work item row, parallel-isolation asserts delegate rebind and stubs
  _get_project_delegate, role update goes through OrgService on an
  editable custom org (plus read-only rejection case), collab_rpc patches
  the single os.name decision point instead of poisoning pathlib, codex
  no-pty builds inside the patch, identity-guard false positives reworded.
- cli_board actions rewritten against the real OfficeServiceFactory seam
  with a tempdir OPC_HOME (old direct-engine stubs were never consulted
  and the tests wrote into the real OPC home).
- cli_app assertions strip ANSI via _plain_output so a color-forcing
  shell (FORCE_COLOR) cannot break plain-text expectations.
- deleted never-runnable test_org_concurrency (pytest.mark.asyncio
  without the plugin, stdlib-only assertions) and three dead skipped
  filesystem-handoff tests.
- pyproject: dev extra (pytest, pytest-timeout) and a 300s per-test
  timeout backstop so a wedged test fails instead of stalling the suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 22:14:47 +08:00

289 lines
11 KiB
Python

from __future__ import annotations
import asyncio
import os
import tempfile
import unittest
import uuid
from unittest import mock
from unittest.mock import AsyncMock
from opc.core.models import ExecutionCheckpoint, Task, TaskStatus
from opc.plugins.cli_board.services.actions import BoardActions
class _StubStore:
def __init__(self) -> None:
self.tasks: dict[str, Task] = {}
self.checkpoints: list[ExecutionCheckpoint] = []
self.resolved: list[tuple[str, str]] = []
async def save_task(self, task: Task) -> None:
self.tasks[task.id] = task
async def get_task(self, task_id: str) -> Task | None:
return self.tasks.get(task_id)
async def get_tasks(self, **_kw):
return list(self.tasks.values())
async def get_pending_checkpoints(self, **_kw):
return [checkpoint for checkpoint in self.checkpoints if checkpoint.status == "pending"]
async def resolve_execution_checkpoint(self, checkpoint_id: str, status: str = "resolved") -> None:
self.resolved.append((checkpoint_id, status))
for checkpoint in self.checkpoints:
if checkpoint.checkpoint_id == checkpoint_id:
checkpoint.status = status
class _StubMemory:
def __init__(self) -> None:
self.ensure_session = AsyncMock()
class _StubEngine:
def __init__(self) -> None:
self.store = _StubStore()
self.memory = _StubMemory()
self.process_message = AsyncMock(return_value="ok")
class _StubFacade:
def __init__(self, engine: _StubEngine) -> None:
self.project_id = "demo"
self._engine = engine
async def ensure_ready(self):
return self._engine
class _StubServiceResult:
def __init__(self, payload: dict) -> None:
self.payload = payload
self.events: list = []
class _StubSessionService:
"""In-memory stand-in for the office SessionService seam.
Mirrors the slice of the production contract that ``BoardActions``
relies on: ``create`` persists a session-backed placeholder Task and
ensures its memory session, ``send`` routes execution to
``engine.process_message`` with the origin task's session, and ``stop``
cancels the target task row. Every call is recorded for assertions.
"""
def __init__(self, engine: _StubEngine, project_id: str) -> None:
self._engine = engine
self._project_id = project_id
self.create_calls: list[dict] = []
self.send_calls: list[dict] = []
self.stop_calls: list[dict] = []
async def create(self, **kwargs) -> _StubServiceResult:
self.create_calls.append(dict(kwargs))
project_id = kwargs.get("project_id", self._project_id)
title = str(kwargs.get("title", "") or "New Chat")
task_id = str(uuid.uuid4())
session_id = str(uuid.uuid4())
await self._engine.memory.ensure_session(
session_id=session_id,
project_id=project_id,
title=title,
mode="primary",
metadata={"interface": kwargs.get("interface", "office_ui")},
)
await self._engine.store.save_task(
Task(
id=task_id,
title=title,
description=str(kwargs.get("description", "") or ""),
project_id=project_id,
session_id=session_id,
metadata={"exec_mode": kwargs.get("exec_mode") or "task"},
)
)
return _StubServiceResult({"project_id": project_id, "task_id": task_id, "session_id": session_id})
async def send(self, **kwargs) -> _StubServiceResult:
self.send_calls.append(dict(kwargs))
task = await self._engine.store.get_task(str(kwargs.get("task_id", "") or ""))
if task is None:
raise ValueError(f"task_not_found: {kwargs.get('task_id')}")
response = await self._engine.process_message(
str(kwargs.get("content", "") or ""),
project_id=kwargs.get("project_id", self._project_id),
session_id=str(task.session_id or ""),
mode=kwargs.get("mode", "task"),
origin_task_id=task.id,
)
return _StubServiceResult(
{
"project_id": kwargs.get("project_id", self._project_id),
"task_id": task.id,
"session_id": str(task.session_id or ""),
"response": response,
}
)
async def stop(self, **kwargs) -> _StubServiceResult:
self.stop_calls.append(dict(kwargs))
task = await self._engine.store.get_task(str(kwargs.get("task_id", "") or ""))
if task is None:
raise ValueError(f"target_not_found: {kwargs.get('task_id')}")
task.status = TaskStatus.CANCELLED
await self._engine.store.save_task(task)
return _StubServiceResult(
{
"project_id": kwargs.get("project_id", self._project_id),
"task_id": task.id,
"status": "cancelled",
}
)
class _StubOfficeServices:
def __init__(self, engine: _StubEngine, project_id: str = "demo") -> None:
self.session = _StubSessionService(engine, project_id)
class BoardActionsTests(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
super().setUp()
# Safety net: even if a code path slipped past the factory patch it
# must never touch the real OPC home.
tmp = tempfile.TemporaryDirectory(prefix="opc-cli-board-actions-test-")
self.addCleanup(tmp.cleanup)
env_patcher = mock.patch.dict(os.environ, {"OPC_HOME": tmp.name})
env_patcher.start()
self.addCleanup(env_patcher.stop)
def _make_actions(self, engine: _StubEngine) -> tuple[BoardActions, _StubOfficeServices]:
"""Patch the OfficeServiceFactory seam so BoardActions uses our stubs.
``BoardActions._run_office_service`` builds a real
``OfficeServiceFactory`` (and with it a real engine + ui_state.db) per
operation; the tests replace that seam with an async context manager
yielding stub services bound to the stub engine.
"""
services = _StubOfficeServices(engine)
class _StubFactory:
def __init__(self, **_kwargs) -> None:
pass
async def __aenter__(self) -> _StubOfficeServices:
return services
async def __aexit__(self, exc_type, exc, tb) -> None:
return None
patcher = mock.patch(
"opc.plugins.cli_board.services.actions.OfficeServiceFactory",
_StubFactory,
)
patcher.start()
self.addCleanup(patcher.stop)
return BoardActions(_StubFacade(engine), project_id="demo"), services
async def test_create_task_creates_session_backed_placeholder(self) -> None:
engine = _StubEngine()
actions, services = self._make_actions(engine)
task = await actions.create_task(title="Draft feature", description="Initial plan")
self.assertIn(task.id, engine.store.tasks)
stored = engine.store.tasks[task.id]
self.assertEqual(stored.title, "Draft feature")
self.assertEqual(stored.description, "Initial plan")
self.assertTrue(stored.session_id)
engine.memory.ensure_session.assert_awaited()
self.assertEqual(len(services.session.create_calls), 1)
call = services.session.create_calls[0]
self.assertEqual(call["project_id"], "demo")
self.assertEqual(call["title"], "Draft feature")
self.assertEqual(call["description"], "Initial plan")
self.assertEqual(call["exec_mode"], "task")
self.assertEqual(call["interface"], "cli_board")
async def test_send_session_message_routes_through_origin_task(self) -> None:
engine = _StubEngine()
task = Task(
id="task-1",
title="Feature task",
description="Implement the feature",
status=TaskStatus.PENDING,
session_id="session-1",
project_id="demo",
)
await engine.store.save_task(task)
actions, services = self._make_actions(engine)
response = await actions.send_session_message("task-1", "please continue")
self.assertEqual(response, "ok")
self.assertEqual(len(services.session.send_calls), 1)
call = services.session.send_calls[0]
self.assertEqual(call["project_id"], "demo")
self.assertEqual(call["task_id"], "task-1")
self.assertEqual(call["content"], "please continue")
self.assertEqual(call["mode"], "task")
engine.process_message.assert_awaited_once()
kwargs = engine.process_message.await_args.kwargs
self.assertEqual(kwargs["session_id"], "session-1")
self.assertEqual(kwargs["origin_task_id"], "task-1")
async def test_cancel_task_marks_related_tasks_and_checkpoints_cancelled(self) -> None:
engine = _StubEngine()
started = asyncio.Event()
release = asyncio.Event() # never set; the run only ends via cancellation
async def _blocked_process_message(*_args, **_kwargs):
started.set()
await release.wait()
return "unreachable"
engine.process_message = AsyncMock(side_effect=_blocked_process_message)
root = Task(
id="root",
title="Root task",
description="Run runtime",
status=TaskStatus.RUNNING,
session_id="session-root",
project_id="demo",
)
linked = Task(
id="child",
title="Child task",
description="Background child",
status=TaskStatus.RUNNING,
session_id="session-child",
project_id="demo",
metadata={"origin_task_id": "root"},
)
await engine.store.save_task(root)
await engine.store.save_task(linked)
engine.store.checkpoints.append(
ExecutionCheckpoint(
checkpoint_id="cp-root",
project_id="demo",
session_id="session-root",
task_id="root",
)
)
actions, services = self._make_actions(engine)
background = asyncio.create_task(actions.send_session_message("root", "keep going"))
await asyncio.wait_for(started.wait(), timeout=5)
await actions.cancel_task("root")
with self.assertRaises(asyncio.CancelledError):
await background
self.assertEqual(engine.store.tasks["root"].status, TaskStatus.CANCELLED)
self.assertEqual(engine.store.tasks["child"].status, TaskStatus.CANCELLED)
self.assertEqual(engine.store.resolved, [("cp-root", "cancelled")])
self.assertEqual(len(services.session.stop_calls), 1)
self.assertEqual(services.session.stop_calls[0]["task_id"], "root")