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

542 lines
22 KiB
Python
Raw Normal View History

"""
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 == "请重新输入"