/** * Unit tests for caller identity resolution and pointer message regression * in call-domain.ts. * * Validates: * - Strict implicit-default policy for caller identity. * - Voice-ingress preflight blocks doomed outbound calls before Twilio dialing. * - Pointer messages are written on successful call start and on failure. */ import { beforeEach, describe, expect, mock, test } from "bun:test"; // Track whether the Twilio provider's initiateCall should succeed or throw let twilioInitiateCallBehavior: "success" | "error" = "success"; let twilioInitiateCallCount = 0; let twilioInitiateCallArgs: Array> = []; mock.module("../calls/twilio-config.js", () => ({ getTwilioConfig: (assistantId?: string) => ({ accountSid: "AC_test", authToken: "test_token", phoneNumber: assistantId === "ast-alpha" ? "+15550003333" : "+15550001111", }), })); mock.module("../calls/twilio-provider.js", () => ({ TwilioVoiceProvider: class { async checkCallerIdEligibility(number: string) { if (number === "+12025550102") { return { eligible: true }; } return { eligible: false, reason: `${number} is not eligible as a caller ID`, }; } async initiateCall(args: Record) { twilioInitiateCallCount++; twilioInitiateCallArgs.push(args); if (twilioInitiateCallBehavior === "error") { throw new Error("Twilio unavailable"); } return { callSid: "CA_test_123" }; } async endCall() { return; } }, })); mock.module("../security/secure-keys.js", () => ({ getSecureKeyAsync: async () => null, })); mock.module("../inbound/platform-callback-registration.js", () => ({ resolveCallbackUrl: async (fn: () => string) => fn(), })); mock.module("../inbound/public-ingress-urls.js", () => ({ getTwilioVoiceWebhookUrl: () => "https://test.example.com/webhooks/twilio/voice/test", getTwilioStatusCallbackUrl: () => "https://test.example.com/webhooks/twilio/status", })); mock.module("../persistence/conversation-title-service.js", () => ({ queueGenerateConversationTitle: () => {}, })); mock.module("../daemon/handlers/config-ingress.js", () => ({ computeGatewayTarget: () => "http://127.0.0.1:7830", handleIngressConfig: async () => {}, syncTwilioWebhooks: async () => ({ success: true }), })); // Credential readiness gate inside preflightVoiceIngress — ready by default. let mockCredentialReadiness: Record = { status: "ready" }; mock.module("../calls/telephony-credential-preflight.js", () => ({ resolveTelephonyCredentialReadiness: async () => mockCredentialReadiness, })); import { clearActiveCallLeases, getActiveCallLease, listActiveCallLeases, } from "../calls/active-call-lease.js"; import { cancelCall, resolveCallerIdentity, startCall, } from "../calls/call-domain.js"; import type { AssistantConfig } from "../config/types.js"; import { getMessages } from "../persistence/conversation-crud.js"; import { getDb } from "../persistence/db-connection.js"; import { initializeDb } from "../persistence/db-init.js"; import { conversations } from "../persistence/schema/index.js"; import { setConfig } from "./helpers/set-config.js"; /** Seed the ingress block startCall's preflight reads for real. */ function seedIngress(enabled: boolean): void { setConfig("ingress", { enabled, publicBaseUrl: "https://test.example.com", }); } // Disable memory so pointer-message writes skip background indexing. setConfig("memory", { enabled: false }); await initializeDb(); beforeEach(() => { resetTables(); clearActiveCallLeases(); twilioInitiateCallBehavior = "success"; twilioInitiateCallCount = 0; twilioInitiateCallArgs = []; seedIngress(true); mockCredentialReadiness = { status: "ready" }; }); let ensuredConvIds = new Set(); function ensureConversation(id: string): void { if (ensuredConvIds.has(id)) { return; } const db = getDb(); const now = Date.now(); db.insert(conversations) .values({ id, title: `Test conversation ${id}`, createdAt: now, updatedAt: now, }) .run(); ensuredConvIds.add(id); } function resetTables(): void { const db = getDb(); db.run("DELETE FROM external_conversation_bindings"); db.run("DELETE FROM call_sessions"); db.run("DELETE FROM messages"); db.run("DELETE FROM conversations"); ensuredConvIds = new Set(); } function getLatestAssistantText(conversationId: string): string | null { const msgs = getMessages(conversationId).filter( (m) => m.role === "assistant", ); if (msgs.length === 0) { return null; } const latest = msgs[msgs.length - 1]; return latest.content .filter( (b): b is { type: "text"; text: string } => b.type === "text" && typeof (b as { text?: unknown }).text === "string", ) .map((b) => b.text) .join(""); } function makeConfig( overrides: { allowPerCallOverride?: boolean; userNumber?: string; } = {}, ): AssistantConfig { return { calls: { callerIdentity: { allowPerCallOverride: overrides.allowPerCallOverride ?? true, userNumber: overrides.userNumber, }, }, } as unknown as AssistantConfig; } describe("resolveCallerIdentity — strict implicit-default policy", () => { test("implicit call defaults to assistant_number", async () => { const result = await resolveCallerIdentity(makeConfig()); expect(result.ok).toBe(true); if (result.ok) { expect(result.mode).toBe("assistant_number"); expect(result.fromNumber).toBe("+15550001111"); expect(result.source).toBe("implicit_default"); } }); test("implicit call uses assistant_number even when userNumber is configured", async () => { const result = await resolveCallerIdentity( makeConfig({ userNumber: "+12025550102" }), ); expect(result.ok).toBe(true); if (result.ok) { expect(result.mode).toBe("assistant_number"); expect(result.fromNumber).toBe("+15550001111"); expect(result.source).toBe("implicit_default"); } }); test("assistant_number resolves from twilio config phone number", async () => { const result = await resolveCallerIdentity(makeConfig()); expect(result.ok).toBe(true); if (result.ok) { expect(result.mode).toBe("assistant_number"); expect(result.fromNumber).toBe("+15550001111"); expect(result.source).toBe("implicit_default"); } }); test("explicit user_number succeeds when eligible", async () => { const result = await resolveCallerIdentity( makeConfig({ userNumber: "+12025550102" }), "user_number", ); expect(result.ok).toBe(true); if (result.ok) { expect(result.mode).toBe("user_number"); expect(result.fromNumber).toBe("+12025550102"); expect(result.source).toBe("user_config"); } }); test("explicit user_number fails when no user phone configured", async () => { const result = await resolveCallerIdentity(makeConfig(), "user_number"); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toContain("user_number"); expect(result.error).toContain("user phone number"); } }); test("explicit user_number fails when number is ineligible", async () => { const result = await resolveCallerIdentity( makeConfig({ userNumber: "+15559999999" }), "user_number", ); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toContain("not eligible"); } }); test("explicit override rejected when allowPerCallOverride=false", async () => { const result = await resolveCallerIdentity( makeConfig({ allowPerCallOverride: false, userNumber: "+12025550102" }), "user_number", ); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toContain("override is disabled"); } }); test("explicit assistant_number override succeeds when allowed", async () => { const result = await resolveCallerIdentity( makeConfig(), "assistant_number", ); expect(result.ok).toBe(true); if (result.ok) { expect(result.mode).toBe("assistant_number"); expect(result.source).toBe("per_call_override"); } }); test("invalid mode returns error", async () => { const result = await resolveCallerIdentity( makeConfig(), "custom_number" as "assistant_number", ); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toContain("Invalid callerIdentityMode"); } }); }); // ── Pointer message regression tests ────────────────────────────── describe("startCall — pointer message regression", () => { test("successful call writes a started pointer to the initiating conversation", async () => { const convId = "conv-domain-ptr-start"; ensureConversation(convId); const result = await startCall({ phoneNumber: "+15559876543", task: "Test call", conversationId: convId, }); expect(result.ok).toBe(true); expect(twilioInitiateCallCount).toBe(1); expect(twilioInitiateCallArgs).toEqual([ { from: "+15550001111", to: "+15559876543", webhookUrl: "https://test.example.com/webhooks/twilio/voice/test", statusCallbackUrl: "https://test.example.com/webhooks/twilio/status", }, ]); // Gateway reconcile triggers have been removed; the gateway reads // credentials and config via TTL caches. if (result.ok) { expect(getActiveCallLease(result.session.id)).toEqual({ callSessionId: result.session.id, providerCallSid: "CA_test_123", updatedAt: expect.any(Number), }); } // Allow async pointer write to flush await new Promise((r) => setTimeout(r, 50)); const text = getLatestAssistantText(convId); expect(text).not.toBeNull(); expect(text!).toContain("+15559876543"); expect(text!).toContain("started"); }); test("fails fast when ingress is disabled and never reaches Twilio dialing", async () => { const convId = "conv-domain-ingress-disabled"; ensureConversation(convId); seedIngress(false); const result = await startCall({ phoneNumber: "+15559876543", task: "Test call", conversationId: convId, }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.status).toBe(503); expect(result.error).toContain("Public ingress"); } expect(twilioInitiateCallCount).toBe(0); await new Promise((r) => setTimeout(r, 50)); const text = getLatestAssistantText(convId); expect(text).not.toBeNull(); expect(text!).toContain("+15559876543"); expect(text!).toContain("failed"); }); test("fails fast when telephony credentials are not ready and never reaches Twilio dialing", async () => { const convId = "conv-domain-creds-not-ready"; ensureConversation(convId); mockCredentialReadiness = { status: "not-ready", missing: [ { kind: "stt", providerId: "deepgram", reason: 'No API key configured for credential provider "deepgram"', }, ], userMessage: 'Phone calls are unavailable because they require an API key for the speech-to-text provider "deepgram".', }; const result = await startCall({ phoneNumber: "+15559876543", task: "Test call", conversationId: convId, }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.status).toBe(503); // The tool-facing error names the offending provider. expect(result.error).toContain('speech-to-text provider "deepgram"'); } // No Twilio call was placed and no call session was created. expect(twilioInitiateCallCount).toBe(0); expect(listActiveCallLeases()).toHaveLength(0); await new Promise((r) => setTimeout(r, 50)); const text = getLatestAssistantText(convId); expect(text).not.toBeNull(); expect(text!).toContain("+15559876543"); expect(text!).toContain("failed"); }); test("failed call writes a failed pointer to the initiating conversation", async () => { const convId = "conv-domain-ptr-fail"; ensureConversation(convId); twilioInitiateCallBehavior = "error"; const result = await startCall({ phoneNumber: "+15559876543", task: "Test call", conversationId: convId, }); expect(result.ok).toBe(false); expect(twilioInitiateCallCount).toBe(1); expect(listActiveCallLeases()).toHaveLength(0); // Allow async pointer write to flush await new Promise((r) => setTimeout(r, 50)); const text = getLatestAssistantText(convId); expect(text).not.toBeNull(); expect(text!).toContain("+15559876543"); expect(text!).toContain("failed"); }); test("canceling an active call releases its persisted keepalive lease", async () => { const convId = "conv-domain-cancel-releases-lease"; ensureConversation(convId); const startResult = await startCall({ phoneNumber: "+15559876543", task: "Test call", conversationId: convId, }); expect(startResult.ok).toBe(true); if (!startResult.ok) { return; } expect(getActiveCallLease(startResult.session.id)).not.toBeNull(); const cancelResult = await cancelCall({ callSessionId: startResult.session.id, reason: "User requested", }); expect(cancelResult.ok).toBe(true); expect(getActiveCallLease(startResult.session.id)).toBeNull(); }); });