openclaw/apps/web/app/api/web-sessions/web-sessions.test.ts
2026-02-17 00:36:01 -08:00

305 lines
12 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock node:fs
vi.mock("node:fs", () => ({
existsSync: vi.fn(() => false),
readFileSync: vi.fn(() => "[]"),
writeFileSync: vi.fn(),
mkdirSync: vi.fn(),
appendFileSync: vi.fn(),
}));
// Mock node:os
vi.mock("node:os", () => ({
homedir: vi.fn(() => "/home/testuser"),
}));
// Mock node:crypto
vi.mock("node:crypto", () => ({
randomUUID: vi.fn(() => "test-uuid-1234"),
}));
describe("Web Sessions API", () => {
beforeEach(() => {
vi.resetModules();
vi.mock("node:fs", () => ({
existsSync: vi.fn(() => false),
readFileSync: vi.fn(() => "[]"),
writeFileSync: vi.fn(),
mkdirSync: vi.fn(),
appendFileSync: vi.fn(),
}));
vi.mock("node:os", () => ({
homedir: vi.fn(() => "/home/testuser"),
}));
vi.mock("node:crypto", () => ({
randomUUID: vi.fn(() => "test-uuid-1234"),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
// ─── GET /api/web-sessions ──────────────────────────────────────
describe("GET /api/web-sessions", () => {
it("returns empty sessions when no index exists", async () => {
const { GET } = await import("./route.js");
const req = new Request("http://localhost/api/web-sessions");
const res = await GET(req);
const json = await res.json();
expect(json.sessions).toEqual([]);
});
it("returns global sessions when no filePath param", async () => {
const { readFileSync: mockReadFile, existsSync: mockExists } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
const sessions = [
{ id: "s1", title: "Chat 1", createdAt: 1, updatedAt: 1, messageCount: 0 },
{ id: "s2", title: "File Chat", createdAt: 2, updatedAt: 2, messageCount: 1, filePath: "doc.md" },
];
vi.mocked(mockReadFile).mockReturnValue(JSON.stringify(sessions) as never);
const { GET } = await import("./route.js");
const req = new Request("http://localhost/api/web-sessions");
const res = await GET(req);
const json = await res.json();
expect(json.sessions).toHaveLength(1);
expect(json.sessions[0].id).toBe("s1");
});
it("filters sessions by filePath param", async () => {
const { readFileSync: mockReadFile, existsSync: mockExists } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
const sessions = [
{ id: "s1", title: "Global", createdAt: 1, updatedAt: 1, messageCount: 0 },
{ id: "s2", title: "Doc Chat", createdAt: 2, updatedAt: 2, messageCount: 1, filePath: "doc.md" },
];
vi.mocked(mockReadFile).mockReturnValue(JSON.stringify(sessions) as never);
const { GET } = await import("./route.js");
const req = new Request("http://localhost/api/web-sessions?filePath=doc.md");
const res = await GET(req);
const json = await res.json();
expect(json.sessions).toHaveLength(1);
expect(json.sessions[0].filePath).toBe("doc.md");
});
it("returns empty when no matching filePath sessions", async () => {
const { readFileSync: mockReadFile, existsSync: mockExists } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
vi.mocked(mockReadFile).mockReturnValue("[]" as never);
const { GET } = await import("./route.js");
const req = new Request("http://localhost/api/web-sessions?filePath=nonexistent.md");
const res = await GET(req);
const json = await res.json();
expect(json.sessions).toEqual([]);
});
});
// ─── POST /api/web-sessions ────────────────────────────────────
describe("POST /api/web-sessions", () => {
it("creates a new session with default title", async () => {
const { writeFileSync: mockWrite } = await import("node:fs");
const { POST } = await import("./route.js");
const req = new Request("http://localhost/api/web-sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const res = await POST(req);
const json = await res.json();
expect(json.session.id).toBe("test-uuid-1234");
expect(json.session.title).toBe("New Chat");
expect(json.session.messageCount).toBe(0);
expect(mockWrite).toHaveBeenCalled();
});
it("creates session with custom title", async () => {
const { POST } = await import("./route.js");
const req = new Request("http://localhost/api/web-sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "My Chat" }),
});
const res = await POST(req);
const json = await res.json();
expect(json.session.title).toBe("My Chat");
});
it("creates file-scoped session with filePath", async () => {
const { POST } = await import("./route.js");
const req = new Request("http://localhost/api/web-sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "File Chat", filePath: "readme.md" }),
});
const res = await POST(req);
const json = await res.json();
expect(json.session.filePath).toBe("readme.md");
});
it("handles invalid JSON body gracefully", async () => {
const { POST } = await import("./route.js");
const req = new Request("http://localhost/api/web-sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "not json",
});
const res = await POST(req);
const json = await res.json();
// Falls back to default title
expect(json.session.title).toBe("New Chat");
});
it("creates jsonl file for new session", async () => {
const { writeFileSync: mockWrite } = await import("node:fs");
const { POST } = await import("./route.js");
const req = new Request("http://localhost/api/web-sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
await POST(req);
// Should write at least the index.json and the empty .jsonl
expect(mockWrite).toHaveBeenCalled();
// Verify that one of the calls is to the jsonl file
const calls = mockWrite.mock.calls.map((c) => String(c[0]));
expect(calls.some((c) => c.endsWith(".jsonl"))).toBe(true);
});
});
// ─── GET /api/web-sessions/[id] ────────────────────────────────
describe("GET /api/web-sessions/[id]", () => {
it("returns session messages", async () => {
const { existsSync: mockExists, readFileSync: mockReadFile } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
const lines = [
JSON.stringify({ id: "m1", role: "user", content: "hello" }),
JSON.stringify({ id: "m2", role: "assistant", content: "hi" }),
].join("\n");
vi.mocked(mockReadFile).mockReturnValue(lines as never);
const { GET } = await import("./[id]/route.js");
const res = await GET(
new Request("http://localhost/api/web-sessions/s1"),
{ params: Promise.resolve({ id: "s1" }) },
);
const json = await res.json();
expect(json.id).toBe("s1");
expect(json.messages).toHaveLength(2);
});
it("returns 404 when session file does not exist", async () => {
const { existsSync: mockExists } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(false);
const { GET } = await import("./[id]/route.js");
const res = await GET(
new Request("http://localhost/api/web-sessions/nonexistent"),
{ params: Promise.resolve({ id: "nonexistent" }) },
);
expect(res.status).toBe(404);
});
it("handles empty session file", async () => {
const { existsSync: mockExists, readFileSync: mockReadFile } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
vi.mocked(mockReadFile).mockReturnValue("" as never);
const { GET } = await import("./[id]/route.js");
const res = await GET(
new Request("http://localhost/api/web-sessions/s1"),
{ params: Promise.resolve({ id: "s1" }) },
);
const json = await res.json();
expect(json.messages).toEqual([]);
});
});
// ─── POST /api/web-sessions/[id]/messages ──────────────────────
describe("POST /api/web-sessions/[id]/messages", () => {
it("appends messages to session file", async () => {
const { existsSync: mockExists, readFileSync: mockReadFile, writeFileSync: _mockWrite } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
vi.mocked(mockReadFile).mockImplementation((p) => {
const s = String(p);
if (s.endsWith("index.json")) {
return JSON.stringify([{ id: "s1", title: "Chat", createdAt: 1, updatedAt: 1, messageCount: 0 }]) as never;
}
return "" as never;
});
const { POST } = await import("./[id]/messages/route.js");
const req = new Request("http://localhost/api/web-sessions/s1/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: [{ id: "m1", role: "user", content: "hello" }],
}),
});
const res = await POST(req, { params: Promise.resolve({ id: "s1" }) });
const json = await res.json();
expect(json.ok).toBe(true);
});
it("auto-creates session file if missing", async () => {
const { existsSync: mockExists, readFileSync: mockReadFile, writeFileSync: _mockWrite } = await import("node:fs");
vi.mocked(mockExists).mockImplementation((p) => {
const s = String(p);
if (s.endsWith(".jsonl")) {return false;}
return true;
});
vi.mocked(mockReadFile).mockImplementation((p) => {
const s = String(p);
if (s.endsWith("index.json")) {return "[]" as never;}
return "" as never;
});
const { POST } = await import("./[id]/messages/route.js");
const req = new Request("http://localhost/api/web-sessions/new-s/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: [{ id: "m1", role: "user", content: "first message" }],
}),
});
const res = await POST(req, { params: Promise.resolve({ id: "new-s" }) });
expect(res.status).toBe(200);
});
it("updates session title when provided", async () => {
const { existsSync: mockExists, readFileSync: mockReadFile, writeFileSync: mockWrite } = await import("node:fs");
vi.mocked(mockExists).mockReturnValue(true);
vi.mocked(mockReadFile).mockImplementation((p) => {
const s = String(p);
if (s.endsWith("index.json")) {
return JSON.stringify([{ id: "s1", title: "Old Title", createdAt: 1, updatedAt: 1, messageCount: 0 }]) as never;
}
return "" as never;
});
const { POST } = await import("./[id]/messages/route.js");
const req = new Request("http://localhost/api/web-sessions/s1/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: [{ id: "m1", role: "user", content: "hello" }],
title: "New Title",
}),
});
const res = await POST(req, { params: Promise.resolve({ id: "s1" }) });
expect(res.status).toBe(200);
// Verify index was written with new title
expect(mockWrite).toHaveBeenCalled();
});
});
});