"""Phase C: infer_turn_mode classifier + mode-aware context injection. Covers: - ``infer_turn_mode`` returns the correct TurnMode for each of the 5 canonical work-item states. - ``ContextAssembler.build_rework_feedback_context`` injects the reviewer's reject reason, rework attempt counter, and structured verdict lists into the prompt when the work item is in READY_FOR_REWORK. - ``ContextAssembler.build_turn_mode_context`` emits a header naming the current turn mode. - An end-to-end rework scenario: a child work item that has been bounced by the reviewer reappears on the dispatcher's queue; the prompt built for its second attempt contains the reviewer's specific reject reason so the agent can address it. """ from __future__ import annotations import tempfile import unittest from pathlib import Path from unittest.mock import AsyncMock from opc.core.models import ( DelegationWorkItem, Phase, Task, TaskStatus, ) from opc.database.store import OPCStore from opc.layer1_perception.context_assembler import ContextAssembler from opc.layer2_organization import comms as file_comms from opc.layer2_organization.prompt_contract import make_prompt_contract from opc.layer2_organization.turn_mode import TurnMode, infer_turn_mode from opc.layer2_organization.work_item_links import set_linked_work_item_id def _build_wi(**kwargs) -> DelegationWorkItem: """Create a DelegationWorkItem with sensible defaults.""" defaults = dict( run_id="r", cell_id="c", role_id="worker", seat_id="seat-worker", manager_role_id="mgr", manager_seat_id="seat-mgr", title="t", ) defaults.update(kwargs) return DelegationWorkItem(**defaults) def _link_task(task: Task, work_item_id: str) -> Task: if work_item_id: set_linked_work_item_id(task, work_item_id) return task class InferTurnModeTests(unittest.TestCase): def test_review_queue_entry_forces_review(self) -> None: wi = _build_wi(phase=Phase.RUNNING) self.assertEqual(infer_turn_mode(wi, is_review_entry=True), TurnMode.REVIEW) def test_review_execution_metadata_flag_is_review(self) -> None: wi = _build_wi( phase=Phase.RUNNING, metadata={"review_execution_work_item": True}, ) self.assertEqual(infer_turn_mode(wi), TurnMode.REVIEW) def test_review_kind_is_review(self) -> None: wi = _build_wi(kind="review", phase=Phase.RUNNING) self.assertEqual(infer_turn_mode(wi), TurnMode.REVIEW) def test_ready_for_rework_is_rework(self) -> None: wi = _build_wi( phase=Phase.READY_FOR_REWORK, metadata={"rework_feedback": "fix X and Y"}, ) self.assertEqual(infer_turn_mode(wi), TurnMode.REWORK) def test_parent_with_deps_and_running_is_integrate(self) -> None: wi = _build_wi( phase=Phase.RUNNING, metadata={ "dependency_work_item_ids": ["child-a", "child-b"], "allowed_delegate_role_ids": ["designer", "copywriter"], "frontier": "resumed", }, ) self.assertEqual(infer_turn_mode(wi), TurnMode.INTEGRATE) def test_manager_without_children_is_delegate(self) -> None: wi = _build_wi( phase=Phase.RUNNING, metadata={"allowed_delegate_role_ids": ["designer", "copywriter"]}, ) self.assertEqual(infer_turn_mode(wi), TurnMode.DELEGATE) def test_leaf_worker_is_execute(self) -> None: wi = _build_wi(phase=Phase.RUNNING) self.assertEqual(infer_turn_mode(wi), TurnMode.EXECUTE) def test_rework_takes_precedence_over_integrate(self) -> None: # A manager who was rejected on their integration turn must be # routed to REWORK (the rework_feedback block is more critical # than the team_deliverables block at this point). wi = _build_wi( phase=Phase.READY_FOR_REWORK, metadata={ "dependency_work_item_ids": ["child-a"], "rework_feedback": "missing acceptance section", }, ) self.assertEqual(infer_turn_mode(wi), TurnMode.REWORK) def test_review_takes_precedence_over_everything(self) -> None: wi = _build_wi( phase=Phase.READY_FOR_REWORK, # would otherwise be REWORK kind="review", ) self.assertEqual(infer_turn_mode(wi), TurnMode.REVIEW) class BuildReworkFeedbackContextTests(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self) -> None: self._tmpdir = tempfile.TemporaryDirectory() self.store = OPCStore(db_path=Path(self._tmpdir.name) / "store.db") await self.store.initialize() self.memory = AsyncMock() self.memory.build_focused_memory_context.return_value = "" self.memory.build_memory_context.return_value = "" self.memory.build_agent_memory_context.return_value = "" self.assembler = ContextAssembler(memory=self.memory, store=self.store) async def asyncTearDown(self) -> None: await self.store.close() self._tmpdir.cleanup() async def test_rework_feedback_is_injected_when_work_item_is_ready_for_rework( self, ) -> None: wi = _build_wi( work_item_id="wi-1", phase=Phase.RUNNING, role_id="designer", seat_id="seat::team::cmo::designer", manager_role_id="cmo", manager_seat_id="seat::team::ceo::cmo", metadata={}, ) await self.store.save_delegation_work_item(wi) await self.store.update_delegation_work_item( "wi-1", phase=Phase.AWAITING_MANAGER_REVIEW, ) await self.store.update_delegation_work_item( "wi-1", phase=Phase.READY_FOR_REWORK, metadata_updates={ "rework_feedback": ( "The video concept is missing two required features (real-time " "subtitles and language coverage claim). Reject." ), "structured_review_verdict": { "label": "reject", "summary": "missing features", "blocking_issues": [ "Video concept omits real-time subtitle rendering.", "No coverage claim for the 100+ languages promise.", ], "followups": [ "Consider a 10-second 'global reach' intro.", ], }, "review_rework_count": 1, "review_owner_role_id": "cmo", }, ) task = Task( id="t1", title="Create video concept", description="fancy promotional video", assigned_to="designer", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", }, ) task = _link_task(task, "wi-1") rendered = await self.assembler.build_rework_feedback_context(task) # Header present self.assertIn("## Reviewer Feedback (Rework Required)", rendered) # Reviewer attribution self.assertIn("Reviewer: cmo", rendered) # Rework attempt counter self.assertIn("Rework attempt: #1", rendered) # The reviewer's actual reject text verbatim self.assertIn( "The video concept is missing two required features", rendered, ) # Blocking issues rendered as bullets self.assertIn("### Blocking Issues", rendered) self.assertIn("Video concept omits real-time subtitle rendering.", rendered) self.assertIn("No coverage claim", rendered) # Follow-ups rendered separately self.assertIn("### Follow-ups", rendered) self.assertIn("Consider a 10-second 'global reach' intro.", rendered) async def test_non_rework_work_item_produces_no_feedback_section(self) -> None: wi = _build_wi( work_item_id="wi-2", phase=Phase.RUNNING, metadata={}, ) await self.store.save_delegation_work_item(wi) task = Task( id="t2", title="Do something", description="normal task", assigned_to="worker", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", }, ) task = _link_task(task, "wi-2") rendered = await self.assembler.build_rework_feedback_context(task) self.assertEqual(rendered, "") async def test_rework_without_feedback_still_empty(self) -> None: # Phase is rework but feedback metadata missing (pathological); # we don't inject an empty block. wi = _build_wi( work_item_id="wi-3", phase=Phase.RUNNING, metadata={}, ) await self.store.save_delegation_work_item(wi) await self.store.update_delegation_work_item( "wi-3", phase=Phase.AWAITING_MANAGER_REVIEW, ) await self.store.update_delegation_work_item( "wi-3", phase=Phase.READY_FOR_REWORK, ) task = Task( id="t3", title="t", description="", assigned_to="worker", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", }, ) task = _link_task(task, "wi-3") rendered = await self.assembler.build_rework_feedback_context(task) self.assertEqual(rendered, "") class BuildTurnModeContextTests(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self) -> None: self._tmpdir = tempfile.TemporaryDirectory() self.store = OPCStore(db_path=Path(self._tmpdir.name) / "store.db") await self.store.initialize() class _Memory: async def build_focused_memory_context(self, **_: object) -> str: return "" async def build_memory_context(self, **_: object) -> str: return "" async def build_agent_memory_context(self, *_: object, **__: object) -> str: return "" self.memory = _Memory() self.assembler = ContextAssembler(memory=self.memory, store=self.store) async def asyncTearDown(self) -> None: await self.store.close() self._tmpdir.cleanup() async def test_emits_header_for_multi_team_org_task(self) -> None: wi = _build_wi( work_item_id="wi-ex", phase=Phase.RUNNING, ) await self.store.save_delegation_work_item(wi) task = Task( id="t-ex", title="t", description="", assigned_to="worker", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", }, ) task = _link_task(task, "wi-ex") rendered = await self.assembler.build_turn_mode_context(task) self.assertTrue(rendered.startswith("## Turn Mode")) self.assertIn("- Required action:", rendered) self.assertIn("execute", rendered) async def test_delegate_mode_renders_runtime_state_and_required_action(self) -> None: wi = _build_wi( work_item_id="wi-delegate", phase=Phase.RUNNING, metadata={"allowed_delegate_role_ids": ["designer", "copywriter"]}, ) await self.store.save_delegation_work_item(wi) task = Task( id="t-delegate", title="t", description="", assigned_to="manager", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", "current_turn_mode": "dispatch_required", }, ) task = _link_task(task, "wi-delegate") rendered = await self.assembler.build_turn_mode_context(task) self.assertTrue(rendered.startswith("## Turn Mode")) self.assertIn("- Runtime state: `dispatch_required`", rendered) self.assertIn("- Required action:", rendered) self.assertIn("delegate", rendered) self.assertIn("outcome-based child WorkItems", rendered) async def test_work_item_assignment_context_renders_manager_planning_packet(self) -> None: wi = _build_wi( work_item_id="wi-assignment", phase=Phase.RUNNING, summary="Legacy summary fallback.", metadata={ "prompt_contract": make_prompt_contract( task_brief="Context: CTO owns the realtime translation app outcome.", upstream_intent_summary="Build a multilingual realtime translator app.", manager_planning_handoff="CEO plan: app build first, marketing after feature list.", manager_outcome_dispatch=True, owned_outcome_kind="execute", scope_key="cto-app-build", deliverables=["src app scaffold", "language coverage matrix"], acceptance_criteria=["child work items can execute without guessing"], dependency_specs=[{"kind": "role_id", "value": "cmo", "raw": "cmo"}], coordination_notes="CMO depends on feature inventory.", delegation_rationale="CTO owns implementation and technical feasibility.", non_overlap_guard="Do not write CMO campaign copy.", ), "brief": "Context: CTO owns the realtime translation app outcome.", "planning_context": "CEO plan: app build first, marketing after feature list.", "scope_key": "cto-app-build", "deliverables": ["src app scaffold", "language coverage matrix"], "acceptance_criteria": ["child work items can execute without guessing"], "dependency_specs": [{"kind": "role_id", "value": "cmo", "raw": "cmo"}], "coordination_notes": "CMO depends on feature inventory.", "delegation_rationale": "CTO owns implementation and technical feasibility.", "non_overlap_guard": "Do not write CMO campaign copy.", "manager_outcome_dispatch": True, "owned_outcome_kind": "execute", "allowed_delegate_role_ids": ["engineer"], }, ) await self.store.save_delegation_work_item(wi) task = Task( id="t-assignment", title="t", description="", assigned_to="cto", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", "original_message": "Build a multilingual realtime translator app.", }, ) task = _link_task(task, "wi-assignment") rendered = await self.assembler.build_work_item_assignment_context(task) self.assertIn("## Work Item Assignment Context", rendered) self.assertNotIn("Original User Request", rendered) self.assertNotIn("Assigned Work Item Brief", rendered) self.assertIn("Manager Planning Handoff", rendered) self.assertIn("CEO plan: app build first", rendered) self.assertNotIn("Context: CTO owns the realtime translation app outcome.", rendered) self.assertIn("Manager outcome turn", rendered) self.assertIn("src app scaffold", rendered) self.assertIn("child work items can execute without guessing", rendered) self.assertIn("CMO depends on feature inventory.", rendered) self.assertIn("Do not write CMO campaign copy.", rendered) view = await self.assembler.build_prompt_assignment_view(task) self.assertEqual( view.primary_task_brief, "Context: CTO owns the realtime translation app outcome.", ) async def test_external_layers_put_current_brief_only_in_primary_task_brief(self) -> None: wi = _build_wi( work_item_id="wi-child", phase=Phase.RUNNING, parent_work_item_id="wi-parent", metadata={ "prompt_contract": make_prompt_contract( task_brief="Context: engineer owns the render script.", upstream_intent_summary="Build the Marvel recap video pipeline.", deliverables=["render.py"], acceptance_criteria=["script runs without guessing"], non_overlap_guard="Do not write marketing copy.", ), "brief": "Context: engineer owns the render script.", "upstream_intent_summary": "Build the Marvel recap video pipeline.", "deliverables": ["render.py"], "acceptance_criteria": ["script runs without guessing"], "non_overlap_guard": "Do not write marketing copy.", }, ) await self.store.save_delegation_work_item(wi) task = Task( id="t-child", title="Engineer child", description="Runtime task description should not own the company brief.", assigned_to="engineer", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", "execution_mode": "company_mode", "original_message": "Full original request that should not be expanded downstream.", }, ) task = _link_task(task, "wi-child") layers = await self.assembler.build_external_context_layers(task, role_id="engineer") self.assertEqual(layers.primary_task_brief, "Context: engineer owns the render script.") self.assertIn("Upstream Intent Summary", layers.company_runtime_context) self.assertIn("Build the Marvel recap video pipeline.", layers.company_runtime_context) self.assertNotIn("Assigned Work Item Brief", layers.company_runtime_context) self.assertNotIn("Original User Request", layers.company_runtime_context) self.assertNotIn("Full original request", layers.company_runtime_context) self.assertNotIn("Context: engineer owns the render script.", layers.company_runtime_context) async def test_assignment_context_requires_prompt_contract_not_legacy_brief_fallback(self) -> None: wi = _build_wi( work_item_id="wi-no-contract", phase=Phase.RUNNING, summary="Legacy summary must not be prompt source.", metadata={ "brief": "Legacy brief must not be prompt source.", "deliverables": ["legacy deliverable"], }, ) await self.store.save_delegation_work_item(wi) task = Task( id="t-no-contract", title="Legacy child", description="Runtime task description must not be prompt source.", assigned_to="engineer", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", "execution_mode": "company_mode", }, ) task = _link_task(task, "wi-no-contract") rendered = await self.assembler.build_work_item_assignment_context(task) layers = await self.assembler.build_external_context_layers(task, role_id="engineer") self.assertEqual(rendered, "") self.assertEqual(layers.primary_task_brief, "") self.assertNotIn("Legacy brief", layers.company_runtime_context) self.assertNotIn("legacy deliverable", layers.company_runtime_context) async def test_synthesize_turn_suppresses_dispatch_only_assignment_fields(self) -> None: wi = _build_wi( work_item_id="wi-synth", phase=Phase.RUNNING, metadata={ "allowed_delegate_role_ids": ["engineer"], "current_turn_mode": "synthesize_required", "prompt_contract": make_prompt_contract( task_brief="Integrate approved child outputs.", upstream_intent_summary="Ship the final package.", manager_planning_handoff="Dispatch plan should not appear during synthesize.", manager_outcome_dispatch=True, owned_outcome_kind="execute", deliverables=["final package"], acceptance_criteria=["approved children are integrated"], ), }, ) await self.store.save_delegation_work_item(wi) task = Task( id="t-synth", title="Synthesize", assigned_to="cto", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", "execution_mode": "company_mode", "current_turn_mode": "synthesize_required", }, ) task = _link_task(task, "wi-synth") rendered = await self.assembler.build_work_item_assignment_context(task) self.assertIn("Ship the final package.", rendered) self.assertIn("final package", rendered) self.assertNotIn("Manager outcome turn", rendered) self.assertNotIn("Manager Planning Handoff", rendered) self.assertNotIn("Dispatch plan should not appear", rendered) async def test_skipped_for_non_multi_team_org(self) -> None: task = Task( id="t-plain", title="t", description="", assigned_to="worker", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={}, ) rendered = await self.assembler.build_turn_mode_context(task) self.assertEqual(rendered, "") class TeamMemoryDigestTests(unittest.TestCase): def test_default_scaffold_is_not_prompt_digest(self) -> None: with tempfile.TemporaryDirectory() as tmp: layout = file_comms.resolve_layout(tmp, "proj1", "session1") file_comms.ensure_layout(layout, ["cto"]) self.assertEqual(file_comms.read_team_memory_digest(layout), "") def test_non_empty_team_memory_is_prompt_digest(self) -> None: with tempfile.TemporaryDirectory() as tmp: layout = file_comms.resolve_layout(tmp, "proj1", "session1") file_comms.ensure_layout(layout, ["cto"]) layout.team_memory_path.write_text( "# Team Memory\n\n- Shared constraint: rollback first.\n", encoding="utf-8", ) digest = file_comms.read_team_memory_digest(layout) self.assertIn("team_memory_path:", digest) self.assertIn("Shared constraint: rollback first.", digest) class ReworkE2EPromptTests(unittest.IsolatedAsyncioTestCase): """End-to-end: a rework'd child work item's build_sections output contains the reviewer's reject reason so the agent's next turn knows what to fix.""" async def asyncSetUp(self) -> None: self._tmpdir = tempfile.TemporaryDirectory() self.store = OPCStore(db_path=Path(self._tmpdir.name) / "store.db") await self.store.initialize() self.memory = AsyncMock() self.memory.build_focused_memory_context = AsyncMock(return_value="") self.memory.build_memory_context = AsyncMock(return_value="") self.memory.build_agent_memory_context = AsyncMock(return_value="") self.assembler = ContextAssembler(memory=self.memory, store=self.store) async def asyncTearDown(self) -> None: await self.store.close() self._tmpdir.cleanup() async def test_rework_prompt_contains_rejecter_reason(self) -> None: # Simulate the designer's work item: it was running, got # bounced by the reviewer, now back in READY_FOR_REWORK with # the reject rationale recorded. wi = _build_wi( work_item_id="designer-wi", phase=Phase.RUNNING, role_id="designer", seat_id="seat::team::cmo::designer", manager_role_id="cmo", manager_seat_id="seat::team::ceo::cmo", ) await self.store.save_delegation_work_item(wi) await self.store.update_delegation_work_item( "designer-wi", phase=Phase.AWAITING_MANAGER_REVIEW ) REJECT_TEXT = ( "The hero-shot animation is too slow; shrink it to 2s max. " "Also the color palette clashes with the logo — use the " "brand-approved palette from the style guide." ) await self.store.update_delegation_work_item( "designer-wi", phase=Phase.READY_FOR_REWORK, metadata_updates={ "rework_feedback": REJECT_TEXT, "review_rework_count": 1, "review_owner_role_id": "cmo", "structured_review_verdict": { "label": "reject", "blocking_issues": [ "Hero animation must be ≤2 seconds.", "Palette violates brand guide.", ], "followups": [], }, }, ) # A dispatcher-rehydrated task for the rework turn. task = Task( id="designer-task-2", title="Create fancy promotional video concept", description="promo video for translation app", assigned_to="designer", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", "execution_mode": "company_mode", "delegation_run_id": "r", "work_item_role_id": "designer", }, ) task = _link_task(task, "designer-wi") sections = await self.assembler.build_sections(task, role_id="designer") # Turn mode section announces "rework" explicitly. self.assertIn("rework", sections["turn_mode"]) # Rework feedback section contains the reviewer's verbatim reason. self.assertIn(REJECT_TEXT, sections["rework_feedback"]) # And the structured blocking issues. self.assertIn("Hero animation must be ≤2 seconds.", sections["rework_feedback"]) self.assertIn("Palette violates brand guide.", sections["rework_feedback"]) # Joined system context includes both blocks. joined = "\n\n".join(part for part in sections.values() if part) self.assertIn(REJECT_TEXT, joined) self.assertIn("## Reviewer Feedback (Rework Required)", joined) self.assertIn("## Turn Mode", joined) class ReworkAfterDispatcherClaimTests(unittest.IsolatedAsyncioTestCase): """Regression coverage for the new14 silent-rework-loop bug. Real-world sequence: reviewer rejects → work item set to READY_FOR_REWORK with rework_feedback in metadata → dispatcher immediately claims and flips phase to RUNNING → context is assembled with phase==RUNNING. The original gates checked ``phase == READY_FOR_REWORK`` and silently dropped both the feedback block and the REWORK turn-mode header, so the agent re-ran the same task with no visibility into the reviewer's objections. Loop continued until escalation. These tests pin the post-fix behaviour: feedback is gated on metadata content, not on phase, so a RUNNING work item with ``rework_feedback`` set still produces the rework prompt block and the REWORK turn-mode header. """ async def asyncSetUp(self) -> None: self._tmpdir = tempfile.TemporaryDirectory() self.store = OPCStore(db_path=Path(self._tmpdir.name) / "store.db") await self.store.initialize() self.memory = AsyncMock() self.memory.build_focused_memory_context = AsyncMock(return_value="") self.memory.build_memory_context = AsyncMock(return_value="") self.memory.build_agent_memory_context = AsyncMock(return_value="") self.assembler = ContextAssembler(memory=self.memory, store=self.store) async def asyncTearDown(self) -> None: await self.store.close() self._tmpdir.cleanup() def test_infer_turn_mode_returns_rework_when_phase_running_but_feedback_set( self, ) -> None: wi = _build_wi( phase=Phase.RUNNING, metadata={ "rework_feedback": "Missing handoffs/cto_app_architecture.md", "review_rework_count": 2, }, ) self.assertEqual(infer_turn_mode(wi), TurnMode.REWORK) def test_infer_turn_mode_running_with_only_rework_count_is_rework(self) -> None: # rework_feedback could be empty if a fix-4 fallback also # missed (defensive); review_rework_count > 0 alone still # signals a rework attempt. wi = _build_wi( phase=Phase.RUNNING, metadata={"review_rework_count": 1}, ) self.assertEqual(infer_turn_mode(wi), TurnMode.REWORK) def test_fresh_running_work_item_is_still_execute(self) -> None: # Sanity: brand-new work items have neither field; classifier # must NOT misfire as REWORK. wi = _build_wi(phase=Phase.RUNNING, metadata={}) self.assertEqual(infer_turn_mode(wi), TurnMode.EXECUTE) async def test_build_rework_feedback_context_fires_when_phase_is_running( self, ) -> None: # Mirror the exact sequence: store the work item with phase # RUNNING and metadata carrying the reviewer's reject reason. wi = _build_wi( work_item_id="wi-running-rework", phase=Phase.RUNNING, role_id="cto", metadata={ "rework_feedback": ( "Missing canonical file: trans_app/handoffs/cto_app_architecture.md" ), "review_rework_count": 3, "review_owner_role_id": "ceo", "structured_review_verdict": { "label": "reject", "blocking_issues": [ "cto_app_architecture.md does not exist under trans_app/handoffs/.", ], "followups": [], }, }, ) await self.store.save_delegation_work_item(wi) task = Task( id="t-running-rework", title="Translate app technical plan and implementation", description="ship the trans_app scaffold", assigned_to="cto", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", }, ) task = _link_task(task, "wi-running-rework") rendered = await self.assembler.build_rework_feedback_context(task) self.assertIn("## Reviewer Feedback (Rework Required)", rendered) self.assertIn("Reviewer: ceo", rendered) self.assertIn("Rework attempt: #3", rendered) self.assertIn("cto_app_architecture.md", rendered) self.assertIn("### Blocking Issues", rendered) async def test_full_e2e_running_phase_still_yields_rework_sections(self) -> None: # build_sections is what the prompt assembler actually calls. # Both the turn_mode and rework_feedback sections must be # populated even when the work item phase has been flipped to # RUNNING by the dispatcher's claim step. wi = _build_wi( work_item_id="wi-e2e-running", phase=Phase.RUNNING, role_id="cto", metadata={ "rework_feedback": "REJECT_FOR_TEST: file X is missing", "review_rework_count": 1, "review_owner_role_id": "ceo", }, ) await self.store.save_delegation_work_item(wi) task = Task( id="t-e2e", title="t", description="", assigned_to="cto", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", "execution_mode": "company_mode", "delegation_run_id": "r", "work_item_role_id": "cto", }, ) task = _link_task(task, "wi-e2e-running") sections = await self.assembler.build_sections(task, role_id="cto") self.assertIn("rework", sections["turn_mode"]) self.assertIn("REJECT_FOR_TEST", sections["rework_feedback"]) async def test_previous_submission_excerpt_is_included(self) -> None: wi = _build_wi( work_item_id="wi-prev", phase=Phase.RUNNING, role_id="cto", metadata={ "rework_feedback": "Missing handoff file", "review_rework_count": 1, }, ) await self.store.save_delegation_work_item(wi) task = Task( id="t-prev", title="t", description="", assigned_to="cto", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", }, result={ "content": ( "已交付。请以本地目录为准: trans_app/. 入口文件: " "FINAL_DELIVERY_INDEX.md" ), }, ) task = _link_task(task, "wi-prev") rendered = await self.assembler.build_rework_feedback_context(task) self.assertIn("### Your Previous Submission", rendered) self.assertIn("已交付", rendered) async def test_previous_submission_truncated_when_huge(self) -> None: wi = _build_wi( work_item_id="wi-huge", phase=Phase.RUNNING, metadata={"rework_feedback": "fix it", "review_rework_count": 1}, ) await self.store.save_delegation_work_item(wi) big = "X" * 5000 task = Task( id="t-huge", title="t", description="", assigned_to="worker", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "runtime_model": "multi_team_org", }, result={"content": big}, ) task = _link_task(task, "wi-huge") rendered = await self.assembler.build_rework_feedback_context(task) self.assertIn("### Your Previous Submission", rendered) self.assertIn("… (truncated)", rendered) async def test_feedback_falls_back_to_task_metadata_when_store_misses( self, ) -> None: # The dispatcher copies rework_feedback into task.metadata # at materialization (company_mode.py:3853-3869). Even if the # work item lookup fails (e.g. store gone or linked work item id # missing), the task-side copy must still drive the prompt. task = Task( id="t-no-wi", title="t", description="", assigned_to="worker", status=TaskStatus.PENDING, project_id="p", session_id="s", metadata={ "rework_feedback": "TASK_META_FEEDBACK: redo the X step", "review_rework_count": 1, "runtime_model": "multi_team_org", }, ) rendered = await self.assembler.build_rework_feedback_context(task) self.assertIn("TASK_META_FEEDBACK", rendered) self.assertIn("Rework attempt: #1", rendered) if __name__ == "__main__": unittest.main()