import { describe, it, expect } from "vitest"; import { buildChatEvent, logChatEvent, CHAT_EVENT_MARKER, CHAT_EVENT_SCHEMA_VERSION, } from "../../src/services/chatAnalyticsService"; import { KnowledgeLevel, TeamCode } from "../../src/types/panit"; describe("chatAnalyticsService.buildChatEvent", () => { it("필수 메타(마커·스키마·outcome·uid)를 항상 포함한다", () => { const ev = buildChatEvent({ outcome: "ok", uid: "u1" }); expect(ev.event).toBe(CHAT_EVENT_MARKER); expect(ev.schema).toBe(CHAT_EVENT_SCHEMA_VERSION); expect(ev.outcome).toBe("ok"); expect(ev.uid).toBe("u1"); }); it("메시지·응답 본문은 어떤 필드에도 담지 않는다(길이만)", () => { const ev = buildChatEvent({ outcome: "ok", uid: "u1", msgLen: 42, }); expect(ev.msgLen).toBe(42); // 본문류 키가 절대 없어야 한다. for (const k of ["message", "reply", "content", "text", "userContent", "assistantContent", "q"]) { expect(ev[k]).toBeUndefined(); } }); it("undefined/null 필드는 제거해 BQ 스키마를 깔끔히 유지한다", () => { const ev = buildChatEvent({ outcome: "crisis_input", uid: "u1", teamCode: null, model: undefined, knowledgeLevel: null, }); expect("teamCode" in ev).toBe(false); expect("model" in ev).toBe(false); expect("knowledgeLevel" in ev).toBe(false); }); it("toolNames에서 toolCount, 토큰에서 tokensTotal을 파생한다", () => { const ev = buildChatEvent({ outcome: "ok", uid: "u1", toolNames: ["get_lineup", "get_team_rank_snapshot"], tokensIn: 1000, tokensOut: 120, tokensCached: 800, }); expect(ev.toolNames).toEqual(["get_lineup", "get_team_rank_snapshot"]); expect(ev.toolCount).toBe(2); expect(ev.tokensTotal).toBe(1120); expect(ev.tokensCached).toBe(800); }); it("빈 toolNames도 toolCount 0으로 명시한다", () => { const ev = buildChatEvent({ outcome: "ok", uid: "u1", toolNames: [] }); expect(ev.toolCount).toBe(0); }); it("성공 케이스의 차원 필드를 그대로 싣는다", () => { const ev = buildChatEvent({ outcome: "ok", uid: "u1", teamCode: TeamCode.OB, threadId: "OB", stylePack: "commentator", knowledgeLevel: KnowledgeLevel.Casual, promptVersion: "2026-06-12.1", provider: "vertex", model: "gemini-3.1-flash-lite", finishReason: "stop", latencyMs: 3400, }); expect(ev.teamCode).toBe(TeamCode.OB); expect(ev.stylePack).toBe("commentator"); expect(ev.knowledgeLevel).toBe(KnowledgeLevel.Casual); expect(ev.finishReason).toBe("stop"); expect(ev.latencyMs).toBe(3400); }); }); describe("chatAnalyticsService.logChatEvent", () => { it("best-effort — 어떤 입력에도 throw하지 않는다", () => { expect(() => logChatEvent({ outcome: "error", uid: "u1", errorCode: "AI_UNAVAILABLE" })).not.toThrow(); }); });