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