Files
Aperant/tests/test_implementation_plan.py
T
Andy 4091d1d4b5 fix: prevent OOM, orphaned agents, and unbounded growth during overnight builds (#1813)
* fix(stability): prevent OOM, orphaned agents, and unbounded growth during overnight builds

Address multiple crash/stability issues observed during long-running autonomous builds:

Backend:
- Skip stuck subtasks in get_next_subtask() using attempt_history.json
- Add retry with exponential backoff + jitter for LadybugDB lock contention
- Time-window filter attempt counts (2h window) to prevent unbounded accumulation
- Trim attempt history per subtask (cap at 50) to bound file size
- Use timezone-aware UTC datetimes throughout recovery manager

Frontend:
- Kill all agents on window close to prevent orphaned processes
- Circuit breaker: kill agents after 10 consecutive renderer disposal errors
- Cap batch queue logs at 100 entries (OOM prevention in IPC batching)
- Cap task log entries at 5000 per task (OOM prevention in store)

Tests:
- Add lock retry logic tests (lock detection, backoff, retry exhaustion)
- Add stuck subtask skipping tests (skip, corrupt JSON, all-stuck)
- Add time-window filtering and attempt trimming tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(stability): address PR review findings for crash stability

- Add .catch() to async killAll() calls to prevent unhandled promise rejections
  (index.ts on window close, utils.ts circuit breaker)
- Reset circuitBreakerTriggered on successful send so it can re-trigger after
  renderer recovery followed by a second crash
- Fix agentManagerRef type to reflect async killAll() signature
- Switch %-format logging to f-strings for consistency with codebase convention

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:23:17 +01:00

1774 lines
62 KiB
Python

#!/usr/bin/env python3
"""
Tests for Implementation Plan Management
========================================
Tests the implementation_plan.py module functionality including:
- Data structures (Subtask, Phase, ImplementationPlan)
- Status transitions
- Progress tracking
- Dependency resolution
- Plan serialization
"""
import json
import pytest
from datetime import datetime
from pathlib import Path
from implementation_plan import (
ImplementationPlan,
Phase,
Subtask,
Verification,
WorkflowType,
PhaseType,
SubtaskStatus,
VerificationType,
create_feature_plan,
create_investigation_plan,
create_refactor_plan,
)
class TestSubtask:
"""Tests for Subtask data structure."""
def test_create_simple_chunk(self):
"""Creates a simple chunk with defaults."""
chunk = Subtask(
id="chunk-1",
description="Implement user model",
)
assert chunk.id == "chunk-1"
assert chunk.description == "Implement user model"
assert chunk.status == SubtaskStatus.PENDING
assert chunk.service is None
assert chunk.files_to_modify == []
assert chunk.files_to_create == []
def test_create_full_chunk(self):
"""Creates a chunk with all fields."""
chunk = Subtask(
id="chunk-2",
description="Add API endpoint",
status=SubtaskStatus.IN_PROGRESS,
service="backend",
files_to_modify=["app/routes.py"],
files_to_create=["app/models/user.py"],
patterns_from=["app/models/profile.py"],
)
assert chunk.service == "backend"
assert "app/routes.py" in chunk.files_to_modify
assert "app/models/user.py" in chunk.files_to_create
def test_chunk_start(self):
"""Subtask can be started."""
chunk = Subtask(id="test", description="Test")
chunk.start(session_id=1)
assert chunk.status == SubtaskStatus.IN_PROGRESS
assert chunk.started_at is not None
assert chunk.session_id == 1
def test_chunk_complete(self):
"""Subtask can be completed."""
chunk = Subtask(id="test", description="Test")
chunk.start(session_id=1)
chunk.complete(output="Done successfully")
assert chunk.status == SubtaskStatus.COMPLETED
assert chunk.completed_at is not None
assert chunk.actual_output == "Done successfully"
def test_chunk_fail(self):
"""Subtask can be marked as failed."""
chunk = Subtask(id="test", description="Test")
chunk.start(session_id=1)
chunk.fail(reason="Test error")
assert chunk.status == SubtaskStatus.FAILED
assert "FAILED: Test error" in chunk.actual_output
def test_chunk_to_dict(self):
"""Subtask serializes to dict correctly."""
chunk = Subtask(
id="chunk-1",
description="Test chunk",
service="backend",
files_to_modify=["file.py"],
)
data = chunk.to_dict()
assert data["id"] == "chunk-1"
assert data["description"] == "Test chunk"
assert data["status"] == "pending"
assert data["service"] == "backend"
assert "file.py" in data["files_to_modify"]
def test_chunk_from_dict(self):
"""Subtask deserializes from dict correctly."""
data = {
"id": "chunk-1",
"description": "Test chunk",
"status": "completed",
"service": "frontend",
}
chunk = Subtask.from_dict(data)
assert chunk.id == "chunk-1"
assert chunk.status == SubtaskStatus.COMPLETED
assert chunk.service == "frontend"
class TestVerification:
"""Tests for Verification data structure."""
def test_command_verification(self):
"""Creates command-type verification."""
verification = Verification(
type=VerificationType.COMMAND,
run="pytest tests/",
)
assert verification.type == VerificationType.COMMAND
assert verification.run == "pytest tests/"
def test_api_verification(self):
"""Creates API-type verification."""
verification = Verification(
type=VerificationType.API,
url="/api/users",
method="POST",
expect_status=201,
)
assert verification.type == VerificationType.API
assert verification.method == "POST"
assert verification.expect_status == 201
def test_verification_to_dict(self):
"""Verification serializes to dict."""
verification = Verification(
type=VerificationType.BROWSER,
scenario="User can upload avatar",
)
data = verification.to_dict()
assert data["type"] == "browser"
assert data["scenario"] == "User can upload avatar"
def test_verification_from_dict(self):
"""Verification deserializes from dict."""
data = {
"type": "command",
"run": "npm test",
}
verification = Verification.from_dict(data)
assert verification.type == VerificationType.COMMAND
assert verification.run == "npm test"
class TestPhase:
"""Tests for Phase data structure."""
def test_create_phase(self):
"""Creates a phase with chunks."""
chunk1 = Subtask(id="c1", description="Chunk 1")
chunk2 = Subtask(id="c2", description="Chunk 2")
phase = Phase(
phase=1,
name="Setup",
type=PhaseType.SETUP,
subtasks=[chunk1, chunk2],
)
assert phase.phase == 1
assert phase.name == "Setup"
assert len(phase.subtasks) == 2
def test_phase_is_complete(self):
"""Phase completion checks all chunks."""
chunk1 = Subtask(id="c1", description="Chunk 1", status=SubtaskStatus.COMPLETED)
chunk2 = Subtask(id="c2", description="Chunk 2", status=SubtaskStatus.COMPLETED)
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2])
assert phase.is_complete() is True
def test_phase_not_complete_with_pending(self):
"""Phase not complete with pending chunks."""
chunk1 = Subtask(id="c1", description="Chunk 1", status=SubtaskStatus.COMPLETED)
chunk2 = Subtask(id="c2", description="Chunk 2", status=SubtaskStatus.PENDING)
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2])
assert phase.is_complete() is False
def test_phase_get_pending_chunks(self):
"""Gets pending chunks from phase."""
chunk1 = Subtask(id="c1", description="Chunk 1", status=SubtaskStatus.COMPLETED)
chunk2 = Subtask(id="c2", description="Chunk 2", status=SubtaskStatus.PENDING)
chunk3 = Subtask(id="c3", description="Chunk 3", status=SubtaskStatus.PENDING)
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2, chunk3])
pending = phase.get_pending_chunks()
assert len(pending) == 2
assert all(c.status == SubtaskStatus.PENDING for c in pending)
def test_phase_get_progress(self):
"""Gets progress counts from phase."""
chunk1 = Subtask(id="c1", description="Chunk 1", status=SubtaskStatus.COMPLETED)
chunk2 = Subtask(id="c2", description="Chunk 2", status=SubtaskStatus.COMPLETED)
chunk3 = Subtask(id="c3", description="Chunk 3", status=SubtaskStatus.PENDING)
phase = Phase(phase=1, name="Test", subtasks=[chunk1, chunk2, chunk3])
completed, total = phase.get_progress()
assert completed == 2
assert total == 3
def test_phase_to_dict(self):
"""Phase serializes to dict."""
chunk = Subtask(id="c1", description="Test")
phase = Phase(
phase=1,
name="Setup",
type=PhaseType.SETUP,
subtasks=[chunk],
depends_on=[],
)
data = phase.to_dict()
assert data["phase"] == 1
assert data["name"] == "Setup"
assert data["type"] == "setup"
assert len(data["chunks"]) == 1
def test_phase_from_dict(self):
"""Phase deserializes from dict."""
data = {
"phase": 2,
"name": "Implementation",
"type": "implementation",
"chunks": [{"id": "c1", "description": "Test"}],
"depends_on": [1],
}
phase = Phase.from_dict(data)
assert phase.phase == 2
assert phase.type == PhaseType.IMPLEMENTATION
assert len(phase.subtasks) == 1
assert 1 in phase.depends_on
class TestImplementationPlan:
"""Tests for ImplementationPlan data structure."""
def test_create_plan(self):
"""Creates an implementation plan."""
plan = ImplementationPlan(
feature="User Authentication",
workflow_type=WorkflowType.FEATURE,
services_involved=["backend", "frontend"],
)
assert plan.feature == "User Authentication"
assert plan.workflow_type == WorkflowType.FEATURE
assert "backend" in plan.services_involved
def test_plan_get_available_phases(self, sample_implementation_plan: dict):
"""Gets phases with satisfied dependencies."""
plan = ImplementationPlan.from_dict(sample_implementation_plan)
# Mark phase 1 as complete
for chunk in plan.phases[0].subtasks:
chunk.status = SubtaskStatus.COMPLETED
available = plan.get_available_phases()
# Phase 2 and 3 depend on phase 1, so they should be available
phase_nums = [p.phase for p in available]
assert 2 in phase_nums
assert 3 in phase_nums
def test_plan_get_next_subtask(self, sample_implementation_plan: dict):
"""Gets next subtask to work on."""
plan = ImplementationPlan.from_dict(sample_implementation_plan)
result = plan.get_next_subtask()
assert result is not None
phase, subtask = result
# Should be first pending subtask in phase 1
assert phase.phase == 1
assert subtask.status == SubtaskStatus.PENDING
def test_plan_get_progress(self, sample_implementation_plan: dict):
"""Gets overall progress."""
plan = ImplementationPlan.from_dict(sample_implementation_plan)
# Complete some subtasks
plan.phases[0].subtasks[0].status = SubtaskStatus.COMPLETED
progress = plan.get_progress()
assert progress["total_phases"] == 3
assert progress["total_subtasks"] == 4 # Based on fixture
assert progress["completed_subtasks"] == 1
assert progress["percent_complete"] == 25.0 # 1/4 = 25%
assert progress["is_complete"] is False
def test_plan_save_and_load(self, temp_dir: Path, sample_implementation_plan: dict):
"""Plan saves and loads correctly."""
plan = ImplementationPlan.from_dict(sample_implementation_plan)
plan_path = temp_dir / "plan.json"
plan.save(plan_path)
loaded = ImplementationPlan.load(plan_path)
assert loaded.feature == plan.feature
assert len(loaded.phases) == len(plan.phases)
assert loaded.updated_at is not None
def test_plan_to_dict(self, sample_implementation_plan: dict):
"""Plan serializes to dict."""
plan = ImplementationPlan.from_dict(sample_implementation_plan)
data = plan.to_dict()
assert data["feature"] == "User Avatar Upload"
assert data["workflow_type"] == "feature"
assert len(data["phases"]) == 3
def test_plan_from_dict(self, sample_implementation_plan: dict):
"""Plan deserializes from dict."""
plan = ImplementationPlan.from_dict(sample_implementation_plan)
assert plan.feature == "User Avatar Upload"
assert plan.workflow_type == WorkflowType.FEATURE
assert len(plan.services_involved) == 3
def test_plan_status_summary(self, sample_implementation_plan: dict):
"""Plan generates status summary."""
plan = ImplementationPlan.from_dict(sample_implementation_plan)
summary = plan.get_status_summary()
assert "User Avatar Upload" in summary
assert "feature" in summary
assert "0%" in summary or "chunks" in summary
class TestCreateFeaturePlan:
"""Tests for create_feature_plan helper."""
def test_creates_basic_plan(self):
"""Creates a feature plan with phases."""
phases_config = [
{
"name": "Backend",
"chunks": [
{"id": "api", "description": "Add API endpoint"},
],
},
{
"name": "Frontend",
"depends_on": [1],
"chunks": [
{"id": "ui", "description": "Add UI component"},
],
},
]
plan = create_feature_plan(
feature="User Profile",
services=["backend", "frontend"],
phases_config=phases_config,
)
assert plan.feature == "User Profile"
assert plan.workflow_type == WorkflowType.FEATURE
assert len(plan.phases) == 2
assert plan.phases[1].depends_on == [1]
def test_sets_parallel_safe(self):
"""Respects parallel_safe flag."""
phases_config = [
{
"name": "Parallel Phase",
"parallel_safe": True,
"chunks": [
{"id": "c1", "description": "Chunk 1"},
{"id": "c2", "description": "Chunk 2"},
],
},
]
plan = create_feature_plan(
feature="Test",
services=["backend"],
phases_config=phases_config,
)
assert plan.phases[0].parallel_safe is True
class TestCreateInvestigationPlan:
"""Tests for create_investigation_plan helper."""
def test_creates_investigation_plan(self):
"""Creates an investigation plan for debugging."""
plan = create_investigation_plan(
bug_description="Login fails for users with special characters",
services=["backend", "frontend"],
)
assert "Fix:" in plan.feature
assert plan.workflow_type == WorkflowType.INVESTIGATION
assert len(plan.phases) == 3 # Reproduce, Investigate, Fix
def test_has_blocked_fix_chunks(self):
"""Fix phase starts blocked."""
plan = create_investigation_plan(
bug_description="Test bug",
services=["backend"],
)
# Fix phase should have blocked chunks
fix_phase = plan.phases[2] # Phase 3 - Fix
assert any(c.status == SubtaskStatus.BLOCKED for c in fix_phase.subtasks)
class TestCreateRefactorPlan:
"""Tests for create_refactor_plan helper."""
def test_creates_refactor_plan(self):
"""Creates a refactor plan with stages."""
stages = [
{
"name": "Add New System",
"chunks": [
{"id": "new-api", "description": "Add new API"},
],
},
{
"name": "Migrate Consumers",
"chunks": [
{"id": "migrate", "description": "Update consumers"},
],
},
{
"name": "Remove Old System",
"chunks": [
{"id": "remove", "description": "Remove old code"},
],
},
]
plan = create_refactor_plan(
refactor_description="Replace auth system",
services=["backend"],
stages=stages,
)
assert plan.workflow_type == WorkflowType.REFACTOR
assert len(plan.phases) == 3
# Each phase should depend on the previous
assert plan.phases[1].depends_on == [1]
assert plan.phases[2].depends_on == [2]
class TestDependencyResolution:
"""Tests for phase dependency resolution."""
def test_no_available_phases_when_deps_not_met(self):
"""No phases available when dependencies aren't met."""
plan = ImplementationPlan(
feature="Test",
phases=[
Phase(phase=1, name="Setup", subtasks=[
Subtask(id="c1", description="Setup", status=SubtaskStatus.PENDING)
]),
Phase(phase=2, name="Build", depends_on=[1], subtasks=[
Subtask(id="c2", description="Build")
]),
],
)
available = plan.get_available_phases()
# Only phase 1 should be available (no dependencies)
assert len(available) == 1
assert available[0].phase == 1
def test_multiple_phases_available_parallel(self):
"""Multiple phases can be available in parallel."""
plan = ImplementationPlan(
feature="Test",
phases=[
Phase(phase=1, name="Setup", subtasks=[
Subtask(id="c1", description="Setup", status=SubtaskStatus.COMPLETED)
]),
Phase(phase=2, name="Backend", depends_on=[1], subtasks=[
Subtask(id="c2", description="Backend")
]),
Phase(phase=3, name="Frontend", depends_on=[1], subtasks=[
Subtask(id="c3", description="Frontend")
]),
],
)
available = plan.get_available_phases()
# Phases 2 and 3 should both be available (both depend only on phase 1)
assert len(available) == 2
phase_nums = [p.phase for p in available]
assert 2 in phase_nums
assert 3 in phase_nums
def test_phase_blocked_by_multiple_deps(self):
"""Phase blocked when any dependency not met."""
plan = ImplementationPlan(
feature="Test",
phases=[
Phase(phase=1, name="Phase1", subtasks=[
Subtask(id="c1", description="C1", status=SubtaskStatus.COMPLETED)
]),
Phase(phase=2, name="Phase2", subtasks=[
Subtask(id="c2", description="C2", status=SubtaskStatus.PENDING)
]),
Phase(phase=3, name="Phase3", depends_on=[1, 2], subtasks=[
Subtask(id="c3", description="C3")
]),
],
)
available = plan.get_available_phases()
# Phase 3 requires both 1 and 2, but 2 isn't complete
phase_nums = [p.phase for p in available]
assert 3 not in phase_nums
class TestSubtaskCritique:
"""Tests for self-critique functionality on subtasks."""
def test_chunk_stores_critique_result(self):
"""Subtask can store critique results."""
chunk = Subtask(id="test", description="Test")
chunk.critique_result = {
"passed": True,
"issues": [],
"suggestions": ["Consider adding error handling"],
}
assert chunk.critique_result["passed"] is True
def test_critique_serializes(self):
"""Critique result serializes correctly."""
chunk = Subtask(id="test", description="Test")
chunk.critique_result = {"passed": False, "issues": ["Missing tests"]}
data = chunk.to_dict()
assert "critique_result" in data
assert data["critique_result"]["passed"] is False
def test_critique_deserializes(self):
"""Critique result deserializes correctly."""
data = {
"id": "test",
"description": "Test",
"critique_result": {"passed": True, "score": 8},
}
chunk = Subtask.from_dict(data)
assert chunk.critique_result is not None
assert chunk.critique_result["score"] == 8
class TestSchemaValidation:
"""Tests for JSON schema validation of implementation plans."""
# =========================================================================
# Valid Schema Tests
# =========================================================================
def test_valid_minimal_plan_schema(self):
"""Minimal valid plan with required fields passes validation."""
valid_plan = {
"feature": "Test Feature",
"workflow_type": "feature",
"phases": [
{
"phase": 1,
"name": "Setup",
"subtasks": [
{"id": "task-1", "description": "Do something", "status": "pending"}
],
}
],
}
plan = ImplementationPlan.from_dict(valid_plan)
assert plan.feature == "Test Feature"
assert plan.workflow_type == WorkflowType.FEATURE
assert len(plan.phases) == 1
assert len(plan.phases[0].subtasks) == 1
def test_valid_full_plan_schema(self):
"""Full plan with all optional fields validates correctly."""
valid_plan = {
"feature": "User Authentication",
"workflow_type": "feature",
"services_involved": ["backend", "frontend", "worker"],
"phases": [
{
"phase": 1,
"name": "Backend Foundation",
"type": "setup",
"depends_on": [],
"parallel_safe": True,
"subtasks": [
{
"id": "subtask-1-1",
"description": "Add user model",
"status": "completed",
"service": "backend",
"files_to_modify": ["app/models.py"],
"files_to_create": ["app/auth.py"],
"patterns_from": ["app/base_model.py"],
"verification": {
"type": "command",
"run": "pytest tests/",
},
"expected_output": "Tests pass",
"actual_output": "All 5 tests passed",
"started_at": "2024-01-01T10:00:00",
"completed_at": "2024-01-01T10:30:00",
"session_id": 1,
}
],
},
{
"phase": 2,
"name": "Frontend Integration",
"type": "implementation",
"depends_on": [1],
"subtasks": [
{
"id": "subtask-2-1",
"description": "Add login form",
"status": "pending",
"service": "frontend",
}
],
},
],
"final_acceptance": [
"User can log in",
"Sessions persist across refreshes",
],
"created_at": "2024-01-01T09:00:00",
"updated_at": "2024-01-01T10:30:00",
"spec_file": "spec.md",
}
plan = ImplementationPlan.from_dict(valid_plan)
assert plan.feature == "User Authentication"
assert len(plan.services_involved) == 3
assert len(plan.phases) == 2
assert plan.phases[0].parallel_safe is True
assert plan.phases[1].depends_on == [1]
assert len(plan.final_acceptance) == 2
def test_all_workflow_types_valid(self):
"""All defined workflow types are accepted."""
workflow_types = ["feature", "refactor", "investigation", "migration", "simple"]
for wf_type in workflow_types:
plan_data = {
"feature": f"Test {wf_type}",
"workflow_type": wf_type,
"phases": [
{
"phase": 1,
"name": "Test Phase",
"subtasks": [
{"id": "t1", "description": "Test", "status": "pending"}
],
}
],
}
plan = ImplementationPlan.from_dict(plan_data)
assert plan.workflow_type.value == wf_type
def test_all_phase_types_valid(self):
"""All defined phase types are accepted."""
phase_types = ["setup", "implementation", "investigation", "integration", "cleanup"]
for phase_type in phase_types:
plan_data = {
"feature": "Test",
"workflow_type": "feature",
"phases": [
{
"phase": 1,
"name": "Test Phase",
"type": phase_type,
"subtasks": [
{"id": "t1", "description": "Test", "status": "pending"}
],
}
],
}
plan = ImplementationPlan.from_dict(plan_data)
assert plan.phases[0].type.value == phase_type
def test_all_subtask_statuses_valid(self):
"""All defined subtask statuses are accepted."""
statuses = ["pending", "in_progress", "completed", "blocked", "failed"]
for status in statuses:
subtask_data = {
"id": "test",
"description": "Test subtask",
"status": status,
}
subtask = Subtask.from_dict(subtask_data)
assert subtask.status.value == status
def test_all_verification_types_valid(self):
"""All defined verification types are accepted."""
ver_types = ["command", "api", "browser", "component", "manual", "none"]
for ver_type in ver_types:
ver_data = {"type": ver_type}
verification = Verification.from_dict(ver_data)
assert verification.type.value == ver_type
# =========================================================================
# Invalid Schema Tests - Missing Required Fields
# =========================================================================
def test_invalid_plan_missing_feature_uses_default(self):
"""Plan without feature field uses default name."""
invalid_plan = {
"workflow_type": "feature",
"phases": [
{
"phase": 1,
"name": "Test",
"subtasks": [
{"id": "t1", "description": "Test", "status": "pending"}
],
}
],
}
plan = ImplementationPlan.from_dict(invalid_plan)
assert plan.feature == "Unnamed Feature"
def test_invalid_plan_missing_workflow_type_uses_default(self):
"""Plan without workflow_type uses default."""
invalid_plan = {
"feature": "Test",
"phases": [
{
"phase": 1,
"name": "Test",
"subtasks": [
{"id": "t1", "description": "Test", "status": "pending"}
],
}
],
}
plan = ImplementationPlan.from_dict(invalid_plan)
assert plan.workflow_type == WorkflowType.FEATURE
def test_invalid_plan_missing_phases_creates_empty_list(self):
"""Plan without phases creates empty phases list."""
invalid_plan = {
"feature": "Test",
"workflow_type": "feature",
}
plan = ImplementationPlan.from_dict(invalid_plan)
assert plan.phases == []
def test_invalid_phase_missing_name_uses_fallback(self):
"""Phase without name uses fallback name."""
plan_data = {
"feature": "Test",
"workflow_type": "feature",
"phases": [
{
"phase": 1,
"subtasks": [
{"id": "t1", "description": "Test", "status": "pending"}
],
}
],
}
plan = ImplementationPlan.from_dict(plan_data)
assert plan.phases[0].name == "Phase 1"
def test_invalid_phase_missing_subtasks_creates_empty_list(self):
"""Phase without subtasks creates empty subtasks list."""
plan_data = {
"feature": "Test",
"workflow_type": "feature",
"phases": [
{
"phase": 1,
"name": "Empty Phase",
}
],
}
plan = ImplementationPlan.from_dict(plan_data)
assert plan.phases[0].subtasks == []
def test_invalid_subtask_missing_status_uses_default(self):
"""Subtask without status defaults to pending."""
subtask_data = {
"id": "test",
"description": "Test subtask",
}
subtask = Subtask.from_dict(subtask_data)
assert subtask.status == SubtaskStatus.PENDING
# =========================================================================
# Invalid Schema Tests - Wrong Types
# =========================================================================
def test_invalid_workflow_type_falls_back_to_feature(self):
"""Unknown workflow_type falls back to feature with warning."""
invalid_plan = {
"feature": "Test",
"workflow_type": "invalid_type",
"phases": [],
}
plan = ImplementationPlan.from_dict(invalid_plan)
assert plan.workflow_type == WorkflowType.FEATURE
def test_invalid_subtask_status_raises_error(self):
"""Invalid subtask status raises ValueError."""
subtask_data = {
"id": "test",
"description": "Test",
"status": "invalid_status",
}
with pytest.raises(ValueError):
Subtask.from_dict(subtask_data)
def test_invalid_phase_type_raises_error(self):
"""Invalid phase type raises ValueError."""
plan_data = {
"feature": "Test",
"workflow_type": "feature",
"phases": [
{
"phase": 1,
"name": "Test",
"type": "invalid_type",
"subtasks": [],
}
],
}
with pytest.raises(ValueError):
ImplementationPlan.from_dict(plan_data)
def test_invalid_verification_type_raises_error(self):
"""Invalid verification type raises ValueError."""
ver_data = {"type": "invalid_type"}
with pytest.raises(ValueError):
Verification.from_dict(ver_data)
# =========================================================================
# Edge Cases
# =========================================================================
def test_empty_plan_schema(self):
"""Completely empty dict creates plan with defaults."""
plan = ImplementationPlan.from_dict({})
assert plan.feature == "Unnamed Feature"
assert plan.workflow_type == WorkflowType.FEATURE
assert plan.phases == []
assert plan.services_involved == []
def test_plan_with_title_field_instead_of_feature(self):
"""Plan with 'title' field instead of 'feature' works."""
plan_data = {
"title": "My Feature Title",
"workflow_type": "feature",
"phases": [],
}
plan = ImplementationPlan.from_dict(plan_data)
assert plan.feature == "My Feature Title"
def test_phase_with_chunks_field_instead_of_subtasks(self):
"""Phase with 'chunks' field (legacy) works."""
plan_data = {
"feature": "Test",
"workflow_type": "feature",
"phases": [
{
"phase": 1,
"name": "Test Phase",
"chunks": [
{"id": "t1", "description": "Test", "status": "pending"}
],
}
],
}
plan = ImplementationPlan.from_dict(plan_data)
assert len(plan.phases[0].subtasks) == 1
assert plan.phases[0].subtasks[0].id == "t1"
def test_plan_preserves_qa_signoff_structure(self):
"""Plan preserves qa_signoff dict structure."""
plan_data = {
"feature": "Test",
"workflow_type": "feature",
"phases": [],
"qa_signoff": {
"status": "approved",
"qa_session": 1,
"timestamp": "2024-01-01T12:00:00",
"tests_passed": {"unit": True, "integration": True},
},
}
plan = ImplementationPlan.from_dict(plan_data)
assert plan.qa_signoff is not None
assert plan.qa_signoff["status"] == "approved"
assert plan.qa_signoff["qa_session"] == 1
assert plan.qa_signoff["tests_passed"]["unit"] is True
def test_subtask_with_all_optional_fields(self):
"""Subtask with all optional fields deserializes correctly."""
subtask_data = {
"id": "complex-task",
"description": "Complex task with all fields",
"status": "completed",
"service": "backend",
"all_services": True,
"files_to_modify": ["file1.py", "file2.py"],
"files_to_create": ["new_file.py"],
"patterns_from": ["pattern.py"],
"verification": {"type": "command", "run": "pytest"},
"expected_output": "Tests pass",
"actual_output": "All tests passed",
"started_at": "2024-01-01T10:00:00",
"completed_at": "2024-01-01T10:30:00",
"session_id": 42,
"critique_result": {"passed": True, "score": 9},
}
subtask = Subtask.from_dict(subtask_data)
assert subtask.id == "complex-task"
assert subtask.service == "backend"
assert subtask.all_services is True
assert len(subtask.files_to_modify) == 2
assert subtask.verification.type == VerificationType.COMMAND
assert subtask.session_id == 42
assert subtask.critique_result["score"] == 9
def test_verification_with_api_fields(self):
"""API verification with all fields deserializes correctly."""
ver_data = {
"type": "api",
"url": "/api/users",
"method": "POST",
"expect_status": 201,
"expect_contains": "user_id",
}
verification = Verification.from_dict(ver_data)
assert verification.type == VerificationType.API
assert verification.url == "/api/users"
assert verification.method == "POST"
assert verification.expect_status == 201
assert verification.expect_contains == "user_id"
def test_verification_with_browser_scenario(self):
"""Browser verification with scenario deserializes correctly."""
ver_data = {
"type": "browser",
"scenario": "User can click login button and see dashboard",
}
verification = Verification.from_dict(ver_data)
assert verification.type == VerificationType.BROWSER
assert verification.scenario == "User can click login button and see dashboard"
def test_plan_round_trip_preserves_data(self):
"""Plan survives to_dict/from_dict round trip."""
original_plan = ImplementationPlan(
feature="Round Trip Test",
workflow_type=WorkflowType.REFACTOR,
services_involved=["backend", "frontend"],
phases=[
Phase(
phase=1,
name="Phase One",
type=PhaseType.SETUP,
subtasks=[
Subtask(
id="task-1",
description="First task",
status=SubtaskStatus.COMPLETED,
service="backend",
files_to_modify=["file.py"],
verification=Verification(
type=VerificationType.COMMAND,
run="pytest",
),
)
],
depends_on=[],
parallel_safe=True,
)
],
final_acceptance=["Feature works"],
)
# Round trip
data = original_plan.to_dict()
restored_plan = ImplementationPlan.from_dict(data)
# Verify
assert restored_plan.feature == original_plan.feature
assert restored_plan.workflow_type == original_plan.workflow_type
assert restored_plan.services_involved == original_plan.services_involved
assert len(restored_plan.phases) == len(original_plan.phases)
assert restored_plan.phases[0].name == original_plan.phases[0].name
assert restored_plan.phases[0].parallel_safe == original_plan.phases[0].parallel_safe
assert len(restored_plan.phases[0].subtasks) == len(original_plan.phases[0].subtasks)
assert restored_plan.phases[0].subtasks[0].id == original_plan.phases[0].subtasks[0].id
assert restored_plan.phases[0].subtasks[0].verification.run == "pytest"
def test_deeply_nested_phases_with_dependencies(self):
"""Plan with complex phase dependencies deserializes correctly."""
plan_data = {
"feature": "Complex Feature",
"workflow_type": "feature",
"phases": [
{
"phase": 1,
"name": "Foundation",
"depends_on": [],
"subtasks": [{"id": "t1", "description": "Task 1", "status": "completed"}],
},
{
"phase": 2,
"name": "Build A",
"depends_on": [1],
"subtasks": [{"id": "t2", "description": "Task 2", "status": "completed"}],
},
{
"phase": 3,
"name": "Build B",
"depends_on": [1],
"subtasks": [{"id": "t3", "description": "Task 3", "status": "pending"}],
},
{
"phase": 4,
"name": "Integration",
"depends_on": [2, 3],
"subtasks": [{"id": "t4", "description": "Task 4", "status": "pending"}],
},
],
}
plan = ImplementationPlan.from_dict(plan_data)
assert len(plan.phases) == 4
assert plan.phases[0].depends_on == []
assert plan.phases[1].depends_on == [1]
assert plan.phases[2].depends_on == [1]
assert plan.phases[3].depends_on == [2, 3]
# Test dependency resolution
available = plan.get_available_phases()
# Phase 1 complete, so phases 2 and 3 should be available (but 3 is pending, 2 is complete)
# Actually phase 2 is also complete, so phase 4 should check if 2 AND 3 are done
# Phase 3 has pending subtask, so phase 4 is not available
phase_nums = [p.phase for p in available]
assert 3 in phase_nums # Phase 3 depends on 1 (complete), has pending work
assert 4 not in phase_nums # Phase 4 depends on 2 AND 3, but 3 not complete
def test_plan_status_fields_preserved(self):
"""Plan status and planStatus fields are preserved."""
plan_data = {
"feature": "Test",
"workflow_type": "feature",
"phases": [],
"status": "in_progress",
"planStatus": "in_progress",
"recoveryNote": "Resumed after crash",
}
plan = ImplementationPlan.from_dict(plan_data)
assert plan.status == "in_progress"
assert plan.planStatus == "in_progress"
assert plan.recoveryNote == "Resumed after crash"
# Verify they serialize back
data = plan.to_dict()
assert data["status"] == "in_progress"
assert data["planStatus"] == "in_progress"
assert data["recoveryNote"] == "Resumed after crash"
class TestEdgeCaseStateTransitions:
"""Tests for edge cases in plan state transitions (stuck, skipped, blocked)."""
# =========================================================================
# BLOCKED Status Tests
# =========================================================================
def test_chunk_blocked_status_initialization(self):
"""Chunk can be initialized with blocked status."""
chunk = Subtask(
id="blocked-task",
description="Task waiting for investigation results",
status=SubtaskStatus.BLOCKED,
)
assert chunk.status == SubtaskStatus.BLOCKED
assert chunk.started_at is None
assert chunk.completed_at is None
def test_chunk_blocked_to_pending_transition(self):
"""Blocked chunk can transition to pending (unblocking)."""
chunk = Subtask(id="test", description="Test", status=SubtaskStatus.BLOCKED)
# Manually unblock by setting to pending
chunk.status = SubtaskStatus.PENDING
assert chunk.status == SubtaskStatus.PENDING
def test_chunk_blocked_to_in_progress_transition(self):
"""Blocked chunk can be started directly (auto-unblock)."""
chunk = Subtask(id="test", description="Test", status=SubtaskStatus.BLOCKED)
chunk.start(session_id=1)
assert chunk.status == SubtaskStatus.IN_PROGRESS
assert chunk.started_at is not None
assert chunk.session_id == 1
def test_blocked_chunk_serialization_roundtrip(self):
"""Blocked status survives serialization/deserialization."""
chunk = Subtask(
id="blocked-task",
description="Blocked task",
status=SubtaskStatus.BLOCKED,
)
data = chunk.to_dict()
restored = Subtask.from_dict(data)
assert restored.status == SubtaskStatus.BLOCKED
assert data["status"] == "blocked"
def test_phase_with_all_blocked_chunks(self):
"""Phase with all blocked chunks is not complete."""
phase = Phase(
phase=1,
name="Blocked Phase",
subtasks=[
Subtask(id="c1", description="Task 1", status=SubtaskStatus.BLOCKED),
Subtask(id="c2", description="Task 2", status=SubtaskStatus.BLOCKED),
],
)
assert phase.is_complete() is False
assert phase.get_pending_subtasks() == [] # Blocked != pending
completed, total = phase.get_progress()
assert completed == 0
assert total == 2
def test_phase_completion_ignores_blocked_chunks(self):
"""Phase is not complete if any chunks are blocked."""
phase = Phase(
phase=1,
name="Mixed Phase",
subtasks=[
Subtask(id="c1", description="Task 1", status=SubtaskStatus.COMPLETED),
Subtask(id="c2", description="Task 2", status=SubtaskStatus.BLOCKED),
],
)
assert phase.is_complete() is False
completed, total = phase.get_progress()
assert completed == 1
assert total == 2
def test_investigation_plan_blocked_fix_chunks(self):
"""Investigation plan has blocked chunks in fix phase."""
plan = create_investigation_plan(
bug_description="User login fails intermittently",
services=["backend"],
)
fix_phase = plan.phases[2] # Phase 3 - Fix
blocked_chunks = [c for c in fix_phase.subtasks if c.status == SubtaskStatus.BLOCKED]
assert len(blocked_chunks) == 2
assert any("fix" in c.id.lower() for c in blocked_chunks)
assert any("regression" in c.id.lower() for c in blocked_chunks)
# =========================================================================
# STUCK Plan Tests
# =========================================================================
def test_plan_stuck_all_phases_blocked(self):
"""Plan is stuck when all available phases have only blocked subtasks."""
plan = ImplementationPlan(
feature="Stuck Plan",
phases=[
Phase(
phase=1,
name="Phase 1",
subtasks=[
Subtask(id="c1", description="Blocked", status=SubtaskStatus.BLOCKED),
],
),
],
)
# No pending subtasks available
result = plan.get_next_subtask()
assert result is None
def test_plan_stuck_due_to_unmet_dependencies(self):
"""Plan is stuck when all phases have unmet dependencies."""
plan = ImplementationPlan(
feature="Dependency Deadlock",
phases=[
Phase(
phase=1,
name="Phase 1",
subtasks=[
Subtask(id="c1", description="Task 1", status=SubtaskStatus.PENDING),
],
depends_on=[2], # Circular dependency
),
Phase(
phase=2,
name="Phase 2",
subtasks=[
Subtask(id="c2", description="Task 2", status=SubtaskStatus.PENDING),
],
depends_on=[1], # Circular dependency
),
],
)
# Both phases depend on each other - neither can proceed
available = plan.get_available_phases()
assert len(available) == 0
result = plan.get_next_subtask()
assert result is None
def test_plan_stuck_message_in_status_summary(self):
"""Status summary shows BLOCKED when no work available."""
plan = ImplementationPlan(
feature="Stuck Feature",
phases=[
Phase(
phase=1,
name="Waiting Phase",
subtasks=[
Subtask(id="c1", description="Blocked task", status=SubtaskStatus.BLOCKED),
],
),
],
)
summary = plan.get_status_summary()
assert "BLOCKED" in summary
assert "No available subtasks" in summary
def test_plan_stuck_with_failed_subtasks(self):
"""Plan with only failed subtasks shows stuck state."""
plan = ImplementationPlan(
feature="Failed Plan",
phases=[
Phase(
phase=1,
name="Phase 1",
subtasks=[
Subtask(id="c1", description="Failed task", status=SubtaskStatus.FAILED),
],
),
],
)
# Failed subtasks are not pending, so no work available
result = plan.get_next_subtask()
assert result is None
progress = plan.get_progress()
assert progress["failed_subtasks"] == 1
assert progress["is_complete"] is False
def test_plan_progress_includes_failed_count(self):
"""Progress tracking includes failed subtask count."""
plan = ImplementationPlan(
feature="Mixed Status",
phases=[
Phase(
phase=1,
name="Phase 1",
subtasks=[
Subtask(id="c1", description="Done", status=SubtaskStatus.COMPLETED),
Subtask(id="c2", description="Failed", status=SubtaskStatus.FAILED),
Subtask(id="c3", description="Blocked", status=SubtaskStatus.BLOCKED),
Subtask(id="c4", description="Pending", status=SubtaskStatus.PENDING),
],
),
],
)
progress = plan.get_progress()
assert progress["completed_subtasks"] == 1
assert progress["failed_subtasks"] == 1
assert progress["total_subtasks"] == 4
assert progress["percent_complete"] == 25.0
assert progress["is_complete"] is False
# =========================================================================
# SKIPPED Scenarios Tests (no explicit status, but behavior tests)
# =========================================================================
def test_phase_skipped_when_no_subtasks(self):
"""Empty phase is considered complete (skipped)."""
phase = Phase(
phase=1,
name="Empty Phase",
subtasks=[],
)
# Empty phase counts as complete
assert phase.is_complete() is True
completed, total = phase.get_progress()
assert completed == 0
assert total == 0
def test_plan_skips_empty_phase_to_next(self):
"""Plan skips empty phases when finding next subtask."""
plan = ImplementationPlan(
feature="Skip Empty Phase",
phases=[
Phase(
phase=1,
name="Empty Setup",
subtasks=[],
),
Phase(
phase=2,
name="Real Work",
depends_on=[1],
subtasks=[
Subtask(id="c1", description="Actual task", status=SubtaskStatus.PENDING),
],
),
],
)
result = plan.get_next_subtask()
assert result is not None
phase, subtask = result
assert phase.phase == 2
assert subtask.id == "c1"
def test_multiple_skipped_phases_chain(self):
"""Chain of empty phases are all skipped correctly."""
plan = ImplementationPlan(
feature="Multi-Skip",
phases=[
Phase(phase=1, name="Empty 1", subtasks=[]),
Phase(phase=2, name="Empty 2", depends_on=[1], subtasks=[]),
Phase(phase=3, name="Empty 3", depends_on=[2], subtasks=[]),
Phase(
phase=4,
name="Work Phase",
depends_on=[3],
subtasks=[
Subtask(id="c1", description="Task", status=SubtaskStatus.PENDING),
],
),
],
)
# All empty phases count as complete, so phase 4 is available
available = plan.get_available_phases()
assert len(available) == 1
assert available[0].phase == 4
def test_completed_phase_skipped_for_next_work(self):
"""Already completed phases are skipped when finding next work."""
plan = ImplementationPlan(
feature="Skip Completed",
phases=[
Phase(
phase=1,
name="Done Phase",
subtasks=[
Subtask(id="c1", description="Done", status=SubtaskStatus.COMPLETED),
],
),
Phase(
phase=2,
name="Work Phase",
depends_on=[1],
subtasks=[
Subtask(id="c2", description="Pending", status=SubtaskStatus.PENDING),
],
),
],
)
result = plan.get_next_subtask()
assert result is not None
phase, subtask = result
assert phase.phase == 2
assert subtask.id == "c2"
# =========================================================================
# Complex State Transition Scenarios
# =========================================================================
def test_blocked_unblocked_complete_transition(self):
"""Full transition from blocked -> pending -> in_progress -> completed."""
chunk = Subtask(id="test", description="Test", status=SubtaskStatus.BLOCKED)
# Unblock
chunk.status = SubtaskStatus.PENDING
assert chunk.status == SubtaskStatus.PENDING
# Start
chunk.start(session_id=1)
assert chunk.status == SubtaskStatus.IN_PROGRESS
assert chunk.started_at is not None
# Complete
chunk.complete(output="Done successfully")
assert chunk.status == SubtaskStatus.COMPLETED
assert chunk.completed_at is not None
assert chunk.actual_output == "Done successfully"
def test_blocked_to_failed_transition(self):
"""Blocked chunk can transition to failed without being started."""
chunk = Subtask(id="test", description="Test", status=SubtaskStatus.BLOCKED)
# Mark as failed directly (e.g., investigation revealed it's not feasible)
chunk.fail(reason="Investigation revealed task is not feasible")
assert chunk.status == SubtaskStatus.FAILED
assert "FAILED: Investigation revealed task is not feasible" in chunk.actual_output
def test_in_progress_subtask_blocks_phase_completion(self):
"""Phase with in_progress subtask is not complete."""
phase = Phase(
phase=1,
name="Active Phase",
subtasks=[
Subtask(id="c1", description="Done", status=SubtaskStatus.COMPLETED),
Subtask(id="c2", description="Working", status=SubtaskStatus.IN_PROGRESS),
],
)
assert phase.is_complete() is False
def test_mixed_blocked_and_failed_prevents_completion(self):
"""Phase with blocked and failed subtasks is not complete."""
phase = Phase(
phase=1,
name="Problematic Phase",
subtasks=[
Subtask(id="c1", description="Blocked", status=SubtaskStatus.BLOCKED),
Subtask(id="c2", description="Failed", status=SubtaskStatus.FAILED),
],
)
assert phase.is_complete() is False
assert phase.get_pending_subtasks() == []
def test_plan_becomes_available_after_unblocking(self):
"""Plan becomes unstuck when blocked subtask is unblocked."""
plan = ImplementationPlan(
feature="Unblock Test",
phases=[
Phase(
phase=1,
name="Blocked Phase",
subtasks=[
Subtask(id="c1", description="Blocked", status=SubtaskStatus.BLOCKED),
],
),
],
)
# Initially stuck
assert plan.get_next_subtask() is None
# Unblock the subtask
plan.phases[0].subtasks[0].status = SubtaskStatus.PENDING
# Now work is available
result = plan.get_next_subtask()
assert result is not None
phase, subtask = result
assert subtask.id == "c1"
def test_failed_subtask_retry_transition(self):
"""Failed subtask can be reset to pending for retry."""
chunk = Subtask(id="test", description="Test", status=SubtaskStatus.FAILED)
chunk.actual_output = "FAILED: Previous error"
# Reset for retry
chunk.status = SubtaskStatus.PENDING
chunk.actual_output = None
chunk.started_at = None
chunk.completed_at = None
assert chunk.status == SubtaskStatus.PENDING
assert chunk.actual_output is None
# Can be started again
chunk.start(session_id=2)
assert chunk.status == SubtaskStatus.IN_PROGRESS
assert chunk.session_id == 2
def test_plan_status_update_with_blocked_subtasks(self):
"""Plan status updates correctly with blocked subtasks."""
plan = ImplementationPlan(
feature="Status Test",
phases=[
Phase(
phase=1,
name="Phase 1",
subtasks=[
Subtask(id="c1", description="Done", status=SubtaskStatus.COMPLETED),
Subtask(id="c2", description="Blocked", status=SubtaskStatus.BLOCKED),
],
),
],
)
plan.update_status_from_subtasks()
# With blocked subtask, plan is still in progress
assert plan.status == "in_progress"
assert plan.planStatus == "in_progress"
def test_all_blocked_subtasks_keeps_plan_in_backlog(self):
"""Plan with all blocked (no completed) subtasks stays in backlog."""
plan = ImplementationPlan(
feature="All Blocked",
phases=[
Phase(
phase=1,
name="Phase 1",
subtasks=[
Subtask(id="c1", description="Blocked 1", status=SubtaskStatus.BLOCKED),
Subtask(id="c2", description="Blocked 2", status=SubtaskStatus.BLOCKED),
],
),
],
)
plan.update_status_from_subtasks()
# All subtasks blocked = effectively pending state = backlog
assert plan.status == "backlog"
assert plan.planStatus == "pending"
# =============================================================================
# STUCK SUBTASK SKIPPING TESTS (progress.py get_next_subtask)
# =============================================================================
class TestStuckSubtaskSkipping:
"""Tests for stuck subtask skipping in progress.get_next_subtask()."""
def _make_plan(self, subtasks):
"""Helper to create a minimal implementation_plan.json dict."""
return {
"feature": "Test",
"workflow_type": "feature",
"phases": [
{
"phase": 1,
"name": "Phase 1",
"depends_on": [],
"subtasks": subtasks,
}
],
}
def _make_attempt_history(self, stuck_ids):
"""Helper to create attempt_history.json with stuck subtasks."""
return {
"subtasks": {},
"stuck_subtasks": [
{"subtask_id": sid, "reason": "stuck", "escalated_at": "2024-01-01T00:00:00"}
for sid in stuck_ids
],
"metadata": {"created_at": "2024-01-01T00:00:00", "last_updated": "2024-01-01T00:00:00"},
}
def test_stuck_subtask_is_skipped(self, temp_dir):
"""Stuck subtasks are skipped when selecting the next subtask."""
from progress import get_next_subtask
spec_dir = temp_dir / "spec"
spec_dir.mkdir(parents=True)
# Create plan with two pending subtasks
plan = self._make_plan([
{"id": "stuck-1", "description": "Stuck task", "status": "pending"},
{"id": "good-1", "description": "Normal task", "status": "pending"},
])
(spec_dir / "implementation_plan.json").write_text(json.dumps(plan))
# Mark stuck-1 as stuck
memory_dir = spec_dir / "memory"
memory_dir.mkdir(parents=True)
history = self._make_attempt_history(["stuck-1"])
(memory_dir / "attempt_history.json").write_text(json.dumps(history))
result = get_next_subtask(spec_dir)
assert result is not None
assert result["id"] == "good-1", "Should skip stuck-1 and select good-1"
def test_normal_subtask_selected_when_stuck_exist(self, temp_dir):
"""Normal pending subtasks are selected even when stuck ones exist."""
from progress import get_next_subtask
spec_dir = temp_dir / "spec"
spec_dir.mkdir(parents=True)
plan = self._make_plan([
{"id": "stuck-a", "description": "Stuck A", "status": "pending"},
{"id": "stuck-b", "description": "Stuck B", "status": "pending"},
{"id": "normal-c", "description": "Normal C", "status": "pending"},
])
(spec_dir / "implementation_plan.json").write_text(json.dumps(plan))
memory_dir = spec_dir / "memory"
memory_dir.mkdir(parents=True)
history = self._make_attempt_history(["stuck-a", "stuck-b"])
(memory_dir / "attempt_history.json").write_text(json.dumps(history))
result = get_next_subtask(spec_dir)
assert result is not None
assert result["id"] == "normal-c"
def test_no_attempt_history_file(self, temp_dir):
"""When attempt_history.json doesn't exist, normal selection proceeds."""
from progress import get_next_subtask
spec_dir = temp_dir / "spec"
spec_dir.mkdir(parents=True)
plan = self._make_plan([
{"id": "task-1", "description": "Task 1", "status": "pending"},
])
(spec_dir / "implementation_plan.json").write_text(json.dumps(plan))
# No memory directory or attempt_history.json
result = get_next_subtask(spec_dir)
assert result is not None
assert result["id"] == "task-1"
def test_corrupted_attempt_history_json(self, temp_dir):
"""When attempt_history.json is corrupted, normal selection proceeds."""
from progress import get_next_subtask
spec_dir = temp_dir / "spec"
spec_dir.mkdir(parents=True)
plan = self._make_plan([
{"id": "task-1", "description": "Task 1", "status": "pending"},
])
(spec_dir / "implementation_plan.json").write_text(json.dumps(plan))
memory_dir = spec_dir / "memory"
memory_dir.mkdir(parents=True)
(memory_dir / "attempt_history.json").write_text("{invalid json!!!")
result = get_next_subtask(spec_dir)
assert result is not None
assert result["id"] == "task-1", "Should still select task when JSON is corrupted"
def test_all_pending_subtasks_stuck_returns_none(self, temp_dir):
"""When ALL pending subtasks are stuck, returns None."""
from progress import get_next_subtask
spec_dir = temp_dir / "spec"
spec_dir.mkdir(parents=True)
plan = self._make_plan([
{"id": "stuck-1", "description": "Stuck 1", "status": "pending"},
{"id": "stuck-2", "description": "Stuck 2", "status": "pending"},
{"id": "done-1", "description": "Done 1", "status": "completed"},
])
(spec_dir / "implementation_plan.json").write_text(json.dumps(plan))
memory_dir = spec_dir / "memory"
memory_dir.mkdir(parents=True)
history = self._make_attempt_history(["stuck-1", "stuck-2"])
(memory_dir / "attempt_history.json").write_text(json.dumps(history))
result = get_next_subtask(spec_dir)
assert result is None, "Should return None when all pending subtasks are stuck"