"""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"]