Files
javis_bot/bot/src/userbot.test.ts
javis-bot 44ebfeafa8 feat: per-call LLM timing, speaker ID, cancel captures on leave
- llm.py: log each Ollama call's caller + total/load/prompt/gen durations
  so a slow voice turn is attributable to a specific internal call
  (router/enrichment/digest/main); a RELOAD marker flags cold reloads.
- voice.ts: track in-flight Opus captures and abort them on session
  destroy(); drop any utterance that finishes after the user left, so no
  trailing post-leave VAD turns are reported.
- userbot.ts: show the speaker's Discord user ID on each transcript line
  (answered and dropped) so it's clear whose audio produced the turn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 00:38:26 +09:00

79 lines
2.8 KiB
TypeScript

import { test, expect } from "bun:test";
// userbot.ts imports the runtime `config`, which requires DISCORD_GUILD_ID.
process.env.DISCORD_GUILD_ID ||= "test-guild";
const { formatTurnMessage } = await import("./userbot.ts");
test("formatTurnMessage shows a per-stage timing breakdown with durations", () => {
const msg = formatTurnMessage({
transcript: "안녕하세요",
reply: "네, 안녕하세요",
listenStartMs: 10_000,
listenEndMs: 12_000, // 듣기 2.0s
llmStartMs: 12_500, // STT/전송 gap 0.5s
llmEndMs: 14_100, // LLM 1.6s
ttsStartMs: 14_100,
ttsEndMs: 15_000, // TTS 0.9s
});
expect(msg).toContain('🗣️ "안녕하세요"');
expect(msg).toContain("🤖 답변: 네, 안녕하세요");
expect(msg).toContain("👂 듣기 2.0초");
expect(msg).toContain("🧠 LLM 1.6초");
expect(msg).toContain("(STT/전송 0.5초)");
expect(msg).toContain("🔊 TTS 0.9초");
// Total spans listening start -> TTS end.
expect(msg).toContain("합계 5.0초");
});
test("formatTurnMessage omits the STT gap note when it is negligible", () => {
const msg = formatTurnMessage({
transcript: "테스트",
reply: "응답",
listenStartMs: 0,
listenEndMs: 1000,
llmStartMs: 1000, // no gap
llmEndMs: 2000,
});
expect(msg).not.toContain("STT/전송");
expect(msg).toContain("🧠 LLM 1.0초");
});
test("formatTurnMessage reports a dropped turn with only the listening stage", () => {
const msg = formatTurnMessage({
transcript: "",
reply: "",
note: "너무 짧음(<300ms)",
listenStartMs: 0,
listenEndMs: 200,
});
expect(msg).toContain("❌ 너무 짧음(<300ms)");
expect(msg).toContain("👂 듣기 0.2초");
expect(msg).not.toContain("🧠 LLM");
expect(msg).not.toContain("🔊 TTS");
});
test("formatTurnMessage shows the speaker user ID when provided", () => {
const answered = formatTurnMessage({ user: "12345", transcript: "안녕", reply: "하이" });
expect(answered).toContain("👤 12345");
const dropped = formatTurnMessage({ user: "67890", transcript: "", reply: "", note: "음성 아님(VAD 차단)" });
expect(dropped).toContain("👤 67890");
expect(dropped).toContain("❌ 음성 아님(VAD 차단)");
});
test("formatTurnMessage falls back to the plain line when no timing is present", () => {
const msg = formatTurnMessage({ transcript: "안녕", reply: "하이" });
expect(msg).toBe('🎤 들음 → 🗣️ "안녕"\n🤖 답변: 하이');
expect(msg).not.toContain("⏱️");
});
test("formatTurnMessage drops out-of-order (negative) spans instead of showing junk", () => {
const msg = formatTurnMessage({
transcript: "안녕",
reply: "하이",
listenStartMs: 5000,
listenEndMs: 4000, // negative span -> omitted
});
expect(msg).not.toContain("👂 듣기");
});