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>
This commit is contained in:
LZH-YS1998
2026-07-26 22:14:47 +08:00
parent 9977c3be57
commit 76c530a9e5
20 changed files with 424 additions and 447 deletions
+169 -7
View File
@@ -1,6 +1,11 @@
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
@@ -53,17 +58,154 @@ class _StubFacade:
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 = BoardActions(_StubFacade(engine), project_id="demo")
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(engine.store.tasks[task.id].title, "Draft feature")
self.assertEqual(engine.store.tasks[task.id].metadata["source"], "cli_board")
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()
@@ -76,19 +218,33 @@ class BoardActionsTests(unittest.IsolatedAsyncioTestCase):
project_id="demo",
)
await engine.store.save_task(task)
actions = BoardActions(_StubFacade(engine), project_id="demo")
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")
self.assertEqual(engine.store.tasks["task-1"].status, TaskStatus.IDLE)
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",
@@ -116,11 +272,17 @@ class BoardActionsTests(unittest.IsolatedAsyncioTestCase):
task_id="root",
)
)
actions = BoardActions(_StubFacade(engine), project_id="demo")
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")
+5 -1
View File
@@ -87,8 +87,12 @@ class CliBoardAppPilotTests(unittest.IsolatedAsyncioTestCase):
await pilot.pause()
self.assertEqual(app.state.pane_focus, "context")
await pilot.press("right")
# Selecting the running task earlier auto-focused the Session
# context tab (CliBoardApp._load_selected_detail), so cycling
# right moves session -> activity.
self.assertEqual(app.state.context_tab, "session")
await pilot.press("right")
self.assertEqual(app.state.context_tab, "activity")
await pilot.press("f")
self.assertFalse(app.state.show_done)