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:
co-authored by
Claude Opus 4.5
parent
c06ca47a39
commit
d9c75813f6
@@ -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
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
# =========================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user