import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { DatabaseSync } from "node:sqlite"; import { afterEach, describe, expect, it } from "vitest"; import { canonicalSessionCatalogDir, openSessionCatalogStore, SESSION_CATALOG_DB_FILENAME, type SessionCatalogRow, } from "../src/core/session-catalog-db.js"; const requireBuiltin = createRequire(import.meta.url); const dirs: string[] = []; function createDir(): string { const dir = mkdtempSync(join(tmpdir(), "pi-catalog-db-")); dirs.push(dir); return dir; } function createRow(path: string, overrides: Partial = {}): SessionCatalogRow { return { path, dir: join(path, ".."), dev: 1, ino: 2, size: 100, mtimeMs: 1_000.5, id: "session-1", modifiedMs: 900, summaryJson: '{"id":"session-1"}', resumeJson: null, indexedMs: 1_100, ...overrides, }; } afterEach(() => { for (const dir of dirs.splice(0)) { rmSync(dir, { recursive: true, force: true }); } }); describe("session catalog store", () => { it("round-trips rows and prunes files that are gone", () => { const dir = createDir(); const store = openSessionCatalogStore(dir); expect(store).toBeDefined(); const kept = join(dir, "kept.jsonl"); const gone = join(dir, "gone.jsonl"); store!.write(createRow(kept, { dir })); store!.write(createRow(gone, { dir, id: "session-2" })); const canonicalDir = canonicalSessionCatalogDir(dir); expect(store!.read(kept)).toMatchObject({ path: join(canonicalDir, "kept.jsonl"), dir: canonicalDir, size: 100, id: "session-1", mtimeMs: 1_000.5, }); store!.retain(dir, new Set([kept])); expect(store!.read(kept)).toBeDefined(); expect(store!.read(gone)).toBeUndefined(); store!.remove(kept); expect(store!.read(kept)).toBeUndefined(); store!.close(); }); it("resolves relative paths against one absolute row", () => { const dir = createDir(); const store = openSessionCatalogStore(dir); const file = join(dir, "a.jsonl"); store!.write(createRow(file, { dir })); expect(store!.read(join(dir, "..", dir.split("/").pop()!, "a.jsonl"))).toBeDefined(); store!.close(); }); it("returns undefined when the directory does not exist", () => { expect(openSessionCatalogStore(join(tmpdir(), "pi-catalog-missing", "nope"))).toBeUndefined(); }); it("recreates a corrupt database file", () => { const dir = createDir(); openSessionCatalogStore(dir)!.close(); writeFileSync(join(dir, SESSION_CATALOG_DB_FILENAME), "not a database"); const store = openSessionCatalogStore(dir); expect(store).toBeDefined(); const file = join(dir, "a.jsonl"); store!.write(createRow(file, { dir })); expect(store!.read(file)).toBeDefined(); store!.close(); }); it("drops rows written by an older schema", () => { const dir = createDir(); const store = openSessionCatalogStore(dir)!; const file = join(dir, "a.jsonl"); store.write(createRow(file, { dir })); store.close(); const sqlite = requireBuiltin("node:sqlite") as { DatabaseSync: new (path: string) => DatabaseSync }; const raw = new sqlite.DatabaseSync(join(dir, SESSION_CATALOG_DB_FILENAME)); raw.exec("PRAGMA user_version = 99"); raw.close(); const reopened = openSessionCatalogStore(dir)!; expect(reopened.read(file)).toBeUndefined(); reopened.write(createRow(file, { dir, id: "session-3" })); expect(reopened.read(file)).toMatchObject({ id: "session-3" }); reopened.close(); }); it("does not replace a database it cannot open", () => { const dir = createDir(); const dbPath = join(dir, SESSION_CATALOG_DB_FILENAME); mkdirSync(dbPath); expect(openSessionCatalogStore(dir)).toBeUndefined(); expect(existsSync(dbPath)).toBe(true); rmSync(dbPath, { recursive: true, force: true }); const store = openSessionCatalogStore(dir); expect(store).toBeDefined(); store!.close(); }); it("never throws after the connection is closed", () => { const dir = createDir(); const store = openSessionCatalogStore(dir)!; const file = join(dir, "a.jsonl"); store.write(createRow(file, { dir })); store.close(); expect(() => store.write(createRow(file, { dir }))).not.toThrow(); expect(() => store.remove(file)).not.toThrow(); expect(() => store.retain(dir, new Set())).not.toThrow(); expect(store.read(file)).toBeUndefined(); expect(readFileSync(join(dir, SESSION_CATALOG_DB_FILENAME)).length).toBeGreaterThan(0); }); });