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:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user