Files
cleveragents-core/features/mocks/mock_strategy_llm.py
T
HAL9000 2dd920078a fix(tests): add resolve_actor_options to mock lifecycle helpers
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
2026-05-28 17:35:08 -04:00

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),
)