2dd920078a
The PR added resolve_actor_options() to StrategyActor, LLMStrategizeActor, and LLMExecuteActor, but the three mock lifecycle SimpleNamespace objects used by tests did not include this method, causing AttributeError at runtime: - features/mocks/mock_strategy_llm.py: make_mock_lifecycle() used by robot/helper_strategy_actor.py (strategy_actor.robot llm-json test) and features/steps/strategy_actor_llm_steps.py - features/steps/llm_actors_coverage_steps.py: _make_mock_lifecycle() used by llm_actors_coverage.feature scenarios - robot/helper_m5_e2e_context.py: inline lifecycle SimpleNamespace used by m5_e2e_verification.robot Execute Phase LLM Uses ACMS Context All three now include resolve_actor_options=MagicMock(return_value=None) matching the None-return contract the actors use when no custom backend is configured. ISSUES CLOSED: #11256
369 lines
9.3 KiB
Python
369 lines
9.3 KiB
Python
"""Mock LLM provider for Strategy Actor BDD tests.
|
|
|
|
Provides deterministic LLM responses for testing the StrategyActor
|
|
without requiring real AI credentials. All mocks live in ``features/``
|
|
per ADR-022 (no mocks in production code).
|
|
|
|
Forgejo: #828
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Canned LLM responses
|
|
# ---------------------------------------------------------------------------
|
|
|
|
STRATEGY_JSON_RESPONSE = """[
|
|
{
|
|
"step": 1,
|
|
"description": "Set up project scaffolding and configuration",
|
|
"resource_requirements": ["project-config", "build-system"],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": []
|
|
},
|
|
{
|
|
"step": 2,
|
|
"description": "Implement core domain models",
|
|
"resource_requirements": ["source-code", "database-schema"],
|
|
"estimated_complexity": "high",
|
|
"risk_score": 0.5,
|
|
"depends_on": [1]
|
|
},
|
|
{
|
|
"step": 3,
|
|
"description": "Add unit tests for domain models",
|
|
"resource_requirements": ["test-framework"],
|
|
"estimated_complexity": "medium",
|
|
"risk_score": 0.2,
|
|
"depends_on": [2]
|
|
},
|
|
{
|
|
"step": 4,
|
|
"description": "Integrate API endpoints",
|
|
"resource_requirements": ["source-code", "api-framework"],
|
|
"estimated_complexity": "medium",
|
|
"risk_score": 0.4,
|
|
"depends_on": [2]
|
|
},
|
|
{
|
|
"step": 5,
|
|
"description": "Run integration tests and validate",
|
|
"resource_requirements": ["test-framework", "ci-pipeline"],
|
|
"estimated_complexity": "medium",
|
|
"risk_score": 0.3,
|
|
"depends_on": [3, 4]
|
|
}
|
|
]"""
|
|
|
|
STRATEGY_NUMBERED_RESPONSE = (
|
|
"1. Set up project scaffolding\n"
|
|
"2. Implement core models\n"
|
|
"3. Add unit tests\n"
|
|
"4. Integrate API endpoints\n"
|
|
"5. Run integration tests"
|
|
)
|
|
|
|
STRATEGY_EMPTY_RESPONSE = ""
|
|
|
|
STRATEGY_NON_JSON_RESPONSE = (
|
|
"Here is my strategy:\n"
|
|
"1. First we should set up the project\n"
|
|
"2. Then implement the features\n"
|
|
"3. Finally run the tests"
|
|
)
|
|
|
|
STRATEGY_CYCLIC_DEPS_RESPONSE = """[
|
|
{
|
|
"step": 1,
|
|
"description": "Step A depends on C",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": [3]
|
|
},
|
|
{
|
|
"step": 2,
|
|
"description": "Step B depends on A",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": [1]
|
|
},
|
|
{
|
|
"step": 3,
|
|
"description": "Step C depends on B",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": [2]
|
|
}
|
|
]"""
|
|
|
|
STRATEGY_PARTIAL_JSON_RESPONSE = """[
|
|
{
|
|
"step": 1,
|
|
"description": "Valid action with missing fields"
|
|
},
|
|
{
|
|
"step": 2,
|
|
"description": "Another action",
|
|
"estimated_complexity": "invalid_value",
|
|
"risk_score": "not_a_number"
|
|
}
|
|
]"""
|
|
|
|
STRATEGY_RISK_CLAMP_RESPONSE = """[
|
|
{
|
|
"step": 1,
|
|
"description": "Action with excessive risk score",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "high",
|
|
"risk_score": 5.0,
|
|
"depends_on": []
|
|
}
|
|
]"""
|
|
|
|
STRATEGY_SELF_DEP_RESPONSE = """[
|
|
{
|
|
"step": 1,
|
|
"description": "Step that depends on itself",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": [1]
|
|
},
|
|
{
|
|
"step": 2,
|
|
"description": "Normal step",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": []
|
|
}
|
|
]"""
|
|
|
|
STRATEGY_DUPLICATE_STEP_RESPONSE = """[
|
|
{
|
|
"step": 3,
|
|
"description": "Step with explicit step number 3",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": []
|
|
},
|
|
{
|
|
"step": 2,
|
|
"description": "Normal step 2",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": []
|
|
},
|
|
{
|
|
"description": "Step without step field at index 2 (fallback 3)",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": []
|
|
}
|
|
]"""
|
|
|
|
STRATEGY_NEGATIVE_RISK_RESPONSE = """[
|
|
{
|
|
"step": 1,
|
|
"description": "Action with negative risk score",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": -0.5,
|
|
"depends_on": []
|
|
}
|
|
]"""
|
|
|
|
STRATEGY_NON_SEQUENTIAL_STEPS_RESPONSE = """[
|
|
{
|
|
"step": 10,
|
|
"description": "First action with step 10",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": []
|
|
},
|
|
{
|
|
"step": 20,
|
|
"description": "Second action with step 20",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "medium",
|
|
"risk_score": 0.3,
|
|
"depends_on": [10]
|
|
},
|
|
{
|
|
"step": 30,
|
|
"description": "Third action with step 30",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "high",
|
|
"risk_score": 0.5,
|
|
"depends_on": [20]
|
|
}
|
|
]"""
|
|
|
|
STRATEGY_NULL_DESCRIPTION_RESPONSE = """[
|
|
{
|
|
"step": 1,
|
|
"description": null,
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": []
|
|
},
|
|
{
|
|
"step": 2,
|
|
"description": "Valid action",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "medium",
|
|
"risk_score": 0.3,
|
|
"depends_on": []
|
|
}
|
|
]"""
|
|
|
|
STRATEGY_TRAILING_BRACKETS_RESPONSE = (
|
|
'[{"step": 1, "description": "Setup project", "resource_requirements": [],'
|
|
' "estimated_complexity": "low", "risk_score": 0.1, "depends_on": []},'
|
|
' {"step": 2, "description": "Implement feature", "resource_requirements": [],'
|
|
' "estimated_complexity": "medium", "risk_score": 0.3, "depends_on": [1]}]'
|
|
" See [this guide] for details."
|
|
)
|
|
|
|
STRATEGY_NAN_RISK_RESPONSE = """[
|
|
{
|
|
"step": 1,
|
|
"description": "Action with NaN risk score",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": "NaN",
|
|
"depends_on": []
|
|
}
|
|
]"""
|
|
|
|
STRATEGY_INF_RISK_RESPONSE = """[
|
|
{
|
|
"step": 1,
|
|
"description": "Action with Inf risk score",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "medium",
|
|
"risk_score": "Infinity",
|
|
"depends_on": []
|
|
}
|
|
]"""
|
|
|
|
STRATEGY_NON_DICT_ITEMS_RESPONSE = """[
|
|
"this is a plain string",
|
|
42,
|
|
{
|
|
"step": 1,
|
|
"description": "Valid action among non-dict items",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.2,
|
|
"depends_on": []
|
|
},
|
|
null
|
|
]"""
|
|
|
|
STRATEGY_BULLET_MARKERS_RESPONSE = (
|
|
"* Set up project structure\n"
|
|
"\u2022 Configure build system\n"
|
|
"- Implement core feature\n"
|
|
"Regular text that should be skipped\n"
|
|
"* Write tests"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mock factories
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def make_mock_llm_response(content: str) -> SimpleNamespace:
|
|
"""Create a mock LLM response with ``.content``."""
|
|
return SimpleNamespace(content=content)
|
|
|
|
|
|
def make_mock_llm(response_content: str) -> MagicMock:
|
|
"""Create a mock LLM that returns a canned response."""
|
|
mock_llm = MagicMock()
|
|
mock_llm.invoke.return_value = make_mock_llm_response(response_content)
|
|
return mock_llm
|
|
|
|
|
|
def make_mock_registry(response_content: str) -> SimpleNamespace:
|
|
"""Create a mock ProviderRegistry returning a mock LLM."""
|
|
mock_llm = make_mock_llm(response_content)
|
|
return SimpleNamespace(
|
|
create_llm=MagicMock(return_value=mock_llm),
|
|
)
|
|
|
|
|
|
def make_mock_lifecycle(
|
|
strategy_actor: str | None = "openai/gpt-4",
|
|
execution_actor: str | None = None,
|
|
) -> SimpleNamespace:
|
|
"""Create a mock lifecycle service."""
|
|
plan = SimpleNamespace(action_name="test-action")
|
|
action = SimpleNamespace(
|
|
strategy_actor=strategy_actor,
|
|
execution_actor=execution_actor or strategy_actor,
|
|
)
|
|
return SimpleNamespace(
|
|
get_plan=MagicMock(return_value=plan),
|
|
get_action=MagicMock(return_value=action),
|
|
resolve_actor_provider_model=MagicMock(return_value=None),
|
|
resolve_actor_options=MagicMock(return_value=None),
|
|
)
|
|
|
|
|
|
def make_mock_acms_pipeline(
|
|
context_summary: str = "Project uses Python 3.12, FastAPI, SQLAlchemy",
|
|
) -> SimpleNamespace:
|
|
"""Create a mock ACMS pipeline."""
|
|
return SimpleNamespace(
|
|
get_context_summary=MagicMock(return_value=context_summary),
|
|
)
|
|
|
|
|
|
def make_failing_acms_pipeline() -> SimpleNamespace:
|
|
"""Create a mock ACMS pipeline that raises on get_context_summary."""
|
|
return SimpleNamespace(
|
|
get_context_summary=MagicMock(side_effect=RuntimeError("ACMS unavailable")),
|
|
)
|
|
|
|
|
|
def make_oversized_actions_response(count: int) -> str:
|
|
"""Generate a JSON array with *count* strategy actions."""
|
|
import json as _json
|
|
|
|
actions = [
|
|
{
|
|
"step": i + 1,
|
|
"description": f"Action number {i + 1}",
|
|
"resource_requirements": [],
|
|
"estimated_complexity": "low",
|
|
"risk_score": 0.1,
|
|
"depends_on": [],
|
|
}
|
|
for i in range(count)
|
|
]
|
|
return _json.dumps(actions)
|
|
|
|
|
|
def make_failing_registry() -> SimpleNamespace:
|
|
"""Create a mock registry whose LLM raises on invoke."""
|
|
mock_llm = MagicMock()
|
|
mock_llm.invoke.side_effect = RuntimeError("LLM provider error")
|
|
return SimpleNamespace(
|
|
create_llm=MagicMock(return_value=mock_llm),
|
|
)
|