131 lines
4.8 KiB
Python
131 lines
4.8 KiB
Python
"""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")
|