Skip to content

Commit 3c45bd4

Browse files
test: update plan-and-task and component tests for workflow DSL
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 7204bf3 commit 3c45bd4

2 files changed

Lines changed: 77 additions & 18 deletions

File tree

tests/integration/test_plan_and_task_flow.py

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2333,7 +2333,6 @@ async def test_derive_workflow_id_falls_back_on_empty_response() -> None:
23332333

23342334
async 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

36013600
def 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

36153612
def 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
41064155
async 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

tests/test_components.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ class TestComponentCount:
360360
"""Test component count limit."""
361361

362362
def test_component_count_limit(self):
363-
"""Test that component count does not exceed 40."""
363+
"""Test that component count does not exceed the current guardrail."""
364364
import ecs_agent.components.definitions as d
365365

366366
count = sum(
@@ -370,7 +370,7 @@ def test_component_count_limit(self):
370370
and dataclasses.is_dataclass(getattr(d, name, None))
371371
and getattr(d, name).__module__ == "ecs_agent.components.definitions"
372372
)
373-
assert count <= 52, f"Component count {count} exceeds limit of 52"
373+
assert count <= 55, f"Component count {count} exceeds limit of 55"
374374

375375

376376
class TestComponentsExportedInInit:

0 commit comments

Comments
 (0)