Files
session-viewer/tests/unit/session-discovery.test.ts
teernisse 69857fa825 Fix session discovery tests to use dynamic paths and add containment test
Replace hardcoded absolute paths in test assertions with dynamically
constructed paths matching the temp directory. This makes tests portable
across environments where path.resolve() produces different results.

Add test verifying that absolute paths pointing outside the projects
directory (e.g. /etc/shadow.jsonl) are rejected by the discovery filter.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 01:10:55 -05:00

205 lines
6.6 KiB
TypeScript

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<string, unknown>[]) {
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 });
});
});