99672292d3
프로덕션 컨테이너는 /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>
119 lines
4.1 KiB
Python
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))
|