fischer-agentkit/tests/unit/test_quality_gate.py

276 lines
11 KiB
Python
Raw Permalink 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.

"""QualityGate 单元测试"""
import asyncio
from unittest.mock import AsyncMock, MagicMock
import pytest
from agentkit.skills.base import QualityGateConfig, Skill, SkillConfig
from agentkit.quality.gate import QualityCheck, QualityGate, QualityResult
# ── 辅助函数 ───────────────────────────────────────────────
def _make_skill(
required_fields: list[str] | None = None,
min_word_count: int = 0,
max_retries: int = 0,
custom_validator: str | None = None,
output_schema: dict | None = None,
) -> Skill:
"""创建测试用 Skill 实例"""
config = SkillConfig.from_dict({
"name": "test_skill",
"agent_type": "test",
"task_mode": "llm_generate",
"prompt": {"identity": "测试技能"},
"quality_gate": {
"required_fields": required_fields or [],
"min_word_count": min_word_count,
"max_retries": max_retries,
"custom_validator": custom_validator,
},
"output_schema": output_schema,
})
return Skill(config)
# ── QualityCheck 测试 ──────────────────────────────────────
class TestQualityCheck:
"""QualityCheck 数据类测试"""
def test_passed_check(self):
check = QualityCheck(name="required_field:title", passed=True)
assert check.name == "required_field:title"
assert check.passed is True
assert check.message is None
def test_failed_check_with_message(self):
check = QualityCheck(
name="required_field:title", passed=False, message="Field 'title' is missing"
)
assert check.passed is False
assert check.message == "Field 'title' is missing"
# ── QualityResult 测试 ─────────────────────────────────────
class TestQualityResult:
"""QualityResult 数据类测试"""
def test_passed_result(self):
result = QualityResult(
passed=True, checks=[QualityCheck(name="x", passed=True)], can_retry=False
)
assert result.passed is True
assert result.can_retry is False
def test_failed_result_with_retry(self):
result = QualityResult(
passed=False,
checks=[QualityCheck(name="x", passed=False, message="fail")],
can_retry=True,
)
assert result.passed is False
assert result.can_retry is True
# ── QualityGate.validate 测试 ──────────────────────────────
class TestQualityGateValidate:
"""QualityGate.validate 多维度质量检查"""
@pytest.fixture
def gate(self) -> QualityGate:
return QualityGate()
async def test_all_required_fields_present(self, gate: QualityGate):
"""所有必填字段都存在 → passed=True"""
skill = _make_skill(required_fields=["title", "content"])
output = {"title": "Hello", "content": "World"}
result = await gate.validate(output, skill)
assert result.passed is True
async def test_missing_required_field(self, gate: QualityGate):
"""缺少必填字段 → passed=False并附带 message"""
skill = _make_skill(required_fields=["title", "content"])
output = {"title": "Hello"} # 缺少 content
result = await gate.validate(output, skill)
assert result.passed is False
field_checks = [c for c in result.checks if c.name == "required_field:content"]
assert len(field_checks) == 1
assert field_checks[0].passed is False
assert "content" in field_checks[0].message
async def test_required_field_present_but_none(self, gate: QualityGate):
"""必填字段存在但值为 None → 视为缺失"""
skill = _make_skill(required_fields=["title"])
output = {"title": None}
result = await gate.validate(output, skill)
assert result.passed is False
async def test_min_word_count_sufficient(self, gate: QualityGate):
"""字数满足最低要求 → passed=True"""
skill = _make_skill(min_word_count=5)
output = {"content": "one two three four five six"}
result = await gate.validate(output, skill)
word_check = [c for c in result.checks if c.name == "min_word_count"]
assert len(word_check) == 1
assert word_check[0].passed is True
async def test_min_word_count_insufficient(self, gate: QualityGate):
"""字数不足 → passed=False附带 message"""
skill = _make_skill(min_word_count=100)
output = {"content": "short text"}
result = await gate.validate(output, skill)
word_check = [c for c in result.checks if c.name == "min_word_count"]
assert len(word_check) == 1
assert word_check[0].passed is False
assert "100" in word_check[0].message
async def test_min_word_count_with_non_string_content(self, gate: QualityGate):
"""content 不是字符串时,转为字符串后计算字数"""
skill = _make_skill(min_word_count=1)
output = {"content": 12345}
result = await gate.validate(output, skill)
word_check = [c for c in result.checks if c.name == "min_word_count"]
assert len(word_check) == 1
assert word_check[0].passed is True # str(12345) = "12345" → 1 word
async def test_json_schema_validation_passes(self, gate: QualityGate):
"""JSON Schema 验证通过"""
schema = {
"type": "object",
"properties": {
"title": {"type": "string"},
},
"required": ["title"],
}
skill = _make_skill(output_schema=schema)
output = {"title": "Hello"}
result = await gate.validate(output, skill)
schema_checks = [c for c in result.checks if c.name == "schema"]
assert len(schema_checks) == 1
assert schema_checks[0].passed is True
async def test_json_schema_validation_fails(self, gate: QualityGate):
"""JSON Schema 验证失败"""
schema = {
"type": "object",
"properties": {
"count": {"type": "integer"},
},
"required": ["count"],
}
skill = _make_skill(output_schema=schema)
output = {"count": "not_an_integer"}
result = await gate.validate(output, skill)
schema_checks = [c for c in result.checks if c.name == "schema"]
assert len(schema_checks) == 1
assert schema_checks[0].passed is False
async def test_max_retries_greater_than_zero(self, gate: QualityGate):
"""max_retries > 0 → can_retry=True"""
skill = _make_skill(max_retries=3)
result = await gate.validate({}, skill)
assert result.can_retry is True
async def test_max_retries_zero(self, gate: QualityGate):
"""max_retries = 0 → can_retry=False"""
skill = _make_skill(max_retries=0)
result = await gate.validate({}, skill)
assert result.can_retry is False
async def test_custom_validator_returns_true(self, gate: QualityGate):
"""自定义验证器返回 True → passed=True"""
import sys
from unittest.mock import MagicMock
mock_module = MagicMock()
mock_validator = AsyncMock(return_value=True)
mock_module.check_output = mock_validator
sys.modules["agentkit.test_validators"] = mock_module
try:
skill = _make_skill(custom_validator="agentkit.test_validators.check_output")
result = await gate.validate({"data": "ok"}, skill)
custom_checks = [c for c in result.checks if c.name == "custom"]
assert len(custom_checks) == 1
assert custom_checks[0].passed is True
finally:
del sys.modules["agentkit.test_validators"]
async def test_custom_validator_returns_false(self, gate: QualityGate):
"""自定义验证器返回 False → passed=False"""
import sys
from unittest.mock import MagicMock
mock_module = MagicMock()
mock_validator = AsyncMock(return_value=False)
mock_module.check_quality = mock_validator
sys.modules["agentkit.test_validators2"] = mock_module
try:
skill = _make_skill(custom_validator="agentkit.test_validators2.check_quality")
result = await gate.validate({"data": "bad"}, skill)
custom_checks = [c for c in result.checks if c.name == "custom"]
assert len(custom_checks) == 1
assert custom_checks[0].passed is False
finally:
del sys.modules["agentkit.test_validators2"]
async def test_custom_validator_does_not_exist(self, gate: QualityGate):
"""自定义验证器不存在 → 跳过passed=True附带 message"""
# 使用白名单前缀但模块不存在
skill = _make_skill(custom_validator="agentkit.nonexistent_module.validator")
result = await gate.validate({"data": "ok"}, skill)
custom_checks = [c for c in result.checks if c.name == "custom"]
assert len(custom_checks) == 1
assert custom_checks[0].passed is True
assert custom_checks[0].message is not None
async def test_empty_quality_gate_config(self, gate: QualityGate):
"""空 quality_gate 配置 → 所有检查通过"""
skill = _make_skill() # 默认空配置
output = {"anything": "goes"}
result = await gate.validate(output, skill)
assert result.passed is True
async def test_passed_is_false_when_any_check_fails(self, gate: QualityGate):
"""任一检查失败 → passed=False"""
skill = _make_skill(required_fields=["title", "body"])
output = {"title": "Hello"} # 缺少 body
result = await gate.validate(output, skill)
assert result.passed is False
async def test_no_output_schema_skips_schema_check(self, gate: QualityGate):
"""无 output_schema → 不执行 schema 检查"""
skill = _make_skill(output_schema=None)
output = {"anything": "goes"}
result = await gate.validate(output, skill)
schema_checks = [c for c in result.checks if c.name == "schema"]
assert len(schema_checks) == 0
async def test_custom_validator_sync_function(self, gate: QualityGate):
"""自定义验证器是同步函数 → 也能正常调用"""
import sys
from unittest.mock import MagicMock
mock_module = MagicMock()
mock_module.sync_check = MagicMock(return_value=True)
sys.modules["test_sync_validators"] = mock_module
try:
skill = _make_skill(custom_validator="test_sync_validators.sync_check")
result = await gate.validate({"data": "ok"}, skill)
custom_checks = [c for c in result.checks if c.name == "custom"]
assert len(custom_checks) == 1
assert custom_checks[0].passed is True
finally:
del sys.modules["test_sync_validators"]