Files

317 lines
13 KiB
Python
Raw Permalink Normal View History

2026-06-25 07:11:44 -04:00
"""Tests for scripts/autopilot.py — functional correctness review.
Covers bugs found during 2026-06-25 review:
1. PHASE_PRIORITY missing code_review
2. cmd_drive suggested implement→bug_find (illegal; should be implement→code_review)
3. cmd_drive had no handler for code_review / code_review:approved
4. _all_tasks didn't skip tasks/complete/ archive dir
5. _all_tasks didn't recurse into subtasks/
6. _all_tasks used wrong path for non-framework projects
7. is_terminal treated human_intervention as terminal (dead-code handler)
8. needs_user_input didn't flag human_intervention as blocked
"""
import importlib.util
import sys
from pathlib import Path
from typing import Optional
import pytest
_AP_PATH = Path.home() / ".automaton" / "scripts" / "autopilot.py"
_spec = importlib.util.spec_from_file_location("autopilot_mod", _AP_PATH)
ap = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(ap)
def _make_task(project: Path, name: str, phase: str = "new") -> Path:
"""Create a task dir + .state (no subprocess)."""
if project == ap.AUTOMATON_DIR:
tp = project / "tasks" / name
else:
tp = project / ".automaton" / "tasks" / name
tp.mkdir(parents=True, exist_ok=True)
(tp / ".state").write_text(phase + "\n")
return tp
def _make_subtask(project: Path, parent: str, name: str, phase: str = "new") -> Path:
if project == ap.AUTOMATON_DIR:
sp = project / "tasks" / parent / "subtasks" / name
else:
sp = project / ".automaton" / "tasks" / parent / "subtasks" / name
sp.mkdir(parents=True, exist_ok=True)
(sp / ".state").write_text(phase + "\n")
return sp
def _write_verdict(task_path: Path, first_line: str) -> None:
(task_path / "VERDICT.md").write_text(first_line + "\nrest of verdict\n")
class _Args:
"""Minimal args object for cmd_* functions."""
def __init__(self, **kwargs):
self.project = kwargs.get("project")
self.max_iterations = kwargs.get("max_iterations")
self.delay = kwargs.get("delay")
self.threshold = kwargs.get("threshold")
@pytest.fixture
def tmp_project(tmp_path):
(tmp_path / ".automaton" / "tasks").mkdir(parents=True)
return tmp_path
class TestPhasePriority:
"""Bug 1: code_review was missing from PHASE_PRIORITY."""
def test_code_review_in_priority(self):
assert "code_review" in ap.PHASE_PRIORITY
def test_code_review_priority_between_implement_and_bug_find(self):
assert ap.PHASE_PRIORITY["code_review"] > ap.PHASE_PRIORITY["implement"]
assert ap.PHASE_PRIORITY["code_review"] < ap.PHASE_PRIORITY["bug_find"]
def test_human_intervention_in_priority(self):
assert "human_intervention" in ap.PHASE_PRIORITY
def test_priorities_monotonically_increase_along_pipeline(self):
pipeline = [
"new", "research", "decomposition", "design", "test_design",
"implement", "code_review", "bug_find", "adversarial_bug_find",
"doc_review", "referee",
]
for i in range(len(pipeline) - 1):
assert ap.PHASE_PRIORITY[pipeline[i]] < ap.PHASE_PRIORITY[pipeline[i + 1]], \
f"{pipeline[i]} ({ap.PHASE_PRIORITY[pipeline[i]]}) should be < {pipeline[i+1]} ({ap.PHASE_PRIORITY[pipeline[i+1]]})"
class TestCmdDriveCodeReview:
"""Bugs 2+3: implement→bug_find was illegal; code_review handlers missing."""
def test_implement_suggests_code_review_not_bug_find(self, tmp_project, capsys):
_make_task(tmp_project, "task-a", "implement")
args = _Args(project=str(tmp_project))
ap.cmd_drive(args)
out = capsys.readouterr().out
assert "--transition code_review" in out
assert "--transition bug_find" not in out
def test_code_review_suggests_awaiting_approval(self, tmp_project, capsys):
_make_task(tmp_project, "task-cr", "code_review")
args = _Args(project=str(tmp_project))
ap.cmd_drive(args)
out = capsys.readouterr().out
assert "--transition code_review:awaiting_approval" in out
def test_code_review_approved_suggests_bug_find(self, tmp_project, capsys):
_make_task(tmp_project, "task-cr2", "code_review:approved")
args = _Args(project=str(tmp_project))
ap.cmd_drive(args)
out = capsys.readouterr().out
assert "--transition bug_find" in out
def test_implement_task_not_stalled(self, tmp_project, capsys):
"""Ensure cmd_drive produces a transition command for implement phase."""
_make_task(tmp_project, "task-impl", "implement")
args = _Args(project=str(tmp_project))
rc = ap.cmd_drive(args)
out = capsys.readouterr().out
assert rc == 0
assert "status.py --transition" in out
class TestAllTasksSkipComplete:
"""Bug 4: _all_tasks included tasks/complete/ as a task."""
def test_complete_dir_excluded(self, tmp_project):
_make_task(tmp_project, "active-task", "implement")
complete_dir = tmp_project / ".automaton" / "tasks" / "complete"
complete_dir.mkdir(parents=True)
(complete_dir / "old-task").mkdir()
(complete_dir / "old-task" / ".state").write_text("complete\n")
tasks = ap._all_tasks(tmp_project)
names = [t[0] for t in tasks]
assert "active-task" in names
assert "complete" not in names
class TestAllTasksSubtasks:
"""Bug 5: _all_tasks didn't recurse into subtasks/."""
def test_subtask_found(self, tmp_project):
_make_task(tmp_project, "parent-task", "decomposition:approved")
_make_subtask(tmp_project, "parent-task", "sub-a", "implement")
_make_subtask(tmp_project, "parent-task", "sub-b", "new")
tasks = ap._all_tasks(tmp_project)
names = [t[0] for t in tasks]
assert "parent-task" in names
assert "parent-task/sub-a" in names
assert "parent-task/sub-b" in names
def test_subtask_scan_all_tasks(self, tmp_project):
_make_task(tmp_project, "parent-task", "decomposition:approved")
_make_subtask(tmp_project, "parent-task", "sub-a", "implement")
scanned = ap.scan_all_tasks(tmp_project)
names = [t["name"] for t in scanned]
assert "parent-task" in names
assert "parent-task/sub-a" in names
class TestAllTasksProjectPath:
"""Bug 6: _all_tasks used project_dir/tasks instead of project_dir/.automaton/tasks."""
def test_non_framework_project_path(self, tmp_project):
_make_task(tmp_project, "proj-task", "implement")
tasks = ap._all_tasks(tmp_project)
names = [t[0] for t in tasks]
assert "proj-task" in names
def test_wrong_path_returns_empty(self, tmp_path):
"""If .automaton/tasks doesn't exist, return empty (not crash)."""
tasks = ap._all_tasks(tmp_path)
assert tasks == []
class TestIsTerminal:
"""Bug 7: human_intervention was treated as terminal."""
def test_complete_is_terminal(self):
assert ap.is_terminal({"phase": "complete"}) is True
def test_human_intervention_not_terminal(self):
assert ap.is_terminal({"phase": "human_intervention"}) is False
def test_none_phase_not_terminal(self):
assert ap.is_terminal({"phase": None}) is False
def test_new_not_terminal(self):
assert ap.is_terminal({"phase": "new"}) is False
class TestNeedsUserInput:
"""Bug 8: human_intervention wasn't flagged as needing user input."""
def test_awaiting_approval_needs_input(self, tmp_project):
tp = _make_task(tmp_project, "task-r", "research:awaiting_approval")
assert ap.needs_user_input({"phase": "research:awaiting_approval", "path": str(tp)}) is True
def test_human_intervention_needs_input(self, tmp_project):
tp = _make_task(tmp_project, "task-hi", "human_intervention")
assert ap.needs_user_input({"phase": "human_intervention", "path": str(tp)}) is True
def test_implement_no_input(self, tmp_project):
tp = _make_task(tmp_project, "task-i", "implement")
assert ap.needs_user_input({"phase": "implement", "path": str(tp)}) is False
def test_verdict_fail_needs_input(self, tmp_project):
tp = _make_task(tmp_project, "task-v", "referee")
_write_verdict(tp, "FAIL: bugs found")
assert ap.needs_user_input({"phase": "referee", "path": str(tp)}) is True
def test_verdict_pass_no_input(self, tmp_project):
tp = _make_task(tmp_project, "task-vp", "referee")
_write_verdict(tp, "PASS: all good")
assert ap.needs_user_input({"phase": "referee", "path": str(tp)}) is False
class TestSortByAdvancement:
"""Verify sort orders code_review correctly after the fix."""
def test_code_review_more_advanced_than_implement(self):
tasks = [
{"name": "impl-task", "base_phase": "implement"},
{"name": "cr-task", "base_phase": "code_review"},
]
sorted_tasks = ap.sort_by_advancement(tasks)
assert sorted_tasks[0]["name"] == "cr-task"
def test_bug_find_more_advanced_than_code_review(self):
tasks = [
{"name": "cr-task", "base_phase": "code_review"},
{"name": "bf-task", "base_phase": "bug_find"},
]
sorted_tasks = ap.sort_by_advancement(tasks)
assert sorted_tasks[0]["name"] == "bf-task"
class TestCmdDriveHumanIntervention:
"""Verify human_intervention is now drivable (not dead code)."""
def test_human_intervention_shows_in_blocked(self, tmp_project, capsys):
_make_task(tmp_project, "task-hi", "human_intervention")
args = _Args(project=str(tmp_project))
ap.cmd_drive(args)
out = capsys.readouterr().out
assert "ORCHESTRATION_BLOCKED" in out
assert "task-hi" in out
assert "--transition referee" in out
def test_human_intervention_not_in_unblocked(self, tmp_project, capsys):
"""human_intervention should be blocked, not selected as NEXT_TASK."""
_make_task(tmp_project, "task-hi", "human_intervention")
_make_task(tmp_project, "task-active", "implement")
args = _Args(project=str(tmp_project))
ap.cmd_drive(args)
out = capsys.readouterr().out
assert "NEXT_TASK: task-active" in out
assert "NEXT_TASK: task-hi" not in out
class TestCmdDriveSummary:
"""Integration: cmd_summary with mixed states."""
def test_summary_counts(self, tmp_project, capsys):
_make_task(tmp_project, "t-new", "new")
_make_task(tmp_project, "t-impl", "implement")
_make_task(tmp_project, "t-blocked", "research:awaiting_approval")
_make_task(tmp_project, "t-hi", "human_intervention")
complete_dir = tmp_project / ".automaton" / "tasks" / "complete"
complete_dir.mkdir()
(complete_dir / "t-done").mkdir()
(complete_dir / "t-done" / ".state").write_text("complete\n")
args = _Args(project=str(tmp_project))
ap.cmd_summary(args)
out = capsys.readouterr().out
assert "3" in out # 3 non-terminal (new, implement, blocked, hi... wait, hi is now non-terminal too)
assert "READY TO DRIVE" in out
assert "AWAITING USER" in out
assert "t-done" not in out # complete dir should not appear
assert "complete" not in out.split("READY")[0] # no "complete" as a task name
class TestCmdDriveAllPhases:
"""Every phase produces a transition command — no silent stalls."""
@pytest.mark.parametrize("phase,expected_fragment", [
("new", "--transition research"),
("research", "--transition research:awaiting_approval"),
("research:approved", "--transition decomposition"),
("decomposition", "--transition decomposition:awaiting_approval"),
("decomposition:approved", "--transition complete"),
("design", "--transition design:awaiting_approval"),
("design:approved", "--transition test_design"),
("test_design", "--transition test_design:awaiting_approval"),
("test_design:approved", "--transition implement"),
("implement", "--transition code_review"),
("code_review", "--transition code_review:awaiting_approval"),
("code_review:approved", "--transition bug_find"),
("bug_find", "--transition adversarial_bug_find"),
("adversarial_bug_find", "--transition doc_review"),
("doc_review", "--transition referee"),
("referee", "--transition complete"),
])
def test_phase_produces_transition(self, tmp_project, capsys, phase, expected_fragment):
_make_task(tmp_project, "test-task", phase)
args = _Args(project=str(tmp_project))
rc = ap.cmd_drive(args)
out = capsys.readouterr().out
assert rc == 0
assert expected_fragment in out, f"Phase '{phase}' should suggest '{expected_fragment}', got:\n{out}"