first
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user