Files
hyungi_document_server/tests/policy/test_policy_loader_schema.py
T
Hyungi Ahn 99672292d3 fix(policy): use container-compatible imports (drop app. prefix)
프로덕션 컨테이너는 /app 을 cwd 로 실행하고 import 는 `from api...`,
`from core...`, `from workers...` 처럼 무접두 스타일을 사용한다.
PR-A 내부 import 가 `from app.policy...`, `from app.ai.envelope` 로
되어 있어서 컨테이너에서 ModuleNotFoundError 발생.

변경:
- app/policy/*.py: `from app.policy.X` → `from policy.X`
- app/services/prompt_versions.py: lazy import 도 `from policy.prompt_render`
- app/ai/envelope.py: 영향 없음 (내부 import 없음)
- tests/policy/*.py: 모두 `from policy.X` / `from ai.envelope` 로 통일
- tests/policy/conftest.py: 로컬 pytest 용 sys.path.insert(app/) 추가
  (MacBook 에서 repo-root 기준 실행 시 app/ 를 package root 로 취급)

CI: pytest tests/policy/ -q → 98 passed (로컬, 동일 결과)
프로덕션: docker exec fastapi python -c "from policy.loader import load_policy" → OK

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 09:42:24 +09:00

119 lines
4.1 KiB
Python

"""domain_policy.yaml 스키마 검증 + cross-reference 체크."""
from __future__ import annotations
import pytest
import yaml
from pydantic import ValidationError
from policy import loader as policy_loader
from policy.schema import DomainPolicy
def test_default_yaml_loads(policy):
"""기본 yaml 이 pydantic 검증 통과."""
assert isinstance(policy, DomainPolicy)
assert policy.version == 1
assert "safety_health" in policy.scope
assert "news" in policy.scope
assert policy.self_declare_semantics == "additive_trigger_only"
def test_subject_domains_count(policy):
"""plan 에서 정의한 9개 subject_domain 전부 존재."""
expected = {
"safety_reference",
"safety_operational",
"msds",
"hazard_specific",
"incident_report",
"health_record",
"safety_video",
"news_item",
"news_digest_request",
}
assert set(policy.subject_domains.keys()) == expected
def test_all_subject_domains_have_suggested_ui_category(policy):
"""storage_category → suggested_ui_category 리네임 확인.
모든 도메인이 실측 enum 에서만 값을 선택.
"""
valid = {"document", "library", "news", "memo", "audio", "video", "law"}
for name, dom in policy.subject_domains.items():
assert dom.suggested_ui_category in valid, (
f"{name}.suggested_ui_category={dom.suggested_ui_category} not in enum"
)
def test_fallback_domain_required(policy):
"""fallback_domain 필수 (INV-6)."""
assert policy.fallback_domain.name == "generic"
assert policy.fallback_domain.suggested_ui_category in {
"document",
"library",
"news",
"memo",
"audio",
"video",
"law",
}
def test_risk_flags_cross_reference_ok(policy):
"""default_risk_flags 에 미정의 flag 참조 없음."""
known = set(policy.risk_flags.keys())
for name, dom in policy.subject_domains.items():
for flag in dom.default_risk_flags:
assert flag in known, f"{name} references undefined flag {flag}"
def test_forbidden_rules_reference_existing_domains(policy):
"""forbidden_for_4b.applies_when_subject_in 의 도메인이 subject_domains 에 존재."""
known = set(policy.subject_domains.keys())
for rule in policy.forbidden_for_4b:
for dom in rule.applies_when_subject_in:
assert dom in known, f"{rule.id} references undefined domain {dom}"
def test_reject_unknown_flag_in_yaml(tmp_path, policy_yaml_path):
"""yaml 에 정의되지 않은 flag 를 subject_domain 이 참조하면 ValidationError."""
with open(policy_yaml_path, encoding="utf-8") as f:
raw = yaml.safe_load(f)
# 가짜 flag 주입
raw["subject_domains"]["safety_reference"]["default_risk_flags"] = [
"does_not_exist_flag"
]
bad_yaml = tmp_path / "bad.yaml"
bad_yaml.write_text(yaml.safe_dump(raw, allow_unicode=True))
policy_loader.clear_cache()
with pytest.raises(ValidationError):
policy_loader.load_policy(str(bad_yaml))
def test_reject_invalid_ui_category(tmp_path, policy_yaml_path):
"""suggested_ui_category 에 enum 외 값 들어가면 ValidationError."""
with open(policy_yaml_path, encoding="utf-8") as f:
raw = yaml.safe_load(f)
raw["subject_domains"]["safety_reference"]["suggested_ui_category"] = "nonexistent"
bad_yaml = tmp_path / "bad_cat.yaml"
bad_yaml.write_text(yaml.safe_dump(raw, allow_unicode=True))
policy_loader.clear_cache()
with pytest.raises(ValidationError):
policy_loader.load_policy(str(bad_yaml))
def test_reject_too_long_synthesis_directive(tmp_path, policy_yaml_path):
"""500 chars 초과 synthesis_directive 는 reject."""
with open(policy_yaml_path, encoding="utf-8") as f:
raw = yaml.safe_load(f)
raw["risk_flags"]["safety_legal_interpretation"]["synthesis_directive"] = "x" * 600
bad_yaml = tmp_path / "bad_dir.yaml"
bad_yaml.write_text(yaml.safe_dump(raw, allow_unicode=True))
policy_loader.clear_cache()
with pytest.raises(ValidationError):
policy_loader.load_policy(str(bad_yaml))