Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions extensions/voice-call/src/manager/events.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, expect, it } from "vitest";

import { processEvent } from "./events.js";
import type { CallManagerContext } from "./context.js";
import { VoiceCallConfigSchema, type VoiceCallConfig } from "../config.js";
import type {
NormalizedEvent,
HangupCallInput,
PlayTtsInput,
InitiateCallInput,
InitiateCallResult,
StartListeningInput,
StopListeningInput,
WebhookContext,
WebhookVerificationResult,
ProviderWebhookParseResult
} from "../types.js";
import type { VoiceCallProvider } from "../providers/base.js";

class FakeProvider implements VoiceCallProvider {
readonly name = "plivo" as const;
readonly playTtsCalls: PlayTtsInput[] = [];
readonly hangupCalls: HangupCallInput[] = [];

verifyWebhook(_ctx: WebhookContext): WebhookVerificationResult { return { ok: true }; }
parseWebhookEvent(_ctx: WebhookContext): ProviderWebhookParseResult { return { events: [], statusCode: 200 }; }
async initiateCall(_input: InitiateCallInput): Promise<InitiateCallResult> { return { providerCallId: "request-uuid", status: "initiated" }; }
async hangupCall(input: HangupCallInput): Promise<void> { this.hangupCalls.push(input); }
async playTts(input: PlayTtsInput): Promise<void> { this.playTtsCalls.push(input); }
async startListening(_input: StartListeningInput): Promise<void> {}
async stopListening(_input: StopListeningInput): Promise<void> {}
}

function createMockContext(config: VoiceCallConfig): CallManagerContext {
return {
activeCalls: new Map(),
providerCallIdMap: new Map(),
processedEventIds: new Set(),
provider: new FakeProvider(),
config,
storePath: "/tmp/store", // Dummy path
webhookUrl: "https://example.com/webhook",
transcriptWaiters: new Map(),
maxDurationTimers: new Map(),
};
}

describe("processEvent", () => {
it("hangs up inbound call if not in allowlist", async () => {
const config = VoiceCallConfigSchema.parse({
enabled: true,
inboundPolicy: "allowlist",
allowFrom: ["+15551234567"],
provider: "plivo", // to match FakeProvider name check if any
// schema requires some fields
fromNumber: "+15550000000",
});

const ctx = createMockContext(config);
const provider = ctx.provider as FakeProvider;

const event: NormalizedEvent = {
id: "evt-rejected",
type: "call.initiated",
callId: "call-uuid-external",
providerCallId: "provider-call-uuid-rejected",
timestamp: Date.now(),
direction: "inbound",
from: "+15559999999", // Not allowed
to: "+15550000000",
};

processEvent(ctx, event);

// Wait for async hangup
await new Promise((resolve) => setTimeout(resolve, 0));

expect(provider.hangupCalls).toHaveLength(1);
expect(provider.hangupCalls[0]).toMatchObject({
providerCallId: "provider-call-uuid-rejected",
reason: "hangup-bot",
});

// Check call was not created
expect(ctx.activeCalls.size).toBe(0);
});
});
19 changes: 18 additions & 1 deletion extensions/voice-call/src/manager/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,24 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v

if (!call && event.direction === "inbound" && event.providerCallId) {
if (!shouldAcceptInbound(ctx.config, event.from)) {
// TODO: Could hang up the call here.
console.log(
`[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`,
);
if (ctx.provider) {
ctx.provider
.hangupCall({
callId: "rejected-call",
providerCallId: event.providerCallId,
reason: "hangup-bot",
})
.catch((err) => {
console.error(
`[voice-call] Failed to hang up rejected call: ${
err instanceof Error ? err.message : String(err)
}`,
);
});
}
return;
}

Expand Down
12 changes: 12 additions & 0 deletions extensions/voice-call/src/manager/events.ts_cleanup
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<<<<<<< SEARCH
console.log(
`[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`,
);
console.log("DEBUG: ctx.provider is:", ctx.provider ? "set" : "null");
if (ctx.provider) {
=======
console.log(
`[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`,
);
if (ctx.provider) {
>>>>>>> REPLACE
18 changes: 18 additions & 0 deletions extensions/voice-call/src/manager/events.ts_debug
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<<<<<<< SEARCH
if (!call && event.direction === "inbound" && event.providerCallId) {
if (!shouldAcceptInbound(ctx.config, event.from)) {
console.log(
`[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`,
);
if (ctx.provider) {
ctx.provider
=======
if (!call && event.direction === "inbound" && event.providerCallId) {
if (!shouldAcceptInbound(ctx.config, event.from)) {
console.log(
`[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`,
);
console.log("DEBUG: ctx.provider is:", ctx.provider ? "set" : "null");
if (ctx.provider) {
ctx.provider
>>>>>>> REPLACE
28 changes: 28 additions & 0 deletions extensions/voice-call/src/manager/events.ts_patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<<<<<<< SEARCH
if (!shouldAcceptInbound(ctx.config, event.from)) {
// TODO: Could hang up the call here.
return;
}
=======
if (!shouldAcceptInbound(ctx.config, event.from)) {
console.log(
`[voice-call] Rejecting inbound call from ${event.from} (providerId: ${event.providerCallId})`,
);
if (ctx.provider) {
ctx.provider
.hangupCall({
callId: "rejected-call",
providerCallId: event.providerCallId,
reason: "hangup-bot",
})
.catch((err) => {
console.error(
`[voice-call] Failed to hang up rejected call: ${
err instanceof Error ? err.message : String(err)
}`,
);
});
}
return;
}
>>>>>>> REPLACE