AgentEvalTool/tests/unit/test_utils_llm.py
sinohqb e0b69fa2b9 v0.4-t1t2: 测试覆盖率 62%→77% + UTC 时区根本修复
## T1: P0 测试补全(+67 个测试)
- test_utils_llm.py: extract_reply_text / extract_content_from_llm_response / parse_json_from_llm_text 各边界
- test_file_repository.py: 分类 CRUD / 树形结构 / 级联删除 / 文件创建/查询/删除/物理文件清理
- test_report.py: generate_report / generate_compare_report / render_markdown / render_json
- test_llm_score.py: OpenAI 格式 / Anthropic content-block 格式 / JSON 回退解析 / 异常降级

## T2: P1 测试补全(+28 个测试)
- test_scenarios.py: 模板列表/字段完整性/规则类型有效性 + YAML/JSON 加载/校验
- test_webhook.py: 未配置不发送 / 正确 payload / secret header / 异常静默忽略
- test_reports_api.py: GET /reports/{id} / /html / /json / /markdown / /compare 集成测试

## UTC 时区根本修复
- storage/db.py: 新增 iso_utc() 函数,确保所有 datetime 序列化输出带 Z 后缀
- runs.py / files.py / report.py: 6 处 .isoformat() → iso_utc()
- 前端 toDate() 兜底仍保留(向下兼容),但后端不再输出无时区时间戳

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 14:19:16 +08:00

150 lines
4.5 KiB
Python

"""Unit tests for agenteval.utils.llm — the shared LLM utility functions."""
import json
import pytest
from agenteval.utils.llm import extract_reply_text, extract_content_from_llm_response, parse_json_from_llm_text
# ── extract_reply_text ───────────────────────────────────────────────────
def test_extract_reply_text_none():
assert extract_reply_text(None) == ""
def test_extract_reply_text_str():
assert extract_reply_text("hello") == "hello"
def test_extract_reply_text_dict_msgbody_dict():
msg = {"msgBody": {"content": "inner text"}}
assert extract_reply_text(msg) == "inner text"
def test_extract_reply_text_dict_msgbody_str():
msg = {"msgBody": "plain body"}
assert extract_reply_text(msg) == "plain body"
def test_extract_reply_text_dict_content_key():
msg = {"content": "direct content"}
assert extract_reply_text(msg) == "direct content"
def test_extract_reply_text_dict_msgbody_none_fallback_content():
# msgBody is falsy → falls back to content key
msg = {"msgBody": None, "content": "fallback"}
assert extract_reply_text(msg) == "fallback"
def test_extract_reply_text_int_coerces_to_str():
assert extract_reply_text(42) == "42"
def test_extract_reply_text_empty_dict():
assert extract_reply_text({}) == ""
# ── extract_content_from_llm_response ────────────────────────────────────
def test_extract_content_string_format():
data = {"choices": [{"message": {"content": "answer text"}}]}
assert extract_content_from_llm_response(data) == "answer text"
def test_extract_content_block_array_text():
data = {
"choices": [{
"message": {
"content": [
{"type": "text", "text": "block one"},
{"type": "text", "text": "block two"},
]
}
}]
}
result = extract_content_from_llm_response(data)
assert "block one" in result
assert "block two" in result
def test_extract_content_block_array_skips_non_text():
data = {
"choices": [{
"message": {
"content": [
{"type": "tool_use", "id": "t1", "input": {}},
{"type": "text", "text": "real answer"},
]
}
}]
}
result = extract_content_from_llm_response(data)
assert result == "real answer"
def test_extract_content_block_uses_content_key_fallback():
# Some providers use "content" instead of "text" inside blocks
data = {
"choices": [{
"message": {
"content": [{"type": "text", "content": "via content key"}]
}
}]
}
assert extract_content_from_llm_response(data) == "via content key"
def test_extract_content_missing_choices():
assert extract_content_from_llm_response({}) == ""
def test_extract_content_empty_choices():
assert extract_content_from_llm_response({"choices": []}) == ""
def test_extract_content_integer_coerced():
data = {"choices": [{"message": {"content": 123}}]}
assert extract_content_from_llm_response(data) == "123"
def test_extract_content_empty_block_list():
data = {"choices": [{"message": {"content": []}}]}
assert extract_content_from_llm_response(data) == ""
# ── parse_json_from_llm_text ─────────────────────────────────────────────
def test_parse_json_object():
result = parse_json_from_llm_text('{"score": 8, "reason": "good"}')
assert result["score"] == 8
def test_parse_json_array():
result = parse_json_from_llm_text('["a", "b", "c"]')
assert result == ["a", "b", "c"]
def test_parse_json_with_surrounding_text():
text = 'Here is the result: {"score": 7} and nothing else.'
result = parse_json_from_llm_text(text)
assert result["score"] == 7
def test_parse_json_array_with_preamble():
text = 'Generated questions: ["q1", "q2", "q3"]'
result = parse_json_from_llm_text(text)
assert result == ["q1", "q2", "q3"]
def test_parse_json_raises_on_no_json():
with pytest.raises((ValueError, json.JSONDecodeError)):
parse_json_from_llm_text("no json here at all")
def test_parse_json_markdown_wrapped():
text = '```json\n{"key": "value"}\n```'
# The fallback bracket-search finds the { in the markdown
result = parse_json_from_llm_text(text)
assert result["key"] == "value"