| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394 |
- """外部协议使用 HTTP MockTransport 验证;这些用例不代表已验证真实模型识别精度。"""
- import time
- import httpx
- import pytest
- from sqlalchemy import select
- from app.domain.models import CacheEntry
- from app.infrastructure.ai import ai, AIClient, AIUnavailable, CircuitBreaker, parse_structured
- from app.infrastructure.database import Session
- from app.shared.config import settings
- from app.shared.schemas import RiskAssessment
- def test_structured_fences_and_invalid_values():
- raw = '模型说明\n```json\n{"risk_level":2,"risk_score":0.6,"risk_factors":["测试"],"reasoning":"明确测试依据"}\n```'
- assert parse_structured(raw, RiskAssessment).risk_level == 2
- with pytest.raises(AIUnavailable):
- parse_structured('{"risk_level":0,"risk_score":99}', RiskAssessment)
- def test_http_schema_key_and_cache(client, monkeypatch):
- monkeypatch.setattr(settings, 'llm_base_url', 'http://contract.test/v1')
- monkeypatch.setattr(settings, 'llm_model', 'contract-risk')
- monkeypatch.setattr(settings, 'ai_api_key', 'contract-key')
- calls = []
- def respond(request):
- calls.append(request)
- assert request.headers['Authorization'] == 'Bearer contract-key'
- return httpx.Response(200, json={'choices': [{'message': {'content': '{"risk_level":1,"risk_score":0.1,"risk_factors":[],"reasoning":"测试依据"}'}}], 'usage': {'total_tokens': 12}})
- async def exercise():
- model = AIClient()
- await model.http.aclose()
- model.http = httpx.AsyncClient(transport=httpx.MockTransport(respond))
- first, usage = await model.structured('测试请求', RiskAssessment)
- second, hit = await model.structured('测试请求', RiskAssessment)
- assert first == second and usage['total_tokens'] == 12 and hit['cache_hit'] is True
- async with Session() as session:
- assert await session.scalar(select(CacheEntry)) is not None
- await model.close()
- client.portal.call(exercise)
- assert len(calls) == 1
- def test_real_models_endpoint_uses_configured_key(client, headers, monkeypatch):
- monkeypatch.setattr(settings, 'vlm_base_url', 'http://contract.test/v1')
- monkeypatch.setattr(settings, 'vlm_model', 'test-vision')
- monkeypatch.setattr(settings, 'ai_api_key', 'contract-key')
- def respond(request):
- assert request.headers['Authorization'] == 'Bearer contract-key'
- return httpx.Response(200, json={'data': [{'id': 'test-vision'}]})
- old = ai.http
- monkeypatch.setattr(ai, 'http', httpx.AsyncClient(transport=httpx.MockTransport(respond)))
- try:
- response = client.post('/api/v1/agents/extract/test', headers=headers['carol'])
- assert response.status_code == 200 and response.json()['data']['status'] == 'success'
- finally:
- client.portal.call(ai.http.aclose)
- monkeypatch.setattr(ai, 'http', old)
- def test_embeddings_reject_fake_shape_and_fallback(client, monkeypatch):
- monkeypatch.setattr(settings, 'embedding_base_url', 'http://broken-embed.test')
- monkeypatch.setattr(settings, 'llm_base_url', 'http://fallback.test/v1')
- requests = []
- def respond(request):
- requests.append(request)
- if request.url.host == 'broken-embed.test':
- return httpx.Response(503)
- return httpx.Response(200, json={'data': [{'index': 0, 'embedding': [1.0] + [0.0] * 1023}]})
- async def exercise():
- model = AIClient(); await model.http.aclose()
- model.http = httpx.AsyncClient(transport=httpx.MockTransport(respond))
- assert len((await model.embeddings(['真实协议测试']))[0]) == 1024
- await model.close()
- model.http = httpx.AsyncClient(transport=httpx.MockTransport(lambda req: httpx.Response(200, json={'vectors': [[0.1, 0.2]]})))
- with pytest.raises(AIUnavailable):
- await model.embeddings(['维度非法测试'])
- await model.close()
- client.portal.call(exercise)
- assert len(requests) == 2
- def test_circuit_half_open_single_probe(client, monkeypatch):
- monkeypatch.setattr(settings, 'ai_failure_threshold', 2)
- monkeypatch.setattr(settings, 'ai_recovery_seconds', 1)
- async def exercise():
- breaker = CircuitBreaker()
- await breaker.enter(); await breaker.finish(False)
- await breaker.enter(); await breaker.finish(False)
- assert breaker.status() == 'open'
- with pytest.raises(AIUnavailable):
- await breaker.enter()
- breaker.opened_at = time.monotonic() - 2
- await breaker.enter()
- assert breaker.status() == 'half_open'
- with pytest.raises(AIUnavailable):
- await breaker.enter()
- await breaker.finish(True)
- assert breaker.status() == 'closed'
- client.portal.call(exercise)
|