Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions sources/core/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,12 +622,11 @@ def _verify_required_inputs(self, step: PlanStep) -> tuple[bool, list[str]]:
missing_inputs.append(required_input)
return len(missing_inputs) == 0, missing_inputs

def _verify_expected_outputs(self, step: PlanStep) -> tuple[bool, list[str]]:
def _verify_expected_outputs(self, step: PlanStep | Task) -> tuple[bool, list[str]]:
"""
Verify that expected outputs files were produced.
Args:
step: The plan step
produced_outputs: List of actually produced outputs
step: The plan step, or a completed task, declaring expected_outputs
Returns:
Tuple[bool, List[str]]: (all_produced, missing_outputs)
"""
Expand Down Expand Up @@ -665,9 +664,8 @@ def _can_execute_step(self, step: PlanStep) -> tuple[bool, list[str]]:
if dep_task is None or dep_task.status != TaskStatus.COMPLETED:
missing_deps.append(dep_name)
continue
expected_outputs = getattr(dep_task, "expected_outputs", None)
if expected_outputs:
outputs_ok, missing_outputs = self._verify_expected_outputs(expected_outputs)
if getattr(dep_task, "expected_outputs", None):
outputs_ok, missing_outputs = self._verify_expected_outputs(dep_task)
if not outputs_ok:
missing_deps.append(
f"{dep_name}[missing_outputs:{','.join(missing_outputs)}]"
Expand Down
98 changes: 98 additions & 0 deletions tests/planner_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Tests for Planner dependency checking.

Focus: ``_can_execute_step`` verifies that a completed dependency actually
produced the files it declared in ``expected_outputs``.
"""

import sys
import tempfile
from pathlib import Path

sys.path.append(str(Path(__file__).parent.parent))

from sources.core.planner import Planner
from sources.core.schema import PlanStep, Task, TaskStatus


def _planner_with(workspace: str, task_history: list[Task]) -> Planner:
"""Build a Planner without running its heavy __init__ (EvolutionEngine, LLM)."""
planner = object.__new__(Planner)
planner.workspace_path = workspace
planner.task_history = task_history
return planner


def _completed_dep(expected_outputs: list[str]) -> Task:
return Task(
name="step_a",
description="produces outputs",
status=TaskStatus.COMPLETED,
expected_outputs=expected_outputs,
)


def _dependent_step() -> PlanStep:
return PlanStep(
name="step_b",
goal_context="ctx",
task="consume step_a outputs",
cost=1,
score=0.0,
depends_on=["step_a"],
)


def test_dependency_outputs_present_allows_execution():
"""A completed dependency whose declared outputs exist unblocks the next step."""
with tempfile.TemporaryDirectory() as workspace:
(Path(workspace) / "result.csv").write_text("a,b\n1,2\n")
planner = _planner_with(workspace, [_completed_dep(["result.csv"])])

can_execute, missing = planner._can_execute_step(_dependent_step())

assert can_execute is True
assert missing == []


def test_dependency_outputs_missing_blocks_execution():
"""A completed dependency whose declared outputs are absent blocks the next step."""
with tempfile.TemporaryDirectory() as workspace:
(Path(workspace) / "unrelated.txt").write_text("noise")
planner = _planner_with(workspace, [_completed_dep(["result.csv"])])

can_execute, missing = planner._can_execute_step(_dependent_step())

assert can_execute is False
assert missing == ["step_a[missing_outputs:result.csv]"]


def test_dependency_without_declared_outputs_is_not_verified():
"""A dependency declaring no outputs is treated as satisfied once completed."""
with tempfile.TemporaryDirectory() as workspace:
planner = _planner_with(workspace, [_completed_dep([])])

can_execute, missing = planner._can_execute_step(_dependent_step())

assert can_execute is True
assert missing == []


def test_incomplete_dependency_blocks_execution():
"""A dependency that has not completed blocks the next step by name."""
with tempfile.TemporaryDirectory() as workspace:
pending = _completed_dep(["result.csv"])
pending.status = TaskStatus.PENDING
planner = _planner_with(workspace, [pending])

can_execute, missing = planner._can_execute_step(_dependent_step())

assert can_execute is False
assert missing == ["step_a"]


if __name__ == "__main__":
test_dependency_outputs_present_allows_execution()
test_dependency_outputs_missing_blocks_execution()
test_dependency_without_declared_outputs_is_not_verified()
test_incomplete_dependency_blocks_execution()
print("planner dependency tests passed")