"""asf-ai tests — MockProvider + AIGateway routing, no network calls.""" from __future__ import annotations import sys from pathlib import Path from uuid import UUID import pytest AI_ROOT = Path(__file__).resolve().parents[1] if str(AI_ROOT) not in sys.path: sys.path.insert(0, str(AI_ROOT)) TENANT_ID = UUID("99999999-0000-0000-0000-000000000001") PROJECT_ID = UUID("88888888-0000-0000-0000-000000000002") JOB_ID = UUID("77777777-0000-0000-0000-000000000003") @pytest.fixture def trace_ctx(): from app.capabilities import TraceContext return TraceContext( tenant_id=TENANT_ID, project_id=PROJECT_ID, job_id=JOB_ID, correlation_id="ai-trace" ) @pytest.fixture def mock_provider(): from app.providers import MockProvider return MockProvider(response_latency_ms=0) class TestMockProviderChat: @pytest.mark.asyncio async def test_chat_mirrors_last_user_in_reply(self, mock_provider, trace_ctx): from app.capabilities import ChatRequest, ChatMessage req = ChatRequest( trace=trace_ctx, messages=[ ChatMessage(role="system", content="You are helpful."), ChatMessage(role="user", content="what is REQ-103?"), ], ) resp = await mock_provider.chat(req) assert resp.provider == "mock" assert resp.model_used == "asf-mock-v1" assert resp.estimated_cost == 0.0 assert "what is REQ-103?" in resp.reply assert resp.input_tokens >= 0 and resp.output_tokens > 0 assert resp.references == ["mock://no-real-data"] class TestMockProviderCodeAnalysis: @pytest.mark.asyncio async def test_analyze_code_detects_todo_and_broad_except(self, mock_provider, trace_ctx): from app.capabilities import CodeAnalysisRequest code = ( "def foo():\n" " # TODO: refactor this later\n" " try:\n" " risky_call()\n" " except Exception:\n" " pass\n" ) req = CodeAnalysisRequest(trace=trace_ctx, code_snippet=code, language="python") resp = await mock_provider.analyze_code(req) kinds = {f.kind for f in resp.findings} assert "todo" in kinds assert "broad_exception" in kinds sevs = {f.severity for f in resp.findings} assert "medium" in sevs # broad_exception is medium assert resp.summary.startswith("Mock: analyzed ") assert resp.input_tokens > 0 @pytest.mark.asyncio async def test_analyze_code_clean_snippet_zero_findings(self, mock_provider, trace_ctx): from app.capabilities import CodeAnalysisRequest req = CodeAnalysisRequest(trace=trace_ctx, code_snippet="x = 1\ny = 2\n", language="python") resp = await mock_provider.analyze_code(req) assert resp.findings == [] assert "0 informational finding" in resp.summary class TestMockProviderTestGeneration: @pytest.mark.asyncio async def test_generate_tests_returns_placeholders(self, mock_provider, trace_ctx): from app.capabilities import TestGenerationRequest req = TestGenerationRequest( trace=trace_ctx, code_snippet="def add(a, b): return a+b\n", language="python", test_framework="pytest", ) resp = await mock_provider.generate_tests(req) assert len(resp.test_cases) >= 2 assert any("test_mock_happy_path" in t for t in resp.test_cases) assert resp.fixtures_needed assert resp.coverage_notes.startswith("Mock response.") assert resp.output_tokens > 0 class TestMockProviderDecomposeRequirement: @pytest.mark.asyncio async def test_decompose_respects_max_subtasks_cap(self, mock_provider, trace_ctx): from app.capabilities import RequirementDecomposeRequest req = RequirementDecomposeRequest( trace=trace_ctx, requirement_text="Build a billing page.", max_subtasks=3 ) resp = await mock_provider.decompose_requirement(req) assert len(resp.subtasks) == 3 # graph edges sanity: second subtask depends on first assert 0 in (resp.subtasks[1].depends_on or []) assert resp.open_questions assert resp.architecture_notes class TestProviderRegistry: def test_registry_register_and_get(self): from app.providers import ProviderRegistry, MockProvider reg = ProviderRegistry() reg.register("m", MockProvider(response_latency_ms=0)) assert reg.names() == ["m"] assert reg.get("m").name == "mock" def test_registry_get_missing_raises_keyerror(self): from app.providers import ProviderRegistry reg = ProviderRegistry() with pytest.raises(KeyError, match="Unknown AI provider"): reg.get("nope")