76c530a9e5
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>
138 lines
5.3 KiB
Python
138 lines
5.3 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from opc.core.config import OPCConfig
|
|
from opc.core.events import EventBus
|
|
from opc.core.models import (
|
|
ReorgChangeSet,
|
|
ReorgProposalStatus,
|
|
ReorgRoleChange,
|
|
ReorgTaskAdjustment,
|
|
Task,
|
|
TaskStatus,
|
|
)
|
|
from opc.database.store import OPCStore
|
|
from opc.layer2_organization.communication import CommunicationManager
|
|
from opc.layer2_organization.org_engine import OrgEngine
|
|
from opc.layer2_organization.reorg_manager import ReorgManager
|
|
from tests._temp_paths import WorkspaceTemporaryDirectory
|
|
|
|
_REAL_TEMPORARY_DIRECTORY = tempfile.TemporaryDirectory
|
|
|
|
|
|
def setUpModule() -> None:
|
|
tempfile.TemporaryDirectory = WorkspaceTemporaryDirectory # type: ignore[assignment]
|
|
|
|
|
|
def tearDownModule() -> None:
|
|
tempfile.TemporaryDirectory = _REAL_TEMPORARY_DIRECTORY # type: ignore[assignment]
|
|
|
|
|
|
class CompanyReorgTests(unittest.IsolatedAsyncioTestCase):
|
|
async def asyncSetUp(self) -> None:
|
|
self.tmpdir = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.tmpdir.cleanup)
|
|
self.root = Path(self.tmpdir.name)
|
|
self.store = OPCStore(self.root / "tasks.db")
|
|
await self.store.initialize()
|
|
self.config = OPCConfig()
|
|
self.org_engine = OrgEngine(self.config, self.root)
|
|
self.communication = CommunicationManager(self.store, EventBus(), org_engine=self.org_engine)
|
|
self.manager = ReorgManager(
|
|
store=self.store,
|
|
org_engine=self.org_engine,
|
|
approval_engine=None,
|
|
communication=self.communication,
|
|
)
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
await self.store.close()
|
|
|
|
async def test_org_level_reorg_requires_user_confirmation_before_apply(self) -> None:
|
|
proposal = await self.manager.propose_reorg(
|
|
project_id="proj1",
|
|
summary="Replace senior_engineer with implementer.",
|
|
changeset=ReorgChangeSet(
|
|
role_changes=[
|
|
ReorgRoleChange(
|
|
action="replace",
|
|
role_id="senior_engineer",
|
|
replacement_role_id="implementer",
|
|
role={
|
|
"id": "implementer",
|
|
"name": "Implementer",
|
|
"responsibility": "Concrete implementation and delivery.",
|
|
},
|
|
)
|
|
]
|
|
),
|
|
source_role_id="coordinator",
|
|
)
|
|
self.assertTrue(proposal.user_confirmation_required)
|
|
self.assertEqual(proposal.status, ReorgProposalStatus.PROPOSED)
|
|
|
|
with self.assertRaises(ValueError):
|
|
await self.manager.apply_reorg(proposal.proposal_id)
|
|
|
|
await self.manager.set_reorg_approval(proposal.proposal_id, approved=True, notes="Looks good.")
|
|
result = await self.manager.apply_reorg(proposal.proposal_id)
|
|
self.assertEqual(result["status"], ReorgProposalStatus.APPLIED.value)
|
|
self.assertIsNotNone(self.org_engine.get_agent("implementer"))
|
|
|
|
async def test_low_risk_task_adjustment_can_auto_apply_for_top_level_role(self) -> None:
|
|
task = Task(
|
|
title="Engineering Execution",
|
|
project_id="proj1",
|
|
assigned_to="senior_engineer",
|
|
status=TaskStatus.PENDING,
|
|
metadata={"work_item_role_id": "senior_engineer", "work_item_projection_id": "engineering_execution"},
|
|
)
|
|
await self.store.save_task(task)
|
|
|
|
result = await self.manager.suggest_task_adjustment(
|
|
project_id="proj1",
|
|
source_role_id="coordinator",
|
|
summary="Reassign engineering execution to qa_analyst for a quick validation pass.",
|
|
changeset=ReorgChangeSet(
|
|
task_adjustments=[
|
|
ReorgTaskAdjustment(
|
|
task_id=task.id,
|
|
action="reassign",
|
|
new_role_id="qa_analyst",
|
|
)
|
|
]
|
|
),
|
|
)
|
|
self.assertTrue(result["auto_applied"])
|
|
updated = await self.store.get_task(task.id)
|
|
assert updated is not None
|
|
self.assertEqual(updated.assigned_to, "qa_analyst")
|
|
self.assertEqual(updated.metadata["reorg_proposal_id"], result["proposal"].proposal_id)
|
|
|
|
async def test_deny_reorg_keeps_existing_roles(self) -> None:
|
|
proposal = await self.manager.propose_reorg(
|
|
project_id="proj1",
|
|
summary="Add temporary architecture role.",
|
|
changeset=ReorgChangeSet(
|
|
role_changes=[
|
|
ReorgRoleChange(
|
|
action="add",
|
|
role={
|
|
"id": "architect",
|
|
"name": "Architect",
|
|
"responsibility": "Architecture design.",
|
|
},
|
|
)
|
|
]
|
|
),
|
|
source_role_id="coordinator",
|
|
)
|
|
await self.manager.set_reorg_approval(proposal.proposal_id, approved=False, notes="Not needed yet.")
|
|
denied = await self.store.get_reorg_proposal(proposal.proposal_id)
|
|
assert denied is not None
|
|
self.assertEqual(denied.status, ReorgProposalStatus.DENIED)
|
|
self.assertIsNone(self.org_engine.get_agent("architect"))
|