ai-robot-core/ai-service/tests/test_slot_validation_servic...

542 lines
22 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.

"""
Tests for Slot Validation Service.
槽位校验服务单元测试
"""
import pytest
from app.services.mid.slot_validation_service import (
SlotValidationService,
SlotValidationErrorCode,
ValidationResult,
SlotValidationError,
BatchValidationResult,
)
class TestSlotValidationService:
"""槽位校验服务测试类"""
@pytest.fixture
def service(self):
"""创建校验服务实例"""
return SlotValidationService()
@pytest.fixture
def string_slot_def(self):
"""字符串类型槽位定义"""
return {
"slot_key": "name",
"type": "string",
"required": False,
"validation_rule": None,
"ask_back_prompt": "请输入您的姓名",
}
@pytest.fixture
def required_string_slot_def(self):
"""必填字符串类型槽位定义"""
return {
"slot_key": "phone",
"type": "string",
"required": True,
"validation_rule": r"^1[3-9]\d{9}$",
"ask_back_prompt": "请输入正确的手机号码",
}
@pytest.fixture
def number_slot_def(self):
"""数字类型槽位定义"""
return {
"slot_key": "age",
"type": "number",
"required": False,
"validation_rule": None,
"ask_back_prompt": "请输入年龄",
}
@pytest.fixture
def boolean_slot_def(self):
"""布尔类型槽位定义"""
return {
"slot_key": "is_student",
"type": "boolean",
"required": False,
"validation_rule": None,
"ask_back_prompt": "是否是学生?",
}
@pytest.fixture
def enum_slot_def(self):
"""枚举类型槽位定义"""
return {
"slot_key": "grade",
"type": "enum",
"required": False,
"options": ["初一", "初二", "初三", "高一", "高二", "高三"],
"validation_rule": None,
"ask_back_prompt": "请选择年级",
}
@pytest.fixture
def array_enum_slot_def(self):
"""数组枚举类型槽位定义"""
return {
"slot_key": "subjects",
"type": "array_enum",
"required": False,
"options": ["语文", "数学", "英语", "物理", "化学"],
"validation_rule": None,
"ask_back_prompt": "请选择学科",
}
@pytest.fixture
def json_schema_slot_def(self):
"""JSON Schema 校验槽位定义"""
return {
"slot_key": "email",
"type": "string",
"required": True,
"validation_rule": '{"type": "string", "format": "email"}',
"ask_back_prompt": "请输入有效的邮箱地址",
}
class TestBasicValidation:
"""基础校验测试"""
def test_empty_validation_rule(self, service, string_slot_def):
"""测试空校验规则(应通过)"""
string_slot_def["validation_rule"] = None
result = service.validate_slot_value(string_slot_def, "test")
assert result.ok is True
assert result.normalized_value == "test"
def test_whitespace_validation_rule(self, service, string_slot_def):
"""测试空白校验规则(应通过)"""
string_slot_def["validation_rule"] = " "
result = service.validate_slot_value(string_slot_def, "test")
assert result.ok is True
def test_no_slot_definition(self, service):
"""测试无槽位定义(动态槽位)"""
# 使用最小定义
minimal_def = {"slot_key": "dynamic_field"}
result = service.validate_slot_value(minimal_def, "any_value")
assert result.ok is True
class TestRegexValidation:
"""正则表达式校验测试"""
def test_regex_match(self, service, required_string_slot_def):
"""测试正则匹配成功"""
result = service.validate_slot_value(
required_string_slot_def, "13800138000"
)
assert result.ok is True
assert result.normalized_value == "13800138000"
def test_regex_mismatch(self, service, required_string_slot_def):
"""测试正则匹配失败"""
result = service.validate_slot_value(
required_string_slot_def, "invalid_phone"
)
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_REGEX_MISMATCH
assert result.ask_back_prompt == "请输入正确的手机号码"
def test_regex_invalid_pattern(self, service, string_slot_def):
"""测试非法正则表达式"""
string_slot_def["validation_rule"] = "[invalid("
result = service.validate_slot_value(string_slot_def, "test")
assert result.ok is False
assert (
result.error_code
== SlotValidationErrorCode.SLOT_VALIDATION_RULE_INVALID
)
def test_regex_with_chinese(self, service, string_slot_def):
"""测试包含中文的正则"""
string_slot_def["validation_rule"] = r"^[\u4e00-\u9fa5]{2,4}$"
result = service.validate_slot_value(string_slot_def, "张三")
assert result.ok is True
result = service.validate_slot_value(string_slot_def, "John")
assert result.ok is False
class TestJsonSchemaValidation:
"""JSON Schema 校验测试"""
def test_json_schema_match(self, service):
"""测试 JSON Schema 匹配成功"""
slot_def = {
"slot_key": "config",
"type": "object",
"validation_rule": '{"type": "object", "properties": {"name": {"type": "string"}}}',
}
result = service.validate_slot_value(slot_def, {"name": "test"})
assert result.ok is True
def test_json_schema_mismatch(self, service):
"""测试 JSON Schema 匹配失败"""
slot_def = {
"slot_key": "count",
"type": "number",
"validation_rule": '{"type": "integer", "minimum": 0, "maximum": 100}',
"ask_back_prompt": "请输入0-100之间的整数",
}
result = service.validate_slot_value(slot_def, 150)
assert result.ok is False
assert (
result.error_code == SlotValidationErrorCode.SLOT_JSON_SCHEMA_MISMATCH
)
assert result.ask_back_prompt == "请输入0-100之间的整数"
def test_json_schema_invalid_json(self, service, string_slot_def):
"""测试非法 JSON Schema"""
string_slot_def["validation_rule"] = "{invalid json}"
result = service.validate_slot_value(string_slot_def, "test")
assert result.ok is False
assert (
result.error_code
== SlotValidationErrorCode.SLOT_VALIDATION_RULE_INVALID
)
def test_json_schema_array(self, service):
"""测试数组类型的 JSON Schema"""
slot_def = {
"slot_key": "items",
"type": "array",
"validation_rule": '{"type": "array", "items": {"type": "string"}}',
}
result = service.validate_slot_value(slot_def, ["a", "b", "c"])
assert result.ok is True
result = service.validate_slot_value(slot_def, [1, 2, 3])
assert result.ok is False
class TestRequiredValidation:
"""必填校验测试"""
def test_required_missing_none(self, service, required_string_slot_def):
"""测试必填字段为 None"""
result = service.validate_slot_value(
required_string_slot_def, None
)
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_REQUIRED_MISSING
def test_required_missing_empty_string(self, service, required_string_slot_def):
"""测试必填字段为空字符串"""
result = service.validate_slot_value(required_string_slot_def, "")
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_REQUIRED_MISSING
def test_required_missing_whitespace(self, service, required_string_slot_def):
"""测试必填字段为空白字符"""
result = service.validate_slot_value(required_string_slot_def, " ")
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_REQUIRED_MISSING
def test_required_present(self, service, required_string_slot_def):
"""测试必填字段有值"""
result = service.validate_slot_value(
required_string_slot_def, "13800138000"
)
assert result.ok is True
def test_not_required_empty(self, service, string_slot_def):
"""测试非必填字段为空"""
result = service.validate_slot_value(string_slot_def, "")
assert result.ok is True
class TestTypeValidation:
"""类型校验测试"""
def test_string_type(self, service, string_slot_def):
"""测试字符串类型"""
result = service.validate_slot_value(string_slot_def, "hello")
assert result.ok is True
assert result.normalized_value == "hello"
def test_string_type_conversion(self, service, string_slot_def):
"""测试字符串类型自动转换"""
result = service.validate_slot_value(string_slot_def, 123)
assert result.ok is True
assert result.normalized_value == "123"
def test_number_type_integer(self, service, number_slot_def):
"""测试数字类型 - 整数"""
result = service.validate_slot_value(number_slot_def, 25)
assert result.ok is True
assert result.normalized_value == 25
def test_number_type_float(self, service, number_slot_def):
"""测试数字类型 - 浮点数"""
result = service.validate_slot_value(number_slot_def, 25.5)
assert result.ok is True
assert result.normalized_value == 25.5
def test_number_type_string_conversion(self, service, number_slot_def):
"""测试数字类型 - 字符串转换"""
result = service.validate_slot_value(number_slot_def, "30")
assert result.ok is True
assert result.normalized_value == 30
def test_number_type_invalid(self, service, number_slot_def):
"""测试数字类型 - 无效值"""
result = service.validate_slot_value(number_slot_def, "not_a_number")
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID
def test_number_type_reject_boolean(self, service, number_slot_def):
"""测试数字类型 - 拒绝布尔值"""
result = service.validate_slot_value(number_slot_def, True)
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID
def test_boolean_type_true(self, service, boolean_slot_def):
"""测试布尔类型 - True"""
result = service.validate_slot_value(boolean_slot_def, True)
assert result.ok is True
assert result.normalized_value is True
def test_boolean_type_false(self, service, boolean_slot_def):
"""测试布尔类型 - False"""
result = service.validate_slot_value(boolean_slot_def, False)
assert result.ok is True
assert result.normalized_value is False
def test_boolean_type_string_true(self, service, boolean_slot_def):
"""测试布尔类型 - 字符串 true"""
result = service.validate_slot_value(boolean_slot_def, "true")
assert result.ok is True
assert result.normalized_value is True
def test_boolean_type_string_yes(self, service, boolean_slot_def):
"""测试布尔类型 - 字符串 yes/是"""
result = service.validate_slot_value(boolean_slot_def, "")
assert result.ok is True
assert result.normalized_value is True
def test_boolean_type_string_false(self, service, boolean_slot_def):
"""测试布尔类型 - 字符串 false"""
result = service.validate_slot_value(boolean_slot_def, "false")
assert result.ok is True
assert result.normalized_value is False
def test_boolean_type_invalid(self, service, boolean_slot_def):
"""测试布尔类型 - 无效值"""
result = service.validate_slot_value(boolean_slot_def, "maybe")
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID
def test_enum_type_valid(self, service, enum_slot_def):
"""测试枚举类型 - 有效值"""
result = service.validate_slot_value(enum_slot_def, "高一")
assert result.ok is True
assert result.normalized_value == "高一"
def test_enum_type_invalid(self, service, enum_slot_def):
"""测试枚举类型 - 无效值"""
result = service.validate_slot_value(enum_slot_def, "大一")
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_ENUM_INVALID
def test_enum_type_not_string(self, service, enum_slot_def):
"""测试枚举类型 - 非字符串"""
result = service.validate_slot_value(enum_slot_def, 123)
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID
def test_array_enum_type_valid(self, service, array_enum_slot_def):
"""测试数组枚举类型 - 有效值"""
result = service.validate_slot_value(
array_enum_slot_def, ["语文", "数学"]
)
assert result.ok is True
def test_array_enum_type_invalid_item(self, service, array_enum_slot_def):
"""测试数组枚举类型 - 无效元素"""
result = service.validate_slot_value(
array_enum_slot_def, ["语文", "生物"]
)
assert result.ok is False
assert (
result.error_code == SlotValidationErrorCode.SLOT_ARRAY_ENUM_INVALID
)
def test_array_enum_type_not_array(self, service, array_enum_slot_def):
"""测试数组枚举类型 - 非数组"""
result = service.validate_slot_value(array_enum_slot_def, "语文")
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID
def test_array_enum_type_non_string_item(self, service, array_enum_slot_def):
"""测试数组枚举类型 - 非字符串元素"""
result = service.validate_slot_value(array_enum_slot_def, ["语文", 123])
assert result.ok is False
assert (
result.error_code == SlotValidationErrorCode.SLOT_ARRAY_ENUM_INVALID
)
class TestBatchValidation:
"""批量校验测试"""
def test_batch_all_valid(self, service, string_slot_def, number_slot_def):
"""测试批量校验 - 全部通过"""
slot_defs = [string_slot_def, number_slot_def]
values = {"name": "张三", "age": 25}
result = service.validate_slots(slot_defs, values)
assert result.ok is True
assert len(result.errors) == 0
assert result.validated_values["name"] == "张三"
assert result.validated_values["age"] == 25
def test_batch_some_invalid(self, service, string_slot_def, number_slot_def):
"""测试批量校验 - 部分失败"""
slot_defs = [string_slot_def, number_slot_def]
values = {"name": "张三", "age": "not_a_number"}
result = service.validate_slots(slot_defs, values)
assert result.ok is False
assert len(result.errors) == 1
assert result.errors[0].slot_key == "age"
def test_batch_missing_required(
self, service, required_string_slot_def, string_slot_def
):
"""测试批量校验 - 缺失必填字段"""
slot_defs = [required_string_slot_def, string_slot_def]
values = {"name": "张三"} # 缺少 phone
result = service.validate_slots(slot_defs, values)
assert result.ok is False
assert len(result.errors) == 1
assert result.errors[0].slot_key == "phone"
assert (
result.errors[0].error_code
== SlotValidationErrorCode.SLOT_REQUIRED_MISSING
)
def test_batch_undefined_slot(self, service, string_slot_def):
"""测试批量校验 - 未定义槽位"""
slot_defs = [string_slot_def]
values = {"name": "张三", "undefined_field": "value"}
result = service.validate_slots(slot_defs, values)
assert result.ok is True
# 未定义槽位应允许通过
assert "undefined_field" in result.validated_values
class TestCombinedValidation:
"""组合校验测试(类型 + 正则/JSON Schema"""
def test_type_and_regex_both_pass(self, service):
"""测试类型和正则都通过"""
slot_def = {
"slot_key": "code",
"type": "string",
"required": True,
"validation_rule": r"^[A-Z]{2}\d{4}$",
}
result = service.validate_slot_value(slot_def, "AB1234")
assert result.ok is True
def test_type_pass_regex_fail(self, service):
"""测试类型通过但正则失败"""
slot_def = {
"slot_key": "code",
"type": "string",
"required": True,
"validation_rule": r"^[A-Z]{2}\d{4}$",
}
result = service.validate_slot_value(slot_def, "ab1234")
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_REGEX_MISMATCH
def test_type_fail_no_regex_check(self, service):
"""测试类型失败时不执行正则校验"""
slot_def = {
"slot_key": "code",
"type": "number",
"required": True,
"validation_rule": r"^\d+$",
}
result = service.validate_slot_value(slot_def, "not_a_number")
assert result.ok is False
assert result.error_code == SlotValidationErrorCode.SLOT_TYPE_INVALID
class TestAskBackPrompt:
"""追问提示语测试"""
def test_ask_back_prompt_on_validation_fail(self, service):
"""测试校验失败时返回 ask_back_prompt"""
slot_def = {
"slot_key": "email",
"type": "string",
"required": True,
"validation_rule": r"^[\w\.-]+@[\w\.-]+\.\w+$",
"ask_back_prompt": "请输入有效的邮箱地址,如 example@domain.com",
}
result = service.validate_slot_value(slot_def, "invalid_email")
assert result.ok is False
assert result.ask_back_prompt == "请输入有效的邮箱地址,如 example@domain.com"
def test_no_ask_back_prompt_on_success(self, service, string_slot_def):
"""测试校验通过时不返回 ask_back_prompt"""
result = service.validate_slot_value(string_slot_def, "valid")
assert result.ok is True
assert result.ask_back_prompt is None
def test_ask_back_prompt_on_required_missing(self, service):
"""测试必填缺失时返回 ask_back_prompt"""
slot_def = {
"slot_key": "name",
"type": "string",
"required": True,
"ask_back_prompt": "请告诉我们您的姓名",
}
result = service.validate_slot_value(slot_def, "")
assert result.ok is False
assert result.ask_back_prompt == "请告诉我们您的姓名"
class TestSlotValidationErrorCode:
"""错误码测试"""
def test_error_code_values(self):
"""测试错误码值"""
assert SlotValidationErrorCode.SLOT_REQUIRED_MISSING == "SLOT_REQUIRED_MISSING"
assert SlotValidationErrorCode.SLOT_TYPE_INVALID == "SLOT_TYPE_INVALID"
assert SlotValidationErrorCode.SLOT_REGEX_MISMATCH == "SLOT_REGEX_MISMATCH"
assert (
SlotValidationErrorCode.SLOT_JSON_SCHEMA_MISMATCH
== "SLOT_JSON_SCHEMA_MISMATCH"
)
assert (
SlotValidationErrorCode.SLOT_VALIDATION_RULE_INVALID
== "SLOT_VALIDATION_RULE_INVALID"
)
class TestValidationResult:
"""ValidationResult 测试"""
def test_success_result(self):
"""测试成功结果"""
result = ValidationResult(ok=True, normalized_value="test")
assert result.ok is True
assert result.normalized_value == "test"
assert result.error_code is None
assert result.error_message is None
def test_failure_result(self):
"""测试失败结果"""
result = ValidationResult(
ok=False,
error_code="SLOT_REGEX_MISMATCH",
error_message="格式不正确",
ask_back_prompt="请重新输入",
)
assert result.ok is False
assert result.error_code == "SLOT_REGEX_MISMATCH"
assert result.error_message == "格式不正确"
assert result.ask_back_prompt == "请重新输入"