story2.5 - implement additional phases in native methodology

- Introduced three new phases: Planning, Coding, and QA Validation to the NativeRunner methodology.
- Updated the manifest.yaml to reflect the new phase structure and associated artifacts.
- Enhanced the NativeRunner class to execute the new phases, including agent invocation for planning and coding.
- Updated tests to validate the new phase structure and execution flow, ensuring correct phase IDs and artifact handling.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
Test User
2026-01-14 15:34:19 +01:00
co-authored by Claude Opus 4.5
parent c06ca47a39
commit d9c75813f6
3 changed files with 808 additions and 150 deletions
@@ -25,18 +25,26 @@ phases:
description: "Generate specification document"
artifacts:
- spec-md
- id: plan
name: "Planning"
description: "Create implementation plan with subtasks"
artifacts:
- implementation-plan-json
- id: validate
name: "Validation"
description: "Validate spec completeness"
- id: planning
name: "Planning"
description: "Create implementation plan from spec via planner agent"
artifacts:
- implementation-plan-json
- id: coding
name: "Coding"
description: "Implement subtasks from the plan via coder agent"
- id: qa_validation
name: "QA Validation"
description: "Validate acceptance criteria via QA reviewer/fixer loop"
artifacts:
- qa-report-md
checkpoints:
- id: after_planning
phase: plan
phase: planning
description: "Review implementation plan before coding"
- id: after_spec
phase: spec
@@ -62,6 +70,10 @@ artifacts:
name: "Implementation Plan"
type: json
path: "implementation_plan.json"
- id: qa-report-md
name: "QA Report"
type: markdown
path: "qa_report.md"
complexity_levels:
- quick
+370 -54
View File
@@ -48,19 +48,22 @@ class NativeRunner:
This class implements the MethodologyRunner Protocol, providing the interface
for the plugin framework to execute the Native methodology.
The Native methodology follows a 6-phase pipeline:
The Native methodology follows an 8-phase pipeline:
1. Discovery - Gather project context and user requirements
2. Requirements - Structure and validate requirements
3. Context - Build codebase context for implementation
4. Spec - Generate specification document
5. Plan - Create implementation plan with subtasks
6. Validate - Validate spec completeness
5. Validate - Validate spec completeness
6. Planning - Create implementation plan via planner agent
7. Coding - Implement subtasks via coder agent
8. QA Validation - Validate via QA reviewer/fixer loop
Delegation Pattern:
- Discovery: delegates to spec.discovery.run_discovery_script
- Requirements: delegates to spec.requirements module
- Context: delegates to spec.context module
- Spec/Plan/Validate: require framework agent infrastructure
- Spec/Validate: require framework agent infrastructure
- Planning/Coding/QA: delegate to agents module
Example:
runner = NativeRunner()
@@ -70,6 +73,9 @@ class NativeRunner:
result = runner.execute_phase(phase.id)
"""
# Default model for agent execution (Story 2.5)
_DEFAULT_AGENT_MODEL: str = "claude-sonnet-4-5-20250929"
def __init__(self) -> None:
"""Initialize NativeRunner instance."""
self._context: RunContext | None = None
@@ -269,8 +275,11 @@ class NativeRunner:
"requirements": self._execute_requirements,
"context": self._execute_context,
"spec": self._execute_spec,
"plan": self._execute_plan,
"validate": self._execute_validate,
# Story 2.5: Implementation phases
"planning": self._execute_planning,
"coding": self._execute_coding,
"qa_validation": self._execute_qa_validation,
}
handler = dispatch.get(phase_id)
@@ -512,41 +521,6 @@ class NativeRunner:
"Use SpecOrchestrator for full pipeline execution.",
)
def _execute_plan(self) -> PhaseResult:
"""Execute the planning phase.
Creates implementation plan via agent execution.
Requires framework agent infrastructure for full implementation.
Returns:
PhaseResult with success status and artifacts
"""
if self._spec_dir is None:
return PhaseResult(
success=False,
phase_id="plan",
error="No spec_dir configured. Set spec_dir in task_config.metadata.",
)
plan_file = self._spec_dir / "implementation_plan.json"
# Check if plan already exists
if plan_file.exists():
return PhaseResult(
success=True,
phase_id="plan",
message="Implementation plan already exists",
artifacts=[str(plan_file)],
)
# Plan generation requires agent execution via framework
return PhaseResult(
success=False,
phase_id="plan",
error="Plan generation requires framework agent infrastructure. "
"Use SpecOrchestrator for full pipeline execution.",
)
def _execute_validate(self) -> PhaseResult:
"""Execute the validation phase.
@@ -586,6 +560,317 @@ class NativeRunner:
error=f"Validation failed: {'; '.join(errors)}",
)
# =========================================================================
# Story 2.5: Implementation Phase Methods
# =========================================================================
def _run_async(self, coro):
"""Run an async coroutine safely from sync context.
Handles the case where we might already be in an async context
by using a ThreadPoolExecutor with asyncio.run().
Args:
coro: The async coroutine to execute
Returns:
The result of the coroutine
Note:
Uses asyncio.run() which creates a new event loop. This is
preferred over get_event_loop() which is deprecated in Python 3.10+.
"""
import asyncio
try:
# Check if we're already in an async context
asyncio.get_running_loop()
# We're in an async context, run in thread
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
return executor.submit(asyncio.run, coro).result()
except RuntimeError:
# No running loop, safe to use asyncio.run
return asyncio.run(coro)
def _execute_planning(self) -> PhaseResult:
"""Execute the planning phase via planner agent.
Invokes the existing planner agent to create implementation_plan.json
from the spec.md document. Uses the Claude SDK client for agent execution.
Returns:
PhaseResult with success status and implementation_plan.json artifact
Story Reference: Story 2.5 AC#1 - Planning phase produces implementation_plan.json
"""
if self._spec_dir is None:
return PhaseResult(
success=False,
phase_id="planning",
error="No spec_dir configured. Set spec_dir in task_config.metadata.",
)
# Check if spec.md exists
spec_file = self._spec_dir / "spec.md"
if not spec_file.exists():
return PhaseResult(
success=False,
phase_id="planning",
error="spec.md not found. Run spec phase first.",
)
# Check if plan already exists
plan_file = self._spec_dir / "implementation_plan.json"
if plan_file.exists():
return PhaseResult(
success=True,
phase_id="planning",
message="Implementation plan already exists",
artifacts=[str(plan_file)],
)
# Report progress
self._invoke_progress_callback("Starting planner agent...", 10.0)
try:
from apps.backend.agents.session import run_agent_session
from apps.backend.core.client import create_client
from apps.backend.prompts_pkg.prompt_generator import (
generate_planner_prompt,
)
from apps.backend.task_logger import LogPhase
project_dir = Path(self._worktree_path or self._project_dir)
# Generate planner prompt
self._invoke_progress_callback("Generating planner prompt...", 20.0)
prompt = generate_planner_prompt(self._spec_dir, project_dir)
# Create client for planner agent
self._invoke_progress_callback("Creating planner agent client...", 30.0)
model = (
self._task_config.metadata.get("model", self._DEFAULT_AGENT_MODEL)
if self._task_config
else self._DEFAULT_AGENT_MODEL
)
client = create_client(
project_dir,
self._spec_dir,
model=model,
agent_type="planner",
max_thinking_tokens=None,
)
# Run planner agent session
self._invoke_progress_callback("Running planner agent...", 50.0)
async def _run_planner():
async with client:
status, response = await run_agent_session(
client,
prompt,
self._spec_dir,
verbose=False,
phase=LogPhase.PLANNING,
)
return status, response
status, response = self._run_async(_run_planner())
self._invoke_progress_callback("Planner agent completed", 90.0)
# Verify the plan was created
if plan_file.exists():
return PhaseResult(
success=True,
phase_id="planning",
message="Implementation plan created by planner agent",
artifacts=[str(plan_file)],
)
else:
return PhaseResult(
success=False,
phase_id="planning",
error=f"Planner agent completed with status '{status}' but implementation_plan.json was not created",
)
except Exception as e:
logger.error(f"Planning phase failed: {e}")
return PhaseResult(
success=False,
phase_id="planning",
error=f"Planning phase failed: {str(e)}",
)
def _execute_coding(self) -> PhaseResult:
"""Execute the coding phase via coder agent.
Loads the implementation_plan.json and executes each subtask via the
coder agent. Updates subtask status in the plan after each execution.
Returns:
PhaseResult with success status
Story Reference: Story 2.5 AC#2 - Coding phase implements subtasks
"""
if self._spec_dir is None:
return PhaseResult(
success=False,
phase_id="coding",
error="No spec_dir configured. Set spec_dir in task_config.metadata.",
)
# Check if plan exists
plan_file = self._spec_dir / "implementation_plan.json"
if not plan_file.exists():
return PhaseResult(
success=False,
phase_id="coding",
error="implementation_plan.json not found. Run planning phase first.",
)
self._invoke_progress_callback("Loading implementation plan...", 5.0)
try:
from apps.backend.agents.coder import run_autonomous_agent
project_dir = Path(self._worktree_path or self._project_dir)
self._invoke_progress_callback("Starting coder agent loop...", 10.0)
model = (
self._task_config.metadata.get("model", self._DEFAULT_AGENT_MODEL)
if self._task_config
else self._DEFAULT_AGENT_MODEL
)
async def _run_coder():
await run_autonomous_agent(
project_dir=project_dir,
spec_dir=self._spec_dir,
model=model,
max_iterations=None,
verbose=False,
source_spec_dir=None,
)
self._run_async(_run_coder())
self._invoke_progress_callback("Coder agent completed", 100.0)
# Check if all subtasks are completed
from apps.backend.progress import is_build_complete
if is_build_complete(self._spec_dir):
return PhaseResult(
success=True,
phase_id="coding",
message="All subtasks implemented successfully",
artifacts=[],
)
else:
# Get progress info
from apps.backend.progress import count_subtasks
completed, total = count_subtasks(self._spec_dir)
return PhaseResult(
success=False,
phase_id="coding",
error=f"Coding incomplete: {completed}/{total} subtasks completed",
)
except Exception as e:
logger.error(f"Coding phase failed: {e}")
return PhaseResult(
success=False,
phase_id="coding",
error=f"Coding phase failed: {str(e)}",
)
def _execute_qa_validation(self) -> PhaseResult:
"""Execute the QA validation phase via QA reviewer/fixer loop.
Runs the QA validation loop that:
1. QA reviewer validates acceptance criteria
2. If issues found, QA fixer applies fixes
3. Loop continues until approved or max iterations
Returns:
PhaseResult with success status and qa_report.md artifact
Story Reference: Story 2.5 AC#3 - QA validation with reviewer/fixer loop
"""
if self._spec_dir is None:
return PhaseResult(
success=False,
phase_id="qa_validation",
error="No spec_dir configured. Set spec_dir in task_config.metadata.",
)
# Check if build is complete
from apps.backend.progress import is_build_complete
if not is_build_complete(self._spec_dir):
return PhaseResult(
success=False,
phase_id="qa_validation",
error="Build not complete. Run coding phase first.",
)
self._invoke_progress_callback("Starting QA validation loop...", 10.0)
try:
from apps.backend.qa.loop import run_qa_validation_loop
project_dir = Path(self._worktree_path or self._project_dir)
model = (
self._task_config.metadata.get("model", self._DEFAULT_AGENT_MODEL)
if self._task_config
else self._DEFAULT_AGENT_MODEL
)
async def _run_qa():
return await run_qa_validation_loop(
project_dir=project_dir,
spec_dir=self._spec_dir,
model=model,
verbose=False,
)
approved = self._run_async(_run_qa())
self._invoke_progress_callback("QA validation completed", 100.0)
# Get QA report artifact
qa_report = self._spec_dir / "qa_report.md"
artifacts = [str(qa_report)] if qa_report.exists() else []
if approved:
return PhaseResult(
success=True,
phase_id="qa_validation",
message="QA validation passed - all acceptance criteria verified",
artifacts=artifacts,
)
else:
return PhaseResult(
success=False,
phase_id="qa_validation",
error="QA validation failed - see qa_report.md for details",
artifacts=artifacts,
)
except Exception as e:
logger.error(f"QA validation phase failed: {e}")
return PhaseResult(
success=False,
phase_id="qa_validation",
error=f"QA validation phase failed: {str(e)}",
)
def get_checkpoints(self) -> list[Checkpoint]:
"""Return checkpoint definitions for Semi-Auto mode.
@@ -639,14 +924,19 @@ class NativeRunner:
# Story 2.4: Progress Reporting Methods
# =========================================================================
# Phase weights for percentage calculation (Story 2.4 Task 6)
# Phase weights for percentage calculation (Story 2.4 Task 6, Story 2.5)
# Phase weights for percentage calculation (Story 2.4 Task 6, Story 2.5)
# Total: 100% (5+5+5+10+5+10+40+20 = 100)
_PHASE_WEIGHTS: dict[str, int] = {
"discovery": 10,
"requirements": 10,
"context": 15,
"spec": 25,
"plan": 20,
"validate": 20,
"discovery": 5,
"requirements": 5,
"context": 5,
"spec": 10,
"validate": 5,
# Story 2.5: Implementation phases
"planning": 10,
"coding": 40,
"qa_validation": 20,
}
def _emit_progress_event(
@@ -831,21 +1121,38 @@ class NativeRunner:
is_optional=False,
),
Phase(
id="plan",
name="Planning",
description="Create implementation plan with subtasks",
id="validate",
name="Validation",
description="Validate spec completeness",
order=5,
status=PhaseStatus.PENDING,
is_optional=False,
),
# Story 2.5: Implementation phases
Phase(
id="validate",
name="Validation",
description="Validate spec completeness",
id="planning",
name="Planning",
description="Create implementation plan from spec via planner agent",
order=6,
status=PhaseStatus.PENDING,
is_optional=False,
),
Phase(
id="coding",
name="Coding",
description="Implement subtasks from the plan via coder agent",
order=7,
status=PhaseStatus.PENDING,
is_optional=False,
),
Phase(
id="qa_validation",
name="QA Validation",
description="Validate acceptance criteria via QA reviewer/fixer loop",
order=8,
status=PhaseStatus.PENDING,
is_optional=False,
),
]
def _init_checkpoints(self) -> None:
@@ -855,7 +1162,7 @@ class NativeRunner:
id="after_planning",
name="Planning Review",
description="Review implementation plan before coding",
phase_id="plan",
phase_id="planning",
status=CheckpointStatus.PENDING,
requires_approval=True,
),
@@ -912,6 +1219,15 @@ class NativeRunner:
phase_id="plan",
content_type="application/json",
),
# Story 2.5: QA artifacts
Artifact(
id="qa-report-md",
artifact_type="markdown",
name="QA Report",
file_path="qa_report.md",
phase_id="qa_validation",
content_type="text/markdown",
),
]
# =========================================================================
+420 -90
View File
@@ -282,16 +282,16 @@ class TestNativeManifestValidation:
manifest = load_manifest(manifest_path)
assert manifest.entry_point == "methodology.NativeRunner"
def test_manifest_has_six_phases(self):
"""Test that manifest defines exactly 6 phases."""
def test_manifest_has_nine_phases(self):
"""Test that manifest defines exactly 9 phases (6 spec + 3 implementation)."""
from apps.backend.methodologies.manifest import load_manifest
manifest_path = NATIVE_METHODOLOGY_DIR / "manifest.yaml"
manifest = load_manifest(manifest_path)
assert len(manifest.phases) == 6
assert len(manifest.phases) == 8
def test_manifest_phase_ids_are_correct(self):
"""Test that phases have the correct IDs per AC #2."""
"""Test that phases have the correct IDs (6 spec + 3 implementation)."""
from apps.backend.methodologies.manifest import load_manifest
manifest_path = NATIVE_METHODOLOGY_DIR / "manifest.yaml"
@@ -302,8 +302,11 @@ class TestNativeManifestValidation:
"requirements",
"context",
"spec",
"plan",
"validate",
# Story 2.5: Implementation phases
"planning",
"coding",
"qa_validation",
]
actual_phase_ids = [phase.id for phase in manifest.phases]
assert actual_phase_ids == expected_phase_ids
@@ -446,7 +449,7 @@ class TestNativeRunnerInitialization:
# Should not raise after initialization
phases = runner.get_phases()
assert len(phases) == 6
assert len(phases) == 8 # 6 spec + 3 implementation phases (Story 2.5)
def test_runner_cannot_initialize_twice(self, mock_context, mock_workspace_manager):
"""Test runner raises error if initialized twice."""
@@ -484,16 +487,16 @@ class TestNativeRunnerPhases:
phases = initialized_runner.get_phases()
assert isinstance(phases, list)
def test_get_phases_returns_six_phases(self, initialized_runner):
"""Test get_phases returns exactly 6 phases."""
def test_get_phases_returns_nine_phases(self, initialized_runner):
"""Test get_phases returns exactly 9 phases (6 spec + 3 implementation)."""
phases = initialized_runner.get_phases()
assert len(phases) == 6
assert len(phases) == 8
def test_phases_have_correct_order(self, initialized_runner):
"""Test phases are in correct execution order."""
"""Test phases are in correct execution order (1-8)."""
phases = initialized_runner.get_phases()
orders = [phase.order for phase in phases]
assert orders == [1, 2, 3, 4, 5, 6]
assert orders == [1, 2, 3, 4, 5, 6, 7, 8]
def test_phases_have_pending_status_initially(self, initialized_runner):
"""Test all phases start with PENDING status."""
@@ -567,13 +570,13 @@ class TestNativeRunnerArtifacts:
artifacts = initialized_runner.get_artifacts()
assert isinstance(artifacts, list)
def test_get_artifacts_returns_four_artifacts(self, initialized_runner):
"""Test get_artifacts returns exactly 4 artifacts."""
def test_get_artifacts_returns_five_artifacts(self, initialized_runner):
"""Test get_artifacts returns exactly 5 artifacts (4 spec + 1 QA)."""
artifacts = initialized_runner.get_artifacts()
assert len(artifacts) == 4
assert len(artifacts) == 5
def test_artifacts_have_expected_ids(self, initialized_runner):
"""Test artifacts have the expected IDs."""
"""Test artifacts have the expected IDs (4 spec + 1 QA)."""
artifacts = initialized_runner.get_artifacts()
expected_ids = {
@@ -581,6 +584,8 @@ class TestNativeRunnerArtifacts:
"context-json",
"spec-md",
"implementation-plan-json",
# Story 2.5: QA artifact
"qa-report-md",
}
actual_ids = {artifact.id for artifact in artifacts}
assert actual_ids == expected_ids
@@ -749,10 +754,14 @@ class TestNativeRunnerGetPhasesStory22:
"""Test NativeRunner get_phases returns correct phase info (Story 2.2 AC#2)."""
def test_get_phases_returns_phases_with_ids(self, initialized_runner):
"""Test phases have expected IDs matching manifest."""
"""Test phases have expected IDs matching manifest (5 spec + 3 implementation)."""
phases = initialized_runner.get_phases()
expected_ids = ["discovery", "requirements", "context", "spec", "plan", "validate"]
expected_ids = [
"discovery", "requirements", "context", "spec", "validate",
# Story 2.5: Implementation phases
"planning", "coding", "qa_validation"
]
actual_ids = [phase.id for phase in phases]
assert actual_ids == expected_ids
@@ -808,13 +817,6 @@ class TestNativeRunnerPhaseExecutionNoSpecDir:
assert result.success is False
assert "spec_dir" in result.error.lower()
def test_plan_fails_without_spec_dir(self, initialized_runner):
"""Test plan phase fails without spec_dir."""
result = initialized_runner.execute_phase("plan")
assert result.success is False
assert "spec_dir" in result.error.lower()
def test_validate_fails_without_spec_dir(self, initialized_runner):
"""Test validate phase fails without spec_dir."""
result = initialized_runner.execute_phase("validate")
@@ -926,33 +928,6 @@ class TestNativeRunnerSpecPhase:
assert len(result.artifacts) == 1
class TestNativeRunnerPlanPhase:
"""Test plan phase execution (Story 2.2 Task 9)."""
def test_plan_fails_without_existing_file(self, initialized_runner_with_spec_dir):
"""Test plan phase fails when implementation_plan.json doesn't exist."""
result = initialized_runner_with_spec_dir.execute_phase("plan")
# Plan generation requires agent infrastructure
assert result.success is False
assert "framework" in result.error.lower() or "agent" in result.error.lower()
def test_plan_returns_existing_if_present(self, initialized_runner_with_spec_dir):
"""Test plan phase succeeds if implementation_plan.json already exists."""
import json
runner = initialized_runner_with_spec_dir
# Create existing plan
plan_file = runner._spec_dir / "implementation_plan.json"
plan_file.write_text(json.dumps({"subtasks": []}))
result = runner.execute_phase("plan")
assert result.success is True
assert "already exists" in result.message.lower()
class TestNativeRunnerValidatePhase:
"""Test validate phase execution (Story 2.2 Task 10)."""
@@ -1722,8 +1697,8 @@ class TestIncrementalProgressReporting:
assert event.phase_id == "spec"
assert event.status == "in_progress"
assert event.message == "Generating specification..."
# 50% within spec phase (35-60%) = 35 + (25 * 0.5) = 47.5% overall
assert event.percentage == 47.5
# Story 2.5: 50% within spec phase (15-25%) = 15 + (10 * 0.5) = 20% overall
assert event.percentage == 20.0
def test_phase_percentage_within_bounds(self, initialized_runner_with_spec_dir):
"""Test incremental progress percentage stays within phase bounds."""
@@ -1741,7 +1716,7 @@ class TestIncrementalProgressReporting:
mock_progress.update = MagicMock()
runner._context.progress = mock_progress
# Emit progress at 50% within spec phase (which is 35-60% overall)
# Story 2.5: Emit progress at 50% within spec phase (which is 15-25% overall)
runner.emit_incremental_progress(
phase_id="spec",
message="Halfway through spec",
@@ -1749,8 +1724,8 @@ class TestIncrementalProgressReporting:
)
event = emitted_events[0]
# Spec phase: start=35%, end=60%, so 50% within = 35 + (25 * 0.5) = 47.5%
assert 35.0 <= event.percentage <= 60.0
# Spec phase: start=15%, end=25%, so 50% within = 15 + (10 * 0.5) = 20%
assert 15.0 <= event.percentage <= 25.0
# =============================================================================
@@ -1903,55 +1878,88 @@ class TestProgressCallbacks:
class TestPhasePercentageCalculation:
"""Test phase percentage calculation (Story 2.4 Task 6)."""
"""Test phase percentage calculation (Story 2.4 Task 6, updated for Story 2.5).
With Story 2.5 (after removing duplicate plan phase), the weights are:
- discovery: 5%, requirements: 5%, context: 5%, spec: 10%, validate: 5%
- planning: 10%, coding: 40%, qa_validation: 20%
Total: 100%
Phase ranges:
- discovery: 0-5%
- requirements: 5-10%
- context: 10-15%
- spec: 15-25%
- validate: 25-30%
- planning: 30-40%
- coding: 40-80%
- qa_validation: 80-100%
"""
def test_discovery_starts_at_zero(self, initialized_runner):
"""Test discovery phase starts at 0%."""
assert initialized_runner._get_phase_start_percentage("discovery") == 0.0
def test_discovery_ends_at_ten(self, initialized_runner):
"""Test discovery phase ends at 10%."""
assert initialized_runner._get_phase_end_percentage("discovery") == 10.0
def test_discovery_ends_at_five(self, initialized_runner):
"""Test discovery phase ends at 5%."""
assert initialized_runner._get_phase_end_percentage("discovery") == 5.0
def test_requirements_starts_at_ten(self, initialized_runner):
"""Test requirements phase starts at 10%."""
assert initialized_runner._get_phase_start_percentage("requirements") == 10.0
def test_requirements_starts_at_five(self, initialized_runner):
"""Test requirements phase starts at 5%."""
assert initialized_runner._get_phase_start_percentage("requirements") == 5.0
def test_requirements_ends_at_twenty(self, initialized_runner):
"""Test requirements phase ends at 20%."""
assert initialized_runner._get_phase_end_percentage("requirements") == 20.0
def test_requirements_ends_at_ten(self, initialized_runner):
"""Test requirements phase ends at 10%."""
assert initialized_runner._get_phase_end_percentage("requirements") == 10.0
def test_context_starts_at_twenty(self, initialized_runner):
"""Test context phase starts at 20%."""
assert initialized_runner._get_phase_start_percentage("context") == 20.0
def test_context_starts_at_ten(self, initialized_runner):
"""Test context phase starts at 10%."""
assert initialized_runner._get_phase_start_percentage("context") == 10.0
def test_context_ends_at_thirty_five(self, initialized_runner):
"""Test context phase ends at 35%."""
assert initialized_runner._get_phase_end_percentage("context") == 35.0
def test_context_ends_at_fifteen(self, initialized_runner):
"""Test context phase ends at 15%."""
assert initialized_runner._get_phase_end_percentage("context") == 15.0
def test_spec_starts_at_thirty_five(self, initialized_runner):
"""Test spec phase starts at 35%."""
assert initialized_runner._get_phase_start_percentage("spec") == 35.0
def test_spec_starts_at_fifteen(self, initialized_runner):
"""Test spec phase starts at 15%."""
assert initialized_runner._get_phase_start_percentage("spec") == 15.0
def test_spec_ends_at_sixty(self, initialized_runner):
"""Test spec phase ends at 60%."""
assert initialized_runner._get_phase_end_percentage("spec") == 60.0
def test_spec_ends_at_twenty_five(self, initialized_runner):
"""Test spec phase ends at 25%."""
assert initialized_runner._get_phase_end_percentage("spec") == 25.0
def test_plan_starts_at_sixty(self, initialized_runner):
"""Test plan phase starts at 60%."""
assert initialized_runner._get_phase_start_percentage("plan") == 60.0
def test_validate_starts_at_twenty_five(self, initialized_runner):
"""Test validate phase starts at 25%."""
assert initialized_runner._get_phase_start_percentage("validate") == 25.0
def test_plan_ends_at_eighty(self, initialized_runner):
"""Test plan phase ends at 80%."""
assert initialized_runner._get_phase_end_percentage("plan") == 80.0
def test_validate_ends_at_thirty(self, initialized_runner):
"""Test validate phase ends at 30%."""
assert initialized_runner._get_phase_end_percentage("validate") == 30.0
def test_validate_starts_at_eighty(self, initialized_runner):
"""Test validate phase starts at 80%."""
assert initialized_runner._get_phase_start_percentage("validate") == 80.0
# Story 2.5: Implementation phase percentage tests
def test_planning_starts_at_thirty(self, initialized_runner):
"""Test planning phase starts at 30%."""
assert initialized_runner._get_phase_start_percentage("planning") == 30.0
def test_validate_ends_at_hundred(self, initialized_runner):
"""Test validate phase ends at 100%."""
assert initialized_runner._get_phase_end_percentage("validate") == 100.0
def test_planning_ends_at_forty(self, initialized_runner):
"""Test planning phase ends at 40%."""
assert initialized_runner._get_phase_end_percentage("planning") == 40.0
def test_coding_starts_at_forty(self, initialized_runner):
"""Test coding phase starts at 40%."""
assert initialized_runner._get_phase_start_percentage("coding") == 40.0
def test_coding_ends_at_eighty(self, initialized_runner):
"""Test coding phase ends at 80%."""
assert initialized_runner._get_phase_end_percentage("coding") == 80.0
def test_qa_validation_starts_at_eighty(self, initialized_runner):
"""Test qa_validation phase starts at 80%."""
assert initialized_runner._get_phase_start_percentage("qa_validation") == 80.0
def test_qa_validation_ends_at_hundred(self, initialized_runner):
"""Test qa_validation phase ends at 100%."""
assert initialized_runner._get_phase_end_percentage("qa_validation") == 100.0
def test_unknown_phase_returns_zero(self, initialized_runner):
"""Test unknown phase returns 0%."""
@@ -2031,3 +2039,325 @@ class TestNativeRunnerCleanup:
runner = NativeRunner()
# Should not raise
runner.cleanup()
# =============================================================================
# Story 2.5: Implementation Phase Tests
# =============================================================================
class TestNativeRunnerPlanningPhase:
"""Test planning implementation phase execution (Story 2.5 AC#1)."""
def test_planning_phase_exists_in_phases(self, initialized_runner):
"""Test planning phase is in phases list."""
phases = initialized_runner.get_phases()
phase_ids = [p.id for p in phases]
assert "planning" in phase_ids
def test_planning_phase_fails_without_spec_dir(self, initialized_runner):
"""Test planning phase fails without spec_dir."""
result = initialized_runner.execute_phase("planning")
assert result.success is False
assert "spec_dir" in result.error.lower()
def test_planning_phase_fails_without_spec_md(self, initialized_runner_with_spec_dir):
"""Test planning phase fails if spec.md doesn't exist."""
result = initialized_runner_with_spec_dir.execute_phase("planning")
assert result.success is False
assert "spec.md" in result.error.lower()
def test_planning_returns_existing_plan(self, initialized_runner_with_spec_dir):
"""Test planning phase returns existing plan if present."""
import json
runner = initialized_runner_with_spec_dir
# Create spec.md and existing plan
spec_file = runner._spec_dir / "spec.md"
spec_file.write_text("# Test Spec\n\nContent here.")
plan_file = runner._spec_dir / "implementation_plan.json"
plan_file.write_text(json.dumps({"subtasks": [{"id": "1.1"}]}))
result = runner.execute_phase("planning")
assert result.success is True
assert "already exists" in result.message.lower()
assert str(plan_file) in result.artifacts
class TestNativeRunnerCodingPhase:
"""Test coding implementation phase execution (Story 2.5 AC#2)."""
def test_coding_phase_exists_in_phases(self, initialized_runner):
"""Test coding phase is in phases list."""
phases = initialized_runner.get_phases()
phase_ids = [p.id for p in phases]
assert "coding" in phase_ids
def test_coding_phase_fails_without_spec_dir(self, initialized_runner):
"""Test coding phase fails without spec_dir."""
result = initialized_runner.execute_phase("coding")
assert result.success is False
assert "spec_dir" in result.error.lower()
def test_coding_phase_fails_without_plan(self, initialized_runner_with_spec_dir):
"""Test coding phase fails if implementation_plan.json doesn't exist."""
result = initialized_runner_with_spec_dir.execute_phase("coding")
assert result.success is False
assert "implementation_plan.json" in result.error.lower()
class TestNativeRunnerQAValidationPhase:
"""Test QA validation phase execution (Story 2.5 AC#3)."""
def test_qa_validation_phase_exists_in_phases(self, initialized_runner):
"""Test qa_validation phase is in phases list."""
phases = initialized_runner.get_phases()
phase_ids = [p.id for p in phases]
assert "qa_validation" in phase_ids
def test_qa_validation_phase_fails_without_spec_dir(self, initialized_runner):
"""Test qa_validation phase fails without spec_dir."""
result = initialized_runner.execute_phase("qa_validation")
assert result.success is False
assert "spec_dir" in result.error.lower()
def test_qa_validation_phase_fails_without_complete_build(self, initialized_runner_with_spec_dir):
"""Test qa_validation phase fails if build not complete."""
import json
runner = initialized_runner_with_spec_dir
# Create incomplete plan (some subtasks not completed)
plan_file = runner._spec_dir / "implementation_plan.json"
plan_file.write_text(json.dumps({
"subtasks": [
{"id": "1.1", "status": "completed"},
{"id": "1.2", "status": "pending"} # Not complete
]
}))
result = runner.execute_phase("qa_validation")
assert result.success is False
assert "complete" in result.error.lower()
class TestNativeRunnerImplementationPhaseIntegration:
"""Test integration between implementation phases (Story 2.5)."""
def test_implementation_phases_have_correct_order(self, initialized_runner):
"""Test implementation phases are ordered after spec phases."""
phases = initialized_runner.get_phases()
# Get phase orders
phase_order = {p.id: p.order for p in phases}
# Implementation phases should come after spec phases
assert phase_order["planning"] > phase_order["validate"]
assert phase_order["coding"] > phase_order["planning"]
assert phase_order["qa_validation"] > phase_order["coding"]
def test_qa_report_artifact_exists(self, initialized_runner):
"""Test qa-report-md artifact is defined."""
artifacts = initialized_runner.get_artifacts()
artifact_ids = [a.id for a in artifacts]
assert "qa-report-md" in artifact_ids
def test_qa_report_artifact_has_correct_phase(self, initialized_runner):
"""Test qa-report-md artifact is associated with qa_validation phase."""
artifacts = initialized_runner.get_artifacts()
qa_artifact = next(a for a in artifacts if a.id == "qa-report-md")
assert qa_artifact.phase_id == "qa_validation"
def test_qa_report_artifact_has_correct_file_path(self, initialized_runner):
"""Test qa-report-md artifact has correct file path."""
artifacts = initialized_runner.get_artifacts()
qa_artifact = next(a for a in artifacts if a.id == "qa-report-md")
assert qa_artifact.file_path == "qa_report.md"
class TestNativeRunnerAgentInvocation:
"""Test that implementation phases invoke correct agent functions (Story 2.5)."""
def test_planning_invokes_run_agent_session(self, initialized_runner_with_spec_dir):
"""Test planning phase calls run_agent_session with correct args."""
import json
from unittest.mock import patch, MagicMock, AsyncMock
runner = initialized_runner_with_spec_dir
# Create spec.md (required for planning)
spec_file = runner._spec_dir / "spec.md"
spec_file.write_text("# Test Spec\n\n## Overview\nTest content")
# Mock the agent session and related functions
mock_session = AsyncMock(return_value=("complete", "response"))
mock_client = MagicMock()
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=None)
with patch(
"apps.backend.agents.session.run_agent_session",
mock_session,
), patch(
"apps.backend.core.client.create_client",
return_value=mock_client,
) as mock_create_client, patch(
"apps.backend.prompts_pkg.prompt_generator.generate_planner_prompt",
return_value="test prompt",
):
# The phase will fail because implementation_plan.json isn't created
# but we can verify the agent was invoked
result = runner.execute_phase("planning")
# Verify create_client was called with correct agent_type
mock_create_client.assert_called_once()
call_kwargs = mock_create_client.call_args
assert call_kwargs[1]["agent_type"] == "planner"
# Verify run_agent_session was called
mock_session.assert_called_once()
def test_coding_invokes_run_autonomous_agent(self, initialized_runner_with_spec_dir):
"""Test coding phase calls run_autonomous_agent with correct args."""
import json
from unittest.mock import patch, MagicMock, AsyncMock
runner = initialized_runner_with_spec_dir
# Create implementation_plan.json (required for coding)
plan_file = runner._spec_dir / "implementation_plan.json"
plan_file.write_text(json.dumps({
"subtasks": [
{"id": "1.1", "title": "Test", "status": "pending"}
]
}))
mock_coder = AsyncMock()
with patch(
"apps.backend.agents.coder.run_autonomous_agent",
mock_coder,
), patch(
"apps.backend.progress.is_build_complete",
return_value=True,
):
result = runner.execute_phase("coding")
# Verify run_autonomous_agent was called
mock_coder.assert_called_once()
# Verify it was called with spec_dir
call_kwargs = mock_coder.call_args
assert call_kwargs[1]["spec_dir"] == runner._spec_dir
def test_qa_validation_invokes_run_qa_validation_loop(self, initialized_runner_with_spec_dir):
"""Test qa_validation phase calls run_qa_validation_loop with correct args."""
import json
from unittest.mock import patch, AsyncMock
runner = initialized_runner_with_spec_dir
# Create complete build (all subtasks completed)
plan_file = runner._spec_dir / "implementation_plan.json"
plan_file.write_text(json.dumps({
"subtasks": [
{"id": "1.1", "status": "completed"},
{"id": "1.2", "status": "completed"}
]
}))
mock_qa_loop = AsyncMock(return_value=True) # Return approved
with patch(
"apps.backend.qa.loop.run_qa_validation_loop",
mock_qa_loop,
), patch(
"apps.backend.progress.is_build_complete",
return_value=True,
):
result = runner.execute_phase("qa_validation")
# Verify run_qa_validation_loop was called
mock_qa_loop.assert_called_once()
# Verify result
assert result.success is True
assert "passed" in result.message.lower()
def test_model_from_task_config_metadata(self, initialized_runner_with_spec_dir):
"""Test that model is read from task_config.metadata if available."""
import json
from unittest.mock import patch, MagicMock, AsyncMock
runner = initialized_runner_with_spec_dir
# Set a custom model in task_config metadata
runner._task_config.metadata["model"] = "claude-opus-4-5-20251101"
# Create spec.md (required for planning)
spec_file = runner._spec_dir / "spec.md"
spec_file.write_text("# Test Spec\n\n## Overview\nTest content")
mock_client = MagicMock()
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=None)
mock_session = AsyncMock(return_value=("complete", "response"))
with patch(
"apps.backend.agents.session.run_agent_session",
mock_session,
), patch(
"apps.backend.core.client.create_client",
return_value=mock_client,
) as mock_create_client, patch(
"apps.backend.prompts_pkg.prompt_generator.generate_planner_prompt",
return_value="test prompt",
):
result = runner.execute_phase("planning")
# Verify create_client was called with the custom model
mock_create_client.assert_called_once()
call_kwargs = mock_create_client.call_args
assert call_kwargs[1]["model"] == "claude-opus-4-5-20251101"
def test_default_model_used_when_not_in_metadata(self, initialized_runner_with_spec_dir):
"""Test that DEFAULT_AGENT_MODEL is used when model not in metadata."""
import json
from unittest.mock import patch, MagicMock, AsyncMock
from apps.backend.methodologies.native.methodology import NativeRunner
runner = initialized_runner_with_spec_dir
# Ensure no model in metadata
if "model" in runner._task_config.metadata:
del runner._task_config.metadata["model"]
# Create spec.md (required for planning)
spec_file = runner._spec_dir / "spec.md"
spec_file.write_text("# Test Spec\n\n## Overview\nTest content")
mock_client = MagicMock()
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=None)
mock_session = AsyncMock(return_value=("complete", "response"))
with patch(
"apps.backend.agents.session.run_agent_session",
mock_session,
), patch(
"apps.backend.core.client.create_client",
return_value=mock_client,
) as mock_create_client, patch(
"apps.backend.prompts_pkg.prompt_generator.generate_planner_prompt",
return_value="test prompt",
):
result = runner.execute_phase("planning")
# Verify create_client was called with the default model
mock_create_client.assert_called_once()
call_kwargs = mock_create_client.call_args
assert call_kwargs[1]["model"] == NativeRunner._DEFAULT_AGENT_MODEL