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>
88 lines
2.8 KiB
Python
88 lines
2.8 KiB
Python
"""EscalationEnvelope JSON round-trip + system injection 형식."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from ai.envelope import EscalationEnvelope
|
|
|
|
|
|
def test_envelope_round_trip():
|
|
env = EscalationEnvelope(
|
|
from_stage="summarize_short",
|
|
escalation_reasons=("long_context", "risk_flag_requires_26b"),
|
|
risk_flags=("safety_legal_interpretation", "multi_doc_dependency"),
|
|
distilled_context="법령 조문 인용 다수. 해석 판단 필요.",
|
|
original_pointers={"doc_ids": ["a", "b"], "paths": ["/p1"]},
|
|
synthesis_directives=("조문 원문 인용 필수.",),
|
|
user_intent="조문 적용 여부",
|
|
draft_hint="조문 인용 후 분리 기술",
|
|
)
|
|
s = env.to_json()
|
|
env2 = EscalationEnvelope.from_json(s)
|
|
assert env == env2
|
|
|
|
|
|
def test_envelope_system_injection_has_key_blocks():
|
|
env = EscalationEnvelope(
|
|
from_stage="ask_pre",
|
|
escalation_reasons=("high_impact",),
|
|
risk_flags=("chemical_hazard",),
|
|
distilled_context="MSDS 주요 성분 A, B 식별",
|
|
synthesis_directives=("MSDS 원문 인용 우선.",),
|
|
)
|
|
block = env.to_system_injection()
|
|
assert "ESCALATION ENVELOPE" in block
|
|
assert "chemical_hazard" in block
|
|
assert "high_impact" in block
|
|
assert "MSDS 원문 인용 우선" in block
|
|
|
|
|
|
def test_envelope_rejects_invalid_from_stage():
|
|
with pytest.raises(ValueError):
|
|
EscalationEnvelope(
|
|
from_stage="nonexistent_stage",
|
|
escalation_reasons=(),
|
|
risk_flags=(),
|
|
distilled_context="",
|
|
)
|
|
|
|
|
|
def test_envelope_requires_tuple_reasons():
|
|
with pytest.raises(TypeError):
|
|
EscalationEnvelope(
|
|
from_stage="triage",
|
|
escalation_reasons=["long_context"], # list, not tuple
|
|
risk_flags=(),
|
|
distilled_context="",
|
|
)
|
|
|
|
|
|
def test_envelope_requires_tuple_flags():
|
|
with pytest.raises(TypeError):
|
|
EscalationEnvelope(
|
|
from_stage="triage",
|
|
escalation_reasons=(),
|
|
risk_flags=["pii_present"], # list, not tuple
|
|
distilled_context="",
|
|
)
|
|
|
|
|
|
def test_envelope_frozen_equality():
|
|
"""frozen dataclass — 동일 필드면 == True."""
|
|
env_a = EscalationEnvelope(
|
|
from_stage="classify",
|
|
escalation_reasons=("long_context",),
|
|
risk_flags=("pii_present",),
|
|
distilled_context="same text",
|
|
)
|
|
env_b = EscalationEnvelope(
|
|
from_stage="classify",
|
|
escalation_reasons=("long_context",),
|
|
risk_flags=("pii_present",),
|
|
distilled_context="same text",
|
|
)
|
|
assert env_a == env_b
|
|
# 참고: original_pointers 가 dict 필드이므로 자동 __hash__ 는 지원되지 않음
|
|
# (envelope 은 JSON transport 용 — set/dict key 로 쓸 필요 없음)
|