"""eid.compose 단위 테스트 — persona→rules→overlay→task 합성 (stdlib only, venv 불필요). 실행: python3 tests/eid/test_compose.py (또는 pytest tests/eid/test_compose.py) """ from __future__ import annotations import sys from pathlib import Path # app/ 를 import 루트로 (repo_root/tests/eid/ → repo_root/app) sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "app")) from eid.compose import ( # noqa: E402 SEP, SubstrateOverflow, _persona, compose, is_composed_surface, rules_present, ) _TASK = "<<>>" def test_order_persona_rules_task(): out = compose("react_ask", _TASK) # persona(이드 정체성) · rules(생성 가드, '보수적'=conservative 룰) · task 모두 존재 assert "이드" in out, "persona 미주입" assert "보수적" in out, "rules(생성 서브셋) 미주입" assert _TASK in out, "task 미포함" # 순서: persona < rules < task assert out.index("이드") < out.index("보수적") < out.index(_TASK), "persona→rules→task 순서 위반" def test_base_surface_has_no_overlay(): out = compose("study_subject_note", _TASK) assert "학습 진단 코치" not in out, "base 표면에 기능 overlay 누출" assert "뉴스 큐레이터" not in out def test_overlay_surface_includes_overlay_between_rules_and_task(): out = compose("study_diagnosis", _TASK) assert "학습 진단 코치" in out, "study overlay 미주입" # overlay 는 rules 뒤, task 앞 assert out.index("보수적") < out.index("학습 진단 코치") < out.index(_TASK) def test_unknown_surface_falls_back_to_base(): out = compose("totally_unknown_surface", _TASK) assert "이드" in out and _TASK in out # persona+rules+task 유지 assert "학습 진단 코치" not in out # overlay 없음 def test_is_composed_surface(): assert is_composed_surface("react_ask") assert is_composed_surface("study_diagnosis") assert not is_composed_surface("classify") # 기계류 9종 = 미등록 assert not is_composed_surface("briefing_comparative") # JSON 기계류 = persona ZERO def test_persona_quiet_on_unknown_variant(): assert _persona("bogus_variant") == "" # quiet fail-open def test_sep_join_present(): out = compose("react_ask", _TASK) assert SEP in out, "합본 구분자 SEP 누락" def test_overflow_failloud_never_silent_drop(): # 아주 작은 budget → non-droppable floor 초과 → SubstrateOverflow(절대 silent drop 안 함) raised = False try: compose("study_diagnosis", _TASK, budget_chars=50) except SubstrateOverflow: raised = True assert raised, "budget 초과인데 silent 통과 — fail-loud 위반" def test_generous_budget_passes(): out = compose("react_ask", _TASK, budget_chars=100_000) assert _TASK in out # 넉넉한 예산 = 통과 def test_study_diagnosis_overlay_placeholders_survive_compose(): # study_diagnosis = study overlay 경로. {weakness_snapshot_block}/{habit_signal_block} 가 # compose 출력(system)에 리터럴로 남아야 surface 가 .replace 로 실데이터 치환 가능. out = compose("study_diagnosis", task="") assert "{weakness_snapshot_block}" in out, "약점 placeholder 누락(overlay degrade)" assert "{habit_signal_block}" in out, "태도 placeholder 누락" filled = out.replace("{weakness_snapshot_block}", "WB").replace("{habit_signal_block}", "HB") assert "{weakness_snapshot_block}" not in filled and "WB" in filled and "HB" in filled def test_eid_chat_surface_registered(): # eid-chat D-1: 채팅 표면 = 자유-prose(base), persona ON, 기능 overlay 없음 (불변식 #3) assert is_composed_surface("eid_chat"), "eid_chat ROUTE_MAP 미등록" out = compose("eid_chat", "") assert "이드" in out, "persona 미주입" assert "보수적" in out, "rules 미주입" assert out.index("이드") < out.index("보수적"), "persona→rules 순서 위반" assert "학습 진단 코치" not in out, "채팅 base 표면에 기능 overlay 누출" def test_rules_present_true_then_false(): # D-6 fail-closed 판정 재료 — vendored rules.md 존재 시 True, 부재 시 False. # _rules() 의 degraded 배너 동작(다른 표면)은 본 헬퍼와 무관하게 유지된다. import eid.compose as c assert rules_present() is True, "vendored rules.md 가 있는데 False" orig = c._SUBSTRATE_DIR try: c._SUBSTRATE_DIR = Path("/nonexistent-substrate-dir-for-test") assert c.rules_present() is False, "rules.md 부재인데 True — fail-closed 판정 불가" finally: c._SUBSTRATE_DIR = orig def test_rules_present_live_judgment(): # D-6 게이트 = 살아있는 판정 — lru_cache(_read) 동결 회귀 방지. # 같은 경로에서 생성→True, 삭제→False 가 즉시 반영돼야 한다. import tempfile import eid.compose as c orig = c._SUBSTRATE_DIR try: with tempfile.TemporaryDirectory() as td: c._SUBSTRATE_DIR = Path(td) rules = Path(td) / "rules.md" assert c.rules_present() is False rules.write_text("rule", encoding="utf-8") assert c.rules_present() is True, "생성이 반영 안 됨 — 캐시 동결" rules.unlink() assert c.rules_present() is False, "삭제가 반영 안 됨 — 캐시 동결" finally: c._SUBSTRATE_DIR = orig def _run(): fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")] fails = 0 for fn in fns: try: fn() print(f" PASS {fn.__name__}") except Exception as exc: # noqa: BLE001 fails += 1 print(f" FAIL {fn.__name__}: {type(exc).__name__}: {exc}") print(f"\n{len(fns) - fails}/{len(fns)} passed") return 1 if fails else 0 if __name__ == "__main__": raise SystemExit(_run())