This repository has been archived on 2026-08-23. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
pi-web/src/server/sessions/sessionArchiveStore.test.ts
T

179 lines
6.6 KiB
TypeScript

import { constants } from "node:fs";
import { access, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it } from "vitest";
import { SessionArchiveStore } from "./sessionArchiveStore.js";
const tempRoots: string[] = [];
describe("SessionArchiveStore", () => {
afterEach(async () => {
await Promise.all(tempRoots.splice(0).map((path) => rm(path, { recursive: true, force: true })));
});
it("moves archived session files out of the active session directory and restores them", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-web-archive-"));
tempRoots.push(root);
const activeDir = join(root, "active");
await mkdir(activeDir, { recursive: true });
const sourcePath = join(activeDir, "2026-01-01_s1.jsonl");
await writeFile(sourcePath, "session contents\n", "utf8");
const store = new SessionArchiveStore(join(root, "archived-sessions.json"), join(root, "archived-files"));
const record = await store.archive({
sessionId: "s1",
cwd: "/workspace",
path: sourcePath,
created: "2026-01-01T00:00:00.000Z",
modified: "2026-01-01T00:01:00.000Z",
messageCount: 2,
firstMessage: "hello",
});
expect(await exists(sourcePath)).toBe(false);
expect(record.originalPath).toBe(sourcePath);
expect(record.archivePath).toBeDefined();
if (record.archivePath === undefined) throw new Error("Expected archive path");
expect(await readFile(record.archivePath, "utf8")).toBe("session contents\n");
await expect(store.list()).resolves.toMatchObject([{ sessionId: "s1", originalPath: sourcePath, archivePath: record.archivePath, messageCount: 2 }]);
await store.restore("s1");
expect(await readFile(sourcePath, "utf8")).toBe("session contents\n");
expect(await exists(record.archivePath)).toBe(false);
await expect(store.list()).resolves.toEqual([]);
});
it("permanently deletes archived session files and records", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-web-archive-delete-"));
tempRoots.push(root);
const activeDir = join(root, "active");
await mkdir(activeDir, { recursive: true });
const sourcePath = join(activeDir, "2026-01-01_s1.jsonl");
await writeFile(sourcePath, "session contents\n", "utf8");
const store = new SessionArchiveStore(join(root, "archived-sessions.json"), join(root, "archived-files"));
const record = await store.archive({
sessionId: "s1",
cwd: "/workspace",
path: sourcePath,
created: "2026-01-01T00:00:00.000Z",
modified: "2026-01-01T00:01:00.000Z",
messageCount: 2,
firstMessage: "hello",
});
if (record.archivePath === undefined) throw new Error("Expected archive path");
await store.deleteArchived("s1");
expect(await exists(sourcePath)).toBe(false);
expect(await exists(record.archivePath)).toBe(false);
await expect(store.list()).resolves.toEqual([]);
});
it("archives and permanently deletes sessions in batches", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-web-archive-batch-"));
tempRoots.push(root);
const activeDir = join(root, "active");
await mkdir(activeDir, { recursive: true });
const sourceA = join(activeDir, "2026-01-01_a.jsonl");
const sourceB = join(activeDir, "2026-01-01_b.jsonl");
await writeFile(sourceA, "a\n", "utf8");
await writeFile(sourceB, "b\n", "utf8");
const store = new SessionArchiveStore(join(root, "archived-sessions.json"), join(root, "archived-files"));
const records = await store.archiveMany([
{
sessionId: "a",
cwd: "/workspace",
path: sourceA,
created: "2026-01-01T00:00:00.000Z",
modified: "2026-01-01T00:01:00.000Z",
messageCount: 1,
firstMessage: "a",
},
{
sessionId: "b",
cwd: "/workspace",
path: sourceB,
created: "2026-01-01T00:00:00.000Z",
modified: "2026-01-01T00:02:00.000Z",
messageCount: 2,
firstMessage: "b",
},
]);
expect(records.map((record) => record.sessionId)).toEqual(["a", "b"]);
expect(await exists(sourceA)).toBe(false);
expect(await exists(sourceB)).toBe(false);
await expect(store.list()).resolves.toMatchObject([{ sessionId: "a" }, { sessionId: "b" }]);
const archivePaths = records.map((record) => record.archivePath);
if (archivePaths.some((path) => path === undefined)) throw new Error("Expected archive paths");
await expect(store.deleteArchivedMany(["a", "b", "missing"])).resolves.toEqual(["a", "b"]);
for (const archivePath of archivePaths) {
if (archivePath === undefined) throw new Error("Expected archive path");
expect(await exists(archivePath)).toBe(false);
}
await expect(store.list()).resolves.toEqual([]);
});
it("prefers exact persisted session IDs over prefix matches and canonicalizes stored cwd", async () => {
const root = await mkdtemp(join(tmpdir(), "pi-web-archive-prefix-"));
tempRoots.push(root);
const archiveFile = join(root, "archived-sessions.json");
const rawCwd = join(root, "workspace", "..", "workspace");
await writeFile(archiveFile, JSON.stringify({
sessions: [
{
sessionId: "abc123",
cwd: rawCwd,
archivedAt: "2026-01-01T00:00:00.000Z",
originalPath: "/sessions/abc123.jsonl",
archivePath: "/archive/abc123.jsonl",
messageCount: 3,
firstMessage: "prefix",
name: "Prefix match",
parentSessionPath: "/sessions/root.jsonl",
},
{
sessionId: "abc",
cwd: rawCwd,
archivedAt: "2026-01-01T00:00:00.000Z",
originalPath: "/sessions/abc.jsonl",
archivePath: "/archive/abc.jsonl",
messageCount: 1,
firstMessage: "exact",
},
],
}), "utf8");
const store = new SessionArchiveStore(archiveFile, join(root, "archived-files"));
await expect(store.get("abc")).resolves.toMatchObject({
sessionId: "abc",
cwd: resolve(rawCwd),
firstMessage: "exact",
});
await expect(store.get("abc1")).resolves.toMatchObject({
sessionId: "abc123",
cwd: resolve(rawCwd),
firstMessage: "prefix",
name: "Prefix match",
parentSessionPath: "/sessions/root.jsonl",
});
await expect(store.isArchived("abc1")).resolves.toBe(true);
await expect(store.isArchived("missing")).resolves.toBe(false);
});
});
async function exists(path: string): Promise<boolean> {
try {
await access(path, constants.F_OK);
return true;
} catch {
return false;
}
}