* 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>
458 lines
16 KiB
TypeScript
458 lines
16 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
|
|
import {
|
|
AIPhoneServiceRegistry,
|
|
createAIPhoneServiceProvider,
|
|
createDefaultAIPhoneServiceProvider,
|
|
} from "./AIPhoneServiceRegistry";
|
|
import type {
|
|
AIPhoneServiceProvider,
|
|
AIPhoneServiceProviderFactory,
|
|
AIPhoneServiceProviderConfig,
|
|
} from "./interfaces/AIPhoneService.interface";
|
|
import { AIPhoneServiceProviderType } from "./interfaces/AIPhoneService.interface";
|
|
|
|
// Mock environment variables
|
|
vi.stubEnv("RETELL_AI_KEY", "test-api-key");
|
|
|
|
vi.mock("./providers/retellAI", () => ({
|
|
RetellAIPhoneServiceProviderFactory: vi.fn().mockImplementation(() => ({
|
|
create: vi.fn().mockReturnValue({
|
|
setupConfiguration: vi.fn(),
|
|
createPhoneCall: vi.fn(),
|
|
createPhoneNumber: vi.fn(),
|
|
}),
|
|
})),
|
|
}));
|
|
|
|
describe("AIPhoneServiceRegistry", () => {
|
|
let mockFactory: AIPhoneServiceProviderFactory;
|
|
let mockProvider: AIPhoneServiceProvider;
|
|
|
|
beforeEach(() => {
|
|
// Clear all factories before each test
|
|
AIPhoneServiceRegistry.clearProviders();
|
|
|
|
// Create mock factory and provider
|
|
mockProvider = {
|
|
setupConfiguration: vi.fn().mockResolvedValue({ llmId: "test-llm", agentId: "test-agent" }),
|
|
deleteConfiguration: vi
|
|
.fn()
|
|
.mockResolvedValue({ success: true, errors: [], deleted: { llm: true, agent: true } }),
|
|
updateModelConfiguration: vi.fn().mockResolvedValue({ llm_id: "test-llm" }),
|
|
getModelDetails: vi.fn().mockResolvedValue({ llm_id: "test-llm" }),
|
|
getAgent: vi.fn().mockResolvedValue({ agent_id: "test-agent" }),
|
|
updateAgent: vi.fn().mockResolvedValue({ agent_id: "test-agent" }),
|
|
createPhoneCall: vi.fn().mockResolvedValue({ call_id: "test-call" }),
|
|
createPhoneNumber: vi.fn().mockResolvedValue({ phone_number: "+1234567890" }),
|
|
deletePhoneNumber: vi.fn().mockResolvedValue(undefined),
|
|
getPhoneNumber: vi.fn().mockResolvedValue({ phone_number: "+1234567890" }),
|
|
updatePhoneNumber: vi.fn().mockResolvedValue({ phone_number: "+1234567890" }),
|
|
importPhoneNumber: vi.fn().mockResolvedValue({ phone_number: "+1234567890" }),
|
|
generatePhoneNumberCheckoutSession: vi
|
|
.fn()
|
|
.mockResolvedValue({ url: "test-url", message: "test-message" }),
|
|
cancelPhoneNumberSubscription: vi.fn().mockResolvedValue({ success: true, message: "test-message" }),
|
|
updatePhoneNumberWithAgents: vi.fn().mockResolvedValue({ message: "test-message" }),
|
|
listAgents: vi.fn().mockResolvedValue({ totalCount: 1, filtered: [] }),
|
|
getAgentWithDetails: vi.fn().mockResolvedValue({ agent_id: "test-agent" }),
|
|
createAgent: vi
|
|
.fn()
|
|
.mockResolvedValue({ id: "test-id", providerAgentId: "test-provider-id", message: "test-message" }),
|
|
updateAgentConfiguration: vi.fn().mockResolvedValue({ message: "test-message" }),
|
|
deleteAgent: vi.fn().mockResolvedValue({ message: "test-message" }),
|
|
createTestCall: vi
|
|
.fn()
|
|
.mockResolvedValue({ callId: "test-call-id", status: "test-status", message: "test-message" }),
|
|
};
|
|
|
|
mockFactory = {
|
|
create: vi.fn().mockReturnValue(mockProvider),
|
|
};
|
|
});
|
|
|
|
describe("initialize", () => {
|
|
it("should initialize with provided configuration", () => {
|
|
AIPhoneServiceRegistry.initialize({
|
|
defaultProvider: "test-provider",
|
|
providers: [
|
|
{ type: "test-provider", factory: mockFactory },
|
|
{ type: "another-provider", factory: mockFactory },
|
|
],
|
|
});
|
|
|
|
expect(AIPhoneServiceRegistry.isInitialized()).toBe(true);
|
|
expect(AIPhoneServiceRegistry.getDefaultProvider()).toBe("test-provider");
|
|
expect(AIPhoneServiceRegistry.getAvailableProviders()).toContain("test-provider");
|
|
expect(AIPhoneServiceRegistry.getAvailableProviders()).toContain("another-provider");
|
|
});
|
|
|
|
it("should set first provider as default if defaultProvider not specified", () => {
|
|
AIPhoneServiceRegistry.initialize({
|
|
providers: [
|
|
{ type: "first-provider", factory: mockFactory },
|
|
{ type: "second-provider", factory: mockFactory },
|
|
],
|
|
});
|
|
|
|
expect(AIPhoneServiceRegistry.getDefaultProvider()).toBe("first-provider");
|
|
});
|
|
|
|
it("should allow initialization without providers", () => {
|
|
AIPhoneServiceRegistry.initialize();
|
|
|
|
expect(AIPhoneServiceRegistry.isInitialized()).toBe(true);
|
|
expect(AIPhoneServiceRegistry.getDefaultProvider()).toBe(AIPhoneServiceProviderType.RETELL_AI);
|
|
});
|
|
});
|
|
|
|
describe("registerProvider", () => {
|
|
it("should register a provider factory", () => {
|
|
AIPhoneServiceRegistry.registerProvider("test-provider", mockFactory);
|
|
|
|
expect(AIPhoneServiceRegistry.isProviderRegistered("test-provider")).toBe(true);
|
|
expect(AIPhoneServiceRegistry.getAvailableProviders()).toContain("test-provider");
|
|
});
|
|
|
|
it("should keep default as retellAI even after registering provider", () => {
|
|
AIPhoneServiceRegistry.registerProvider("test-provider", mockFactory);
|
|
|
|
expect(AIPhoneServiceRegistry.getDefaultProvider()).toBe(AIPhoneServiceProviderType.RETELL_AI);
|
|
});
|
|
|
|
it("should allow overriding existing provider", () => {
|
|
const newMockFactory: AIPhoneServiceProviderFactory = {
|
|
create: vi.fn().mockReturnValue(mockProvider),
|
|
};
|
|
|
|
AIPhoneServiceRegistry.registerProvider("test-provider", mockFactory);
|
|
AIPhoneServiceRegistry.registerProvider("test-provider", newMockFactory);
|
|
|
|
const retrievedFactory = AIPhoneServiceRegistry.getProviderFactory("test-provider");
|
|
expect(retrievedFactory).toBe(newMockFactory);
|
|
});
|
|
});
|
|
|
|
describe("getProviderFactory", () => {
|
|
it("should return registered factory", () => {
|
|
AIPhoneServiceRegistry.registerProvider("test-provider", mockFactory);
|
|
|
|
const retrievedFactory = AIPhoneServiceRegistry.getProviderFactory("test-provider");
|
|
expect(retrievedFactory).toBe(mockFactory);
|
|
});
|
|
|
|
it("should return undefined for unregistered provider", () => {
|
|
const retrievedFactory = AIPhoneServiceRegistry.getProviderFactory("nonexistent");
|
|
expect(retrievedFactory).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("createProvider", () => {
|
|
it("should create provider instance using registered factory", () => {
|
|
const config: AIPhoneServiceProviderConfig = { apiKey: "test-key" };
|
|
AIPhoneServiceRegistry.registerProvider("test-provider", mockFactory);
|
|
|
|
const provider = AIPhoneServiceRegistry.createProvider("test-provider", config);
|
|
|
|
expect(mockFactory.create).toHaveBeenCalledWith(config);
|
|
expect(provider).toBe(mockProvider);
|
|
});
|
|
|
|
it("should throw error for unregistered provider", () => {
|
|
const config: AIPhoneServiceProviderConfig = { apiKey: "test-key" };
|
|
|
|
expect(() => {
|
|
AIPhoneServiceRegistry.createProvider("nonexistent", config);
|
|
}).toThrow("AI phone service provider 'nonexistent' not found. Available providers: ");
|
|
});
|
|
|
|
it("should include available providers in error message when factory not found", () => {
|
|
AIPhoneServiceRegistry.registerProvider("provider1", mockFactory);
|
|
AIPhoneServiceRegistry.registerProvider("provider2", mockFactory);
|
|
|
|
expect(() => {
|
|
AIPhoneServiceRegistry.createProvider("nonexistent", {});
|
|
}).toThrow("Available providers: provider1, provider2");
|
|
});
|
|
});
|
|
|
|
describe("createDefaultProvider", () => {
|
|
it("should create provider using default provider type", () => {
|
|
const config: AIPhoneServiceProviderConfig = { apiKey: "test-key" };
|
|
AIPhoneServiceRegistry.initialize({
|
|
defaultProvider: "test-provider",
|
|
providers: [{ type: "test-provider", factory: mockFactory }],
|
|
});
|
|
|
|
const provider = AIPhoneServiceRegistry.createDefaultProvider(config);
|
|
|
|
expect(mockFactory.create).toHaveBeenCalledWith(config);
|
|
expect(provider).toBe(mockProvider);
|
|
});
|
|
|
|
it("should throw error when default provider is not registered", () => {
|
|
const config: AIPhoneServiceProviderConfig = { apiKey: "test-key" };
|
|
|
|
expect(() => {
|
|
AIPhoneServiceRegistry.createDefaultProvider(config);
|
|
}).toThrow("AI phone service provider 'retellAI' not found");
|
|
});
|
|
});
|
|
|
|
describe("setDefaultProvider", () => {
|
|
it("should set default provider when provider is registered", () => {
|
|
AIPhoneServiceRegistry.registerProvider("new-default", mockFactory);
|
|
|
|
AIPhoneServiceRegistry.setDefaultProvider("new-default");
|
|
|
|
expect(AIPhoneServiceRegistry.getDefaultProvider()).toBe("new-default");
|
|
});
|
|
|
|
it("should throw error when setting unregistered provider as default", () => {
|
|
expect(() => {
|
|
AIPhoneServiceRegistry.setDefaultProvider("nonexistent");
|
|
}).toThrow("Cannot set default provider to 'nonexistent' - provider not registered");
|
|
});
|
|
});
|
|
|
|
describe("getDefaultProvider", () => {
|
|
it("should return current default provider", () => {
|
|
AIPhoneServiceRegistry.initialize({
|
|
defaultProvider: "test-provider",
|
|
providers: [{ type: "test-provider", factory: mockFactory }],
|
|
});
|
|
|
|
expect(AIPhoneServiceRegistry.getDefaultProvider()).toBe("test-provider");
|
|
});
|
|
|
|
it("should return retellAI as default provider", () => {
|
|
expect(AIPhoneServiceRegistry.getDefaultProvider()).toBe(AIPhoneServiceProviderType.RETELL_AI);
|
|
});
|
|
});
|
|
|
|
describe("getAvailableProviders", () => {
|
|
it("should return empty array when no providers registered", () => {
|
|
expect(AIPhoneServiceRegistry.getAvailableProviders()).toEqual([]);
|
|
});
|
|
|
|
it("should return array of registered provider names", () => {
|
|
AIPhoneServiceRegistry.registerProvider("provider1", mockFactory);
|
|
AIPhoneServiceRegistry.registerProvider("provider2", mockFactory);
|
|
|
|
const providers = AIPhoneServiceRegistry.getAvailableProviders();
|
|
expect(providers).toContain("provider1");
|
|
expect(providers).toContain("provider2");
|
|
expect(providers).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe("isProviderRegistered", () => {
|
|
it("should return true for registered provider", () => {
|
|
AIPhoneServiceRegistry.registerProvider("test-provider", mockFactory);
|
|
|
|
expect(AIPhoneServiceRegistry.isProviderRegistered("test-provider")).toBe(true);
|
|
});
|
|
|
|
it("should return false for unregistered provider", () => {
|
|
expect(AIPhoneServiceRegistry.isProviderRegistered("nonexistent")).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("createAIPhoneServiceProvider", () => {
|
|
let mockFactory: AIPhoneServiceProviderFactory;
|
|
let mockProvider: AIPhoneServiceProvider;
|
|
|
|
beforeEach(() => {
|
|
// Clear all factories before each test
|
|
AIPhoneServiceRegistry.clearProviders();
|
|
|
|
mockProvider = {
|
|
setupConfiguration: vi.fn(),
|
|
deleteConfiguration: vi.fn(),
|
|
updateModelConfiguration: vi.fn(),
|
|
getModelDetails: vi.fn(),
|
|
getAgent: vi.fn(),
|
|
updateAgent: vi.fn(),
|
|
createPhoneCall: vi.fn(),
|
|
createPhoneNumber: vi.fn(),
|
|
deletePhoneNumber: vi.fn(),
|
|
getPhoneNumber: vi.fn(),
|
|
updatePhoneNumber: vi.fn(),
|
|
importPhoneNumber: vi.fn(),
|
|
generatePhoneNumberCheckoutSession: vi.fn(),
|
|
cancelPhoneNumberSubscription: vi.fn(),
|
|
updatePhoneNumberWithAgents: vi.fn(),
|
|
listAgents: vi.fn(),
|
|
getAgentWithDetails: vi.fn(),
|
|
createAgent: vi.fn(),
|
|
updateAgentConfiguration: vi.fn(),
|
|
deleteAgent: vi.fn(),
|
|
createTestCall: vi.fn(),
|
|
};
|
|
|
|
mockFactory = {
|
|
create: vi.fn().mockReturnValue(mockProvider),
|
|
};
|
|
});
|
|
|
|
it("should automatically initialize registry when not initialized", () => {
|
|
// Registry starts uninitialized
|
|
expect(AIPhoneServiceRegistry.isInitialized()).toBe(false);
|
|
|
|
// Should automatically initialize the registry and create provider
|
|
expect(() => {
|
|
createAIPhoneServiceProvider({
|
|
config: { apiKey: "test-key" },
|
|
});
|
|
}).not.toThrow();
|
|
|
|
// Registry should now be initialized
|
|
expect(AIPhoneServiceRegistry.isInitialized()).toBe(true);
|
|
});
|
|
|
|
it("should create provider with specified type and config", () => {
|
|
AIPhoneServiceRegistry.initialize({
|
|
providers: [{ type: "custom-provider", factory: mockFactory }],
|
|
});
|
|
|
|
// For custom providers, we must provide API key in config since there's no env var mapping
|
|
const provider = createAIPhoneServiceProvider({
|
|
providerType: "custom-provider",
|
|
config: { apiKey: "custom-key", enableLogging: false },
|
|
});
|
|
|
|
expect(mockFactory.create).toHaveBeenCalledWith({
|
|
apiKey: "custom-key",
|
|
enableLogging: false,
|
|
});
|
|
expect(provider).toBe(mockProvider);
|
|
});
|
|
|
|
it("should use default provider when type not specified", () => {
|
|
AIPhoneServiceRegistry.initialize({
|
|
defaultProvider: AIPhoneServiceProviderType.RETELL_AI, // Use RETELL_AI as default since we have its API key
|
|
providers: [{ type: AIPhoneServiceProviderType.RETELL_AI, factory: mockFactory }],
|
|
});
|
|
|
|
const provider = createAIPhoneServiceProvider();
|
|
|
|
expect(mockFactory.create).toHaveBeenCalledWith({
|
|
apiKey: "test-api-key",
|
|
enableLogging: true, // Should be true when NODE_ENV is not production
|
|
});
|
|
expect(provider).toBe(mockProvider);
|
|
});
|
|
|
|
it("should merge provided config with defaults", () => {
|
|
AIPhoneServiceRegistry.initialize({
|
|
providers: [{ type: AIPhoneServiceProviderType.RETELL_AI, factory: mockFactory }],
|
|
});
|
|
|
|
createAIPhoneServiceProvider({
|
|
providerType: AIPhoneServiceProviderType.RETELL_AI,
|
|
config: { enableLogging: false },
|
|
});
|
|
|
|
expect(mockFactory.create).toHaveBeenCalledWith({
|
|
apiKey: "test-api-key",
|
|
enableLogging: false,
|
|
});
|
|
});
|
|
|
|
it("should override default apiKey with provided config", () => {
|
|
AIPhoneServiceRegistry.initialize({
|
|
providers: [{ type: AIPhoneServiceProviderType.RETELL_AI, factory: mockFactory }],
|
|
});
|
|
|
|
createAIPhoneServiceProvider({
|
|
providerType: AIPhoneServiceProviderType.RETELL_AI,
|
|
config: { apiKey: "override-key" },
|
|
});
|
|
|
|
expect(mockFactory.create).toHaveBeenCalledWith({
|
|
apiKey: "override-key",
|
|
enableLogging: true,
|
|
});
|
|
});
|
|
|
|
it("should throw error when default provider is not registered", () => {
|
|
AIPhoneServiceRegistry.initialize(); // Initialize without providers
|
|
|
|
expect(() => {
|
|
createAIPhoneServiceProvider();
|
|
}).toThrow("AI phone service provider 'retellAI' not found");
|
|
});
|
|
});
|
|
|
|
describe("createDefaultAIPhoneServiceProvider", () => {
|
|
let mockFactory: AIPhoneServiceProviderFactory;
|
|
let mockProvider: AIPhoneServiceProvider;
|
|
|
|
beforeEach(() => {
|
|
AIPhoneServiceRegistry.clearProviders();
|
|
|
|
mockProvider = {
|
|
setupConfiguration: vi.fn(),
|
|
deleteConfiguration: vi.fn(),
|
|
updateModelConfiguration: vi.fn(),
|
|
getModelDetails: vi.fn(),
|
|
getAgent: vi.fn(),
|
|
updateAgent: vi.fn(),
|
|
createPhoneCall: vi.fn(),
|
|
createPhoneNumber: vi.fn(),
|
|
deletePhoneNumber: vi.fn(),
|
|
getPhoneNumber: vi.fn(),
|
|
updatePhoneNumber: vi.fn(),
|
|
importPhoneNumber: vi.fn(),
|
|
generatePhoneNumberCheckoutSession: vi.fn(),
|
|
cancelPhoneNumberSubscription: vi.fn(),
|
|
updatePhoneNumberWithAgents: vi.fn(),
|
|
listAgents: vi.fn(),
|
|
getAgentWithDetails: vi.fn(),
|
|
createAgent: vi.fn(),
|
|
updateAgentConfiguration: vi.fn(),
|
|
deleteAgent: vi.fn(),
|
|
createTestCall: vi.fn(),
|
|
};
|
|
|
|
mockFactory = {
|
|
create: vi.fn().mockReturnValue(mockProvider),
|
|
};
|
|
|
|
AIPhoneServiceRegistry.initialize({
|
|
defaultProvider: AIPhoneServiceProviderType.RETELL_AI,
|
|
providers: [{ type: AIPhoneServiceProviderType.RETELL_AI, factory: mockFactory }],
|
|
});
|
|
});
|
|
|
|
it("should create provider using default configuration", () => {
|
|
const provider = createDefaultAIPhoneServiceProvider();
|
|
|
|
expect(mockFactory.create).toHaveBeenCalledWith({
|
|
apiKey: "test-api-key",
|
|
enableLogging: true,
|
|
});
|
|
expect(provider).toBe(mockProvider);
|
|
});
|
|
|
|
it("should merge provided config with defaults", () => {
|
|
const customConfig = { enableLogging: false, apiKey: "custom-key" };
|
|
const provider = createDefaultAIPhoneServiceProvider(customConfig);
|
|
|
|
expect(mockFactory.create).toHaveBeenCalledWith({
|
|
apiKey: "custom-key",
|
|
enableLogging: false,
|
|
});
|
|
expect(provider).toBe(mockProvider);
|
|
});
|
|
|
|
it("should use default provider type internally", () => {
|
|
// This test ensures createDefaultAIPhoneServiceProvider calls createAIPhoneServiceProvider correctly
|
|
const provider = createDefaultAIPhoneServiceProvider();
|
|
|
|
expect(provider).toBe(mockProvider);
|
|
expect(mockFactory.create).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|