# backend/tests/test_nl_parser.py — 자연어 파서 골든 + 휴리스틱 폴백 import pytest from app.automation.nl_parser import parse_rule from tests._fake_llm import FakeLLMProvider GOLDEN = [ ( "출장 전날엔 저녁 일정 비워줘", "cal", "출장 전날이 되면", "18시 이후 일정이 있으면", "다른 날로 옮기자고 제안", ), ( "뉴스레터는 모아서 저녁에 보여줘", "mail", "뉴스레터 메일 도착", None, "받은편지함 건너뛰고 18:30 다이제스트로", ), ( "운동을 3일 거르면 산책 잡아줘", "life", "운동 기록 3일 연속 없음", "저녁에 빈 시간이 있으면", "30분 산책 블록 제안", ), ] @pytest.mark.parametrize("text,cat,trig,cond,action", GOLDEN) def test_golden_examples(text, cat, trig, cond, action): r = parse_rule(text) assert r.matched and r.cat == cat assert r.trigger == trig and r.cond == cond and r.action == action assert r.model == "example" def test_heuristic_fallback_classifies_cat(): # examples 미일치 → 휴리스틱(명시 provider 로 환경(Ollama 가동 여부) 무관 결정적) r = parse_rule("영수증 들어오면 자동으로 정리해줘", provider=FakeLLMProvider()) assert r.matched and r.cat == "life" and r.model == "heuristic" def test_empty_input_not_matched(): r = parse_rule("") # 빈 입력은 LLM 호출 없이 미매칭 assert not r.matched class _FakeProvider: name = "fake" model = "fake-llm" def health(self): return {"reachable": True} def generate_json(self, prompt, schema=None): return { "name": "회의록 자동화", "cat": "cal", "trigger": "회의 종료", "cond": None, "action": "요약 작성", "confidence": 0.9, } def classify_capture(self, raw, context): # pragma: no cover raise NotImplementedError def test_llm_path_uses_provider_json(): r = parse_rule("회의 끝나면 요약해줘", provider=_FakeProvider()) assert r.matched and r.cat == "cal" and r.action == "요약 작성" assert r.model == "llm:fake-llm" class _BrokenProvider(_FakeProvider): def generate_json(self, prompt, schema=None): raise RuntimeError("llm down") def test_llm_exception_falls_back_to_heuristic(): r = parse_rule("영수증 들어오면 정리해줘", provider=_BrokenProvider()) assert r.matched and r.model == "heuristic"