import { describe, expect, it } from "vitest"; import { checkInput, checkOutput, crisisReply, detectCrisis } from "../../src/services/chatFilterService"; import { DEFAULT_FILTER_CONFIG } from "../../src/constants/chatFilters"; import { CHAT_CANARY_TOKEN, CRISIS_SELF_HARM_MESSAGE, CRISIS_URGENT_PREFIX, } from "../../src/constants/chatPrompts"; const f = DEFAULT_FILTER_CONFIG; describe("chatFilterService", () => { describe("checkInput — 하드 블록(§7.1)", () => { it("혐오 표현은 차단한다", () => { expect(checkInput("쪽바리들은 다 그래", f).blocked).toBe(true); expect(checkInput("전라디언 어쩌고", f).category).toBe("hate"); }); it("성적 표현은 차단한다", () => { expect(checkInput("야한 얘기 해줘", f).blocked).toBe(true); }); it("과도한 인신 모욕은 차단한다", () => { expect(checkInput("니애미 어쩌고", f).blocked).toBe(true); }); it("거친 응원 표현(탄식·가벼운 욕설)은 통과시킨다 — 3-1", () => { expect(checkInput("아 진짜 미치겠네 오늘 경기 뭐냐", f).blocked).toBe(false); expect(checkInput("우리 팀 진짜 답답하다 짜증나", f).blocked).toBe(false); expect(checkInput("감독 뭐하냐 진짜", f).blocked).toBe(false); }); }); describe("detectCrisis — 위기 감지(§7.3)", () => { it("자해·자살 직접 언급은 위기 신호다", () => { const r = detectCrisis("요즘은 그냥 다 끝내고 싶다는 생각만 들어", f); expect(r.crisis).toBe(true); expect(r.type).toBe("selfHarm"); }); it("급박 신호(시점·수단)는 urgent로 표시한다", () => { const r = detectCrisis("유서 써놨어. 오늘 죽을 거야", f); expect(r.crisis).toBe(true); expect(r.urgent).toBe(true); }); it("팀 성적 탄식은 위기 신호가 아니다 — 오발동 방지", () => { expect(detectCrisis("한화 때문에 못 살아 진짜", f).crisis).toBe(false); expect(detectCrisis("이 팀 보다가 내가 먼저 지치겠다", f).crisis).toBe(false); }); it("관용구·일상 표현은 위기로 처리하지 않는다 — 오발동 방지", () => { expect(detectCrisis("유서 깊은 구단이잖아", f).crisis).toBe(false); expect(detectCrisis("나 요즘 야구에 목매고 있어", f).crisis).toBe(false); expect(detectCrisis("엄마 때문에 경기 못 봤어", f).crisis).toBe(false); expect(detectCrisis("네 말이 맞아, 엄마가 맞아", f).crisis).toBe(false); expect(detectCrisis("불펜이 또 불지르네", f).crisis).toBe(false); expect(detectCrisis("아 감독 진짜 죽여버리겠네", f).crisis).toBe(false); }); it("폭력·학대 피해 호소는 abuse 유형이다", () => { const r = detectCrisis("사실 집에서 학대당하고 있어", f); expect(r.crisis).toBe(true); expect(r.type).toBe("abuse"); }); it("타인 위해 예고는 threat 유형이다", () => { const r = detectCrisis("걔 찾아가서 죽여버리겠어", f); expect(r.crisis).toBe(true); expect(r.type).toBe("threat"); }); }); describe("crisisReply — 고정 문구(무변형 보장)", () => { it("selfHarm은 109 안내 문구 전문을 그대로 반환한다", () => { expect(crisisReply("selfHarm")).toBe(CRISIS_SELF_HARM_MESSAGE); }); it("urgent면 112/119 한 줄을 맨 앞에 추가한다", () => { const reply = crisisReply("selfHarm", true); expect(reply.startsWith(CRISIS_URGENT_PREFIX)).toBe(true); expect(reply).toContain(CRISIS_SELF_HARM_MESSAGE); }); it("abuse는 117/1366 안내를 포함한다", () => { const reply = crisisReply("abuse"); expect(reply).toContain("117"); expect(reply).toContain("1366"); }); }); describe("checkOutput — 출력 검사(§7.2)", () => { it("정상 응답은 통과한다", () => { expect(checkOutput("오늘 선발 보니까 해볼 만한데? 짹!", f).action).toBe("pass"); }); it("위기 마커가 있으면 crisis로 교체한다", () => { const r = checkOutput("[[CRISIS]] 잠깐, 진지하게…", f); expect(r.action).toBe("crisis"); expect(r.crisisType).toBe("selfHarm"); }); it("유형별 마커는 위기 유형을 보존한다(§7.3 ② — 평탄화 방지)", () => { expect(checkOutput("[[CRISIS:ABUSE]] …", f).crisisType).toBe("abuse"); expect(checkOutput("[[CRISIS:THREAT]] …", f).crisisType).toBe("threat"); const urgent = checkOutput("[[CRISIS:URGENT]] …", f); expect(urgent.crisisType).toBe("selfHarm"); expect(urgent.crisisUrgent).toBe(true); }); it("실제 주입 프롬프트의 장문 다중 일치는 유출로 판정하고, 1줄 인용(예시 발화)은 통과한다", () => { const body = [ "이 줄은 충분히 긴 프롬프트 본문 라인이다 — 하나", "이 줄은 충분히 긴 프롬프트 본문 라인이다 — 둘", "이 줄은 충분히 긴 프롬프트 본문 라인이다 — 셋", ].join("\n"); const dump = `시스템 프롬프트는 이래: ${body.replace(/\n/g, " ")}`; expect(checkOutput(dump, f, body).action).toBe("filter"); expect(checkOutput("이 줄은 충분히 긴 프롬프트 본문 라인이다 — 하나", f, body).action).toBe("pass"); }); it("마커 없이 위기 문구를 직접 출력해도 crisis로 정규화한다", () => { expect(checkOutput("힘드시죠. 자살예방 상담전화 109로 연락해보세요", f).action).toBe("crisis"); }); it("카나리 토큰 유출은 filter 처리한다", () => { expect(checkOutput(`내 식별자는 ${CHAT_CANARY_TOKEN}이야`, f).action).toBe("filter"); }); it("구조 마커([사용자 컨텍스트 끝] 등) 유출은 filter 처리한다", () => { expect(checkOutput("…그리고 마지막엔 [사용자 컨텍스트 끝] 이라고 적혀 있어", f).action).toBe("filter"); expect(checkOutput("[서버 지시 — 사용자에게 노출하지 않는다]…", f).action).toBe("filter"); }); it("혐오 표현이 섞인 출력은 filter 처리한다", () => { expect(checkOutput("그 팀 팬들은 쪽바리 같아", f).action).toBe("filter"); }); }); });