Files
hyungi_document_server/tests/eid/test_dispatch.py
T
hyungi 6a85087b83 feat(eid): 이드 persona substrate W2~W4 — DS compose·약점진단·egress 코드층 박탈
전 로컬 LLM 관통 '이드' persona substrate 의 Document Server 측 빌드(W2~W4).
설계 = PKM eid-persona-substrate(r1~r3 수렴) / impl = eid-persona-impl.

W2 — compose + 표면 배선:
- app/eid/compose.py: persona→rules→overlay→task 단일 system 문자열 + 정적 ROUTE_MAP
  (런타임 sniffing 아님) + rules 부재 fail-loud · persona 부재 quiet · overflow fail-loud.
- 자유-prose 3 표면(react_ask·study_subject_note·study_question_explanation) 중복 정체성·
  generic 정책 trim + compose 배선(AIClient 에 additive system 파라미터). 도메인 calibration 보존.
- STRICT JSON 기계류(briefing_comparative·digest_topic)는 persona-ZERO 동결(불변식 #3).
- app/prompts/substrate/: persona(외부 컴파일 산출물 vendor) + rules(생성 가드 서브셋) + overlay 5.

W3 — migration + 워커 + study_diagnosis:
- migration 301~305: eid_* append-only 원장(약점/복습초안/회고) + approval_requests(가변 큐) + 일정 파생뷰 2.
- app/workers/study_weakness.py: study_question_progress.pattern_state 집계로 약점 derived 산출
  (LLM 0) + bounded tier(watch/review/focus). nightly cron.
- study_diagnosis 표면: 최신 스냅샷을 코치 언어로 번역(약점 판정은 코드, LLM 은 블록 값만 인용).

W4-1 — egress 코드층 박탈:
- app/eid/ai.py EidAIClient: 이드 표면 = call_primary(내부 MLX) only. 외부 LLM fallback 경로
  구조적 봉쇄(call_fallback raise · 자동 fallback 제거 · 외부 endpoint 차단). egress 워커는 분리 유지.

load-bearing 정정 3(환경 grounding 강제, 설계 회귀 아님):
- rules = 운영 ruleset 전체 → 생성 가드 서브셋(HTML 산출물 룰이 study task 와 충돌).
- append-only = REVOKE → CREATE RULE DO INSTEAD NOTHING(단일 owner role 은 REVOKE 무효 +
  migration 검증기가 plpgsql BEGIN 거부) + actor/source_* NOT NULL 스탬프.
- 이드 LLM 봉쇄 = path discipline → EidAIClient 구조화.

검증: eid 순수 단위테스트 30 통과 + py_compile + migration 검증기 모사 + egress 적대감사 COMPLETE.
DB/LLM/httpx 의존 테스트(append-only RULE·EidAIClient·E2E)는 staging(Docker) 가동.
W4-2 네트워크 belt 은 조건부 보류(코드층 1차 충분, P0-3② 원격 실측 후 hard-gate 시 승격).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:13:20 +09:00

106 lines
3.0 KiB
Python

"""eid.tools.dispatch 단위 테스트 — 고정 enum · 동적해석 0 · egress 잠금 (stdlib only).
실행: python3 tests/eid/test_dispatch.py (또는 pytest)
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "app"))
from eid.tools.dispatch import ( # noqa: E402
ALLOWED_ACTIONS,
_FORBIDDEN_EGRESS_VERBS,
EidAction,
_HANDLERS,
dispatch,
register_handler,
)
def _reset_handlers():
_HANDLERS.clear()
def test_unknown_action_rejected():
_reset_handlers()
r = dispatch("frobnicate")
assert r.ok is False
assert "unknown" in r.reason.lower() or "화이트리스트" in r.reason
def test_no_egress_verb_in_enum():
# 이중 보증: 화이트리스트 ∩ egress verb = 0
assert ALLOWED_ACTIONS.isdisjoint(_FORBIDDEN_EGRESS_VERBS)
def test_egress_verb_dispatch_rejected():
_reset_handlers()
for verb in ("send_smtp_email", "create_caldav_todo", "call_fallback", "httpx"):
r = dispatch(verb)
assert r.ok is False, f"egress verb {verb} 가 통과됨"
def test_external_approval_immediate_reject_no_enqueue():
_reset_handlers()
r = dispatch("request_external_approval", {"to": "x@y.com", "body": "..."})
assert r.ok is False
assert "거부" in r.reason or "권한 0" in r.reason # Phase1 즉시거부
def test_external_approval_handler_cannot_register():
raised = False
try:
register_handler(EidAction.REQUEST_EXTERNAL_APPROVAL, lambda a: None)
except ValueError:
raised = True
assert raised, "request_external_approval 핸들러 등록이 허용됨(즉시거부 위반)"
def test_registered_handler_runs():
_reset_handlers()
register_handler(EidAction.READ_DOCUMENTS, lambda a: {"rows": 3, "echo": a})
r = dispatch("read_documents", {"q": "vessel"})
assert r.ok is True
assert r.data == {"rows": 3, "echo": {"q": "vessel"}}
def test_unregistered_known_action_rejected():
_reset_handlers()
# 화이트리스트엔 있으나 핸들러 미등록(W3 이전) → reject (동적 해석으로 새지 않음)
r = dispatch("read_events")
assert r.ok is False
assert "미등록" in r.reason or "handler" in r.reason.lower()
def test_handler_error_becomes_reject():
_reset_handlers()
def _boom(_a):
raise RuntimeError("db down")
register_handler(EidAction.READ_STUDY, _boom)
r = dispatch("read_study")
assert r.ok is False
assert "error" in r.reason.lower()
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())