6a85087b83
전 로컬 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>
106 lines
4.3 KiB
Python
106 lines
4.3 KiB
Python
"""eid_* append-only 구조강제 + 파생뷰 PG 통합 테스트 (W3, review #1).
|
|
|
|
설계 불변식 #8 의 load-bearing 부분 = DB 강제. 단일 owner role pkm 이라 REVOKE 무효 +
|
|
migration 검증기가 plpgsql BEGIN 거부 → RULE(DO INSTEAD NOTHING) + NOT NULL 스탬프로 강제.
|
|
순수함수 테스트(test_compose/test_weakness_compute)로는 검증 불가한 'DB 가 실제로 막는가'를 본다.
|
|
|
|
★ 실행 환경: Postgres(Docker 스택, migrations 301-305 적용 후) 필요 — MacBook 로컬엔 PG 없어
|
|
hard-fail(skip 아님). test_worker_jobs_smoke.py 와 동일 idiom(_worker_pool_helpers).
|
|
staging(devsbx/개발서버 배포 후)에서 가동. 트랜잭션 rollback 으로 테스트 행 오염 0.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "app"))
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # tests/ (helpers)
|
|
|
|
from sqlalchemy import text # noqa: E402
|
|
from sqlalchemy.exc import IntegrityError # noqa: E402
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine # noqa: E402
|
|
|
|
from _worker_pool_helpers import ensure_user, get_database_url # noqa: E402
|
|
|
|
_VALID_INSERT = (
|
|
"INSERT INTO eid_study_weakness "
|
|
"(user_id, weaknesses, habit_signals, trend_label, actor, source_generated_at) "
|
|
"VALUES (:u, '[]'::jsonb, '{}'::jsonb, '악화', 'eid', now()) RETURNING id"
|
|
)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def uid():
|
|
return await ensure_user("test-eid-append-only")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unstamped_insert_rejected(uid):
|
|
"""actor 스탬프 누락 INSERT → NOT NULL 위반 (owner 도 적용 — 스탬프 없는 행 거부)."""
|
|
engine = create_async_engine(get_database_url())
|
|
sm = async_sessionmaker(engine, expire_on_commit=False)
|
|
try:
|
|
with pytest.raises(IntegrityError):
|
|
async with sm() as s:
|
|
await s.execute(
|
|
text(
|
|
"INSERT INTO eid_study_weakness "
|
|
"(user_id, weaknesses, habit_signals, trend_label, source_generated_at) "
|
|
"VALUES (:u, '[]'::jsonb, '{}'::jsonb, '정체', now())" # actor 누락
|
|
),
|
|
{"u": uid},
|
|
)
|
|
await s.commit()
|
|
finally:
|
|
await engine.dispose()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_and_delete_are_no_op(uid):
|
|
"""RULE DO INSTEAD NOTHING — owner pkm 의 UPDATE/DELETE 도 행을 못 바꾼다(append-only)."""
|
|
engine = create_async_engine(get_database_url())
|
|
sm = async_sessionmaker(engine, expire_on_commit=False)
|
|
try:
|
|
async with sm() as s:
|
|
wid = (await s.execute(text(_VALID_INSERT), {"u": uid})).scalar_one()
|
|
await s.flush() # 같은 트랜잭션 내 가시 (commit 안 함 → 끝에 rollback 으로 오염 0)
|
|
|
|
await s.execute(
|
|
text("UPDATE eid_study_weakness SET trend_label='개선' WHERE id=:i"), {"i": wid}
|
|
)
|
|
tl = (
|
|
await s.execute(text("SELECT trend_label FROM eid_study_weakness WHERE id=:i"), {"i": wid})
|
|
).scalar_one()
|
|
assert tl == "악화", "UPDATE 가 값을 바꿈 — RULE 미적용(append-only 깨짐)"
|
|
|
|
await s.execute(text("DELETE FROM eid_study_weakness WHERE id=:i"), {"i": wid})
|
|
cnt = (
|
|
await s.execute(text("SELECT count(*) FROM eid_study_weakness WHERE id=:i"), {"i": wid})
|
|
).scalar_one()
|
|
assert cnt == 1, "DELETE 가 행을 지움 — RULE 미적용(append-only 깨짐)"
|
|
|
|
await s.rollback() # 테스트 행 폐기
|
|
finally:
|
|
await engine.dispose()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_schedule_views_queryable(uid):
|
|
"""v_schedule_today / v_schedule_defer_pattern 정의 유효성 smoke (enum 리터럴·LATERAL·date_trunc).
|
|
|
|
뷰가 invalid 면 CREATE 시점 또는 SELECT 시점에 에러 → 쿼리 성공 = DDL 유효.
|
|
"""
|
|
engine = create_async_engine(get_database_url())
|
|
sm = async_sessionmaker(engine, expire_on_commit=False)
|
|
try:
|
|
async with sm() as s:
|
|
await s.execute(text("SELECT * FROM v_schedule_today LIMIT 1"))
|
|
await s.execute(text("SELECT * FROM v_schedule_defer_pattern LIMIT 1"))
|
|
finally:
|
|
await engine.dispose()
|