Files
cleveragents-core/features/mocks/mock_ai_provider.py
T

279 lines
9.4 KiB
Python

"""Mock AI provider for testing purposes only.
This mock implementation is only used during tests, following the mock
placement rule that all mocks must exist in the features/ directory,
never in production code (ADR-022).
"""
import os
from collections.abc import Callable, Iterator
from cleveragents.domain.models.core import (
Change,
Context,
OperationType,
Plan,
Project,
)
from cleveragents.domain.providers.ai_provider import (
ActorInvocationContext,
ProviderResponse,
)
class MockAIProvider:
"""Mock AI provider that generates simple test responses.
This provider is used only during testing to avoid calling real AI APIs.
"""
def __init__(self, model_id: str = "mock-gpt-4"):
"""Initialize the mock provider.
Args:
model_id: Mock model identifier
"""
self._model_id = model_id
self._name = "MockProvider"
def generate_changes(
self,
project: Project,
plan: Plan,
contexts: list[Context],
actor_context: ActorInvocationContext | None = None,
progress_callback: Callable[[int], None] | None = None,
) -> ProviderResponse:
"""Generate mock code changes based on the plan.
Args:
project: The project being modified
plan: The plan containing instructions
contexts: List of context files to consider
progress_callback: Optional callback for progress updates (0-100)
Returns:
ProviderResponse containing mock changes
"""
# Check if provider should fail (for testing error handling)
if os.environ.get("CLEVERAGENTS_MOCK_SHOULD_FAIL") == "true":
return ProviderResponse(
changes=[],
model_used=self._model_id,
token_count=0,
error_message="Mock provider configured to fail for testing",
)
# Simulate progress
if progress_callback:
for i in range(0, 101, 20):
progress_callback(i)
# Generate simple mock changes based on the prompt
prompt_lower = plan.prompt.lower() if plan.prompt else ""
# Check if provider should generate invalid code (for testing validation)
if os.environ.get("CLEVERAGENTS_MOCK_INVALID_CODE") == "true":
changes = [
Change(
id=None,
plan_id=plan.id if plan.id else 0,
file_path="invalid.py",
operation=OperationType.CREATE,
original_content=None,
new_content=(
"# Invalid Python code\n"
"def broken_function(\n"
" # Missing closing parenthesis and colon\n"
" # This will fail syntax validation\n"
),
applied=False,
applied_at=None,
new_path=None,
)
]
return ProviderResponse(
changes=changes,
model_used=self._model_id,
token_count=50,
error_message=None,
)
if not plan.id:
# Create temporary changes without plan_id for tests
changes = [
Change(
id=None,
plan_id=0, # Use 0 as placeholder
file_path="example.py",
operation=OperationType.CREATE,
original_content=None,
new_content="# Mock AI generated code\nprint('Hello from CleverAgents!')\n",
applied=False,
applied_at=None,
new_path=None,
)
]
else:
# Determine what kind of change to generate based on prompt
if "error handling" in prompt_lower or "exception" in prompt_lower:
changes = [
Change(
id=None,
plan_id=plan.id,
file_path="error_handler.py",
operation=OperationType.CREATE,
original_content=None,
new_content="""# Mock error handling code
def handle_error(error):
\"\"\"Mock error handler.\"\"\"
print(f"Error occurred: {error}")
return None
""",
applied=False,
applied_at=None,
new_path=None,
)
]
elif "test" in prompt_lower:
changes = [
Change(
id=None,
plan_id=plan.id,
file_path="test_example.py",
operation=OperationType.CREATE,
original_content=None,
new_content="""# Mock test file
import pytest
def test_example():
\"\"\"Mock test.\"\"\"
assert True
""",
applied=False,
applied_at=None,
new_path=None,
)
]
elif "refactor" in prompt_lower or "improve" in prompt_lower:
# Create a modification change if context files exist
if contexts and len(contexts) > 0:
file_path = contexts[0].path
changes = [
Change(
id=None,
plan_id=plan.id,
file_path=file_path,
operation=OperationType.MODIFY,
original_content=contexts[0].content,
new_content=(
f"# Refactored by mock AI\n{contexts[0].content}"
),
applied=False,
applied_at=None,
new_path=None,
)
]
else:
# Default to creating a new file
changes = [
Change(
id=None,
plan_id=plan.id,
file_path="refactored.py",
operation=OperationType.CREATE,
original_content=None,
new_content=(
"# Mock refactored code\n"
"class RefactoredClass:\n"
" pass\n"
),
applied=False,
applied_at=None,
new_path=None,
)
]
else:
# Default change
changes = [
Change(
id=None,
plan_id=plan.id,
file_path="example.py",
operation=OperationType.CREATE,
original_content=None,
new_content=(
"# Mock AI generated code\n"
"print('Hello from CleverAgents!')\n"
),
applied=False,
applied_at=None,
new_path=None,
)
]
return ProviderResponse(
changes=changes,
model_used=self._model_id,
token_count=100,
error_message=None,
)
def stream_changes(
self,
project: Project,
plan: Plan,
contexts: list[Context],
actor_context: ActorInvocationContext | None = None,
progress_callback: Callable[[int], None] | None = None,
) -> Iterator[dict[str, object]]:
"""Stream mock workflow events for CLI coverage."""
def _stream() -> Iterator[dict[str, object]]:
if progress_callback:
progress_callback(5)
response = self.generate_changes(
project,
plan,
contexts,
progress_callback=None,
)
for node in (
"load_context",
"analyze_requirements",
"generate_plan",
"validate",
):
event_payload: dict[str, object] = {}
if node == "generate_plan":
event_payload = {
"status": "completed",
"change_count": len(response.changes),
}
elif node == "validate":
event_payload = {"status": "completed"}
yield {node: event_payload}
yield {"__end__": {"response": response}}
return _stream()
@property
def name(self) -> str:
"""Get the provider name."""
return self._name
@name.setter
def name(self, value: str) -> None:
"""Allow overriding the provider name in tests."""
self._name = value
@property
def model_id(self) -> str:
"""Get the model identifier."""
return self._model_id
@model_id.setter
def model_id(self, value: str) -> None:
"""Allow overriding the model identifier in tests."""
self._model_id = value