Files
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

109 lines
4.1 KiB
Python

"""Prompt rendering + policy_version hash 검증."""
from __future__ import annotations
import pytest
from policy import prompt_render
from policy.prompt_render import (
KNOWN_4B_TASKS,
KNOWN_26B_TASKS,
policy_version,
render_26b,
render_4b,
)
ALL_4B_TASKS = sorted(KNOWN_4B_TASKS)
ALL_26B_TASKS = sorted(KNOWN_26B_TASKS)
@pytest.mark.parametrize("task", ALL_4B_TASKS)
def test_render_4b_basic(policy, task):
rendered = render_4b(task, subject_domain="safety_reference", policy=policy)
# placeholder 가 남아있지 않아야 함 (정책 주입된 것들)
assert "{forbidden_block}" not in rendered
assert "{subject_description}" not in rendered
assert "{confidence_threshold}" not in rendered
assert "{context_cap}" not in rendered
# 실제 금지 섹션 텍스트 포함
assert "4B 절대 금지" in rendered
# 사용자 input placeholder 는 남아있어야 함 (이중 중괄호 → 단일로 이스케이프됨)
# 단, render 시점 이후 .format() 으로 주입되므로 {filename} 같은 건 나중에 치환
@pytest.mark.parametrize("task", ALL_26B_TASKS)
def test_render_26b_basic(policy, task):
rendered = render_26b(task, subject_domain="safety_reference", policy=policy)
assert "{forbidden_block}" not in rendered
assert "{subject_description}" not in rendered
assert "4B 절대 금지" in rendered
def test_render_4b_rejects_26b_task(policy):
with pytest.raises(ValueError):
render_4b("p3c_deep_summary", subject_domain="msds", policy=policy)
def test_render_26b_rejects_4b_task(policy):
with pytest.raises(ValueError):
render_26b("p3a_short_summary", subject_domain="msds", policy=policy)
def test_render_uses_fallback_for_unknown_domain(policy):
"""unknown subject 도 fallback_domain.description 이 사용되어 렌더 성공."""
rendered = render_4b("p1_triage", subject_domain="__unknown__", policy=policy)
assert policy.fallback_domain.description in rendered
def test_render_different_domain_different_forbidden_block(policy):
"""도메인별로 forbidden 블록 내용이 달라짐."""
msds = render_4b("p3a_short_summary", subject_domain="msds", policy=policy)
news = render_4b("p3a_short_summary", subject_domain="news_item", policy=policy)
# msds 는 safety_sufficiency_assertion 규칙 포함
assert "safety_sufficiency_assertion" in msds
# news_item 은 news_multi_source_synthesis 규칙 포함
assert "news_multi_source_synthesis" in news
# =====================================================================
# policy_version hash — deterministic
# =====================================================================
@pytest.mark.parametrize("task", ALL_4B_TASKS + ALL_26B_TASKS)
def test_policy_version_deterministic(policy_yaml_path, task):
v1 = policy_version(task, policy_path=policy_yaml_path)
v2 = policy_version(task, policy_path=policy_yaml_path)
assert v1 == v2
def test_policy_version_length(policy_yaml_path):
v = policy_version("p3a_short_summary", policy_path=policy_yaml_path)
assert len(v) == 12
# hex 문자열인지 확인
int(v, 16) # raises ValueError if not hex
def test_policy_version_differs_across_tasks(policy_yaml_path):
v_a = policy_version("p1_triage", policy_path=policy_yaml_path)
v_b = policy_version("p3a_short_summary", policy_path=policy_yaml_path)
assert v_a != v_b, "다른 template 은 다른 hash 가 나와야 함"
def test_policy_version_changes_when_yaml_changes(tmp_path, policy_yaml_path):
"""yaml 을 바꾸면 hash 가 변한다."""
original = policy_version("p3a_short_summary", policy_path=policy_yaml_path)
# yaml 복사본 수정
modified = tmp_path / "modified.yaml"
from pathlib import Path
original_text = Path(policy_yaml_path).read_text(encoding="utf-8")
# 주석 한 줄 추가 — 구조 유지하면서 bytes 만 변경
modified.write_text(original_text + "\n# test modification\n", encoding="utf-8")
prompt_render.clear_cache()
changed = policy_version("p3a_short_summary", policy_path=str(modified))
assert original != changed, "yaml 바뀌면 hash 도 바뀌어야 함"