// [nexus] Tests for assistant handoff route (Plan 33-03) import { describe, it, expect, vi, beforeEach } from "vitest"; // --------------------------------------------------------------------------- // Mock chatService so we can track calls without a real DB // --------------------------------------------------------------------------- const mockGetConversation = vi.fn(); const mockListMessages = vi.fn(); const mockCreateConversation = vi.fn(); const mockAddSystemMessage = vi.fn(); vi.mock("../services/chat.js", () => ({ chatService: () => ({ getConversation: mockGetConversation, listMessages: mockListMessages, createConversation: mockCreateConversation, addSystemMessage: mockAddSystemMessage, }), })); // Mock authz — assertBoard is a no-op in tests vi.mock("../routes/authz.js", () => ({ assertBoard: vi.fn(), })); // --------------------------------------------------------------------------- // Import the route factory under test // --------------------------------------------------------------------------- const { buildHandoffSummary, assistantHandoffRoutes } = await import("../routes/assistant-handoff.js"); // --------------------------------------------------------------------------- // Unit tests for buildHandoffSummary (pure function) // --------------------------------------------------------------------------- describe("buildHandoffSummary", () => { it("concatenates user messages into a summary", () => { const messages = [ { role: "user", content: "Hello world" }, { role: "assistant", content: "Hi there" }, { role: "user", content: "Can you build a login page?" }, ]; const summary = buildHandoffSummary(messages as Array<{ role: string; content: string }>); expect(summary).toContain("Hello world"); expect(summary).toContain("Can you build a login page?"); expect(summary).not.toContain("Hi there"); // assistant message excluded }); it("excludes non-user messages (assistant, system)", () => { const messages = [ { role: "system", content: "You are a helpful assistant" }, { role: "user", content: "Build me a dashboard" }, { role: "assistant", content: "Sure!" }, ]; const summary = buildHandoffSummary(messages as Array<{ role: string; content: string }>); expect(summary).toContain("Build me a dashboard"); expect(summary).not.toContain("You are a helpful assistant"); expect(summary).not.toContain("Sure!"); }); it("caps summary at 1500 chars", () => { const longContent = "A".repeat(600); const messages = [ { role: "user", content: longContent }, { role: "user", content: longContent }, { role: "user", content: longContent }, ]; const summary = buildHandoffSummary(messages as Array<{ role: string; content: string }>); expect(summary.length).toBeLessThanOrEqual(1500); }); it("returns empty string if no user messages", () => { const messages = [ { role: "assistant", content: "Hello!" }, { role: "system", content: "Context here" }, ]; const summary = buildHandoffSummary(messages as Array<{ role: string; content: string }>); expect(summary).toBe(""); }); }); // --------------------------------------------------------------------------- // Integration-style tests for the route handler // --------------------------------------------------------------------------- describe("POST /conversations/:id/assistant-handoff", () => { beforeEach(() => { vi.clearAllMocks(); mockGetConversation.mockResolvedValue({ id: "conv-123", companyId: "company-abc", agentId: null, title: "Test conversation", }); mockListMessages.mockResolvedValue({ items: [ { role: "user", content: "I want to build a todo app" }, { role: "assistant", content: "Great idea! Let me help." }, { role: "user", content: "It should have drag-and-drop" }, ], hasMore: false, nextCursor: null, }); mockCreateConversation.mockResolvedValue({ id: "new-conv-999", companyId: "company-abc", title: "Project from assistant chat", agentId: null, }); mockAddSystemMessage.mockResolvedValue({ id: "msg-handoff-1", role: "system", content: "summary", messageType: "handoff_context", }); }); it("creates a new conversation with handoff_context system message", async () => { const router = assistantHandoffRoutes({} as any); // Find the POST handler for /conversations/:id/assistant-handoff const layer = (router as any).stack.find( (l: any) => l.route?.path === "/conversations/:id/assistant-handoff" && l.route?.methods?.post, ); expect(layer).toBeDefined(); const handler = layer.route.stack[0].handle; const req = { params: { id: "conv-123" }, actor: { type: "board" } } as any; const res = { json: vi.fn(), status: vi.fn().mockReturnThis() } as any; await handler(req, res); expect(mockGetConversation).toHaveBeenCalledWith("conv-123"); expect(mockListMessages).toHaveBeenCalledWith("conv-123", { limit: 20 }); expect(mockCreateConversation).toHaveBeenCalledWith( "company-abc", expect.objectContaining({ title: "Project from assistant chat" }), ); expect(mockAddSystemMessage).toHaveBeenCalledWith( "new-conv-999", expect.objectContaining({ messageType: "handoff_context" }), ); expect(res.json).toHaveBeenCalledWith({ targetConversationId: "new-conv-999" }); }); it("builds summary from user messages only", async () => { const router = assistantHandoffRoutes({} as any); const layer = (router as any).stack.find( (l: any) => l.route?.path === "/conversations/:id/assistant-handoff" && l.route?.methods?.post, ); const handler = layer.route.stack[0].handle; const req = { params: { id: "conv-123" }, actor: { type: "board" } } as any; const res = { json: vi.fn(), status: vi.fn().mockReturnThis() } as any; await handler(req, res); const callArgs = mockAddSystemMessage.mock.calls[0]; const systemMessageContent = callArgs[1].content; expect(systemMessageContent).toContain("I want to build a todo app"); expect(systemMessageContent).toContain("It should have drag-and-drop"); expect(systemMessageContent).not.toContain("Great idea! Let me help."); }); it("caps summary content at 1500 chars in the system message", async () => { const longContent = "X".repeat(600); mockListMessages.mockResolvedValue({ items: [ { role: "user", content: longContent }, { role: "user", content: longContent }, { role: "user", content: longContent }, ], hasMore: false, nextCursor: null, }); const router = assistantHandoffRoutes({} as any); const layer = (router as any).stack.find( (l: any) => l.route?.path === "/conversations/:id/assistant-handoff" && l.route?.methods?.post, ); const handler = layer.route.stack[0].handle; const req = { params: { id: "conv-123" }, actor: { type: "board" } } as any; const res = { json: vi.fn(), status: vi.fn().mockReturnThis() } as any; await handler(req, res); const callArgs = mockAddSystemMessage.mock.calls[0]; expect(callArgs[1].content.length).toBeLessThanOrEqual(1500); }); });