Files
Edu/services/ai/tests/test_services.py
2026-07-10 18:57:39 +08:00

195 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""服务层测试ChatService / QuestionService / ExpressionService."""
import json
from src.ai.models.question import GenerateQuestionRequest
from src.ai.providers import ProviderFailoverChain
from src.ai.providers.circuit_breaker import CircuitBreaker
from src.ai.services.chat_service import ChatService
from src.ai.services.evaluation import QualityGate, RuleValidator
from src.ai.services.expression_service import ExpressionService
from src.ai.services.question_service import QuestionService
from .conftest import MockProvider
def _make_chain(provider: MockProvider) -> ProviderFailoverChain:
return ProviderFailoverChain([provider], CircuitBreaker())
class TestChatService:
"""ChatService 测试."""
async def test_chat_success(self) -> None:
"""聊天成功."""
chain = _make_chain(MockProvider(response_content="你好"))
svc = ChatService(failover_chain=chain, default_model="gpt-4o")
data = await svc.chat(messages=[{"role": "user", "content": "hi"}])
assert data.content == "你好"
assert data.degraded is False
assert data.usage.total_tokens == 30
async def test_chat_degraded(self) -> None:
"""LLM 不可用时降级."""
chain = _make_chain(MockProvider(fail=True))
svc = ChatService(failover_chain=chain)
data = await svc.chat(messages=[{"role": "user", "content": "hi"}])
assert data.degraded is True
assert "degraded" in data.content
async def test_stream_chat(self) -> None:
"""流式聊天."""
chain = _make_chain(MockProvider(stream_chunks=["hello", " world"]))
svc = ChatService(failover_chain=chain)
chunks = []
async for chunk in svc.stream_chat(messages=[{"role": "user", "content": "hi"}]):
chunks.append(chunk)
assert len(chunks) == 2
assert chunks[-1].done is True
async def test_stream_chat_degraded(self) -> None:
"""流式降级."""
chain = _make_chain(MockProvider(fail=True))
svc = ChatService(failover_chain=chain)
chunks = []
async for chunk in svc.stream_chat(messages=[{"role": "user", "content": "hi"}]):
chunks.append(chunk)
assert chunks[-1].done is True
assert "degraded" in chunks[-1].content
def test_build_system_prompt_no_template(self) -> None:
"""无模板服务时返回默认 prompt."""
chain = _make_chain(MockProvider())
svc = ChatService(failover_chain=chain)
prompt = svc._build_system_prompt({})
assert "educational assistant" in prompt.lower()
class TestQuestionService:
"""QuestionService 测试."""
async def test_generate_success(self) -> None:
"""生成题目成功."""
output = json.dumps({
"question": "1+1等于几",
"answer": "2",
"explanation": "基础加法",
"difficulty": "easy",
"question_type": "short_answer",
})
chain = _make_chain(MockProvider(response_content=output))
gate = QualityGate(rule_validator=RuleValidator())
svc = QuestionService(
failover_chain=chain,
quality_gate=gate,
default_model="gpt-4o",
)
request = GenerateQuestionRequest(
prompt="生成加法题",
subject="数学",
difficulty="easy",
)
data = await svc.generate(request)
assert data.question == "1+1等于几"
assert data.answer == "2"
assert data.degraded is False
async def test_generate_degraded_llm_fail(self) -> None:
"""LLM 失败降级."""
chain = _make_chain(MockProvider(fail=True))
svc = QuestionService(failover_chain=chain)
request = GenerateQuestionRequest(prompt="生成题目", subject="数学")
data = await svc.generate(request)
assert data.degraded is True
assert "degraded" in data.explanation
async def test_generate_invalid_json(self) -> None:
"""LLM 输出非 JSON 降级."""
chain = _make_chain(MockProvider(response_content="这不是JSON"))
svc = QuestionService(failover_chain=chain)
request = GenerateQuestionRequest(prompt="生成题目", subject="数学")
data = await svc.generate(request)
# 非JSON → 规则校验失败 → degraded
assert data.degraded is True
async def test_stream_generate(self) -> None:
"""流式生成题目."""
output = json.dumps({"question": "", "answer": ""})
chain = _make_chain(MockProvider(stream_chunks=[output]))
svc = QuestionService(failover_chain=chain)
request = GenerateQuestionRequest(prompt="生成题目", subject="数学")
chunks = []
async for chunk in svc.stream_generate(request):
chunks.append(chunk)
assert chunks[-1].done is True
assert chunks[-1].complete_question is not None
async def test_stream_generate_degraded(self) -> None:
"""流式生成降级."""
chain = _make_chain(MockProvider(fail=True))
svc = QuestionService(failover_chain=chain)
request = GenerateQuestionRequest(prompt="生成题目", subject="数学")
chunks = []
async for chunk in svc.stream_generate(request):
chunks.append(chunk)
assert chunks[-1].done is True
assert chunks[-1].complete_question is not None
assert chunks[-1].complete_question.degraded is True
def test_fallback_prompt(self) -> None:
"""降级 prompt."""
chain = _make_chain(MockProvider())
svc = QuestionService(failover_chain=chain)
request = GenerateQuestionRequest(prompt="测试", subject="语文")
prompt = svc._fallback_prompt(request)
assert "语文" in prompt
class TestExpressionService:
"""ExpressionService 测试."""
async def test_optimize_success(self) -> None:
"""优化成功."""
output = json.dumps({
"optimized": "优化后的文字",
"suggestions": ["建议1"],
})
chain = _make_chain(MockProvider(response_content=output))
svc = ExpressionService(failover_chain=chain)
data = await svc.optimize(text="原始文字")
assert data.optimized == "优化后的文字"
assert data.suggestions == ["建议1"]
assert data.degraded is False
async def test_optimize_degraded(self) -> None:
"""LLM 失败降级."""
chain = _make_chain(MockProvider(fail=True))
svc = ExpressionService(failover_chain=chain)
data = await svc.optimize(text="原始文字")
assert data.degraded is True
assert data.optimized == "原始文字"
async def test_optimize_invalid_json(self) -> None:
"""非 JSON 输出降级."""
chain = _make_chain(MockProvider(response_content="纯文本"))
svc = ExpressionService(failover_chain=chain)
data = await svc.optimize(text="原始文字")
assert data.degraded is True
assert "json" in data.degraded_reason.lower()
async def test_optimize_markdown_json(self) -> None:
"""markdown 包裹 JSON 能解析."""
output = '```json\n{"optimized": "ok", "suggestions": []}\n```'
chain = _make_chain(MockProvider(response_content=output))
svc = ExpressionService(failover_chain=chain)
data = await svc.optimize(text="原始文字")
assert data.optimized == "ok"
def test_fallback_prompt(self) -> None:
"""降级 prompt."""
chain = _make_chain(MockProvider())
svc = ExpressionService(failover_chain=chain)
prompt = svc._fallback_prompt("文字", "上下文")
assert "文字" in prompt
assert "上下文" in prompt