Files
Aperant/tests/agents/test_agent_flow.py
AndyMik90andClaude Opus 4.6 1f60699f38 fix: resolve QA validation deadlock when subtasks are stuck or failed
When a coding agent marks a subtask as "stuck", QA validation would never
start because is_build_complete() requires ALL subtasks to have status
"completed". This creates a deadlock: coder exits (no more subtasks to
work on), but QA never triggers.

Changes:
- Add is_build_ready_for_qa() that considers builds ready when all
  subtasks reach a terminal state (completed, failed, or stuck)
- Update mark_subtask_stuck() to also set status="failed" in
  implementation_plan.json, keeping plan file in sync with reality
- Reorder QA loop to check human feedback before build completeness,
  so QA_FIX_REQUEST.md bypasses the build gate as intended
- Replace is_build_complete() with is_build_ready_for_qa() in
  should_run_qa() and CLI qa commands
- Add 20 new tests covering is_build_ready_for_qa() edge cases and
  mark_subtask_stuck() plan update behavior

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-17 15:33:06 +01:00

1688 lines
67 KiB
Python

#!/usr/bin/env python3
"""
Test Suite for Agent Flow Integration
======================================
Tests for planner→coder→QA state transitions including:
- Planner to coder transition logic
- Handoff data preservation
- Post-session processing for different subtask states
- State transition detection and handling
Note: Uses temp_git_repo fixture from conftest.py for proper git isolation.
"""
import json
import subprocess
import sys
from pathlib import Path
from unittest.mock import AsyncMock, patch
import pytest
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "apps" / "backend"))
# =============================================================================
# TEST FIXTURES
# =============================================================================
@pytest.fixture
def test_env(temp_git_repo: Path):
"""Create a test environment using the shared temp_git_repo fixture.
This fixture uses the properly isolated git repo from conftest.py which
handles all git environment variable cleanup and restoration.
The temp_git_repo fixture creates a temp_dir and initializes a git repo there.
temp_git_repo yields the path to that initialized repo (which is temp_dir itself).
Yields:
tuple: (temp_dir, spec_dir, project_dir) - no manual cleanup needed as
conftest.py handles environment cleanup automatically.
"""
# temp_git_repo IS the temp_dir with the git repo initialized in it
temp_dir = temp_git_repo
spec_dir = temp_dir / "spec"
project_dir = temp_dir # The git repo is in temp_dir
spec_dir.mkdir(parents=True, exist_ok=True)
yield temp_dir, spec_dir, project_dir
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
def create_implementation_plan(spec_dir: Path, subtasks: list[dict]) -> Path:
"""Create an implementation_plan.json with the given subtasks."""
plan = {
"feature": "Test Feature",
"workflow_type": "feature",
"status": "in_progress",
"phases": [
{
"id": "phase-1",
"name": "Test Phase",
"type": "implementation",
"subtasks": subtasks
}
]
}
plan_file = spec_dir / "implementation_plan.json"
plan_file.write_text(json.dumps(plan, indent=2))
return plan_file
def get_latest_commit(project_dir: Path) -> str:
"""Get the hash of the latest git commit."""
result = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=project_dir,
capture_output=True,
text=True
)
return result.stdout.strip() if result.returncode == 0 else ""
# =============================================================================
# PLANNER TO CODER TRANSITION TESTS
# =============================================================================
class TestPlannerToCoderTransition:
"""Tests for the planner→coder state transition logic."""
def test_first_run_flag_indicates_planner_mode(self, test_env):
"""Test that first_run=True indicates planner mode."""
from prompts import is_first_run
temp_dir, spec_dir, project_dir = test_env
# Empty spec directory - should be first run (planner mode)
assert is_first_run(spec_dir) is True, "Empty spec should be first run"
# Create implementation plan - should no longer be first run
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Test task", "status": "pending"}
])
assert is_first_run(spec_dir) is False, "Spec with plan should not be first run"
def test_transition_from_planning_to_coding_phase(self, test_env):
"""Test that planning phase transitions to coding phase correctly."""
from progress import get_next_subtask
temp_dir, spec_dir, project_dir = test_env
# Create implementation plan with pending subtask
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Implement feature", "status": "pending"}
])
# After planner creates plan, get_next_subtask should return the first pending subtask
next_subtask = get_next_subtask(spec_dir)
assert next_subtask is not None, "Should find next subtask after planning"
assert next_subtask.get("id") == "subtask-1", "Should return first pending subtask"
assert next_subtask.get("status") == "pending", "Subtask should be pending"
def test_planner_completion_enables_coder_session(self, test_env):
"""Test that planner completion (plan created) enables coder session."""
from progress import is_build_complete, count_subtasks
temp_dir, spec_dir, project_dir = test_env
# Create plan with pending subtasks
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "pending"},
{"id": "subtask-2", "description": "Task 2", "status": "pending"}
])
# Build should not be complete - coder needs to work
assert is_build_complete(spec_dir) is False, "Build should not be complete with pending subtasks"
# Should have subtasks to work on
completed, total = count_subtasks(spec_dir)
assert total == 2, "Should have 2 total subtasks"
assert completed == 0, "Should have 0 completed subtasks"
def test_planning_to_coding_subtask_info_preserved(self, test_env):
"""Test that subtask information is preserved during phase transition."""
from agents.utils import load_implementation_plan, find_subtask_in_plan
temp_dir, spec_dir, project_dir = test_env
# Create plan with detailed subtask info
subtask_data = {
"id": "subtask-1",
"description": "Implement user authentication",
"status": "pending",
"files_to_modify": ["app/auth.py", "app/routes.py"],
"files_to_create": ["app/services/oauth.py"],
"patterns_from": ["tests/test_auth.py"],
"verification": {
"type": "command",
"command": "pytest tests/test_auth.py -v"
}
}
create_implementation_plan(spec_dir, [subtask_data])
# Load plan and find subtask
plan = load_implementation_plan(spec_dir)
subtask = find_subtask_in_plan(plan, "subtask-1")
# Verify all data preserved
assert subtask is not None, "Should find subtask in plan"
assert subtask["id"] == "subtask-1", "ID should be preserved"
assert subtask["description"] == "Implement user authentication", "Description preserved"
assert subtask["files_to_modify"] == ["app/auth.py", "app/routes.py"], "Files to modify preserved"
assert subtask["files_to_create"] == ["app/services/oauth.py"], "Files to create preserved"
assert subtask["verification"]["command"] == "pytest tests/test_auth.py -v", "Verification preserved"
# =============================================================================
# POST-SESSION PROCESSING TESTS
# =============================================================================
class TestPostSessionProcessing:
"""Tests for post_session_processing function."""
async def test_completed_subtask_records_success(self, test_env):
"""Test that completed subtask is recorded as successful."""
from recovery import RecoveryManager
from agents.session import post_session_processing
temp_dir, spec_dir, project_dir = test_env
# Create plan with completed subtask
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Test task", "status": "completed"}
])
recovery_manager = RecoveryManager(spec_dir, project_dir)
commit_before = get_latest_commit(project_dir)
# Mock memory-related functions to avoid side effects
with patch("agents.session.extract_session_insights", new_callable=AsyncMock) as mock_insights, \
patch("agents.session.save_session_memory", new_callable=AsyncMock) as mock_memory:
mock_insights.return_value = {"file_insights": [], "patterns_discovered": []}
mock_memory.return_value = (True, "file")
result = await post_session_processing(
spec_dir=spec_dir,
project_dir=project_dir,
subtask_id="subtask-1",
session_num=1,
commit_before=commit_before,
commit_count_before=1,
recovery_manager=recovery_manager,
linear_enabled=False,
)
assert result is True, "Completed subtask should return True"
# Verify attempt was recorded
history = recovery_manager.get_subtask_history("subtask-1")
assert len(history["attempts"]) == 1, "Should have 1 attempt"
assert history["attempts"][0]["success"] is True, "Attempt should be successful"
assert history["status"] == "completed", "Status should be completed"
async def test_in_progress_subtask_records_failure(self, test_env):
"""Test that in_progress subtask is recorded as incomplete."""
from recovery import RecoveryManager
from agents.session import post_session_processing
temp_dir, spec_dir, project_dir = test_env
# Create plan with in_progress subtask
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Test task", "status": "in_progress"}
])
recovery_manager = RecoveryManager(spec_dir, project_dir)
commit_before = get_latest_commit(project_dir)
# Mock check_and_recover to prevent the recovery flow from resetting attempt history
with patch("agents.session.extract_session_insights", new_callable=AsyncMock) as mock_insights, \
patch("agents.session.save_session_memory", new_callable=AsyncMock) as mock_memory, \
patch("agents.session.check_and_recover", return_value=None):
mock_insights.return_value = {"file_insights": [], "patterns_discovered": []}
mock_memory.return_value = (True, "file")
result = await post_session_processing(
spec_dir=spec_dir,
project_dir=project_dir,
subtask_id="subtask-1",
session_num=1,
commit_before=commit_before,
commit_count_before=1,
recovery_manager=recovery_manager,
linear_enabled=False,
)
assert result is False, "In-progress subtask should return False"
# Verify attempt was recorded as failed
history = recovery_manager.get_subtask_history("subtask-1")
assert len(history["attempts"]) == 1, "Should have 1 attempt"
assert history["attempts"][0]["success"] is False, "Attempt should be unsuccessful"
async def test_pending_subtask_records_failure(self, test_env):
"""Test that pending (no progress) subtask is recorded as failure."""
from recovery import RecoveryManager
from agents.session import post_session_processing
temp_dir, spec_dir, project_dir = test_env
# Create plan with pending subtask (no progress made)
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Test task", "status": "pending"}
])
recovery_manager = RecoveryManager(spec_dir, project_dir)
commit_before = get_latest_commit(project_dir)
with patch("agents.session.extract_session_insights", new_callable=AsyncMock) as mock_insights, \
patch("agents.session.save_session_memory", new_callable=AsyncMock) as mock_memory:
mock_insights.return_value = {"file_insights": [], "patterns_discovered": []}
mock_memory.return_value = (True, "file")
result = await post_session_processing(
spec_dir=spec_dir,
project_dir=project_dir,
subtask_id="subtask-1",
session_num=1,
commit_before=commit_before,
commit_count_before=1,
recovery_manager=recovery_manager,
linear_enabled=False,
)
assert result is False, "Pending subtask should return False"
# =============================================================================
# SUBTASK STATE TRANSITION TESTS
# =============================================================================
class TestSubtaskStateTransitions:
"""Tests for subtask state transition handling."""
def test_find_subtask_in_plan(self, test_env):
"""Test finding a subtask by ID in the plan."""
from agents.utils import load_implementation_plan, find_subtask_in_plan
temp_dir, spec_dir, project_dir = test_env
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "First task", "status": "completed"},
{"id": "subtask-2", "description": "Second task", "status": "pending"},
{"id": "subtask-3", "description": "Third task", "status": "pending"}
])
plan = load_implementation_plan(spec_dir)
# Test finding existing subtasks
subtask1 = find_subtask_in_plan(plan, "subtask-1")
assert subtask1 is not None, "Should find subtask-1"
assert subtask1["description"] == "First task"
subtask2 = find_subtask_in_plan(plan, "subtask-2")
assert subtask2 is not None, "Should find subtask-2"
assert subtask2["status"] == "pending"
# Test finding non-existent subtask
missing = find_subtask_in_plan(plan, "subtask-999")
assert missing is None, "Should return None for missing subtask"
def test_find_phase_for_subtask(self, test_env):
"""Test finding the phase containing a subtask."""
from agents.utils import load_implementation_plan, find_phase_for_subtask
temp_dir, spec_dir, project_dir = test_env
# Create plan with multiple phases
plan = {
"feature": "Test Feature",
"workflow_type": "feature",
"status": "in_progress",
"phases": [
{
"id": "phase-1",
"name": "Setup Phase",
"type": "setup",
"subtasks": [
{"id": "subtask-1-1", "description": "Setup DB", "status": "completed"}
]
},
{
"id": "phase-2",
"name": "Implementation Phase",
"type": "implementation",
"subtasks": [
{"id": "subtask-2-1", "description": "Implement feature", "status": "pending"},
{"id": "subtask-2-2", "description": "Add tests", "status": "pending"}
]
}
]
}
plan_file = spec_dir / "implementation_plan.json"
plan_file.write_text(json.dumps(plan, indent=2))
loaded_plan = load_implementation_plan(spec_dir)
# Find phase for subtask in first phase
phase1 = find_phase_for_subtask(loaded_plan, "subtask-1-1")
assert phase1 is not None, "Should find phase for subtask-1-1"
assert phase1["name"] == "Setup Phase", "Should be setup phase"
# Find phase for subtask in second phase
phase2 = find_phase_for_subtask(loaded_plan, "subtask-2-1")
assert phase2 is not None, "Should find phase for subtask-2-1"
assert phase2["name"] == "Implementation Phase", "Should be implementation phase"
# Find phase for non-existent subtask
missing_phase = find_phase_for_subtask(loaded_plan, "subtask-999")
assert missing_phase is None, "Should return None for missing subtask"
def test_get_next_subtask_skips_completed(self, test_env):
"""Test that get_next_subtask skips completed subtasks."""
from progress import get_next_subtask
temp_dir, spec_dir, project_dir = test_env
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "First task", "status": "completed"},
{"id": "subtask-2", "description": "Second task", "status": "completed"},
{"id": "subtask-3", "description": "Third task", "status": "pending"}
])
next_subtask = get_next_subtask(spec_dir)
assert next_subtask is not None, "Should find pending subtask"
assert next_subtask["id"] == "subtask-3", "Should skip completed and return first pending"
def test_build_complete_when_all_subtasks_done(self, test_env):
"""Test that build is complete when all subtasks are completed."""
from progress import is_build_complete
temp_dir, spec_dir, project_dir = test_env
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "First task", "status": "completed"},
{"id": "subtask-2", "description": "Second task", "status": "completed"},
{"id": "subtask-3", "description": "Third task", "status": "completed"}
])
assert is_build_complete(spec_dir) is True, "Build should be complete when all subtasks done"
# =============================================================================
# HANDOFF DATA PRESERVATION TESTS
# =============================================================================
class TestHandoffDataPreservation:
"""Tests for data preservation during agent handoffs."""
def test_subtask_context_loading(self, test_env):
"""Test that subtask context is properly loaded for coder."""
from prompt_generator import load_subtask_context
temp_dir, spec_dir, project_dir = test_env
# Create spec.md
(spec_dir / "spec.md").write_text("# Test Spec\n\nTest content")
# Create context.json
context = {
"files_to_modify": [
{"path": "app/main.py", "reason": "Add feature"}
],
"files_to_reference": [
{"path": "app/utils.py", "reason": "Pattern reference"}
]
}
(spec_dir / "context.json").write_text(json.dumps(context))
subtask = {
"id": "subtask-1",
"description": "Implement feature",
"files_to_modify": ["app/main.py"],
"patterns_from": ["app/utils.py"]
}
loaded_context = load_subtask_context(spec_dir, project_dir, subtask)
# Verify context structure
assert "patterns" in loaded_context or "files_to_modify" in loaded_context, \
"Context should have patterns or files"
def test_recovery_hints_passed_to_coder(self, test_env):
"""Test that recovery hints are available for retry attempts."""
from recovery import RecoveryManager
temp_dir, spec_dir, project_dir = test_env
recovery_manager = RecoveryManager(spec_dir, project_dir)
# Record a failed attempt
recovery_manager.record_attempt(
subtask_id="subtask-1",
session=1,
success=False,
approach="First approach using async/await",
error="Import error - module not found"
)
# Get recovery hints
hints = recovery_manager.get_recovery_hints("subtask-1")
assert len(hints) > 0, "Should have recovery hints after failure"
assert any("Previous attempts: 1" in hint for hint in hints), "Should mention attempt count"
def test_commit_tracking_across_sessions(self, test_env):
"""Test that commit tracking works across sessions."""
from recovery import RecoveryManager
temp_dir, spec_dir, project_dir = test_env
recovery_manager = RecoveryManager(spec_dir, project_dir)
# Get initial commit
initial_commit = get_latest_commit(project_dir)
# Record it as good
recovery_manager.record_good_commit(initial_commit, "subtask-1")
# Create a new commit
test_file = project_dir / "new_file.txt"
test_file.write_text("New content")
subprocess.run(["git", "add", "."], cwd=project_dir, capture_output=True)
subprocess.run(["git", "commit", "-m", "Add new file"], cwd=project_dir, capture_output=True)
new_commit = get_latest_commit(project_dir)
# Record new commit
recovery_manager.record_good_commit(new_commit, "subtask-2")
# Verify last good commit is the new one
last_good = recovery_manager.get_last_good_commit()
assert last_good == new_commit, "Last good commit should be the newest"
# =============================================================================
# PLAN VALIDATION TESTS (for planner output)
# =============================================================================
class TestPlannerOutputValidation:
"""Tests for validating planner output before transition to coder."""
def test_plan_must_have_pending_subtasks(self, test_env):
"""Test that valid plan has at least one pending subtask."""
from progress import get_next_subtask
temp_dir, spec_dir, project_dir = test_env
# Create plan with only completed subtasks
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Done task", "status": "completed"}
])
next_subtask = get_next_subtask(spec_dir)
assert next_subtask is None, "No pending subtasks should return None"
def test_plan_without_phases_returns_none(self, test_env):
"""Test that plan without phases returns None for next subtask."""
from progress import get_next_subtask
temp_dir, spec_dir, project_dir = test_env
# Create empty plan
plan = {
"feature": "Test Feature",
"workflow_type": "feature",
"status": "in_progress",
"phases": []
}
plan_file = spec_dir / "implementation_plan.json"
plan_file.write_text(json.dumps(plan, indent=2))
next_subtask = get_next_subtask(spec_dir)
assert next_subtask is None, "Empty phases should return None"
def test_missing_plan_returns_none(self, test_env):
"""Test that missing plan file returns None."""
from progress import get_next_subtask
temp_dir, spec_dir, project_dir = test_env
# Don't create any plan file
next_subtask = get_next_subtask(spec_dir)
assert next_subtask is None, "Missing plan should return None"
# =============================================================================
# SUBTASK COMPLETION DETECTION TESTS
# =============================================================================
class TestSubtaskCompletionDetection:
"""Tests for subtask completion detection and status counting."""
def test_count_subtasks_basic(self, test_env):
"""Test basic subtask counting."""
from progress import count_subtasks
temp_dir, spec_dir, project_dir = test_env
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"},
{"id": "subtask-2", "description": "Task 2", "status": "pending"},
{"id": "subtask-3", "description": "Task 3", "status": "pending"}
])
completed, total = count_subtasks(spec_dir)
assert total == 3, "Should have 3 total subtasks"
assert completed == 1, "Should have 1 completed subtask"
def test_count_subtasks_empty_plan(self, test_env):
"""Test counting with empty plan returns zeros."""
from progress import count_subtasks
temp_dir, spec_dir, project_dir = test_env
# No plan file exists
completed, total = count_subtasks(spec_dir)
assert completed == 0, "Empty plan should have 0 completed"
assert total == 0, "Empty plan should have 0 total"
def test_count_subtasks_detailed_all_statuses(self, test_env):
"""Test detailed counting with all status types."""
from progress import count_subtasks_detailed
temp_dir, spec_dir, project_dir = test_env
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"},
{"id": "subtask-2", "description": "Task 2", "status": "in_progress"},
{"id": "subtask-3", "description": "Task 3", "status": "pending"},
{"id": "subtask-4", "description": "Task 4", "status": "failed"}
])
counts = count_subtasks_detailed(spec_dir)
assert counts["total"] == 4, "Should have 4 total subtasks"
assert counts["completed"] == 1, "Should have 1 completed"
assert counts["in_progress"] == 1, "Should have 1 in_progress"
assert counts["pending"] == 1, "Should have 1 pending"
assert counts["failed"] == 1, "Should have 1 failed"
def test_count_subtasks_detailed_unknown_status_treated_as_pending(self, test_env):
"""Test that unknown status values are treated as pending."""
from progress import count_subtasks_detailed
temp_dir, spec_dir, project_dir = test_env
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "unknown_status"},
{"id": "subtask-2", "description": "Task 2", "status": "completed"}
])
counts = count_subtasks_detailed(spec_dir)
assert counts["total"] == 2, "Should have 2 total subtasks"
assert counts["completed"] == 1, "Should have 1 completed"
assert counts["pending"] == 1, "Unknown status should count as pending"
def test_is_build_complete_true_when_all_done(self, test_env):
"""Test is_build_complete returns True when all subtasks completed."""
from progress import is_build_complete
temp_dir, spec_dir, project_dir = test_env
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"},
{"id": "subtask-2", "description": "Task 2", "status": "completed"}
])
assert is_build_complete(spec_dir) is True, "Build should be complete"
def test_is_build_complete_false_with_in_progress(self, test_env):
"""Test is_build_complete returns False with in_progress subtask."""
from progress import is_build_complete
temp_dir, spec_dir, project_dir = test_env
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"},
{"id": "subtask-2", "description": "Task 2", "status": "in_progress"}
])
assert is_build_complete(spec_dir) is False, "Build should not be complete with in_progress"
def test_is_build_complete_false_with_failed(self, test_env):
"""Test is_build_complete returns False with failed subtask."""
from progress import is_build_complete
temp_dir, spec_dir, project_dir = test_env
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"},
{"id": "subtask-2", "description": "Task 2", "status": "failed"}
])
assert is_build_complete(spec_dir) is False, "Build should not be complete with failed task"
def test_is_build_complete_false_with_empty_plan(self, test_env):
"""Test is_build_complete returns False for empty plan."""
from progress import is_build_complete
temp_dir, spec_dir, project_dir = test_env
# No plan file
assert is_build_complete(spec_dir) is False, "Empty plan should not be complete"
# Empty phases
plan = {
"feature": "Test Feature",
"workflow_type": "feature",
"status": "in_progress",
"phases": []
}
(spec_dir / "implementation_plan.json").write_text(json.dumps(plan))
assert is_build_complete(spec_dir) is False, "Plan with no subtasks should not be complete"
def test_get_progress_percentage(self, test_env):
"""Test progress percentage calculation."""
from progress import get_progress_percentage
temp_dir, spec_dir, project_dir = test_env
# 50% complete
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"},
{"id": "subtask-2", "description": "Task 2", "status": "pending"}
])
percentage = get_progress_percentage(spec_dir)
assert percentage == 50.0, "Should be 50% complete"
def test_get_progress_percentage_empty_plan(self, test_env):
"""Test progress percentage for empty plan is 0."""
from progress import get_progress_percentage
temp_dir, spec_dir, project_dir = test_env
# No plan file
percentage = get_progress_percentage(spec_dir)
assert percentage == 0.0, "Empty plan should be 0%"
def test_subtask_status_transition_to_completed(self, test_env):
"""Test detecting subtask transition from pending to completed."""
from agents.utils import load_implementation_plan, find_subtask_in_plan
from progress import is_build_complete
temp_dir, spec_dir, project_dir = test_env
# Start with pending subtask
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "pending"}
])
plan = load_implementation_plan(spec_dir)
subtask = find_subtask_in_plan(plan, "subtask-1")
assert subtask["status"] == "pending", "Initial status should be pending"
assert is_build_complete(spec_dir) is False, "Should not be complete"
# Update to completed
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"}
])
plan = load_implementation_plan(spec_dir)
subtask = find_subtask_in_plan(plan, "subtask-1")
assert subtask["status"] == "completed", "Updated status should be completed"
assert is_build_complete(spec_dir) is True, "Should now be complete"
def test_subtask_status_transition_through_in_progress(self, test_env):
"""Test detecting subtask transition through in_progress state."""
from agents.utils import load_implementation_plan, find_subtask_in_plan
from progress import count_subtasks_detailed
temp_dir, spec_dir, project_dir = test_env
# Start pending
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "pending"}
])
counts = count_subtasks_detailed(spec_dir)
assert counts["pending"] == 1, "Should have 1 pending"
assert counts["in_progress"] == 0, "Should have 0 in_progress"
# Move to in_progress
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "in_progress"}
])
counts = count_subtasks_detailed(spec_dir)
assert counts["pending"] == 0, "Should have 0 pending"
assert counts["in_progress"] == 1, "Should have 1 in_progress"
# Complete
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"}
])
counts = count_subtasks_detailed(spec_dir)
assert counts["in_progress"] == 0, "Should have 0 in_progress"
assert counts["completed"] == 1, "Should have 1 completed"
def test_multiple_subtasks_completion_sequence(self, test_env):
"""Test completion detection as subtasks complete one by one."""
from progress import count_subtasks, is_build_complete
temp_dir, spec_dir, project_dir = test_env
# Start with all pending
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "pending"},
{"id": "subtask-2", "description": "Task 2", "status": "pending"},
{"id": "subtask-3", "description": "Task 3", "status": "pending"}
])
completed, total = count_subtasks(spec_dir)
assert completed == 0 and total == 3, "Initial: 0/3"
assert is_build_complete(spec_dir) is False
# Complete first subtask
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"},
{"id": "subtask-2", "description": "Task 2", "status": "pending"},
{"id": "subtask-3", "description": "Task 3", "status": "pending"}
])
completed, total = count_subtasks(spec_dir)
assert completed == 1 and total == 3, "After first: 1/3"
assert is_build_complete(spec_dir) is False
# Complete second subtask
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"},
{"id": "subtask-2", "description": "Task 2", "status": "completed"},
{"id": "subtask-3", "description": "Task 3", "status": "pending"}
])
completed, total = count_subtasks(spec_dir)
assert completed == 2 and total == 3, "After second: 2/3"
assert is_build_complete(spec_dir) is False
# Complete all subtasks
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"},
{"id": "subtask-2", "description": "Task 2", "status": "completed"},
{"id": "subtask-3", "description": "Task 3", "status": "completed"}
])
completed, total = count_subtasks(spec_dir)
assert completed == 3 and total == 3, "Final: 3/3"
assert is_build_complete(spec_dir) is True
def test_get_next_subtask_returns_first_pending_after_completed(self, test_env):
"""Test get_next_subtask returns correct subtask after completions."""
from progress import get_next_subtask
temp_dir, spec_dir, project_dir = test_env
# First and second completed, third pending
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"},
{"id": "subtask-2", "description": "Task 2", "status": "completed"},
{"id": "subtask-3", "description": "Task 3", "status": "pending"}
])
next_subtask = get_next_subtask(spec_dir)
assert next_subtask is not None, "Should find next subtask"
assert next_subtask["id"] == "subtask-3", "Should return subtask-3"
def test_get_next_subtask_none_when_all_complete(self, test_env):
"""Test get_next_subtask returns None when all complete."""
from progress import get_next_subtask
temp_dir, spec_dir, project_dir = test_env
create_implementation_plan(spec_dir, [
{"id": "subtask-1", "description": "Task 1", "status": "completed"},
{"id": "subtask-2", "description": "Task 2", "status": "completed"}
])
next_subtask = get_next_subtask(spec_dir)
assert next_subtask is None, "Should return None when all complete"
def test_completion_detection_with_multi_phase_plan(self, test_env):
"""Test completion detection across multiple phases."""
from progress import is_build_complete, count_subtasks
temp_dir, spec_dir, project_dir = test_env
# Multi-phase plan
plan = {
"feature": "Test Feature",
"workflow_type": "feature",
"status": "in_progress",
"phases": [
{
"id": "phase-1",
"name": "Setup Phase",
"type": "setup",
"subtasks": [
{"id": "subtask-1-1", "description": "Setup DB", "status": "completed"}
]
},
{
"id": "phase-2",
"name": "Implementation Phase",
"type": "implementation",
"subtasks": [
{"id": "subtask-2-1", "description": "Implement feature", "status": "pending"},
{"id": "subtask-2-2", "description": "Add tests", "status": "pending"}
]
}
]
}
(spec_dir / "implementation_plan.json").write_text(json.dumps(plan))
completed, total = count_subtasks(spec_dir)
assert completed == 1 and total == 3, "Should count across phases: 1/3"
assert is_build_complete(spec_dir) is False, "Should not be complete"
# Complete all in second phase
plan["phases"][1]["subtasks"][0]["status"] = "completed"
plan["phases"][1]["subtasks"][1]["status"] = "completed"
(spec_dir / "implementation_plan.json").write_text(json.dumps(plan))
completed, total = count_subtasks(spec_dir)
assert completed == 3 and total == 3, "All phases complete: 3/3"
assert is_build_complete(spec_dir) is True, "Should be complete"
# =============================================================================
# QA LOOP AND FIXER INTERACTION TESTS
# =============================================================================
class TestQALoopStateTransitions:
"""Tests for QA loop state transitions in agent flow context."""
def test_qa_not_required_when_build_incomplete(self, test_env):
"""QA should not run when build is incomplete."""
from qa_loop import save_implementation_plan
# Import the real is_build_ready_for_qa to patch at the right level
from core.progress import is_build_ready_for_qa as real_is_build_ready_for_qa
temp_dir, spec_dir, project_dir = test_env
# Create plan with pending subtasks
plan = {
"feature": "Test Feature",
"phases": [
{
"phase": 1,
"name": "Test",
"subtasks": [
{"id": "c1", "description": "Task 1", "status": "completed"},
{"id": "c2", "description": "Task 2", "status": "pending"},
],
},
],
}
save_implementation_plan(spec_dir, plan)
# Patch is_build_ready_for_qa where it's used (qa.criteria) to use real implementation
# This is needed because test_qa_criteria.py module-level mocks may pollute
with patch('qa.criteria.is_build_ready_for_qa', side_effect=real_is_build_ready_for_qa):
from qa.criteria import should_run_qa
assert should_run_qa(spec_dir) is False, "QA should not run with pending subtasks"
def test_qa_required_when_build_complete(self, test_env):
"""QA should run when build is complete and not yet approved."""
from qa_loop import save_implementation_plan
from core.progress import is_build_ready_for_qa as real_is_build_ready_for_qa
temp_dir, spec_dir, project_dir = test_env
# Create plan with all completed subtasks
plan = {
"feature": "Test Feature",
"phases": [
{
"phase": 1,
"name": "Test",
"subtasks": [
{"id": "c1", "description": "Task 1", "status": "completed"},
{"id": "c2", "description": "Task 2", "status": "completed"},
],
},
],
}
save_implementation_plan(spec_dir, plan)
# Patch is_build_ready_for_qa where it's used (qa.criteria) to use real implementation
with patch('qa.criteria.is_build_ready_for_qa', side_effect=real_is_build_ready_for_qa):
from qa.criteria import should_run_qa
assert should_run_qa(spec_dir) is True, "QA should run when build complete"
def test_qa_not_required_when_already_approved(self, test_env):
"""QA should not run when build is already approved."""
from qa_loop import save_implementation_plan
from core.progress import is_build_ready_for_qa as real_is_build_ready_for_qa
temp_dir, spec_dir, project_dir = test_env
plan = {
"feature": "Test Feature",
"phases": [
{
"phase": 1,
"name": "Test",
"subtasks": [
{"id": "c1", "description": "Task 1", "status": "completed"},
],
},
],
"qa_signoff": {
"status": "approved",
"qa_session": 1,
"timestamp": "2024-01-01T12:00:00",
},
}
save_implementation_plan(spec_dir, plan)
# Patch is_build_ready_for_qa where it's used (qa.criteria) to use real implementation
with patch('qa.criteria.is_build_ready_for_qa', side_effect=real_is_build_ready_for_qa):
from qa.criteria import should_run_qa
assert should_run_qa(spec_dir) is False, "QA should not run when already approved"
class TestQAFixerInteraction:
"""Tests for QA reviewer to fixer handoff and interaction."""
def test_fixer_should_run_when_qa_rejected(self, test_env):
"""Fixer should run when QA rejected the build."""
from qa_loop import should_run_fixes, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
plan = {
"feature": "Test Feature",
"qa_signoff": {
"status": "rejected",
"qa_session": 1,
"issues_found": [{"title": "Missing test", "type": "unit_test"}],
},
}
save_implementation_plan(spec_dir, plan)
assert should_run_fixes(spec_dir) is True, "Fixer should run when QA rejected"
def test_fixer_should_not_run_when_qa_approved(self, test_env):
"""Fixer should not run when QA approved the build."""
from qa_loop import should_run_fixes, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
plan = {
"feature": "Test Feature",
"qa_signoff": {
"status": "approved",
"qa_session": 1,
"tests_passed": {"unit": True, "integration": True, "e2e": True},
},
}
save_implementation_plan(spec_dir, plan)
assert should_run_fixes(spec_dir) is False, "Fixer should not run when approved"
def test_fixer_should_not_run_at_max_iterations(self, test_env):
"""Fixer should not run when max iterations reached."""
from qa_loop import should_run_fixes, save_implementation_plan, MAX_QA_ITERATIONS
temp_dir, spec_dir, project_dir = test_env
plan = {
"feature": "Test Feature",
"qa_signoff": {
"status": "rejected",
"qa_session": MAX_QA_ITERATIONS,
"issues_found": [{"title": "Recurring issue", "type": "unit_test"}],
},
}
save_implementation_plan(spec_dir, plan)
assert should_run_fixes(spec_dir) is False, "Fixer should not run at max iterations"
def test_fixer_fixes_applied_state(self, test_env):
"""Test transition to fixes_applied state after fixer runs."""
from qa_loop import is_fixes_applied, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
# Simulate fixer completing and setting fixes_applied
plan = {
"feature": "Test Feature",
"qa_signoff": {
"status": "fixes_applied",
"ready_for_qa_revalidation": True,
"qa_session": 1,
},
}
save_implementation_plan(spec_dir, plan)
assert is_fixes_applied(spec_dir) is True, "Should detect fixes_applied state"
def test_fixer_fixes_not_ready_for_revalidation(self, test_env):
"""Test fixes_applied but not ready for revalidation."""
from qa_loop import is_fixes_applied, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
plan = {
"feature": "Test Feature",
"qa_signoff": {
"status": "fixes_applied",
"ready_for_qa_revalidation": False,
"qa_session": 1,
},
}
save_implementation_plan(spec_dir, plan)
assert is_fixes_applied(spec_dir) is False, "Should not be ready when flag is False"
class TestQAVerdictHandling:
"""Tests for QA verdict handling and status management."""
def test_qa_approved_verdict(self, test_env):
"""Test QA approved verdict is correctly detected."""
from qa_loop import is_qa_approved, is_qa_rejected, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
plan = {
"feature": "Test Feature",
"qa_signoff": {
"status": "approved",
"qa_session": 1,
"timestamp": "2024-01-01T12:00:00",
"tests_passed": {"unit": True, "integration": True, "e2e": True},
},
}
save_implementation_plan(spec_dir, plan)
assert is_qa_approved(spec_dir) is True, "Should detect approved status"
assert is_qa_rejected(spec_dir) is False, "Should not detect rejected when approved"
def test_qa_rejected_verdict(self, test_env):
"""Test QA rejected verdict is correctly detected."""
from qa_loop import is_qa_approved, is_qa_rejected, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
plan = {
"feature": "Test Feature",
"qa_signoff": {
"status": "rejected",
"qa_session": 1,
"timestamp": "2024-01-01T12:00:00",
"issues_found": [{"title": "Missing test", "type": "unit_test"}],
},
}
save_implementation_plan(spec_dir, plan)
assert is_qa_rejected(spec_dir) is True, "Should detect rejected status"
assert is_qa_approved(spec_dir) is False, "Should not detect approved when rejected"
def test_qa_no_verdict_yet(self, test_env):
"""Test when no QA verdict has been made yet."""
from qa_loop import is_qa_approved, is_qa_rejected, get_qa_signoff_status, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
plan = {
"feature": "Test Feature",
"phases": [],
}
save_implementation_plan(spec_dir, plan)
assert get_qa_signoff_status(spec_dir) is None, "Should have no signoff status"
assert is_qa_approved(spec_dir) is False, "Should not be approved with no verdict"
assert is_qa_rejected(spec_dir) is False, "Should not be rejected with no verdict"
def test_qa_iteration_count_tracking(self, test_env):
"""Test QA iteration count is tracked correctly."""
from qa_loop import get_qa_iteration_count, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
# First iteration
plan = {
"feature": "Test Feature",
"qa_signoff": {"status": "rejected", "qa_session": 1},
}
save_implementation_plan(spec_dir, plan)
assert get_qa_iteration_count(spec_dir) == 1, "Should be iteration 1"
# Second iteration
plan["qa_signoff"]["qa_session"] = 2
save_implementation_plan(spec_dir, plan)
assert get_qa_iteration_count(spec_dir) == 2, "Should be iteration 2"
# Third iteration
plan["qa_signoff"]["qa_session"] = 3
save_implementation_plan(spec_dir, plan)
assert get_qa_iteration_count(spec_dir) == 3, "Should be iteration 3"
def test_qa_iteration_count_zero_when_no_signoff(self, test_env):
"""Test iteration count is 0 when no QA sessions yet."""
from qa_loop import get_qa_iteration_count, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
plan = {"feature": "Test Feature", "phases": []}
save_implementation_plan(spec_dir, plan)
assert get_qa_iteration_count(spec_dir) == 0, "Should be 0 with no signoff"
class TestQALoopWorkflow:
"""Integration tests for complete QA loop workflow."""
def test_full_qa_workflow_approved_first_try(self, test_env):
"""Test complete QA workflow where build passes on first try."""
from qa_loop import (
should_run_qa,
should_run_fixes,
is_qa_approved,
save_implementation_plan,
)
temp_dir, spec_dir, project_dir = test_env
# Build complete, QA should run
plan = {
"feature": "Test Feature",
"phases": [
{
"phase": 1,
"name": "Test",
"subtasks": [
{"id": "c1", "description": "Task 1", "status": "completed"},
],
},
],
}
save_implementation_plan(spec_dir, plan)
assert should_run_qa(spec_dir) is True, "QA should run initially"
# QA approves
plan["qa_signoff"] = {
"status": "approved",
"qa_session": 1,
"tests_passed": {"unit": True, "integration": True, "e2e": True},
}
save_implementation_plan(spec_dir, plan)
# Verify end state
assert is_qa_approved(spec_dir) is True, "Should be approved"
assert should_run_qa(spec_dir) is False, "QA should not run again"
assert should_run_fixes(spec_dir) is False, "Fixer should not run"
def test_full_qa_workflow_with_one_rejection(self, test_env):
"""Test QA workflow with one rejection followed by approval."""
from qa_loop import (
should_run_qa,
should_run_fixes,
is_qa_approved,
is_qa_rejected,
is_fixes_applied,
get_qa_iteration_count,
save_implementation_plan,
)
temp_dir, spec_dir, project_dir = test_env
# Build complete
plan = {
"feature": "Test Feature",
"phases": [
{
"phase": 1,
"name": "Test",
"subtasks": [
{"id": "c1", "description": "Task 1", "status": "completed"},
],
},
],
}
save_implementation_plan(spec_dir, plan)
# First QA session - rejected
plan["qa_signoff"] = {
"status": "rejected",
"qa_session": 1,
"issues_found": [{"title": "Missing test", "type": "unit_test"}],
}
save_implementation_plan(spec_dir, plan)
assert is_qa_rejected(spec_dir) is True, "Should be rejected"
assert should_run_fixes(spec_dir) is True, "Fixer should run"
assert get_qa_iteration_count(spec_dir) == 1, "Should be iteration 1"
# Fixer applies fixes
plan["qa_signoff"] = {
"status": "fixes_applied",
"ready_for_qa_revalidation": True,
"qa_session": 1,
}
save_implementation_plan(spec_dir, plan)
assert is_fixes_applied(spec_dir) is True, "Fixes should be applied"
# Second QA session - approved
plan["qa_signoff"] = {
"status": "approved",
"qa_session": 2,
"tests_passed": {"unit": True, "integration": True, "e2e": True},
}
save_implementation_plan(spec_dir, plan)
assert is_qa_approved(spec_dir) is True, "Should be approved"
assert get_qa_iteration_count(spec_dir) == 2, "Should be iteration 2"
def test_qa_workflow_multiple_rejections(self, test_env):
"""Test QA workflow with multiple rejections until max iterations."""
from qa_loop import (
should_run_fixes,
is_qa_rejected,
get_qa_iteration_count,
save_implementation_plan,
MAX_QA_ITERATIONS,
)
temp_dir, spec_dir, project_dir = test_env
plan = {"feature": "Test Feature", "phases": []}
# Simulate multiple rejections
for i in range(1, MAX_QA_ITERATIONS + 1):
plan["qa_signoff"] = {
"status": "rejected",
"qa_session": i,
"issues_found": [{"title": f"Issue {i}", "type": "unit_test"}],
}
save_implementation_plan(spec_dir, plan)
assert is_qa_rejected(spec_dir) is True, f"Should be rejected at iteration {i}"
assert get_qa_iteration_count(spec_dir) == i, f"Should be iteration {i}"
if i < MAX_QA_ITERATIONS:
assert should_run_fixes(spec_dir) is True, f"Fixer should run at iteration {i}"
else:
assert should_run_fixes(spec_dir) is False, "Fixer should not run at max iterations"
class TestQASignoffDataStructure:
"""Tests for QA signoff data structure validation."""
def test_approved_signoff_has_tests_passed(self, test_env):
"""Test approved signoff includes tests_passed field."""
from qa_loop import get_qa_signoff_status, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
plan = {
"feature": "Test Feature",
"qa_signoff": {
"status": "approved",
"qa_session": 1,
"timestamp": "2024-01-01T12:00:00",
"tests_passed": {
"unit": True,
"integration": True,
"e2e": True,
},
},
}
save_implementation_plan(spec_dir, plan)
status = get_qa_signoff_status(spec_dir)
assert status is not None, "Should have signoff status"
assert "tests_passed" in status, "Approved signoff should have tests_passed"
assert status["tests_passed"]["unit"] is True, "Unit tests should be True"
def test_rejected_signoff_has_issues_found(self, test_env):
"""Test rejected signoff includes issues_found field."""
from qa_loop import get_qa_signoff_status, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
plan = {
"feature": "Test Feature",
"qa_signoff": {
"status": "rejected",
"qa_session": 1,
"timestamp": "2024-01-01T12:00:00",
"issues_found": [
{"title": "Missing test", "type": "unit_test"},
{"title": "Validation error", "type": "acceptance"},
],
},
}
save_implementation_plan(spec_dir, plan)
status = get_qa_signoff_status(spec_dir)
assert status is not None, "Should have signoff status"
assert "issues_found" in status, "Rejected signoff should have issues_found"
assert len(status["issues_found"]) == 2, "Should have 2 issues"
def test_issues_have_title_and_type(self, test_env):
"""Test that issues in rejected signoff have required fields."""
from qa_loop import get_qa_signoff_status, save_implementation_plan
temp_dir, spec_dir, project_dir = test_env
plan = {
"feature": "Test Feature",
"qa_signoff": {
"status": "rejected",
"qa_session": 1,
"issues_found": [
{"title": "Test failure", "type": "unit_test"},
],
},
}
save_implementation_plan(spec_dir, plan)
status = get_qa_signoff_status(spec_dir)
issue = status["issues_found"][0]
assert "title" in issue, "Issue should have title"
assert "type" in issue, "Issue should have type"
assert issue["title"] == "Test failure", "Title should match"
assert issue["type"] == "unit_test", "Type should match"
# =============================================================================
# WORKTREE ISOLATION TESTS
# =============================================================================
class TestWorktreeIsolation:
"""Tests for worktree isolation to verify concurrent agents don't conflict."""
def test_multiple_worktrees_have_separate_branches(self, test_env):
"""Multiple worktrees for different specs have separate branches."""
from worktree import WorktreeManager
temp_dir, spec_dir, project_dir = test_env
manager = WorktreeManager(project_dir)
manager.setup()
# Create two worktrees for different specs
info1 = manager.create_worktree("spec-agent-1")
info2 = manager.create_worktree("spec-agent-2")
# Each worktree should have a unique branch
assert info1.branch != info2.branch, "Worktrees should have different branches"
assert info1.branch == "auto-claude/spec-agent-1", f"Expected branch auto-claude/spec-agent-1, got {info1.branch}"
assert info2.branch == "auto-claude/spec-agent-2", f"Expected branch auto-claude/spec-agent-2, got {info2.branch}"
# Each worktree should have a unique path
assert info1.path != info2.path, "Worktrees should have different paths"
assert info1.path.exists(), "First worktree path should exist"
assert info2.path.exists(), "Second worktree path should exist"
def test_changes_in_one_worktree_dont_affect_another(self, test_env):
"""Changes made in one worktree don't affect other worktrees."""
from worktree import WorktreeManager
temp_dir, spec_dir, project_dir = test_env
manager = WorktreeManager(project_dir)
manager.setup()
# Create two worktrees
info1 = manager.create_worktree("spec-isolation-1")
info2 = manager.create_worktree("spec-isolation-2")
# Make changes in first worktree
file1 = info1.path / "agent1_work.txt"
file1.write_text("Work from agent 1")
subprocess.run(["git", "add", "."], cwd=info1.path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Agent 1 work"],
cwd=info1.path, capture_output=True
)
# Make different changes in second worktree
file2 = info2.path / "agent2_work.txt"
file2.write_text("Work from agent 2")
subprocess.run(["git", "add", "."], cwd=info2.path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Agent 2 work"],
cwd=info2.path, capture_output=True
)
# Verify changes are isolated
assert (info1.path / "agent1_work.txt").exists(), "Agent 1 file should exist in worktree 1"
assert not (info1.path / "agent2_work.txt").exists(), "Agent 2 file should NOT exist in worktree 1"
assert (info2.path / "agent2_work.txt").exists(), "Agent 2 file should exist in worktree 2"
assert not (info2.path / "agent1_work.txt").exists(), "Agent 1 file should NOT exist in worktree 2"
# Verify main branch is unaffected
assert not (project_dir / "agent1_work.txt").exists(), "Agent 1 file should NOT exist in main"
assert not (project_dir / "agent2_work.txt").exists(), "Agent 2 file should NOT exist in main"
def test_concurrent_worktree_operations_dont_conflict(self, test_env):
"""Concurrent operations on different worktrees don't cause conflicts."""
from worktree import WorktreeManager
temp_dir, spec_dir, project_dir = test_env
manager = WorktreeManager(project_dir)
manager.setup()
# Create multiple worktrees simulating concurrent agents
worktrees = []
for i in range(3):
info = manager.create_worktree(f"concurrent-spec-{i}")
worktrees.append(info)
# Simulate concurrent work - each "agent" modifies the same file in their worktree
for i, info in enumerate(worktrees):
# Each worktree starts with the same file (from base branch)
modified_file = info.path / "test.txt"
modified_file.write_text(f"Modified by agent {i}")
subprocess.run(["git", "add", "."], cwd=info.path, capture_output=True)
subprocess.run(
["git", "commit", "-m", f"Agent {i} modification"],
cwd=info.path, capture_output=True
)
# Verify each worktree has its own version
for i, info in enumerate(worktrees):
content = (info.path / "test.txt").read_text()
assert content == f"Modified by agent {i}", f"Worktree {i} should have agent {i}'s changes"
# Verify all worktrees still exist and are valid
all_worktrees = manager.list_all_worktrees()
assert len(all_worktrees) == 3, f"Should have 3 worktrees, got {len(all_worktrees)}"
def test_worktree_isolation_with_spec_directories(self, test_env):
"""Worktrees properly isolate spec-related directories."""
from worktree import WorktreeManager
temp_dir, spec_dir, project_dir = test_env
manager = WorktreeManager(project_dir)
manager.setup()
# Create worktree
info = manager.create_worktree("spec-dir-test")
# Create a spec directory structure in the worktree
worktree_spec_dir = info.path / ".auto-claude" / "specs" / "spec-dir-test"
worktree_spec_dir.mkdir(parents=True)
# Create implementation plan in the worktree's spec directory
plan = {
"feature": "Test Feature",
"phases": [
{
"id": "phase-1",
"name": "Test",
"subtasks": [
{"id": "subtask-1", "description": "Test", "status": "pending"}
]
}
]
}
plan_file = worktree_spec_dir / "implementation_plan.json"
plan_file.write_text(json.dumps(plan, indent=2))
# Verify the spec directory exists only in the worktree
assert worktree_spec_dir.exists(), "Spec dir should exist in worktree"
# Main project directory should not have this spec directory
# (the .auto-claude/specs path may exist but not this specific spec)
main_spec_dir = project_dir / ".auto-claude" / "specs" / "spec-dir-test"
assert not main_spec_dir.exists(), "Worktree spec dir should NOT exist in main project"
def test_worktree_can_be_removed_without_affecting_others(self, test_env):
"""Removing one worktree doesn't affect other worktrees."""
from worktree import WorktreeManager
temp_dir, spec_dir, project_dir = test_env
manager = WorktreeManager(project_dir)
manager.setup()
# Create three worktrees
info1 = manager.create_worktree("removal-test-1")
info2 = manager.create_worktree("removal-test-2")
info3 = manager.create_worktree("removal-test-3")
# Make some changes in each
for info in [info1, info2, info3]:
(info.path / f"{info.spec_name}.txt").write_text(f"Data for {info.spec_name}")
subprocess.run(["git", "add", "."], cwd=info.path, capture_output=True)
subprocess.run(
["git", "commit", "-m", f"Commit for {info.spec_name}"],
cwd=info.path, capture_output=True
)
# Remove the middle worktree
manager.remove_worktree("removal-test-2", delete_branch=True)
# Verify the removed worktree is gone
assert not info2.path.exists(), "Removed worktree path should not exist"
# Verify other worktrees still exist and are intact
assert info1.path.exists(), "First worktree should still exist"
assert info3.path.exists(), "Third worktree should still exist"
# Verify other worktrees still have their data
assert (info1.path / "removal-test-1.txt").exists(), "First worktree data should be intact"
assert (info3.path / "removal-test-3.txt").exists(), "Third worktree data should be intact"
# Verify the listing is correct
remaining = manager.list_all_worktrees()
assert len(remaining) == 2, f"Should have 2 remaining worktrees, got {len(remaining)}"
def test_worktree_merge_isolation(self, test_env):
"""Merging one worktree doesn't affect other worktrees."""
from worktree import WorktreeManager
temp_dir, spec_dir, project_dir = test_env
manager = WorktreeManager(project_dir)
manager.setup()
# Create two worktrees
info1 = manager.create_worktree("merge-test-1")
info2 = manager.create_worktree("merge-test-2")
# Make changes in first worktree
(info1.path / "feature1.txt").write_text("Feature 1 implementation")
subprocess.run(["git", "add", "."], cwd=info1.path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Add feature 1"],
cwd=info1.path, capture_output=True
)
# Make changes in second worktree
(info2.path / "feature2.txt").write_text("Feature 2 implementation")
subprocess.run(["git", "add", "."], cwd=info2.path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Add feature 2"],
cwd=info2.path, capture_output=True
)
# Merge first worktree
result = manager.merge_worktree("merge-test-1", delete_after=False)
assert result is True, "Merge should succeed"
# Verify feature 1 is in main
assert (project_dir / "feature1.txt").exists(), "Feature 1 should be merged to main"
# Verify feature 2 is NOT in main yet
assert not (project_dir / "feature2.txt").exists(), "Feature 2 should NOT be in main yet"
# Verify second worktree is unaffected
assert info2.path.exists(), "Second worktree should still exist"
assert (info2.path / "feature2.txt").exists(), "Second worktree should still have feature 2"
def test_get_or_create_worktree_returns_existing(self, test_env):
"""get_or_create_worktree returns existing worktree instead of creating new."""
from worktree import WorktreeManager
temp_dir, spec_dir, project_dir = test_env
manager = WorktreeManager(project_dir)
manager.setup()
# Create a worktree and add some data
info1 = manager.create_worktree("existing-test")
marker_file = info1.path / "marker.txt"
marker_file.write_text("This is a marker")
subprocess.run(["git", "add", "."], cwd=info1.path, capture_output=True)
subprocess.run(
["git", "commit", "-m", "Add marker"],
cwd=info1.path, capture_output=True
)
# get_or_create should return the existing worktree
info2 = manager.get_or_create_worktree("existing-test")
# Should be the same worktree with the marker file
assert info2.path == info1.path, "Should return same worktree path"
assert info2.branch == info1.branch, "Should return same branch"
assert marker_file.exists(), "Marker file should still exist"
# =============================================================================
# MAIN ENTRY POINT
# =============================================================================
def run_all_tests():
"""Run all tests using pytest."""
sys.exit(pytest.main([__file__, "-v", "--tb=short"]))
if __name__ == "__main__":
run_all_tests()