Files
calendar/packages/features/calAIPhone/providers/retellAI/RetellSDKClient.test.ts
T
d4bff9d6b1 feat: Cal.ai Self Serve #2 (#22995)
* feat: Cal.ai Self Serve #2

* chore: fix import and remove logs

* fix: update checkout session

* fix: type errors and test

* fix: imports

* fix: type err

* fix: type error

* fix: tests

* chore: save progress

* fix: workflow flow

* fix: workflow update bug

* tests: add unit tests for retell ai webhoo

* fix: status code

* fix: test and delete bug

* fix: add dynamic variables

* fix: type err

* chore: update unit test

* fix: type error

* chore: update default prompt

* fix: type errors

* fix: workflow permissions

* fix: workflow page

* fix: translations

* feat: add call duration

* chore: add booking uid

* fix: button positioning

* chore: update tests

* chore: improvements

* chore: some more improvements

* refactor: improvements

* refactor: code feedback

* refactor: improvements

* feat: enable credits for orgs (#23077)

* Show credits UI for orgs

* fix stripe callback url when buying credits

* give orgs 20% credits

* add test for calulating credits

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* fix: types

* fix: types

* chore: error

* fix: type error

* fix: type error

* chore: mock env

* feat: add idempotency key to prevent double charging

* chore: add userId and teamId

* fix: skip inbound calls

* chore: update tests

* feat: add feature flag for voice agent

* feat: finish test call and other improvements

* chore: add alert

* chore: update .env.example

* chore: improvements

* fix: update tests

* refactor: remove un necessary

* feat: add setup badge

* chore: improvements

* fix: use referene id

* chore: improvements

* fix: type error

* fix: type

* refactor: change pricing logic

* refactor: update tests

* fix: conflicts

* fix: billing link for orgs

* fix: types

* refactor: move feature flag up

* fix: alert and test call credit check

* fix: update unit tests

* fix: feedback

* refactor: improvements

* refactor: move handlers to separate files

* fix: types

* fix: missing import

* fix: type

* refactor: change general tools functions handling

* refactor: use repository

* refactor: improvements

* fix: types

* fix: type errorr

* fix: auth check

* feat: add creditFor

* fix: update defualt prompt

* fix: throw error on frontend

* fix: update unit tests

* fix: use deleteAllWorkflowReminders

* refactor: add connect phone number

* refactor: improvements

* chore: translation

* chore: update message

* chore: translation

* design improvements buy number dialog

* add translation for error message

* use translation key in error message

* refactor: improve connect phone number tab

* feat: support un saved workflow to tests

* chore: remove un used

* fix: remove un used

* fix: remove un used

* refactor: similify billing

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-08-29 05:04:05 +01:00

440 lines
13 KiB
TypeScript

import { Retell } from "retell-sdk";
import { describe, it, expect, beforeEach, vi, beforeAll } from "vitest";
import logger from "@calcom/lib/logger";
import type {
CreateLLMRequest,
UpdateLLMRequest,
CreateAgentRequest,
UpdateAgentRequest,
CreatePhoneNumberParams,
ImportPhoneNumberParams,
RetellDynamicVariables,
} from "./types";
let RetellSDKClient: typeof import("./RetellSDKClient").RetellSDKClient;
vi.mock("retell-sdk", () => ({
Retell: vi.fn().mockImplementation(() => ({
llm: {
create: vi.fn(),
retrieve: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
},
agent: {
create: vi.fn(),
retrieve: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
},
phoneNumber: {
create: vi.fn(),
import: vi.fn(),
delete: vi.fn(),
retrieve: vi.fn(),
update: vi.fn(),
},
call: {
createPhoneCall: vi.fn(),
},
})),
}));
vi.mock("@calcom/lib/logger", () => ({
default: {
getSubLogger: vi.fn().mockReturnValue({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}),
},
}));
const TEST_API_KEY = "test-retell-api-key";
describe("RetellSDKClient", () => {
beforeAll(() => {
const originalEnv = process.env.RETELL_AI_KEY;
vi.stubEnv("RETELL_AI_KEY", TEST_API_KEY);
return () => {
if (originalEnv !== undefined) {
vi.stubEnv("RETELL_AI_KEY", originalEnv);
} else {
vi.unstubAllEnvs();
}
};
});
let client: RetellSDKClient;
let mockRetellInstance: any;
let mockLogger: any;
beforeEach(() => {
vi.clearAllMocks();
mockLogger = {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
};
mockRetellInstance = {
llm: {
create: vi.fn(),
retrieve: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
},
agent: {
create: vi.fn(),
retrieve: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
},
phoneNumber: {
create: vi.fn(),
import: vi.fn(),
delete: vi.fn(),
retrieve: vi.fn(),
update: vi.fn(),
},
call: {
createPhoneCall: vi.fn(),
},
};
(Retell as any).mockImplementation(() => mockRetellInstance);
});
describe("constructor", () => {
beforeAll(async () => {
// Reset modules and reimport with mocked env var
vi.resetModules();
const moduleImport = await import("./RetellSDKClient");
RetellSDKClient = moduleImport.RetellSDKClient;
});
it("should create client with default logger when no custom logger provided", () => {
client = new RetellSDKClient();
expect(logger.getSubLogger).toHaveBeenCalledWith({ prefix: ["retellSDKClient:"] });
expect(Retell).toHaveBeenCalledWith({ apiKey: TEST_API_KEY });
});
it("should create client with custom logger", () => {
client = new RetellSDKClient(mockLogger);
expect(logger.getSubLogger).not.toHaveBeenCalled();
expect(Retell).toHaveBeenCalledWith({ apiKey: TEST_API_KEY });
});
it("should throw error when RETELL_AI_KEY is not configured", async () => {
vi.unstubAllEnvs();
vi.stubEnv("RETELL_AI_KEY", "");
vi.resetModules();
const { RetellSDKClient: TestRetellSDKClient } = await import("./RetellSDKClient");
expect(() => {
new TestRetellSDKClient();
}).toThrow("RETELL_API_KEY is not configured");
// Restore the env var
vi.stubEnv("RETELL_AI_KEY", TEST_API_KEY);
});
});
describe("LLM operations", () => {
beforeEach(() => {
client = new RetellSDKClient(mockLogger);
});
describe("createLLM", () => {
it("should create and return LLM", async () => {
const mockRequest: CreateLLMRequest = {
general_prompt: "Test prompt",
};
const mockResponse = { llm_id: "test-llm-id" };
mockRetellInstance.llm.create.mockResolvedValue(mockResponse);
const result = await client.createLLM(mockRequest);
expect(mockRetellInstance.llm.create).toHaveBeenCalledWith(mockRequest);
expect(result).toEqual(mockResponse);
});
});
describe("getLLM", () => {
it("should get LLM", async () => {
const llmId = "test-llm-id";
const mockResponse = { llm_id: llmId, general_prompt: "Test prompt" };
mockRetellInstance.llm.retrieve.mockResolvedValue(mockResponse);
const result = await client.getLLM(llmId);
expect(mockRetellInstance.llm.retrieve).toHaveBeenCalledWith(llmId);
expect(result).toEqual(mockResponse);
});
});
describe("updateLLM", () => {
it("should update LLM", async () => {
const llmId = "test-llm-id";
const updateData: UpdateLLMRequest = {
general_prompt: "Updated prompt",
};
const mockResponse = { llm_id: llmId };
mockRetellInstance.llm.update.mockResolvedValue(mockResponse);
const result = await client.updateLLM(llmId, updateData);
expect(mockRetellInstance.llm.update).toHaveBeenCalledWith(llmId, updateData);
expect(result).toEqual(mockResponse);
});
});
describe("deleteLLM", () => {
it("should delete LLM", async () => {
const llmId = "test-llm-id";
mockRetellInstance.llm.delete.mockResolvedValue(undefined);
await client.deleteLLM(llmId);
expect(mockRetellInstance.llm.delete).toHaveBeenCalledWith(llmId);
});
});
});
describe("Agent operations", () => {
beforeEach(() => {
client = new RetellSDKClient(mockLogger);
});
describe("createAgent", () => {
it("should create agent", async () => {
const mockRequest: CreateAgentRequest = {
agent_name: "Test Agent",
response_engine: { type: "retell-llm", llm_id: "test-llm-id" },
voice_id: "test-voice-id",
};
const mockResponse = { agent_id: "test-agent-id", agent_name: "Test Agent" };
mockRetellInstance.agent.create.mockResolvedValue(mockResponse);
const result = await client.createAgent(mockRequest);
expect(mockRetellInstance.agent.create).toHaveBeenCalledWith(mockRequest);
expect(result).toEqual(mockResponse);
});
});
describe("getAgent", () => {
it("should get agent", async () => {
const agentId = "test-agent-id";
const mockResponse = { agent_id: agentId, agent_name: "Test Agent" };
mockRetellInstance.agent.retrieve.mockResolvedValue(mockResponse);
const result = await client.getAgent(agentId);
expect(mockRetellInstance.agent.retrieve).toHaveBeenCalledWith(agentId);
expect(result).toEqual(mockResponse);
});
});
describe("updateAgent", () => {
it("should update agent", async () => {
const agentId = "test-agent-id";
const updateData: UpdateAgentRequest = {
agent_name: "Updated Agent",
};
const mockResponse = { agent_id: agentId, agent_name: "Updated Agent" };
mockRetellInstance.agent.update.mockResolvedValue(mockResponse);
const result = await client.updateAgent(agentId, updateData);
expect(mockRetellInstance.agent.update).toHaveBeenCalledWith(agentId, updateData);
expect(result).toEqual(mockResponse);
});
});
describe("deleteAgent", () => {
it("should delete agent", async () => {
const agentId = "test-agent-id";
mockRetellInstance.agent.delete.mockResolvedValue(undefined);
await client.deleteAgent(agentId);
expect(mockRetellInstance.agent.delete).toHaveBeenCalledWith(agentId);
});
});
});
describe("Phone number operations", () => {
beforeEach(() => {
client = new RetellSDKClient(mockLogger);
});
describe("createPhoneNumber", () => {
it("should create phone number", async () => {
const phoneData: CreatePhoneNumberParams = {
area_code: 415,
nickname: "Test Phone",
};
const mockResponse = { phone_number: "+14155551234" };
mockRetellInstance.phoneNumber.create.mockResolvedValue(mockResponse);
const result = await client.createPhoneNumber(phoneData);
expect(mockRetellInstance.phoneNumber.create).toHaveBeenCalledWith(phoneData);
expect(result).toEqual(mockResponse);
});
});
describe("importPhoneNumber", () => {
it("should import phone number", async () => {
const importData: ImportPhoneNumberParams = {
phone_number: "+14155551234",
termination_uri: "https://example.com/webhook",
sip_trunk_auth_username: "username",
sip_trunk_auth_password: "password",
};
const mockResponse = { phone_number: "+14155551234" };
mockRetellInstance.phoneNumber.import.mockResolvedValue(mockResponse);
const result = await client.importPhoneNumber(importData);
expect(mockRetellInstance.phoneNumber.import).toHaveBeenCalledWith(importData);
expect(result).toEqual(mockResponse);
});
});
describe("deletePhoneNumber", () => {
it("should delete phone number", async () => {
const phoneNumber = "+14155551234";
mockRetellInstance.phoneNumber.delete.mockResolvedValue(undefined);
await client.deletePhoneNumber(phoneNumber);
expect(mockRetellInstance.phoneNumber.delete).toHaveBeenCalledWith(phoneNumber);
});
});
describe("getPhoneNumber", () => {
it("should get phone number", async () => {
const phoneNumber = "+14155551234";
const mockResponse = { phone_number: phoneNumber };
mockRetellInstance.phoneNumber.retrieve.mockResolvedValue(mockResponse);
const result = await client.getPhoneNumber(phoneNumber);
expect(mockRetellInstance.phoneNumber.retrieve).toHaveBeenCalledWith(phoneNumber);
expect(result).toEqual(mockResponse);
});
});
describe("updatePhoneNumber", () => {
it("should update phone number", async () => {
const phoneNumber = "+14155551234";
const updateData = {
inbound_agent_id: "new-inbound",
};
const mockResponse = { phone_number: phoneNumber };
mockRetellInstance.phoneNumber.update.mockResolvedValue(mockResponse);
const result = await client.updatePhoneNumber(phoneNumber, updateData);
expect(mockRetellInstance.phoneNumber.update).toHaveBeenCalledWith(phoneNumber, updateData);
expect(result).toEqual(mockResponse);
});
});
});
describe("Call operations", () => {
beforeEach(() => {
client = new RetellSDKClient(mockLogger);
});
describe("createPhoneCall", () => {
it("should create phone call", async () => {
const callData = {
fromNumber: "+14155551234",
toNumber: "+14155555678",
dynamicVariables: {
name: "John Doe",
email: "john@example.com",
} as RetellDynamicVariables,
};
const mockResponse = { call_id: "test-call-id" };
mockRetellInstance.call.createPhoneCall.mockResolvedValue(mockResponse);
const result = await client.createPhoneCall(callData);
expect(mockRetellInstance.call.createPhoneCall).toHaveBeenCalledWith({
from_number: "+14155551234",
to_number: "+14155555678",
retell_llm_dynamic_variables: {
name: "John Doe",
email: "john@example.com",
},
});
expect(result).toEqual(mockResponse);
});
it("should handle undefined dynamic variables", async () => {
const callData = {
fromNumber: "+14155551234",
toNumber: "+14155555678",
dynamicVariables: undefined,
};
const mockResponse = { call_id: "test-call-id" };
mockRetellInstance.call.createPhoneCall.mockResolvedValue(mockResponse);
const result = await client.createPhoneCall(callData);
expect(mockRetellInstance.call.createPhoneCall).toHaveBeenCalledWith({
from_number: "+14155551234",
to_number: "+14155555678",
retell_llm_dynamic_variables: undefined,
});
expect(result).toEqual(mockResponse);
});
});
});
describe("Edge cases", () => {
beforeEach(() => {
client = new RetellSDKClient(mockLogger);
});
it("should handle special characters in phone numbers", async () => {
const phoneNumber = "+1 (415) 555-1234";
const mockResponse = { phone_number: phoneNumber };
mockRetellInstance.phoneNumber.retrieve.mockResolvedValue(mockResponse);
const result = await client.getPhoneNumber(phoneNumber);
expect(mockRetellInstance.phoneNumber.retrieve).toHaveBeenCalledWith(phoneNumber);
expect(result).toEqual(mockResponse);
});
});
});