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>
109 lines
4.1 KiB
Python
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 도 바뀌어야 함"
|