import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { basename, join, relative } from "path"; import { afterEach, describe, expect, it } from "vitest"; import { discoverSessionFiles, discoverSessions, type SessionDiscoveryProgress, } from "../src/core/session-discovery.ts"; const tempDirs: string[] = []; function createTempDir(): string { const dir = mkdtempSync(join(tmpdir(), "pi-session-discovery-")); tempDirs.push(dir); return dir; } afterEach(() => { for (const dir of tempDirs.splice(0)) { rmSync(dir, { recursive: true, force: true }); } }); function writeJsonl(path: string, lines: unknown[]): void { writeFileSync(path, `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`); } describe("session discovery", () => { it("finds session jsonl files recursively", async () => { const root = createTempDir(); const projectA = join(root, "--project-a--"); const projectB = join(root, "--project-b--", "nested"); mkdirSync(projectA, { recursive: true }); mkdirSync(projectB, { recursive: true }); writeJsonl(join(projectA, "a.jsonl"), [ { type: "session", version: 3, id: "a", timestamp: "2026-01-01T00:00:00.000Z", cwd: "/a" }, ]); writeJsonl(join(projectB, "b.jsonl"), [ { type: "session", version: 3, id: "b", timestamp: "2026-01-02T00:00:00.000Z", cwd: "/b" }, ]); writeFileSync(join(projectA, "notes.txt"), "not a session"); const files = await discoverSessionFiles({ sessionsRoot: root }); expect(files.map((file) => relative(root, file))).toEqual([ join("--project-a--", "a.jsonl"), join("--project-b--", "nested", "b.jsonl"), ]); }); it("returns metadata for valid sessions and skips invalid jsonl files", async () => { const root = createTempDir(); const projectA = join(root, "--project-a--"); const projectB = join(root, "--project-b--"); mkdirSync(projectA, { recursive: true }); mkdirSync(projectB, { recursive: true }); writeJsonl(join(projectA, "a.jsonl"), [ { type: "session", version: 3, id: "session-a", timestamp: "2026-01-01T00:00:00.000Z", cwd: "/work/a", parentSession: "/parent/session.jsonl", }, ]); writeJsonl(join(projectB, "b.jsonl"), [ { type: "session", version: 3, id: "session-b", timestamp: "2026-01-02T00:00:00.000Z", cwd: "/work/b" }, ]); writeFileSync(join(projectB, "invalid.jsonl"), "not json\n"); const progress: SessionDiscoveryProgress[] = []; const sessions = await discoverSessions({ sessionsRoot: root, onProgress: (update) => progress.push(update) }); expect(sessions.map((session) => session.sessionId)).toEqual(["session-a", "session-b"]); expect(sessions[0]).toMatchObject({ path: join(projectA, "a.jsonl"), relativePath: join("--project-a--", "a.jsonl"), sessionDir: projectA, sessionDirName: basename(projectA), sessionId: "session-a", cwd: "/work/a", header: { type: "session", version: 3, id: "session-a", timestamp: "2026-01-01T00:00:00.000Z", cwd: "/work/a", parentSession: "/parent/session.jsonl", }, }); expect(sessions[0].createdAt?.toISOString()).toBe("2026-01-01T00:00:00.000Z"); expect(sessions[0].modifiedAt).toBeInstanceOf(Date); expect(sessions[0].sizeBytes).toBeGreaterThan(0); expect(progress.at(-1)).toMatchObject({ phase: "read", foundFiles: 3, processedFiles: 3, sessions: 2 }); }); });