733 lines
23 KiB
Python
733 lines
23 KiB
Python
"""
|
|
Contract tests for Metadata Governance module.
|
|
[AC-IDSMETA-13~22] Verify provider API matches openapi.provider.yaml contract.
|
|
|
|
Contract Level: L2
|
|
Reference: spec/metadata-governance/openapi.provider.yaml
|
|
"""
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
from typing import Any
|
|
|
|
|
|
class MetadataSchema:
|
|
"""
|
|
[AC-IDSMETA-13] MetadataSchema contract model.
|
|
Matches openapi.provider.yaml MetadataSchema schema.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
id: str,
|
|
field_key: str,
|
|
label: str,
|
|
type: str,
|
|
required: bool,
|
|
scope: list[str],
|
|
status: str,
|
|
options: list[str] | None = None,
|
|
default: str | int | float | bool | None = None,
|
|
is_filterable: bool = True,
|
|
is_rank_feature: bool = False,
|
|
):
|
|
self.id = id
|
|
self.field_key = field_key
|
|
self.label = label
|
|
self.type = type
|
|
self.required = required
|
|
self.scope = scope
|
|
self.status = status
|
|
self.options = options
|
|
self.default = default
|
|
self.is_filterable = is_filterable
|
|
self.is_rank_feature = is_rank_feature
|
|
|
|
def validate(self) -> tuple[bool, list[str]]:
|
|
errors = []
|
|
if not self.field_key:
|
|
errors.append("field_key is required")
|
|
if not self.label:
|
|
errors.append("label is required")
|
|
if self.type not in ["string", "number", "boolean", "enum", "array_enum"]:
|
|
errors.append(f"Invalid type: {self.type}")
|
|
if self.status not in ["draft", "active", "deprecated"]:
|
|
errors.append(f"Invalid status: {self.status}")
|
|
if not self.scope:
|
|
errors.append("scope must have at least one item")
|
|
for s in self.scope:
|
|
if s not in ["kb_document", "intent_rule", "script_flow", "prompt_template"]:
|
|
errors.append(f"Invalid scope value: {s}")
|
|
return len(errors) == 0, errors
|
|
|
|
|
|
class MetadataSchemaCreateRequest:
|
|
"""
|
|
[AC-IDSMETA-13] Create request contract model.
|
|
"""
|
|
|
|
VALID_FIELD_KEY_PATTERN = r"^[a-z0-9_]+$"
|
|
VALID_TYPES = ["string", "number", "boolean", "enum", "array_enum"]
|
|
VALID_STATUSES = ["draft", "active", "deprecated"]
|
|
VALID_SCOPES = ["kb_document", "intent_rule", "script_flow", "prompt_template"]
|
|
|
|
def __init__(
|
|
self,
|
|
field_key: str,
|
|
label: str,
|
|
type: str,
|
|
required: bool,
|
|
scope: list[str],
|
|
status: str,
|
|
options: list[str] | None = None,
|
|
default: str | int | float | bool | None = None,
|
|
is_filterable: bool = True,
|
|
is_rank_feature: bool = False,
|
|
):
|
|
self.field_key = field_key
|
|
self.label = label
|
|
self.type = type
|
|
self.required = required
|
|
self.scope = scope
|
|
self.status = status
|
|
self.options = options
|
|
self.default = default
|
|
self.is_filterable = is_filterable
|
|
self.is_rank_feature = is_rank_feature
|
|
|
|
def validate(self) -> tuple[bool, list[str]]:
|
|
import re
|
|
errors = []
|
|
|
|
if not self.field_key or len(self.field_key) < 1 or len(self.field_key) > 64:
|
|
errors.append("field_key must be 1-64 characters")
|
|
elif not re.match(self.VALID_FIELD_KEY_PATTERN, self.field_key):
|
|
errors.append(f"field_key must match pattern {self.VALID_FIELD_KEY_PATTERN}")
|
|
|
|
if not self.label or len(self.label) < 1 or len(self.label) > 64:
|
|
errors.append("label must be 1-64 characters")
|
|
|
|
if self.type not in self.VALID_TYPES:
|
|
errors.append(f"type must be one of {self.VALID_TYPES}")
|
|
|
|
if self.status not in self.VALID_STATUSES:
|
|
errors.append(f"status must be one of {self.VALID_STATUSES}")
|
|
|
|
if not self.scope or len(self.scope) < 1:
|
|
errors.append("scope must have at least one item")
|
|
else:
|
|
for s in self.scope:
|
|
if s not in self.VALID_SCOPES:
|
|
errors.append(f"Invalid scope value: {s}")
|
|
|
|
if self.type in ["enum", "array_enum"] and (not self.options or len(self.options) == 0):
|
|
errors.append(f"type '{self.type}' requires non-empty options")
|
|
|
|
if self.options:
|
|
if len(self.options) != len(set(self.options)):
|
|
errors.append("options must have unique values")
|
|
|
|
return len(errors) == 0, errors
|
|
|
|
|
|
class MetadataSchemaUpdateRequest:
|
|
"""
|
|
[AC-IDSMETA-14] Update request contract model.
|
|
"""
|
|
|
|
VALID_STATUSES = ["draft", "active", "deprecated"]
|
|
VALID_SCOPES = ["kb_document", "intent_rule", "script_flow", "prompt_template"]
|
|
|
|
def __init__(
|
|
self,
|
|
label: str | None = None,
|
|
required: bool | None = None,
|
|
options: list[str] | None = None,
|
|
default: str | int | float | bool | None = None,
|
|
scope: list[str] | None = None,
|
|
is_filterable: bool | None = None,
|
|
is_rank_feature: bool | None = None,
|
|
status: str | None = None,
|
|
):
|
|
self.label = label
|
|
self.required = required
|
|
self.options = options
|
|
self.default = default
|
|
self.scope = scope
|
|
self.is_filterable = is_filterable
|
|
self.is_rank_feature = is_rank_feature
|
|
self.status = status
|
|
|
|
def validate(self) -> tuple[bool, list[str]]:
|
|
errors = []
|
|
|
|
if self.label is not None and (len(self.label) < 1 or len(self.label) > 64):
|
|
errors.append("label must be 1-64 characters")
|
|
|
|
if self.status is not None and self.status not in self.VALID_STATUSES:
|
|
errors.append(f"status must be one of {self.VALID_STATUSES}")
|
|
|
|
if self.scope is not None:
|
|
if len(self.scope) < 1:
|
|
errors.append("scope must have at least one item")
|
|
else:
|
|
for s in self.scope:
|
|
if s not in self.VALID_SCOPES:
|
|
errors.append(f"Invalid scope value: {s}")
|
|
|
|
if self.options is not None:
|
|
if len(self.options) != len(set(self.options)):
|
|
errors.append("options must have unique values")
|
|
|
|
return len(errors) == 0, errors
|
|
|
|
|
|
class DecompositionTemplate:
|
|
"""
|
|
[AC-IDSMETA-21, AC-IDSMETA-22] DecompositionTemplate contract model.
|
|
"""
|
|
|
|
VALID_VERSION_PATTERN = r"^v?[0-9]+\.[0-9]+\.[0-9]+$"
|
|
VALID_STATUSES = ["draft", "active", "deprecated"]
|
|
|
|
def __init__(
|
|
self,
|
|
id: str,
|
|
name: str,
|
|
template_content: str,
|
|
version: str,
|
|
status: str,
|
|
):
|
|
self.id = id
|
|
self.name = name
|
|
self.template_content = template_content
|
|
self.version = version
|
|
self.status = status
|
|
|
|
def validate(self) -> tuple[bool, list[str]]:
|
|
import re
|
|
errors = []
|
|
|
|
if not self.name or len(self.name) < 1 or len(self.name) > 100:
|
|
errors.append("name must be 1-100 characters")
|
|
|
|
if not self.template_content or len(self.template_content) < 20:
|
|
errors.append("template_content must be at least 20 characters")
|
|
|
|
if not re.match(self.VALID_VERSION_PATTERN, self.version):
|
|
errors.append(f"version must match pattern {self.VALID_VERSION_PATTERN}")
|
|
|
|
if self.status not in self.VALID_STATUSES:
|
|
errors.append(f"status must be one of {self.VALID_STATUSES}")
|
|
|
|
return len(errors) == 0, errors
|
|
|
|
|
|
class ErrorResponse:
|
|
"""
|
|
Error response contract model.
|
|
"""
|
|
|
|
def __init__(self, code: str, message: str, details: dict[str, Any] | None = None):
|
|
self.code = code
|
|
self.message = message
|
|
self.details = details
|
|
|
|
def validate(self) -> tuple[bool, list[str]]:
|
|
errors = []
|
|
if not self.code:
|
|
errors.append("code is required")
|
|
if not self.message:
|
|
errors.append("message is required")
|
|
return len(errors) == 0, errors
|
|
|
|
|
|
class TestMetadataSchemaContract:
|
|
"""
|
|
[AC-IDSMETA-13] Test MetadataSchema matches OpenAPI contract.
|
|
"""
|
|
|
|
def test_required_fields_present(self):
|
|
"""MetadataSchema must have all required fields."""
|
|
schema = MetadataSchema(
|
|
id="test-id",
|
|
field_key="grade",
|
|
label="年级",
|
|
type="enum",
|
|
required=True,
|
|
scope=["kb_document"],
|
|
status="active",
|
|
)
|
|
is_valid, errors = schema.validate()
|
|
assert is_valid, f"Validation failed: {errors}"
|
|
|
|
def test_field_key_pattern_validation(self):
|
|
"""field_key must match ^[a-z0-9_]+$ pattern."""
|
|
valid_keys = ["grade", "subject_name", "type1", "kb_type"]
|
|
for key in valid_keys:
|
|
schema = MetadataSchema(
|
|
id="test-id",
|
|
field_key=key,
|
|
label="Test",
|
|
type="string",
|
|
required=False,
|
|
scope=["kb_document"],
|
|
status="draft",
|
|
)
|
|
is_valid, _ = schema.validate()
|
|
assert is_valid, f"Valid key '{key}' should pass"
|
|
|
|
def test_field_key_rejects_invalid(self):
|
|
"""field_key must reject invalid patterns."""
|
|
invalid_keys = ["Grade", "subject-name", "test key", "test.key"]
|
|
for key in invalid_keys:
|
|
request = MetadataSchemaCreateRequest(
|
|
field_key=key,
|
|
label="Test",
|
|
type="string",
|
|
required=False,
|
|
scope=["kb_document"],
|
|
status="draft",
|
|
)
|
|
is_valid, _ = request.validate()
|
|
assert not is_valid, f"Invalid key '{key}' should fail"
|
|
|
|
def test_type_enum_values(self):
|
|
"""type must be one of: string, number, boolean, enum, array_enum."""
|
|
valid_types = ["string", "number", "boolean", "enum", "array_enum"]
|
|
for t in valid_types:
|
|
schema = MetadataSchema(
|
|
id="test-id",
|
|
field_key="test",
|
|
label="Test",
|
|
type=t,
|
|
required=False,
|
|
scope=["kb_document"],
|
|
status="active",
|
|
)
|
|
is_valid, _ = schema.validate()
|
|
assert is_valid, f"Valid type '{t}' should pass"
|
|
|
|
def test_type_rejects_invalid(self):
|
|
"""type must reject invalid values."""
|
|
schema = MetadataSchema(
|
|
id="test-id",
|
|
field_key="test",
|
|
label="Test",
|
|
type="invalid_type",
|
|
required=False,
|
|
scope=["kb_document"],
|
|
status="active",
|
|
)
|
|
is_valid, _ = schema.validate()
|
|
assert not is_valid
|
|
|
|
def test_status_enum_values(self):
|
|
"""status must be one of: draft, active, deprecated."""
|
|
valid_statuses = ["draft", "active", "deprecated"]
|
|
for s in valid_statuses:
|
|
schema = MetadataSchema(
|
|
id="test-id",
|
|
field_key="test",
|
|
label="Test",
|
|
type="string",
|
|
required=False,
|
|
scope=["kb_document"],
|
|
status=s,
|
|
)
|
|
is_valid, _ = schema.validate()
|
|
assert is_valid, f"Valid status '{s}' should pass"
|
|
|
|
def test_scope_enum_values(self):
|
|
"""scope items must be valid."""
|
|
valid_scopes = [
|
|
["kb_document"],
|
|
["intent_rule"],
|
|
["script_flow"],
|
|
["prompt_template"],
|
|
["kb_document", "intent_rule"],
|
|
]
|
|
for scope in valid_scopes:
|
|
schema = MetadataSchema(
|
|
id="test-id",
|
|
field_key="test",
|
|
label="Test",
|
|
type="string",
|
|
required=False,
|
|
scope=scope,
|
|
status="active",
|
|
)
|
|
is_valid, _ = schema.validate()
|
|
assert is_valid, f"Valid scope '{scope}' should pass"
|
|
|
|
def test_scope_rejects_invalid(self):
|
|
"""scope must reject invalid values."""
|
|
schema = MetadataSchema(
|
|
id="test-id",
|
|
field_key="test",
|
|
label="Test",
|
|
type="string",
|
|
required=False,
|
|
scope=["invalid_scope"],
|
|
status="active",
|
|
)
|
|
is_valid, _ = schema.validate()
|
|
assert not is_valid
|
|
|
|
def test_scope_requires_at_least_one(self):
|
|
"""scope must have at least one item."""
|
|
schema = MetadataSchema(
|
|
id="test-id",
|
|
field_key="test",
|
|
label="Test",
|
|
type="string",
|
|
required=False,
|
|
scope=[],
|
|
status="active",
|
|
)
|
|
is_valid, _ = schema.validate()
|
|
assert not is_valid
|
|
|
|
|
|
class TestMetadataSchemaCreateRequestContract:
|
|
"""
|
|
[AC-IDSMETA-13] Test MetadataSchemaCreateRequest validation.
|
|
"""
|
|
|
|
def test_valid_create_request(self):
|
|
"""Valid create request should pass."""
|
|
request = MetadataSchemaCreateRequest(
|
|
field_key="grade",
|
|
label="年级",
|
|
type="enum",
|
|
required=True,
|
|
scope=["kb_document"],
|
|
status="draft",
|
|
options=["初一", "初二", "初三"],
|
|
)
|
|
is_valid, errors = request.validate()
|
|
assert is_valid, f"Validation failed: {errors}"
|
|
|
|
def test_enum_type_requires_options(self):
|
|
"""[AC-IDSMETA-03] enum type requires non-empty options."""
|
|
request = MetadataSchemaCreateRequest(
|
|
field_key="grade",
|
|
label="年级",
|
|
type="enum",
|
|
required=True,
|
|
scope=["kb_document"],
|
|
status="draft",
|
|
options=None,
|
|
)
|
|
is_valid, _ = request.validate()
|
|
assert not is_valid
|
|
|
|
def test_array_enum_type_requires_options(self):
|
|
"""[AC-IDSMETA-03] array_enum type requires non-empty options."""
|
|
request = MetadataSchemaCreateRequest(
|
|
field_key="subjects",
|
|
label="学科",
|
|
type="array_enum",
|
|
required=False,
|
|
scope=["kb_document"],
|
|
status="draft",
|
|
options=None,
|
|
)
|
|
is_valid, _ = request.validate()
|
|
assert not is_valid
|
|
|
|
def test_options_must_be_unique(self):
|
|
"""[AC-IDSMETA-03] options must have unique values."""
|
|
request = MetadataSchemaCreateRequest(
|
|
field_key="grade",
|
|
label="年级",
|
|
type="enum",
|
|
required=True,
|
|
scope=["kb_document"],
|
|
status="draft",
|
|
options=["初一", "初一", "初二"],
|
|
)
|
|
is_valid, _ = request.validate()
|
|
assert not is_valid
|
|
|
|
def test_field_key_length_constraints(self):
|
|
"""field_key must be 1-64 characters."""
|
|
request = MetadataSchemaCreateRequest(
|
|
field_key="",
|
|
label="Test",
|
|
type="string",
|
|
required=False,
|
|
scope=["kb_document"],
|
|
status="draft",
|
|
)
|
|
is_valid, _ = request.validate()
|
|
assert not is_valid
|
|
|
|
request = MetadataSchemaCreateRequest(
|
|
field_key="a" * 65,
|
|
label="Test",
|
|
type="string",
|
|
required=False,
|
|
scope=["kb_document"],
|
|
status="draft",
|
|
)
|
|
is_valid, _ = request.validate()
|
|
assert not is_valid
|
|
|
|
def test_label_length_constraints(self):
|
|
"""label must be 1-64 characters."""
|
|
request = MetadataSchemaCreateRequest(
|
|
field_key="test",
|
|
label="",
|
|
type="string",
|
|
required=False,
|
|
scope=["kb_document"],
|
|
status="draft",
|
|
)
|
|
is_valid, _ = request.validate()
|
|
assert not is_valid
|
|
|
|
|
|
class TestMetadataSchemaUpdateRequestContract:
|
|
"""
|
|
[AC-IDSMETA-14] Test MetadataSchemaUpdateRequest validation.
|
|
"""
|
|
|
|
def test_valid_update_request(self):
|
|
"""Valid update request should pass."""
|
|
request = MetadataSchemaUpdateRequest(
|
|
label="更新后的标签",
|
|
status="deprecated",
|
|
)
|
|
is_valid, errors = request.validate()
|
|
assert is_valid, f"Validation failed: {errors}"
|
|
|
|
def test_partial_update(self):
|
|
"""Partial update with only some fields should pass."""
|
|
request = MetadataSchemaUpdateRequest(status="active")
|
|
is_valid, _ = request.validate()
|
|
assert is_valid
|
|
|
|
def test_empty_update(self):
|
|
"""Empty update should pass (all fields optional)."""
|
|
request = MetadataSchemaUpdateRequest()
|
|
is_valid, _ = request.validate()
|
|
assert is_valid
|
|
|
|
def test_status_transition_to_deprecated(self):
|
|
"""[AC-IDSMETA-14] Status can be updated to deprecated."""
|
|
request = MetadataSchemaUpdateRequest(status="deprecated")
|
|
is_valid, _ = request.validate()
|
|
assert is_valid
|
|
|
|
|
|
class TestDecompositionTemplateContract:
|
|
"""
|
|
[AC-IDSMETA-21, AC-IDSMETA-22] Test DecompositionTemplate validation.
|
|
"""
|
|
|
|
def test_valid_template(self):
|
|
"""Valid template should pass."""
|
|
template = DecompositionTemplate(
|
|
id="template-1",
|
|
name="数据拆解模板",
|
|
template_content="这是一个数据拆解模板,用于分析和归类待录入文本...",
|
|
version="1.0.0",
|
|
status="active",
|
|
)
|
|
is_valid, errors = template.validate()
|
|
assert is_valid, f"Validation failed: {errors}"
|
|
|
|
def test_version_format_with_v_prefix(self):
|
|
"""version can have optional 'v' prefix."""
|
|
template = DecompositionTemplate(
|
|
id="template-1",
|
|
name="Test",
|
|
template_content="a" * 20,
|
|
version="v1.0.0",
|
|
status="active",
|
|
)
|
|
is_valid, _ = template.validate()
|
|
assert is_valid
|
|
|
|
def test_version_format_without_v_prefix(self):
|
|
"""version can be without 'v' prefix."""
|
|
template = DecompositionTemplate(
|
|
id="template-1",
|
|
name="Test",
|
|
template_content="a" * 20,
|
|
version="2.1.3",
|
|
status="active",
|
|
)
|
|
is_valid, _ = template.validate()
|
|
assert is_valid
|
|
|
|
def test_version_rejects_invalid_format(self):
|
|
"""version must match semver pattern."""
|
|
invalid_versions = ["1.0", "v1", "1.0.0.0", "latest"]
|
|
for v in invalid_versions:
|
|
template = DecompositionTemplate(
|
|
id="template-1",
|
|
name="Test",
|
|
template_content="a" * 20,
|
|
version=v,
|
|
status="active",
|
|
)
|
|
is_valid, _ = template.validate()
|
|
assert not is_valid, f"Invalid version '{v}' should fail"
|
|
|
|
def test_template_content_min_length(self):
|
|
"""template_content must be at least 20 characters."""
|
|
template = DecompositionTemplate(
|
|
id="template-1",
|
|
name="Test",
|
|
template_content="short",
|
|
version="1.0.0",
|
|
status="active",
|
|
)
|
|
is_valid, _ = template.validate()
|
|
assert not is_valid
|
|
|
|
def test_name_length_constraints(self):
|
|
"""name must be 1-100 characters."""
|
|
template = DecompositionTemplate(
|
|
id="template-1",
|
|
name="",
|
|
template_content="a" * 20,
|
|
version="1.0.0",
|
|
status="active",
|
|
)
|
|
is_valid, _ = template.validate()
|
|
assert not is_valid
|
|
|
|
template = DecompositionTemplate(
|
|
id="template-1",
|
|
name="a" * 101,
|
|
template_content="a" * 20,
|
|
version="1.0.0",
|
|
status="active",
|
|
)
|
|
is_valid, _ = template.validate()
|
|
assert not is_valid
|
|
|
|
|
|
class TestErrorResponseContract:
|
|
"""
|
|
Test ErrorResponse matches OpenAPI contract.
|
|
"""
|
|
|
|
def test_required_fields(self):
|
|
"""ErrorResponse must have code and message."""
|
|
response = ErrorResponse(
|
|
code="VALIDATION_ERROR",
|
|
message="Invalid request",
|
|
)
|
|
is_valid, errors = response.validate()
|
|
assert is_valid, f"Validation failed: {errors}"
|
|
|
|
def test_optional_details(self):
|
|
"""ErrorResponse can have optional details."""
|
|
response = ErrorResponse(
|
|
code="VALIDATION_ERROR",
|
|
message="Multiple validation errors",
|
|
details={"fields": ["field_key", "label"]},
|
|
)
|
|
is_valid, _ = response.validate()
|
|
assert is_valid
|
|
|
|
|
|
class TestACTraceability:
|
|
"""
|
|
[QA-IDSMETA-01] Verify AC traceability in openapi.provider.yaml.
|
|
"""
|
|
|
|
def test_ac_idsmeta_13_traceability(self):
|
|
"""
|
|
[AC-IDSMETA-13] Verify field status management (draft/active/deprecated).
|
|
OpenAPI: listMetadataSchemas, createMetadataSchema
|
|
"""
|
|
valid_statuses = ["draft", "active", "deprecated"]
|
|
for status in valid_statuses:
|
|
schema = MetadataSchema(
|
|
id="test-id",
|
|
field_key="test",
|
|
label="Test",
|
|
type="string",
|
|
required=False,
|
|
scope=["kb_document"],
|
|
status=status,
|
|
)
|
|
is_valid, _ = schema.validate()
|
|
assert is_valid
|
|
|
|
def test_ac_idsmeta_14_traceability(self):
|
|
"""
|
|
[AC-IDSMETA-14] Verify deprecated field handling.
|
|
OpenAPI: updateMetadataSchema with status=deprecated
|
|
"""
|
|
request = MetadataSchemaUpdateRequest(status="deprecated")
|
|
is_valid, _ = request.validate()
|
|
assert is_valid
|
|
|
|
def test_ac_idsmeta_21_22_traceability(self):
|
|
"""
|
|
[AC-IDSMETA-21, AC-IDSMETA-22] Verify decomposition template contract.
|
|
OpenAPI: listDecompositionTemplates, createDecompositionTemplate
|
|
"""
|
|
template = DecompositionTemplate(
|
|
id="template-1",
|
|
name="拆解模板",
|
|
template_content="这是一个拆解模板,用于分析待录入文本的归类...",
|
|
version="1.0.0",
|
|
status="active",
|
|
)
|
|
is_valid, _ = template.validate()
|
|
assert is_valid
|
|
|
|
|
|
class TestContractLevelCompliance:
|
|
"""
|
|
Verify L2 contract level compliance.
|
|
"""
|
|
|
|
def test_schema_completeness(self):
|
|
"""L2 requires complete schema with required/optional fields clearly defined."""
|
|
schema = MetadataSchema(
|
|
id="test-id",
|
|
field_key="grade",
|
|
label="年级",
|
|
type="enum",
|
|
required=True,
|
|
scope=["kb_document", "intent_rule"],
|
|
status="active",
|
|
options=["初一", "初二", "初三"],
|
|
default="初一",
|
|
is_filterable=True,
|
|
is_rank_feature=False,
|
|
)
|
|
is_valid, _ = schema.validate()
|
|
assert is_valid
|
|
|
|
def test_error_response_schema(self):
|
|
"""L2 requires defined error response schema."""
|
|
error = ErrorResponse(
|
|
code="SCHEMA_NOT_FOUND",
|
|
message="Metadata schema not found",
|
|
details={"schema_id": "non-existent-id"},
|
|
)
|
|
is_valid, _ = error.validate()
|
|
assert is_valid
|
|
|
|
def test_field_validation_rules(self):
|
|
"""L2 requires clear field validation rules."""
|
|
request = MetadataSchemaCreateRequest(
|
|
field_key="valid_key_123",
|
|
label="Valid Label",
|
|
type="string",
|
|
required=False,
|
|
scope=["kb_document"],
|
|
status="draft",
|
|
)
|
|
is_valid, errors = request.validate()
|
|
assert is_valid, f"Validation failed: {errors}"
|