feat: restore voice-call plugin parity

This commit is contained in:
Peter Steinberger
2026-01-12 21:40:22 +00:00
parent 3467b0ba07
commit 42c17adb5e
27 changed files with 6036 additions and 516 deletions

View File

@@ -1,6 +1,23 @@
import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
let runtimeStub: {
config: { toNumber?: string };
manager: {
initiateCall: ReturnType<typeof vi.fn>;
continueCall: ReturnType<typeof vi.fn>;
speak: ReturnType<typeof vi.fn>;
endCall: ReturnType<typeof vi.fn>;
getCall: ReturnType<typeof vi.fn>;
getCallByProviderCallId: ReturnType<typeof vi.fn>;
};
stop: ReturnType<typeof vi.fn>;
};
vi.mock("../../extensions/voice-call/src/runtime.js", () => ({
createVoiceCallRuntime: vi.fn(async () => runtimeStub),
}));
import plugin from "../../extensions/voice-call/index.js";
const noopLogger = {
@@ -37,129 +54,74 @@ function setup(config: Record<string, unknown>): Registered {
}
describe("voice-call plugin", () => {
beforeEach(() => vi.restoreAllMocks());
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("starts a log provider call and returns sid", async () => {
const { methods } = setup({ provider: "log" });
const handler = methods.get("voicecall.start");
const respond = vi.fn();
await handler({ params: { to: "+123", message: "Hi" }, respond });
expect(respond).toHaveBeenCalledTimes(1);
const [ok, payload] = respond.mock.calls[0];
expect(ok).toBe(true);
expect(payload.sid).toMatch(/^log-/);
expect(payload.status).toBe("queued");
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("fetches status via log provider", async () => {
const { methods } = setup({ provider: "log" });
it("initiates a call via voicecall.initiate", async () => {
const { methods } = setup({ provider: "mock" });
const handler = methods.get("voicecall.initiate");
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");
const respond = vi.fn();
await handler({ params: { sid: "log-1" }, respond });
await handler?.({ params: { callId: "call-1" }, respond });
const [ok, payload] = respond.mock.calls[0];
expect(ok).toBe(true);
expect(payload.status).toBe("mock");
expect(payload.found).toBe(true);
});
it("calls Twilio start endpoint", async () => {
const fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
sid: "CA123",
status: "queued",
to: "+1555",
from: "+1444",
}),
it("tool get_status returns json payload", async () => {
const { tools } = setup({ provider: "mock" });
const tool = tools[0] as { execute: (id: string, params: unknown) => any };
const result = await tool.execute("id", {
action: "get_status",
callId: "call-1",
});
// @ts-expect-error partial global
global.fetch = fetch;
const { methods } = setup({
provider: "twilio",
twilio: {
accountSid: "AC123",
authToken: "tok",
from: "+1444",
statusCallbackUrl: "https://callback.test/status",
},
});
const handler = methods.get("voicecall.start");
const respond = vi.fn();
await handler({ params: { to: "+1555", message: "Hello" }, respond });
expect(fetch).toHaveBeenCalledTimes(1);
const [ok, payload] = respond.mock.calls[0];
expect(ok).toBe(true);
expect(payload.sid).toBe("CA123");
expect(payload.provider).toBe("twilio");
expect(result.details.found).toBe(true);
});
it("fetches Twilio status", async () => {
const fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ status: "in-progress", to: "+1" }),
});
// @ts-expect-error partial global
global.fetch = fetch;
const { methods } = setup({
provider: "twilio",
twilio: { accountSid: "AC123", authToken: "tok", from: "+1444" },
});
const handler = methods.get("voicecall.status");
const respond = vi.fn();
await handler({ params: { sid: "CA123" }, respond });
expect(fetch).toHaveBeenCalled();
const [ok, payload] = respond.mock.calls[0];
expect(ok).toBe(true);
expect(payload.status).toBe("in-progress");
});
it("fails start when Twilio returns error", async () => {
const fetch = vi.fn().mockResolvedValue({
ok: false,
status: 401,
text: async () => "bad creds",
});
// @ts-expect-error partial global
global.fetch = fetch;
const { methods } = setup({
provider: "twilio",
twilio: { accountSid: "AC123", authToken: "tok", from: "+1444" },
});
const handler = methods.get("voicecall.start");
const respond = vi.fn();
await handler({ params: { to: "+1555" }, respond });
const [ok, payload] = respond.mock.calls[0];
expect(ok).toBe(false);
expect(String(payload.error)).toContain("twilio call failed");
});
it("requires twilio credentials in config schema", () => {
expect(() =>
setup({ provider: "twilio", twilio: { accountSid: "AC", from: "+1" } }),
).toThrow(/accountSid/);
});
it("tool status mode without sid throws", async () => {
const { tools } = setup({ provider: "log" });
const tool = tools[0];
await expect(
// @ts-expect-error minimal shape
tool.execute("id", { mode: "status" }),
).rejects.toThrow(/sid required/);
});
it("log provider status avoids fetch", async () => {
// @ts-expect-error ensure fetch undefined
delete global.fetch;
const { methods } = setup({ provider: "log" });
const handler = methods.get("voicecall.status");
const respond = vi.fn();
await handler({ params: { sid: "log-1" }, respond });
expect(respond).toHaveBeenCalledWith(true, {
sid: "log-1",
status: "mock",
provider: "log",
});
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) => any };
const result = await tool.execute("id", { mode: "status" });
expect(String(result.details.error)).toContain("sid required");
});
it("CLI start prints JSON", async () => {
@@ -175,7 +137,7 @@ describe("voice-call plugin", () => {
version: "0",
source: "test",
config: {},
pluginConfig: { provider: "log" },
pluginConfig: { provider: "mock" },
logger: noopLogger,
registerGatewayMethod: () => {},
registerTool: () => {},
@@ -197,9 +159,10 @@ describe("voice-call plugin", () => {
resolvePath: (p: string) => p,
});
await program.parseAsync(["voicecall", "start", "--to", "+1"], {
from: "user",
});
await program.parseAsync(
["voicecall", "start", "--to", "+1", "--message", "Hello"],
{ from: "user" },
);
expect(logSpy).toHaveBeenCalled();
logSpy.mockRestore();
});