feat: restore voice-call plugin parity
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user