import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { Command } from "commander"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; let runtimeStub: { config: { toNumber?: string }; manager: { initiateCall: ReturnType; continueCall: ReturnType; speak: ReturnType; endCall: ReturnType; getCall: ReturnType; getCallByProviderCallId: ReturnType; }; stop: ReturnType; }; vi.mock("../../extensions/voice-call/src/runtime.js", () => ({ createVoiceCallRuntime: vi.fn(async () => runtimeStub), })); import plugin from "../../extensions/voice-call/index.js"; const noopLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }; type Registered = { methods: Map; tools: unknown[]; }; type RegisterVoiceCall = (api: Record) => void | Promise; type RegisterCliContext = { program: Command; config: Record; workspaceDir?: string; logger: typeof noopLogger; }; function setup(config: Record): Registered { const methods = new Map(); const tools: unknown[] = []; plugin.register({ id: "voice-call", name: "Voice Call", description: "test", version: "0", source: "test", config: {}, pluginConfig: config, runtime: { tts: { textToSpeechTelephony: vi.fn() } } as unknown as Parameters< typeof plugin.register >[0]["runtime"], logger: noopLogger, registerGatewayMethod: (method: string, handler: unknown) => methods.set(method, handler), registerTool: (tool: unknown) => tools.push(tool), registerCli: () => {}, registerService: () => {}, resolvePath: (p: string) => p, } as unknown as Parameters[0]); return { methods, tools }; } async function registerVoiceCallCli(program: Command) { const { register } = plugin as unknown as { register: RegisterVoiceCall; }; await register({ id: "voice-call", name: "Voice Call", description: "test", version: "0", source: "test", config: {}, pluginConfig: { provider: "mock" }, runtime: { tts: { textToSpeechTelephony: vi.fn() } }, logger: noopLogger, registerGatewayMethod: () => {}, registerTool: () => {}, registerCli: (fn: (ctx: RegisterCliContext) => void) => fn({ program, config: {}, workspaceDir: undefined, logger: noopLogger, }), registerService: () => {}, resolvePath: (p: string) => p, }); } describe("voice-call plugin", () => { beforeEach(() => { runtimeStub = { config: { toNumber: "+15550001234" }, manager: { initiateCall: vi.fn(async () => ({ callId: "call-1", success: true })), continueCall: vi.fn(async () => ({ success: true, transcript: "hello", })), speak: vi.fn(async () => ({ success: true })), endCall: vi.fn(async () => ({ success: true })), getCall: vi.fn((id: string) => (id === "call-1" ? { callId: "call-1" } : undefined)), getCallByProviderCallId: vi.fn(() => undefined), }, stop: vi.fn(async () => {}), }; }); afterEach(() => vi.restoreAllMocks()); it("registers gateway methods", () => { const { methods } = setup({ provider: "mock" }); expect(methods.has("voicecall.initiate")).toBe(true); expect(methods.has("voicecall.continue")).toBe(true); expect(methods.has("voicecall.speak")).toBe(true); expect(methods.has("voicecall.end")).toBe(true); expect(methods.has("voicecall.status")).toBe(true); expect(methods.has("voicecall.start")).toBe(true); }); it("initiates a call via voicecall.initiate", async () => { const { methods } = setup({ provider: "mock" }); const handler = methods.get("voicecall.initiate") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); await handler?.({ params: { message: "Hi" }, respond }); expect(runtimeStub.manager.initiateCall).toHaveBeenCalled(); const [ok, payload] = respond.mock.calls[0]; expect(ok).toBe(true); expect(payload.callId).toBe("call-1"); }); it("returns call status", async () => { const { methods } = setup({ provider: "mock" }); const handler = methods.get("voicecall.status") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); await handler?.({ params: { callId: "call-1" }, respond }); const [ok, payload] = respond.mock.calls[0]; expect(ok).toBe(true); expect(payload.found).toBe(true); }); it("tool get_status returns json payload", async () => { const { tools } = setup({ provider: "mock" }); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise; }; const result = (await tool.execute("id", { action: "get_status", callId: "call-1", })) as { details: { found?: boolean } }; expect(result.details.found).toBe(true); }); it("legacy tool status without sid returns error payload", async () => { const { tools } = setup({ provider: "mock" }); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise; }; const result = (await tool.execute("id", { mode: "status" })) as { details: { error?: unknown }; }; expect(String(result.details.error)).toContain("sid required"); }); it("CLI latency summarizes turn metrics from JSONL", async () => { const program = new Command(); const tmpFile = path.join(os.tmpdir(), `voicecall-latency-${Date.now()}.jsonl`); fs.writeFileSync( tmpFile, [ JSON.stringify({ metadata: { lastTurnLatencyMs: 100, lastTurnListenWaitMs: 70 } }), JSON.stringify({ metadata: { lastTurnLatencyMs: 200, lastTurnListenWaitMs: 110 } }), ].join("\n") + "\n", "utf8", ); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); try { await registerVoiceCallCli(program); await program.parseAsync(["voicecall", "latency", "--file", tmpFile, "--last", "10"], { from: "user", }); expect(logSpy).toHaveBeenCalled(); const printed = String(logSpy.mock.calls.at(-1)?.[0] ?? ""); expect(printed).toContain('"recordsScanned": 2'); expect(printed).toContain('"p50Ms": 100'); expect(printed).toContain('"p95Ms": 200'); } finally { logSpy.mockRestore(); fs.unlinkSync(tmpFile); } }); it("CLI start prints JSON", async () => { const program = new Command(); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); await registerVoiceCallCli(program); await program.parseAsync(["voicecall", "start", "--to", "+1", "--message", "Hello"], { from: "user", }); expect(logSpy).toHaveBeenCalled(); logSpy.mockRestore(); }); });