import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; // Mock active-runs module vi.mock("@/lib/active-runs", () => ({ startRun: vi.fn(), hasActiveRun: vi.fn(() => false), subscribeToRun: vi.fn(), persistUserMessage: vi.fn(), abortRun: vi.fn(() => false), getActiveRun: vi.fn(), getRunningSessionIds: vi.fn(() => []), })); // Mock workspace module vi.mock("@/lib/workspace", () => ({ resolveAgentWorkspacePrefix: vi.fn(() => null), })); describe("Chat API routes", () => { beforeEach(() => { vi.resetModules(); // Re-wire mocks vi.mock("@/lib/active-runs", () => ({ startRun: vi.fn(), hasActiveRun: vi.fn(() => false), subscribeToRun: vi.fn(), persistUserMessage: vi.fn(), abortRun: vi.fn(() => false), getActiveRun: vi.fn(), getRunningSessionIds: vi.fn(() => []), })); vi.mock("@/lib/workspace", () => ({ resolveAgentWorkspacePrefix: vi.fn(() => null), })); }); afterEach(() => { vi.restoreAllMocks(); }); // ─── POST /api/chat ────────────────────────────────────────────── describe("POST /api/chat", () => { it("returns 400 when no user message text", async () => { const { POST } = await import("./route.js"); const req = new Request("http://localhost/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: [{ role: "user", parts: [{ type: "text", text: "" }] }], }), }); const res = await POST(req); expect(res.status).toBe(400); }); it("returns 409 when active run exists for session", async () => { const { hasActiveRun } = await import("@/lib/active-runs"); vi.mocked(hasActiveRun).mockReturnValue(true); const { POST } = await import("./route.js"); const req = new Request("http://localhost/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: [{ role: "user", parts: [{ type: "text", text: "hello" }] }], sessionId: "s1", }), }); const res = await POST(req); expect(res.status).toBe(409); }); it("starts a run and returns streaming response", async () => { const { startRun, hasActiveRun, subscribeToRun } = await import("@/lib/active-runs"); vi.mocked(hasActiveRun).mockReturnValue(false); vi.mocked(subscribeToRun).mockReturnValue(() => {}); const { POST } = await import("./route.js"); const req = new Request("http://localhost/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: [ { id: "m1", role: "user", parts: [{ type: "text", text: "hello" }] }, ], sessionId: "s1", }), }); const res = await POST(req); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("text/event-stream"); expect(startRun).toHaveBeenCalled(); }); it("persists user message when sessionId provided", async () => { const { hasActiveRun, subscribeToRun, persistUserMessage } = await import("@/lib/active-runs"); vi.mocked(hasActiveRun).mockReturnValue(false); vi.mocked(subscribeToRun).mockReturnValue(() => {}); const { POST } = await import("./route.js"); const req = new Request("http://localhost/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: [ { id: "m1", role: "user", parts: [{ type: "text", text: "hi" }] }, ], sessionId: "s1", }), }); await POST(req); expect(persistUserMessage).toHaveBeenCalledWith("s1", expect.objectContaining({ id: "m1" })); }); it("resolves workspace file paths in message", async () => { const { resolveAgentWorkspacePrefix } = await import("@/lib/workspace"); vi.mocked(resolveAgentWorkspacePrefix).mockReturnValue("workspace"); const { startRun, hasActiveRun, subscribeToRun } = await import("@/lib/active-runs"); vi.mocked(hasActiveRun).mockReturnValue(false); vi.mocked(subscribeToRun).mockReturnValue(() => {}); const { POST } = await import("./route.js"); const req = new Request("http://localhost/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: [ { id: "m1", role: "user", parts: [{ type: "text", text: "[Context: workspace file 'doc.md']" }], }, ], sessionId: "s1", }), }); await POST(req); expect(startRun).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("workspace/doc.md"), }), ); }); }); // ─── POST /api/chat/stop ──────────────────────────────────────── describe("POST /api/chat/stop", () => { it("returns 400 when sessionId missing", async () => { const { POST } = await import("./stop/route.js"); const req = new Request("http://localhost/api/chat/stop", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); const res = await POST(req); expect(res.status).toBe(400); }); it("aborts run and returns result", async () => { const { abortRun } = await import("@/lib/active-runs"); vi.mocked(abortRun).mockReturnValue(true); const { POST } = await import("./stop/route.js"); const req = new Request("http://localhost/api/chat/stop", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId: "s1" }), }); const res = await POST(req); expect(res.status).toBe(200); const json = await res.json(); expect(json.aborted).toBe(true); }); it("returns aborted=false for unknown session", async () => { const { abortRun } = await import("@/lib/active-runs"); vi.mocked(abortRun).mockReturnValue(false); const { POST } = await import("./stop/route.js"); const req = new Request("http://localhost/api/chat/stop", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId: "nonexistent" }), }); const res = await POST(req); const json = await res.json(); expect(json.aborted).toBe(false); }); it("handles invalid JSON body gracefully", async () => { const { POST } = await import("./stop/route.js"); const req = new Request("http://localhost/api/chat/stop", { method: "POST", headers: { "Content-Type": "application/json" }, body: "not json", }); const res = await POST(req); expect(res.status).toBe(400); }); }); // ─── GET /api/chat/active ──────────────────────────────────────── describe("GET /api/chat/active", () => { it("returns empty sessionIds when no active runs", async () => { const { GET } = await import("./active/route.js"); const res = GET(); const json = await res.json(); expect(json.sessionIds).toEqual([]); }); it("returns active session IDs", async () => { const { getRunningSessionIds } = await import("@/lib/active-runs"); vi.mocked(getRunningSessionIds).mockReturnValue(["s1", "s2"]); const { GET } = await import("./active/route.js"); const res = GET(); const json = await res.json(); expect(json.sessionIds).toEqual(["s1", "s2"]); }); }); // ─── GET /api/chat/stream ─────────────────────────────────────── describe("GET /api/chat/stream", () => { it("returns 400 when sessionId is missing", async () => { const { GET } = await import("./stream/route.js"); const req = new Request("http://localhost/api/chat/stream"); const res = await GET(req); expect(res.status).toBe(400); }); it("returns 404 when no run exists for session", async () => { const { getActiveRun } = await import("@/lib/active-runs"); vi.mocked(getActiveRun).mockReturnValue(undefined); const { GET } = await import("./stream/route.js"); const req = new Request("http://localhost/api/chat/stream?sessionId=nonexistent"); const res = await GET(req); expect(res.status).toBe(404); }); it("returns SSE stream for active run", async () => { const { getActiveRun, subscribeToRun } = await import("@/lib/active-runs"); vi.mocked(getActiveRun).mockReturnValue({ status: "running" } as never); vi.mocked(subscribeToRun).mockReturnValue(() => {}); const { GET } = await import("./stream/route.js"); const req = new Request("http://localhost/api/chat/stream?sessionId=s1"); const res = await GET(req); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("text/event-stream"); expect(res.headers.get("X-Run-Active")).toBe("true"); }); it("returns X-Run-Active=false for completed run", async () => { const { getActiveRun, subscribeToRun } = await import("@/lib/active-runs"); vi.mocked(getActiveRun).mockReturnValue({ status: "completed" } as never); vi.mocked(subscribeToRun).mockReturnValue(() => {}); const { GET } = await import("./stream/route.js"); const req = new Request("http://localhost/api/chat/stream?sessionId=s1"); const res = await GET(req); expect(res.headers.get("X-Run-Active")).toBe("false"); }); }); });