import { describe, expect, it, vi } from "vitest"; import { appRouter } from "./routers"; import type { TrpcContext } from "./_core/context"; type AuthenticatedUser = NonNullable; /* ─── Context helpers ─── */ function createContext(role: "admin" | "agent" | "user" | null): TrpcContext { if (!role) { return { user: null, req: { protocol: "https", headers: {} } as TrpcContext["req"], res: { clearCookie: vi.fn() } as unknown as TrpcContext["res"], }; } const user: AuthenticatedUser = { id: role === "admin" ? 1 : role === "agent" ? 2 : 3, openId: `${role}-openid`, email: `${role}@homelegance.com`, name: `Test ${role.charAt(0).toUpperCase() + role.slice(1)}`, loginMethod: "manus", role, 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 database ─── */ const mockConversations = [ { id: 1, sessionId: "s1", visitorName: "Alice", visitorEmail: "alice@test.com", status: "active", assignedAgentId: 2, metadata: {}, createdAt: new Date(), updatedAt: new Date() }, { id: 2, sessionId: "s2", visitorName: "Bob", visitorEmail: "bob@test.com", status: "escalated", assignedAgentId: null, metadata: {}, createdAt: new Date(), updatedAt: new Date() }, { id: 3, sessionId: "s3", visitorName: "Charlie", visitorEmail: null, status: "resolved", assignedAgentId: 1, metadata: {}, createdAt: new Date(), updatedAt: new Date() }, ]; const mockAgents = [ { id: 1, name: "Admin User", email: "admin@homelegance.com", role: "admin" }, { id: 2, name: "Agent User", email: "agent@homelegance.com", role: "agent" }, ]; vi.mock("./db", () => { const users = [ { id: 1, openId: "admin-openid", name: "Test Admin", email: "admin@homelegance.com", role: "admin", lastSignedIn: new Date(), createdAt: new Date(), updatedAt: new Date() }, { id: 2, openId: "agent-openid", name: "Test Agent", email: "agent@homelegance.com", role: "agent", lastSignedIn: new Date(), createdAt: new Date(), updatedAt: new Date() }, { id: 3, openId: "user-openid", name: "Test User", email: "user@homelegance.com", role: "user", lastSignedIn: new Date(), createdAt: new Date(), updatedAt: new Date() }, ]; return { createConversation: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date(), updatedAt: new Date() })), getConversations: vi.fn(async () => []), getConversationsAdvanced: vi.fn(async (params: any) => ({ conversations: [ { id: 1, sessionId: "s1", visitorName: "Alice", status: "active", assignedAgentId: 2, customerId: "CUST-001", salesRep: "Jane Smith", agentName: "Test Agent", createdAt: new Date(), updatedAt: new Date() }, { id: 2, sessionId: "s2", visitorName: "Bob", status: "escalated", assignedAgentId: null, customerId: null, salesRep: null, agentName: null, createdAt: new Date(), updatedAt: new Date() }, ], total: 25, page: params?.page || 1, pageSize: params?.pageSize || 20, totalPages: 2, })), getConversationById: vi.fn(async (id: number) => ({ id, sessionId: "test", status: "active", createdAt: new Date(), updatedAt: new Date() })), getConversationBySessionId: vi.fn(async () => null), updateConversationStatus: vi.fn(async (id: number, status: string) => ({ id, status })), getConversationStats: vi.fn(async () => ({ total: 25, active: 10, escalated: 5, resolved: 7, closed: 3 })), addMessage: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date() })), getMessagesByConversation: vi.fn(async () => []), getConversationMessageCounts: vi.fn(async (ids: number[]) => { const counts: Record = {}; ids.forEach((id, i) => { counts[id] = (i + 1) * 3; }); return counts; }), getAgentUsers: vi.fn(async () => [ { id: 1, name: "Admin User", email: "admin@homelegance.com", role: "admin" }, { id: 2, name: "Agent User", email: "agent@homelegance.com", role: "agent" }, ]), bulkUpdateConversationStatus: vi.fn(async (ids: number[], status: string) => ({ updated: ids.length })), deleteConversations: vi.fn(async (ids: number[]) => ({ deleted: ids.length })), saveWorkflow: vi.fn(async (wid: string, nodes: any[], edges: any[]) => ({ workflowId: wid, nodeCount: nodes.length, edgeCount: edges.length })), getWorkflow: vi.fn(async () => ({ nodes: [], edges: [] })), getAllUsers: vi.fn(async () => users), updateUserRole: vi.fn(async (userId: number, role: string) => { const u = users.find(u => u.id === userId); if (!u) return null; return { ...u, role }; }), getUserById: vi.fn(async (userId: number) => users.find(u => u.id === userId)), getUserByEmail: vi.fn(async (email: string) => users.find(u => u.email === email) || null), deleteUser: vi.fn(async (userId: number) => users.find(u => u.id === userId) || null), getUserByEmailWithPassword: vi.fn(async () => null), createUserWithPassword: vi.fn(async (data: any) => ({ id: 10, ...data })), updateUserPassword: vi.fn(async () => {}), createPasswordResetToken: vi.fn(async (data: any) => ({ id: 1, ...data })), getPasswordResetToken: vi.fn(async () => null), markPasswordResetTokenUsed: vi.fn(async () => {}), createInvitation: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date() })), getAllInvitations: vi.fn(async () => []), getInvitationByToken: vi.fn(async () => null), getInvitationByEmail: vi.fn(async () => []), updateInvitationStatus: vi.fn(async (id: number, status: string) => ({ id, status })), expireOldInvitations: vi.fn(async () => {}), createAuditLog: vi.fn(async (data: any) => ({ id: 1, ...data, createdAt: new Date() })), getAuditLogs: vi.fn(async () => []), upsertUser: vi.fn(), getUserByOpenId: vi.fn(), }; }); vi.mock("./_core/llm", () => ({ invokeLLM: vi.fn(async () => ({ id: "test", created: Date.now(), model: "test", choices: [{ index: 0, message: { role: "assistant", content: "Test response from Ellie" }, finish_reason: "stop" }], })), })); /* ═══════════════════════════════════════════════════════════════ ADVANCED CONVERSATION QUERIES ═══════════════════════════════════════════════════════════════ */ describe("Advanced conversation queries", () => { it("returns paginated conversations with message counts for admin", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, sortBy: "updated", sortOrder: "desc", }); expect(result.total).toBe(25); expect(result.page).toBe(1); expect(result.totalPages).toBe(2); expect(result.conversations).toHaveLength(2); // Message counts should be enriched expect(result.conversations[0]).toHaveProperty("messageCount"); expect(typeof result.conversations[0].messageCount).toBe("number"); }); it("returns paginated conversations for agent role", async () => { const caller = appRouter.createCaller(createContext("agent")); const result = await caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, sortBy: "updated", sortOrder: "desc", }); expect(result.total).toBe(25); expect(result.conversations).toHaveLength(2); }); it("rejects advanced query for regular user", async () => { const caller = appRouter.createCaller(createContext("user")); await expect( caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, sortBy: "updated", sortOrder: "desc", }) ).rejects.toThrow("Agent or admin access required"); }); it("rejects advanced query for unauthenticated user", async () => { const caller = appRouter.createCaller(createContext(null)); await expect( caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, sortBy: "updated", sortOrder: "desc", }) ).rejects.toThrow(); }); it("supports filtering by status", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, status: "escalated", sortBy: "updated", sortOrder: "desc", }); expect(result).toBeDefined(); expect(result.conversations).toBeDefined(); }); it("supports filtering by agent", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, agentId: 2, sortBy: "updated", sortOrder: "desc", }); expect(result).toBeDefined(); }); it("supports search query", async () => { const caller = appRouter.createCaller(createContext("agent")); const result = await caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, search: "Alice", sortBy: "updated", sortOrder: "desc", }); expect(result).toBeDefined(); }); it("supports date range filtering", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, dateFrom: "2025-01-01", dateTo: "2026-12-31", sortBy: "created", sortOrder: "asc", }); expect(result).toBeDefined(); }); it("supports page 2 navigation", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.conversationsAdvanced({ page: 2, pageSize: 20, sortBy: "updated", sortOrder: "desc", }); expect(result.page).toBe(2); }); it("supports sorting by customerId", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, sortBy: "customerId", sortOrder: "asc", }); expect(result).toBeDefined(); expect(result.conversations).toBeDefined(); }); it("supports sorting by salesRep", async () => { const caller = appRouter.createCaller(createContext("agent")); const result = await caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, sortBy: "salesRep", sortOrder: "desc", }); expect(result).toBeDefined(); expect(result.conversations).toBeDefined(); }); it("supports sorting by agent name", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, sortBy: "agent", sortOrder: "asc", }); expect(result).toBeDefined(); expect(result.conversations).toBeDefined(); }); it("returns agentName field in conversation data", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, sortBy: "updated", sortOrder: "desc", }); // First conversation has an assigned agent expect(result.conversations[0]).toHaveProperty("agentName"); expect(result.conversations[0].agentName).toBe("Test Agent"); // Second conversation has no assigned agent expect(result.conversations[1]).toHaveProperty("agentName"); expect(result.conversations[1].agentName).toBeNull(); }); it("returns customerId and salesRep fields in conversation data", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.conversationsAdvanced({ page: 1, pageSize: 20, sortBy: "updated", sortOrder: "desc", }); expect(result.conversations[0]).toHaveProperty("customerId"); expect(result.conversations[0].customerId).toBe("CUST-001"); expect(result.conversations[0]).toHaveProperty("salesRep"); expect(result.conversations[0].salesRep).toBe("Jane Smith"); }); }); /* ═══════════════════════════════════════════════════════════════ AGENT LIST ═══════════════════════════════════════════════════════════════ */ describe("Agent list for filter dropdown", () => { it("returns agent list for admin", async () => { const caller = appRouter.createCaller(createContext("admin")); const agents = await caller.agent.agents(); expect(agents).toHaveLength(2); expect(agents[0]).toHaveProperty("name"); expect(agents[0]).toHaveProperty("role"); }); it("returns agent list for agent role", async () => { const caller = appRouter.createCaller(createContext("agent")); const agents = await caller.agent.agents(); expect(agents).toHaveLength(2); }); it("rejects agent list for regular user", async () => { const caller = appRouter.createCaller(createContext("user")); await expect(caller.agent.agents()).rejects.toThrow("Agent or admin access required"); }); }); /* ═══════════════════════════════════════════════════════════════ BULK ACTIONS ═══════════════════════════════════════════════════════════════ */ describe("Bulk conversation actions", () => { it("allows agent to bulk resolve conversations", async () => { const caller = appRouter.createCaller(createContext("agent")); const result = await caller.agent.bulkUpdateStatus({ conversationIds: [1, 2, 3], status: "resolved", }); expect(result.updated).toBe(3); }); it("allows admin to bulk close conversations", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.bulkUpdateStatus({ conversationIds: [1, 2], status: "closed", }); expect(result.updated).toBe(2); }); it("rejects bulk action for regular user", async () => { const caller = appRouter.createCaller(createContext("user")); await expect( caller.agent.bulkUpdateStatus({ conversationIds: [1], status: "resolved" }) ).rejects.toThrow("Agent or admin access required"); }); it("rejects bulk action for unauthenticated user", async () => { const caller = appRouter.createCaller(createContext(null)); await expect( caller.agent.bulkUpdateStatus({ conversationIds: [1], status: "resolved" }) ).rejects.toThrow(); }); }); /* ═══════════════════════════════════════════════════════════════ DELETE CONVERSATIONS (admin only) ═══════════════════════════════════════════════════════════════ */ describe("Delete conversations (admin only)", () => { it("allows admin to delete conversations", async () => { const caller = appRouter.createCaller(createContext("admin")); const result = await caller.agent.deleteConversations({ conversationIds: [1, 2], }); expect(result.deleted).toBe(2); }); it("rejects delete for agent role", async () => { const caller = appRouter.createCaller(createContext("agent")); await expect( caller.agent.deleteConversations({ conversationIds: [1] }) ).rejects.toThrow(); }); it("rejects delete for regular user", async () => { const caller = appRouter.createCaller(createContext("user")); await expect( caller.agent.deleteConversations({ conversationIds: [1] }) ).rejects.toThrow(); }); it("rejects delete for unauthenticated user", async () => { const caller = appRouter.createCaller(createContext(null)); await expect( caller.agent.deleteConversations({ conversationIds: [1] }) ).rejects.toThrow(); }); });