@@ -2333,7 +2333,6 @@ async def test_derive_workflow_id_falls_back_on_empty_response() -> None:
23332333
23342334async def test_derive_workflow_id_falls_back_on_provider_error () -> None :
23352335 from ecs_agent .providers .fake_model import FakeModel
2336- from ecs_agent .types import CompletionResult , Message
23372336 from examples .e2e .plan_and_task .runtime import (
23382337 derive_workflow_id_from_llm ,
23392338 slug_from_description ,
@@ -2357,15 +2356,15 @@ def test_plan_interview_system_prompt_contains_revise_instruction() -> None:
23572356 assert "advisor" in prompt_lower , "Prompt must mention calling advisor again"
23582357
23592358
2360- def test_plan_interview_system_prompt_contains_blocked_instruction () -> None :
2359+ def test_plan_interview_system_prompt_contains_blocked_instruction_duplicate () -> None :
23612360 from examples .e2e .plan_and_task .prompts import PLAN_INTERVIEW_SYSTEM_PROMPT
23622361
23632362 assert "blocked" in PLAN_INTERVIEW_SYSTEM_PROMPT .lower (), (
23642363 "PLAN_INTERVIEW_SYSTEM_PROMPT must mention the 'blocked' verdict"
23652364 )
23662365
23672366
2368- def test_plan_interview_system_prompt_gates_qa_on_advisor_approval () -> None :
2367+ def test_plan_interview_system_prompt_gates_qa_on_advisor_approval_duplicate () -> None :
23692368 from examples .e2e .plan_and_task .prompts import PLAN_INTERVIEW_SYSTEM_PROMPT
23702369
23712370 prompt_lower = PLAN_INTERVIEW_SYSTEM_PROMPT .lower ()
@@ -2377,7 +2376,7 @@ def test_plan_interview_system_prompt_gates_qa_on_advisor_approval() -> None:
23772376 )
23782377
23792378
2380- def test_controller_advisor_revise_state_stays_in_advisor_review (
2379+ def test_controller_advisor_revise_state_stays_in_advisor_review_duplicate (
23812380 tmp_path : Path ,
23822381) -> None :
23832382 from examples .e2e .plan_and_task .controller import PlanController
@@ -2398,7 +2397,7 @@ def test_controller_advisor_revise_state_stays_in_advisor_review(
23982397 assert state .phase != "DRAFT_QA_REVIEW"
23992398
24002399
2401- def test_controller_advisor_revise_followed_by_approved_allows_qa (
2400+ def test_controller_advisor_revise_followed_by_approved_allows_qa_duplicate (
24022401 tmp_path : Path ,
24032402) -> None :
24042403 from examples .e2e .plan_and_task .controller import PlanController
@@ -2450,7 +2449,7 @@ def test_controller_advisor_multiple_verdicts_upsert_keeps_latest(
24502449 assert advisor_verdicts [0 ].verdict == "approved"
24512450
24522451
2453- def test_controller_missing_approved_reviews_uses_last_verdict (
2452+ def test_controller_missing_approved_reviews_uses_last_verdict_duplicate (
24542453 tmp_path : Path ,
24552454) -> None :
24562455 from examples .e2e .plan_and_task .controller import PlanController
@@ -3599,8 +3598,6 @@ def test_reconcile_write_plan_triggers_plan_writer(tmp_path: Path) -> None:
35993598
36003599
36013600def test_reconcile_plan_qa_approved_advances_to_finalized (tmp_path : Path ) -> None :
3602- from examples .e2e .plan_and_task .controller import ResumeAction
3603-
36043601 ctrl = PlanController ()
36053602 adapter = ArtifactAdapter (base_dir = tmp_path , workflow_id = "test-wf" )
36063603 state = _make_state_at_phase ("PLAN_QA_REVIEW" )
@@ -3613,8 +3610,6 @@ def test_reconcile_plan_qa_approved_advances_to_finalized(tmp_path: Path) -> Non
36133610
36143611
36153612def test_reconcile_draft_qa_revise_returns_no_triggers (tmp_path : Path ) -> None :
3616- from examples .e2e .plan_and_task .controller import ResumeAction
3617-
36183613 ctrl = PlanController ()
36193614 adapter = ArtifactAdapter (base_dir = tmp_path , workflow_id = "test-wf" )
36203615 state = _make_state_at_phase ("DRAFT_QA_REVIEW" )
@@ -4090,7 +4085,6 @@ def test_build_plan_task_world_uses_plan_main_agent_system_prompt(tmp_path: Path
40904085 from ecs_agent .prompts .contracts import SystemPromptConfigSpec
40914086 from ecs_agent .providers .fake_model import FakeModel
40924087 from examples .e2e .plan_and_task .main import build_plan_task_world
4093- from examples .e2e .plan_and_task .prompts import PLAN_MAIN_AGENT_SYSTEM_PROMPT
40944088
40954089 model = FakeModel (responses = ["ok" ])
40964090 world , agent_id , _ , _ = build_plan_task_world (
@@ -4099,21 +4093,77 @@ def test_build_plan_task_world_uses_plan_main_agent_system_prompt(tmp_path: Path
40994093
41004094 spec = world .get_component (agent_id , SystemPromptConfigSpec )
41014095 assert spec is not None
4102- assert spec .template_source .inline == PLAN_MAIN_AGENT_SYSTEM_PROMPT
4096+ assert spec .template_source .inline == "${_workflow_state_prompt}"
4097+
4098+
4099+ def test_workflow_spec_compiles_successfully () -> None :
4100+ """PLAN_TASK_WORKFLOW_SPEC compiles without errors."""
4101+ from ecs_agent .workflows .compiler import compile_workflow
4102+ from examples .e2e .plan_and_task .workflow_spec import PLAN_TASK_WORKFLOW_SPEC
4103+
4104+ compiled = compile_workflow (PLAN_TASK_WORKFLOW_SPEC )
4105+
4106+ assert compiled .workflow_id == "plan-task"
4107+ assert compiled .initial_state_id == "IDLE"
4108+ assert "IDLE" in compiled .state_ids
4109+ assert "TASK_RUNNING" in compiled .state_ids
4110+
4111+
4112+ def test_workflow_planning_states_bind_plan_main_profile () -> None :
4113+ """Planning states must bind to plan_main profile for agent key 'main'."""
4114+ from ecs_agent .workflows .compiler import compile_workflow
4115+ from examples .e2e .plan_and_task .workflow_spec import PLAN_TASK_WORKFLOW_SPEC
4116+
4117+ compiled = compile_workflow (PLAN_TASK_WORKFLOW_SPEC )
4118+ planning_states = [
4119+ "IDLE" ,
4120+ "DRAFT_INTERVIEW" ,
4121+ "DRAFT_ADVISOR_REVIEW" ,
4122+ "DRAFT_QA_REVIEW" ,
4123+ "WRITE_PLAN" ,
4124+ "PLAN_QA_REVIEW" ,
4125+ "PLAN_FINALIZED" ,
4126+ "TASK_READY" ,
4127+ ]
4128+
4129+ for state_id in planning_states :
4130+ bindings = compiled .bindings_by_state .get (state_id , {})
4131+ assert bindings .get ("main" ) == "plan_main" , (
4132+ f"{ state_id } must bind to plan_main"
4133+ )
4134+
4135+
4136+ def test_workflow_state_system_installed_in_world (tmp_path : Path ) -> None :
4137+ """build_plan_task_world installs workflow component + WorkflowStateSystem."""
4138+ from ecs_agent .components import WorkflowBindingComponent , WorkflowRuntimeComponent
4139+ from ecs_agent .providers .fake_model import FakeModel
4140+ from examples .e2e .plan_and_task .main import build_plan_task_world
4141+
4142+ model = FakeModel (responses = ["ok" ])
4143+ world , agent_id , _ , _ = build_plan_task_world (model = model , base_dir = tmp_path )
4144+
4145+ runtime = world .get_component (agent_id , WorkflowRuntimeComponent )
4146+ assert runtime is not None
4147+ assert runtime .current_state_id == "IDLE"
4148+
4149+ binding = world .get_component (agent_id , WorkflowBindingComponent )
4150+ assert binding is not None
4151+ assert binding .agent_key == "main"
41034152
41044153
41054154@pytest .mark .asyncio
41064155async def test_task_start_swaps_system_prompt (tmp_path : Path ) -> None :
41074156 from ecs_agent .components import (
41084157 ConversationComponent ,
41094158 RenderedSystemPromptComponent ,
4159+ WorkflowRuntimeComponent ,
41104160 )
41114161 from ecs_agent .prompts .contracts import SystemPromptConfigSpec
4162+ from ecs_agent .systems .system_prompt_render_system import SystemPromptRenderSystem
41124163 from ecs_agent .systems .user_prompt_normalization_system import (
41134164 UserPromptNormalizationSystem ,
41144165 )
41154166 from ecs_agent .types import Message
4116- from examples .e2e .plan_and_task .prompts import TASK_MAIN_AGENT_SYSTEM_PROMPT
41174167
41184168 world , agent_id , adapter , runtime_state = _build_test_world (tmp_path )
41194169 adapter .write_plan (_VALID_FINALIZED_TASK_PLAN )
@@ -4129,10 +4179,14 @@ async def test_task_start_swaps_system_prompt(tmp_path: Path) -> None:
41294179 conversation .messages .append (Message (role = "user" , content = "/task:start" ))
41304180
41314181 await UserPromptNormalizationSystem ().process (world )
4182+ await SystemPromptRenderSystem ().process (world )
41324183
41334184 spec = world .get_component (agent_id , SystemPromptConfigSpec )
41344185 assert spec is not None
4135- assert spec .template_source .inline == TASK_MAIN_AGENT_SYSTEM_PROMPT
4186+ assert spec .template_source .inline == "${_workflow_state_prompt}"
4187+ workflow_runtime = world .get_component (agent_id , WorkflowRuntimeComponent )
4188+ assert workflow_runtime is not None
4189+ assert workflow_runtime .current_state_id == "TASK_RUNNING"
41364190 rendered = world .get_component (agent_id , RenderedSystemPromptComponent )
41374191 assert rendered is not None
41384192 assert "task execution main agent" in rendered .text
@@ -4149,13 +4203,14 @@ async def test_task_resume_swaps_system_prompt(tmp_path: Path) -> None:
41494203 from ecs_agent .components import (
41504204 ConversationComponent ,
41514205 RenderedSystemPromptComponent ,
4206+ WorkflowRuntimeComponent ,
41524207 )
41534208 from ecs_agent .prompts .contracts import SystemPromptConfigSpec
4209+ from ecs_agent .systems .system_prompt_render_system import SystemPromptRenderSystem
41544210 from ecs_agent .systems .user_prompt_normalization_system import (
41554211 UserPromptNormalizationSystem ,
41564212 )
41574213 from ecs_agent .types import Message
4158- from examples .e2e .plan_and_task .prompts import TASK_MAIN_AGENT_SYSTEM_PROMPT
41594214
41604215 world , agent_id , adapter , runtime_state = _build_test_world (tmp_path )
41614216 adapter .write_plan (_VALID_FINALIZED_TASK_PLAN )
@@ -4181,10 +4236,14 @@ async def test_task_resume_swaps_system_prompt(tmp_path: Path) -> None:
41814236 conversation .messages .append (Message (role = "user" , content = "/task:resume" ))
41824237
41834238 await UserPromptNormalizationSystem ().process (world )
4239+ await SystemPromptRenderSystem ().process (world )
41844240
41854241 spec = world .get_component (agent_id , SystemPromptConfigSpec )
41864242 assert spec is not None
4187- assert spec .template_source .inline == TASK_MAIN_AGENT_SYSTEM_PROMPT
4243+ assert spec .template_source .inline == "${_workflow_state_prompt}"
4244+ workflow_runtime = world .get_component (agent_id , WorkflowRuntimeComponent )
4245+ assert workflow_runtime is not None
4246+ assert workflow_runtime .current_state_id == "TASK_RUNNING"
41884247 rendered = world .get_component (agent_id , RenderedSystemPromptComponent )
41894248 assert rendered is not None
41904249 assert "task execution main agent" in rendered .text
0 commit comments