import { describe, expect, it, vi, beforeEach } from "vitest"; import { appRouter } from "./routers"; import type { TrpcContext } from "./_core/context"; type AuthenticatedUser = NonNullable; function createPublicContext(): TrpcContext { return { user: null, req: { protocol: "https", headers: {} } as TrpcContext["req"], res: { clearCookie: vi.fn() } as unknown as TrpcContext["res"], }; } function createAuthContext(): TrpcContext { const user: AuthenticatedUser = { id: 1, openId: "agent-user", email: "agent@homelegance.com", name: "Test Agent", loginMethod: "manus", role: "admin", createdAt: new Date(), updatedAt: new Date(), lastSignedIn: new Date(), }; return { user, req: { protocol: "https", headers: {} } as TrpcContext["req"], res: { clearCookie: vi.fn() } as unknown as TrpcContext["res"], }; } // Mock the database module vi.mock("./db", () => { const conversations = new Map(); const messageStore = new Map(); let convIdCounter = 1; let msgIdCounter = 1; return { createConversation: vi.fn(async (data: any) => { const id = convIdCounter++; const conv = { id, ...data, createdAt: new Date(), updatedAt: new Date() }; conversations.set(data.sessionId, conv); messageStore.set(id, []); return conv; }), getConversations: vi.fn(async (status?: string) => { const all = Array.from(conversations.values()); if (status) return all.filter((c) => c.status === status); return all; }), getConversationById: vi.fn(async (id: number) => { return Array.from(conversations.values()).find((c) => c.id === id); }), getConversationBySessionId: vi.fn(async (sessionId: string) => { return conversations.get(sessionId); }), updateConversationStatus: vi.fn(async (id: number, status: string, agentId?: number) => { const conv = Array.from(conversations.values()).find((c) => c.id === id); if (conv) { conv.status = status; if (agentId) conv.assignedAgentId = agentId; conversations.set(conv.sessionId, conv); } return conv; }), getConversationStats: vi.fn(async () => ({ total: conversations.size, active: Array.from(conversations.values()).filter((c) => c.status === "active").length, escalated: Array.from(conversations.values()).filter((c) => c.status === "escalated").length, resolved: 0, closed: 0, })), addMessage: vi.fn(async (data: any) => { const id = msgIdCounter++; const msg = { id, ...data, createdAt: new Date() }; const msgs = messageStore.get(data.conversationId) || []; msgs.push(msg); messageStore.set(data.conversationId, msgs); return msg; }), getMessagesByConversation: vi.fn(async (conversationId: number) => { return messageStore.get(conversationId) || []; }), saveWorkflow: vi.fn(async (workflowId: string, nodes: any[], edges: any[]) => ({ workflowId, nodeCount: nodes.length, edgeCount: edges.length, })), getWorkflow: vi.fn(async () => ({ nodes: [], edges: [] })), getWorkflowSuggestions: vi.fn(async () => []), updateWorkflowSuggestionStatus: vi.fn(async (id: number, status: string, reviewedById: number) => ({ id, status })), bulkCreateWorkflowSuggestions: vi.fn(async (suggestions: any[]) => ({ created: suggestions.length })), getDb: vi.fn(async () => ({ select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), limit: vi.fn(async () => [ { content: "What is the shipping time?", sender: "visitor" }, { content: "How do I check my order status?", sender: "visitor" }, { content: "Do you have modern bedroom sets?", sender: "visitor" }, ]), })), upsertUser: vi.fn(), getUserByOpenId: vi.fn(), // Analytics trackAnalyticsEvent: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date() })), getAnalyticsEvents: vi.fn(async () => []), getAnalyticsSummary: vi.fn(async () => ({ totalConversations: 150, resolvedByBot: 95, escalatedToAgent: 40, abandoned: 15, avgResponseTime: 1.8, resolutionRate: 63.3, topIntents: [ { intent: "order_status", count: 45 }, { intent: "shipping", count: 35 }, { intent: "returns", count: 25 }, ], })), // Data Sources getDataSources: vi.fn(async () => []), createDataSource: vi.fn(async (data: any) => ({ id: 1, ...data, status: "active", createdAt: new Date(), updatedAt: new Date() })), updateDataSource: vi.fn(async (id: number, data: any) => ({ id, ...data })), deleteDataSource: vi.fn(async () => true), // API Connections getApiConnections: vi.fn(async () => []), createApiConnection: vi.fn(async (data: any) => ({ id: 1, ...data, status: "active", createdAt: new Date(), updatedAt: new Date() })), updateApiConnection: vi.fn(async (id: number, data: any) => ({ id, ...data })), deleteApiConnection: vi.fn(async () => true), testApiConnection: vi.fn(async () => ({ success: true, responseTime: 120, statusCode: 200 })), }; }); // Mock the LLM module vi.mock("./_core/llm", () => ({ invokeLLM: vi.fn(async () => ({ id: "test", created: Date.now(), model: "test", choices: [ { index: 0, message: { role: "assistant", content: "I can help you find furniture! What style are you looking for?" }, finish_reason: "stop", }, ], })), })); describe("chat.startSession", () => { it("creates a new conversation and returns sessionId", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); const result = await caller.chat.startSession({}); expect(result).toHaveProperty("sessionId"); expect(result).toHaveProperty("conversationId"); expect(typeof result.sessionId).toBe("string"); expect(result.sessionId.length).toBeGreaterThan(0); }); it("creates a session with visitor name", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); const result = await caller.chat.startSession({ visitorName: "John Doe" }); expect(result.sessionId).toBeTruthy(); expect(result.conversationId).toBeGreaterThan(0); }); }); describe("chat.sendMessage", () => { it("sends a message and receives an AI response", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); // Start a session first const session = await caller.chat.startSession({}); // Send a message const result = await caller.chat.sendMessage({ sessionId: session.sessionId, content: "I'm looking for a sofa", }); expect(result).toHaveProperty("reply"); expect(result.reply).toBeTruthy(); expect(typeof result.reply).toBe("string"); }); it("triggers escalation when user asks for human agent", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); const session = await caller.chat.startSession({}); const result = await caller.chat.sendMessage({ sessionId: session.sessionId, content: "I want to speak to human agent please", }); expect(result.status).toBe("escalated"); expect(result.reply).toContain("team member"); }); it("throws error for invalid session", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); await expect( caller.chat.sendMessage({ sessionId: "nonexistent", content: "Hello" }) ).rejects.toThrow("Conversation not found"); }); }); describe("chat.getMessages", () => { it("returns messages for a valid session", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); const session = await caller.chat.startSession({}); const result = await caller.chat.getMessages({ sessionId: session.sessionId }); expect(result).toHaveProperty("messages"); expect(result).toHaveProperty("status"); expect(Array.isArray(result.messages)).toBe(true); }); it("returns empty for invalid session", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); const result = await caller.chat.getMessages({ sessionId: "nonexistent" }); expect(result.messages).toEqual([]); expect(result.status).toBe("closed"); }); }); describe("agent.stats", () => { it("requires authentication", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); await expect(caller.agent.stats()).rejects.toThrow(); }); it("returns stats for authenticated user", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const stats = await caller.agent.stats(); expect(stats).toHaveProperty("total"); expect(stats).toHaveProperty("active"); expect(stats).toHaveProperty("escalated"); expect(stats).toHaveProperty("resolved"); expect(stats).toHaveProperty("closed"); }); }); describe("agent.reply", () => { it("requires authentication", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); await expect( caller.agent.reply({ conversationId: 1, content: "Hello" }) ).rejects.toThrow(); }); }); describe("workflow.save", () => { it("requires authentication", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); await expect( caller.workflow.save({ workflowId: "test", nodes: [], edges: [], }) ).rejects.toThrow(); }); it("saves workflow for authenticated user", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.save({ workflowId: "test-workflow", nodes: [ { workflowId: "test-workflow", nodeId: "n1", type: "greeting", label: "Welcome", config: { message: "Hello!" }, positionX: 100, positionY: 100, }, ], edges: [], }); expect(result.workflowId).toBe("test-workflow"); expect(result.nodeCount).toBe(1); expect(result.edgeCount).toBe(0); }); }); describe("workflow.load", () => { it("loads workflow for authenticated user", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.load({ workflowId: "test-workflow" }); expect(result).toHaveProperty("nodes"); expect(result).toHaveProperty("edges"); }); }); describe("workflow.save with new node types", () => { it("saves customer_data node", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.save({ workflowId: "test-workflow", nodes: [ { workflowId: "test-workflow", nodeId: "n_cust", type: "customer_data", label: "Customer Lookup", config: { apiEndpoint: "/api/crm/customer", lookupField: "customerId", returnFields: ["name", "email"] }, positionX: 100, positionY: 200, }, ], edges: [], }); expect(result.nodeCount).toBe(1); }); it("saves sales_order node", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.save({ workflowId: "test-workflow", nodes: [ { workflowId: "test-workflow", nodeId: "n_ord", type: "sales_order", label: "Order History", config: { apiEndpoint: "/api/orders/history", lookupField: "customerId" }, positionX: 200, positionY: 300, }, ], edges: [], }); expect(result.nodeCount).toBe(1); }); it("saves guardrail node with blocked topics", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.save({ workflowId: "test-workflow", nodes: [ { workflowId: "test-workflow", nodeId: "n_guard", type: "guardrail", label: "Content Filter", config: { blockedTopics: ["revenue", "margin", "internal pricing"], blockedMessage: "Sorry, I cannot share that information.", }, positionX: 300, positionY: 100, }, ], edges: [], }); expect(result.nodeCount).toBe(1); }); it("saves greeting node with customer ID detection config", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.save({ workflowId: "test-workflow", nodes: [ { workflowId: "test-workflow", nodeId: "n_greet", type: "greeting", label: "Welcome & Identify", config: { message: "Welcome!", detectCustomerId: true, customerIdSource: "session", personalizedGreeting: "Welcome back, {{customerName}}!", }, positionX: 400, positionY: 50, }, ], edges: [], }); expect(result.nodeCount).toBe(1); }); it("saves mixed workflow with all new node types", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.save({ workflowId: "full-workflow", nodes: [ { workflowId: "full-workflow", nodeId: "n1", type: "greeting", label: "Welcome", config: { detectCustomerId: true }, positionX: 0, positionY: 0 }, { workflowId: "full-workflow", nodeId: "n2", type: "guardrail", label: "Filter", config: { blockedTopics: ["revenue"] }, positionX: 100, positionY: 0 }, { workflowId: "full-workflow", nodeId: "n3", type: "customer_data", label: "CRM", config: { apiEndpoint: "/api/crm" }, positionX: 200, positionY: 0 }, { workflowId: "full-workflow", nodeId: "n4", type: "sales_order", label: "Orders", config: { apiEndpoint: "/api/orders" }, positionX: 300, positionY: 0 }, { workflowId: "full-workflow", nodeId: "n5", type: "intent", label: "Detect", config: { intents: ["search"] }, positionX: 400, positionY: 0 }, { workflowId: "full-workflow", nodeId: "n6", type: "end", label: "End", config: {}, positionX: 500, positionY: 0 }, ], edges: [ { workflowId: "full-workflow", sourceNodeId: "n1", targetNodeId: "n2" }, { workflowId: "full-workflow", sourceNodeId: "n2", targetNodeId: "n3" }, { workflowId: "full-workflow", sourceNodeId: "n3", targetNodeId: "n4" }, { workflowId: "full-workflow", sourceNodeId: "n4", targetNodeId: "n5" }, { workflowId: "full-workflow", sourceNodeId: "n5", targetNodeId: "n6" }, ], }); expect(result.nodeCount).toBe(6); expect(result.edgeCount).toBe(5); }); }); describe("workflow.getSuggestions", () => { it("requires authentication", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); await expect( caller.workflow.getSuggestions({ workflowId: "test" }) ).rejects.toThrow(); }); it("returns suggestions for authenticated admin", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.getSuggestions({ workflowId: "test-workflow" }); expect(Array.isArray(result)).toBe(true); }); it("accepts optional status filter", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.getSuggestions({ workflowId: "test-workflow", status: "pending" }); expect(Array.isArray(result)).toBe(true); }); }); describe("workflow.reviewSuggestion", () => { it("requires authentication", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); await expect( caller.workflow.reviewSuggestion({ suggestionId: 1, status: "approved" }) ).rejects.toThrow(); }); it("approves a suggestion", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.reviewSuggestion({ suggestionId: 1, status: "approved" }); expect(result.id).toBe(1); expect(result.status).toBe("approved"); }); it("declines a suggestion", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.reviewSuggestion({ suggestionId: 2, status: "declined" }); expect(result.id).toBe(2); expect(result.status).toBe("declined"); }); it("sets a suggestion to waiting", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.reviewSuggestion({ suggestionId: 3, status: "waiting" }); expect(result.id).toBe(3); expect(result.status).toBe("waiting"); }); }); describe("workflow.generateSuggestions", () => { it("requires authentication", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); await expect( caller.workflow.generateSuggestions({ workflowId: "test" }) ).rejects.toThrow(); }); it("generates suggestions from conversation data", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.workflow.generateSuggestions({ workflowId: "test-workflow" }); expect(result).toHaveProperty("suggestions"); expect(result).toHaveProperty("message"); }); }); /* ─── Analytics Tests ─── */ describe("analytics.summary", () => { it("requires admin authentication", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); await expect( caller.analytics.summary({}) ).rejects.toThrow(); }); it("returns analytics summary data", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.analytics.summary({}); expect(result).toHaveProperty("totalConversations"); expect(result).toHaveProperty("resolvedByBot"); expect(result).toHaveProperty("resolutionRate"); expect(result).toHaveProperty("topIntents"); expect(result.totalConversations).toBe(150); expect(result.resolutionRate).toBeCloseTo(63.3); }); }); describe("analytics.track", () => { it("tracks an analytics event", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.analytics.track({ eventType: "resolved_by_bot", conversationId: 1, metadata: { intent: "order_status" }, }); expect(result).toHaveProperty("id"); }); }); /* ─── Data Sources Tests ─── */ describe("dataSources.list", () => { it("requires admin authentication", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); await expect( caller.dataSources.list() ).rejects.toThrow(); }); it("returns list of data sources", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.dataSources.list(); expect(Array.isArray(result)).toBe(true); }); }); describe("dataSources.create", () => { it("creates a new data source", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.dataSources.create({ name: "Product Catalog", type: "url", config: { url: "https://api.homelegance.com/products" }, }); expect(result).toHaveProperty("id"); }); }); describe("dataSources.delete", () => { it("deletes a data source", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.dataSources.delete({ id: 1 }); expect(result).toHaveProperty("success"); }); }); /* ─── API Connections Tests ─── */ describe("apiConnections.list", () => { it("requires admin authentication", async () => { const ctx = createPublicContext(); const caller = appRouter.createCaller(ctx); await expect( caller.apiConnections.list() ).rejects.toThrow(); }); it("returns list of API connections", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.apiConnections.list(); expect(Array.isArray(result)).toBe(true); }); }); describe("apiConnections.create", () => { it("creates a new API connection", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.apiConnections.create({ name: "Order Status API", description: "Check order status", category: "orders", method: "GET", endpoint: "https://api.homelegance.com/orders/{orderId}", headers: { Authorization: "Bearer {{API_KEY}}" }, inputVariables: [{ name: "orderId", type: "string", description: "Order ID" }], outputVariables: [{ name: "status", type: "string", description: "Order status" }], }); expect(result).toHaveProperty("id"); }); }); describe("apiConnections.delete", () => { it("deletes an API connection", async () => { const ctx = createAuthContext(); const caller = appRouter.createCaller(ctx); const result = await caller.apiConnections.delete({ id: 1 }); expect(result).toHaveProperty("success"); }); });