import asyncio import httpx import pytest from fastapi import HTTPException from B01_Dashboard import B01_Dashboard_Knowledge_Chat as chat def test_greeting_answers_without_search_or_api_key(monkeypatch): monkeypatch.delenv("GEMINI_API_KEY", raising=False) monkeypatch.setattr( chat, "search", lambda _question: (_ for _ in ()).throw(AssertionError("searched")) ) result = asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="안녕!"), {})) assert result == { "answer": "안녕하세요! 법령이나 지침에 대해 궁금한 점을 물어보세요.", "sources": [], } def test_natural_question_finds_term_without_particle(monkeypatch): monkeypatch.setattr( chat, "_sections", lambda: [("original/원가계산/품셈.md", "# 일위대가\n공사 한 단위에 필요한 비용을 계산")], ) assert chat.search("일위대가가 뭐야")[0]["path"] == "original/원가계산/품셈.md" def test_missing_api_key_is_reported_before_search(monkeypatch): monkeypatch.delenv("GEMINI_API_KEY", raising=False) monkeypatch.setattr(chat, "search", lambda _question: []) with pytest.raises(HTTPException) as error: asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="일위대가가 뭐야"), {})) assert error.value.status_code == 503 def test_busy_gemini_is_retried_and_reported(monkeypatch): monkeypatch.setenv("GEMINI_API_KEY", "test-key") monkeypatch.setattr(chat, "search", lambda _question: []) calls = [] class Client: def __init__(self, timeout): pass async def __aenter__(self): return self async def __aexit__(self, *_args): pass async def post(self, *_args, **_kwargs): calls.append(1) return type("Response", (), {"status_code": 503})() async def no_wait(_seconds): pass monkeypatch.setattr(chat.httpx, "AsyncClient", Client) monkeypatch.setattr(chat.asyncio, "sleep", no_wait) with pytest.raises(HTTPException) as error: asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="일위대가가 뭐야"), {})) assert len(calls) == 3 assert "요청이 몰려" in error.value.detail def test_gemini_timeout_is_reported(monkeypatch): monkeypatch.setenv("GEMINI_API_KEY", "test-key") monkeypatch.setattr(chat, "search", lambda _question: []) class Client: def __init__(self, timeout): pass async def __aenter__(self): return self async def __aexit__(self, *_args): pass async def post(self, *_args, **_kwargs): raise httpx.ReadTimeout("timed out") monkeypatch.setattr(chat.httpx, "AsyncClient", Client) with pytest.raises(HTTPException) as error: asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="일위대가가 뭐야"), {})) assert error.value.status_code == 504 assert "시간 초과" in error.value.detail def test_chat_searches_all_knowledge_markdown_except_excluded_folders(monkeypatch, tmp_path): public = tmp_path / "original" / "법률" guide = tmp_path / "technical_info" private = tmp_path / "original" / "실무문서" stmate = tmp_path / "original" / "원가계산" / "STmate" dasan = tmp_path / "original" / "원가계산" / "다산소프트" public.mkdir(parents=True) guide.mkdir() private.mkdir() stmate.mkdir(parents=True) dasan.mkdir() (public / "현행.md").write_text("# 임도 기준\n" + "임도 설치 기준 내용 " * 10, encoding="utf-8") (guide / "_검토.md").write_text("# 임도 기준\n기술정보 안내 " * 10, encoding="utf-8") (private / "사례.md").write_text("# 임도 기준\n비공개 사례 " * 10, encoding="utf-8") (stmate / "자료.md").write_text("# 임도 기준\nSTmate 자료 " * 10, encoding="utf-8") (dasan / "자료.md").write_text("# 임도 기준\n다산소프트 자료 " * 10, encoding="utf-8") monkeypatch.setattr(chat, "KNOWLEDGE", tmp_path) monkeypatch.setenv("GEMINI_API_KEY", "test-key") chat._sections.cache_clear() sent = {} class Response: status_code = 200 def raise_for_status(self): pass def json(self): return {"candidates": [{"content": {"parts": [{"text": "설명 [1]"}]}}]} class Client: def __init__(self, timeout): pass async def __aenter__(self): return self async def __aexit__(self, *_args): pass async def post(self, url, *, headers, json): sent["prompt"] = json["contents"][0]["parts"][0]["text"] return Response() monkeypatch.setattr(chat.httpx, "AsyncClient", Client) result = asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="임도 기준"), {})) assert result["answer"] == "설명 [1]" paths = {source["path"] for source in result["sources"]} assert paths == {"original/법률/현행.md"} assert "실무문서" not in sent["prompt"] assert "비공개 사례" not in sent["prompt"] assert "STmate" not in sent["prompt"] assert "다산소프트" not in sent["prompt"] assert "기술정보 안내" not in sent["prompt"] chat._sections.cache_clear()