import { describe, it, expect } from "vitest"; import { discoverSessions } from "../../src/server/services/session-discovery.js"; import path from "path"; import fs from "fs/promises"; import os from "os"; /** Helper to write a sessions-index.json in the real { version, entries } format */ function makeIndex(entries: Record[]) { return JSON.stringify({ version: 1, entries }); } describe("session-discovery", () => { it("discovers sessions from { version, entries } format", async () => { const tmpDir = path.join(os.tmpdir(), `sv-test-${Date.now()}`); const projectDir = path.join(tmpDir, "test-project"); await fs.mkdir(projectDir, { recursive: true }); const sessionPath = path.join(projectDir, "sess-001.jsonl"); await fs.writeFile( path.join(projectDir, "sessions-index.json"), makeIndex([ { sessionId: "sess-001", fullPath: sessionPath, summary: "Test session", firstPrompt: "Hello", created: "2025-10-15T10:00:00Z", modified: "2025-10-15T11:00:00Z", messageCount: 5, }, ]) ); const sessions = await discoverSessions(tmpDir); expect(sessions).toHaveLength(1); expect(sessions[0].id).toBe("sess-001"); expect(sessions[0].summary).toBe("Test session"); expect(sessions[0].project).toBe("test-project"); expect(sessions[0].messageCount).toBe(5); expect(sessions[0].path).toBe(sessionPath); await fs.rm(tmpDir, { recursive: true }); }); it("also handles legacy raw array format", async () => { const tmpDir = path.join(os.tmpdir(), `sv-test-legacy-${Date.now()}`); const projectDir = path.join(tmpDir, "legacy-project"); await fs.mkdir(projectDir, { recursive: true }); // Raw array (not wrapped in { version, entries }) await fs.writeFile( path.join(projectDir, "sessions-index.json"), JSON.stringify([ { sessionId: "legacy-001", summary: "Legacy format", created: "2025-10-15T10:00:00Z", modified: "2025-10-15T11:00:00Z", }, ]) ); const sessions = await discoverSessions(tmpDir); expect(sessions).toHaveLength(1); expect(sessions[0].id).toBe("legacy-001"); await fs.rm(tmpDir, { recursive: true }); }); it("handles missing projects directory gracefully", async () => { const sessions = await discoverSessions("/nonexistent/path"); expect(sessions).toEqual([]); }); it("handles corrupt index files gracefully", async () => { const tmpDir = path.join(os.tmpdir(), `sv-test-corrupt-${Date.now()}`); const projectDir = path.join(tmpDir, "corrupt-project"); await fs.mkdir(projectDir, { recursive: true }); await fs.writeFile( path.join(projectDir, "sessions-index.json"), "not valid json {" ); const sessions = await discoverSessions(tmpDir); expect(sessions).toEqual([]); await fs.rm(tmpDir, { recursive: true }); }); it("aggregates across multiple project directories", async () => { const tmpDir = path.join(os.tmpdir(), `sv-test-multi-${Date.now()}`); const proj1 = path.join(tmpDir, "project-a"); const proj2 = path.join(tmpDir, "project-b"); await fs.mkdir(proj1, { recursive: true }); await fs.mkdir(proj2, { recursive: true }); await fs.writeFile( path.join(proj1, "sessions-index.json"), makeIndex([{ sessionId: "a-001", created: "2025-01-01T00:00:00Z", modified: "2025-01-01T00:00:00Z" }]) ); await fs.writeFile( path.join(proj2, "sessions-index.json"), makeIndex([{ sessionId: "b-001", created: "2025-01-02T00:00:00Z", modified: "2025-01-02T00:00:00Z" }]) ); const sessions = await discoverSessions(tmpDir); expect(sessions).toHaveLength(2); const ids = sessions.map((s) => s.id); expect(ids).toContain("a-001"); expect(ids).toContain("b-001"); await fs.rm(tmpDir, { recursive: true }); }); it("rejects paths with traversal segments", async () => { const tmpDir = path.join(os.tmpdir(), `sv-test-traversal-${Date.now()}`); const projectDir = path.join(tmpDir, "traversal-project"); await fs.mkdir(projectDir, { recursive: true }); const goodPath = path.join(projectDir, "good-001.jsonl"); await fs.writeFile( path.join(projectDir, "sessions-index.json"), makeIndex([ { sessionId: "evil-001", fullPath: "/home/ubuntu/../../../etc/passwd", created: "2025-10-15T10:00:00Z", modified: "2025-10-15T11:00:00Z", }, { sessionId: "evil-002", fullPath: "/home/ubuntu/sessions/not-a-jsonl.txt", created: "2025-10-15T10:00:00Z", modified: "2025-10-15T11:00:00Z", }, { sessionId: "good-001", fullPath: goodPath, created: "2025-10-15T10:00:00Z", modified: "2025-10-15T11:00:00Z", }, ]) ); const sessions = await discoverSessions(tmpDir); expect(sessions).toHaveLength(1); expect(sessions[0].id).toBe("good-001"); await fs.rm(tmpDir, { recursive: true }); }); it("rejects absolute paths outside the projects directory", async () => { const tmpDir = path.join(os.tmpdir(), `sv-test-containment-${Date.now()}`); const projectDir = path.join(tmpDir, "contained-project"); await fs.mkdir(projectDir, { recursive: true }); await fs.writeFile( path.join(projectDir, "sessions-index.json"), makeIndex([ { sessionId: "escaped-001", fullPath: "/etc/shadow.jsonl", created: "2025-10-15T10:00:00Z", modified: "2025-10-15T11:00:00Z", }, { sessionId: "escaped-002", fullPath: "/tmp/other-dir/secret.jsonl", created: "2025-10-15T10:00:00Z", modified: "2025-10-15T11:00:00Z", }, ]) ); const sessions = await discoverSessions(tmpDir); expect(sessions).toHaveLength(0); await fs.rm(tmpDir, { recursive: true }); }); it("uses fullPath from index entry", async () => { const tmpDir = path.join(os.tmpdir(), `sv-test-fp-${Date.now()}`); const projectDir = path.join(tmpDir, "fp-project"); await fs.mkdir(projectDir, { recursive: true }); const sessionPath = path.join(projectDir, "fp-001.jsonl"); await fs.writeFile( path.join(projectDir, "sessions-index.json"), makeIndex([ { sessionId: "fp-001", fullPath: sessionPath, created: "2025-10-15T10:00:00Z", modified: "2025-10-15T11:00:00Z", }, ]) ); const sessions = await discoverSessions(tmpDir); expect(sessions[0].path).toBe(sessionPath); await fs.rm(tmpDir, { recursive: true }); }); });