250896cdfa
- deep 분기 _eid_chat_deep: 비생성 probe → phase:searching → agentic_ask_loop
(tool_choice=auto 가 검색 여부 자율 판단, 검색 불요는 early-exit 대화) → final_answer
+ eid_sources envelope → DONE. heartbeat {phase:ping}(~10s, 프록시 idle timeout 차단)
· mid-stream BackendUnavailable → in-stream error envelope · disconnect 시 task.cancel()
+ await(고아화·27B 점유 방지).
- daily = call_stream 무변경(맥미니 대화). deep = 맥북 27B ReAct (tool calling 27B 전용,
맥미니 26B token-leak 미검증). 멀티턴 = 메시지 단독 처리(agentic_ask_loop query: str,
history 2단계 백로그).
- EidEvidenceCard.svelte 접이식 근거 카드(sources 순서번호·제목·점수) + 프론트 SSE 파서
확장(ping/searching/error/eid_sources) + 검색 중 표시 + 이력 보존.
- 테스트: deep 4건(검색성/대화성/probe-503/mid-stream-error) + 기존 call_stream 회귀 daily
로 이전 = 29 passed.
- 동반(이전 eid-chat 세션 미커밋): /api/eid/status endpoint + llm_gate.gate_status +
test_eid_status (채팅 대기 UI 의 '대기 vs 고장' 구분용, 5 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
4.2 KiB
Python
113 lines
4.2 KiB
Python
"""GET /api/eid/status endpoint 테스트 — inline ASGI app (DB 의존 0).
|
|
|
|
★ 실행 환경: fastapi + httpx 필요 → test_eid_chat_endpoint.py 동일 idiom.
|
|
★ DB 0 / LLM 0: get_current_user 는 dependency_overrides 로 대체, gate 점유는
|
|
llm_gate.gate_status monkeypatch (eid_chat 이 모듈 attribute 로 호출하므로 유효).
|
|
★ 무인증 케이스는 실제 auth 경로지만 decode 단계에서 거부돼 DB 접근 전 반환.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from fastapi import FastAPI
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "app"))
|
|
|
|
from api.eid_chat import router as eid_chat_router # noqa: E402
|
|
from core.auth import get_current_user # noqa: E402
|
|
from services.search import llm_gate # noqa: E402
|
|
|
|
|
|
def _build_app(*, override_auth: bool = True) -> FastAPI:
|
|
"""main.py 등록 방식과 동일 prefix(/api/eid)로 라우터만 올린 inline app."""
|
|
app = FastAPI()
|
|
app.include_router(eid_chat_router, prefix="/api/eid")
|
|
if override_auth:
|
|
app.dependency_overrides[get_current_user] = lambda: types.SimpleNamespace(
|
|
id=1, username="test-user"
|
|
)
|
|
return app
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def client():
|
|
async with AsyncClient(
|
|
transport=ASGITransport(app=_build_app()), base_url="http://test"
|
|
) as ac:
|
|
yield ac
|
|
|
|
|
|
# ── 401 무인증 ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unauthenticated_rejected():
|
|
async with AsyncClient(
|
|
transport=ASGITransport(app=_build_app(override_auth=False)),
|
|
base_url="http://test",
|
|
) as ac:
|
|
# 헤더 자체 부재 — HTTPBearer 단계 거부 (fastapi 기본 403, 버전별 401 허용)
|
|
r = await ac.get("/api/eid/status")
|
|
assert r.status_code in (401, 403)
|
|
# 위조 토큰 — decode_token 실패 → 401 (DB 접근 전 거부)
|
|
r2 = await ac.get(
|
|
"/api/eid/status", headers={"Authorization": "Bearer bogus-token"}
|
|
)
|
|
assert r2.status_code == 401
|
|
|
|
|
|
# ── 200 shape ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_200_shape(client, monkeypatch):
|
|
"""응답 shape — daily 키 아래 busy/inflight/waiters 3필드, 타입 고정."""
|
|
monkeypatch.setattr(
|
|
llm_gate, "gate_status", lambda: {"inflight": False, "waiters": 0}
|
|
)
|
|
r = await client.get("/api/eid/status")
|
|
assert r.status_code == 200, r.text
|
|
js = r.json()
|
|
assert set(js.keys()) == {"daily"}
|
|
assert set(js["daily"].keys()) == {"busy", "inflight", "waiters"}
|
|
assert isinstance(js["daily"]["busy"], bool)
|
|
assert isinstance(js["daily"]["inflight"], bool)
|
|
assert isinstance(js["daily"]["waiters"], int)
|
|
|
|
|
|
# ── busy 판정 — gate_status monkeypatch ──────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"snap, expected",
|
|
[
|
|
# 유휴 — busy=false (근사: 외부 소비자 점유는 미포착)
|
|
(
|
|
{"inflight": False, "waiters": 0},
|
|
{"busy": False, "inflight": False, "waiters": 0},
|
|
),
|
|
# inflight 만 — busy=true (확실)
|
|
(
|
|
{"inflight": True, "waiters": 0},
|
|
{"busy": True, "inflight": True, "waiters": 0},
|
|
),
|
|
# waiters 만 — busy=true (inflight or waiters>0 의 or 분기)
|
|
(
|
|
{"inflight": False, "waiters": 3},
|
|
{"busy": True, "inflight": False, "waiters": 3},
|
|
),
|
|
],
|
|
)
|
|
async def test_busy_from_gate_status(client, monkeypatch, snap, expected):
|
|
monkeypatch.setattr(llm_gate, "gate_status", lambda: dict(snap))
|
|
r = await client.get("/api/eid/status")
|
|
assert r.status_code == 200, r.text
|
|
assert r.json() == {"daily": expected}
|