import { existsSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { describe, expect, it } from "vitest"; import { SessionManager } from "../../src/core/session-manager.ts"; const UUID_V7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; describe("SessionManager.newSession with custom id", () => { it("uses the provided id instead of generating one", () => { const session = SessionManager.inMemory(); session.newSession({ id: "my-custom-id" }); expect(session.getSessionId()).toBe("my-custom-id"); }); it("allows alphanumeric session ids with interior punctuation", () => { const session = SessionManager.inMemory(); session.newSession({ id: "abc-123_def.456" }); expect(session.getSessionId()).toBe("abc-123_def.456"); }); it("rejects invalid custom session ids", () => { const invalidIds = ["", "-abc", "abc-", "_abc", "abc_", ".abc", "abc.", "abc/def", "abc\\def", "abc def"]; for (const id of invalidIds) { const session = SessionManager.inMemory(); expect(() => session.newSession({ id })).toThrow( "Session id must be non-empty, contain only alphanumeric characters", ); } }); it("generates a UUIDv7 id when no id is provided", () => { const session = SessionManager.inMemory(); session.newSession(); const id = session.getSessionId(); expect(id).toBeDefined(); expect(id).not.toBe(""); expect(id).toMatch(UUID_V7_RE); }); it("generates a UUIDv7 id when options is provided without id", () => { const session = SessionManager.inMemory(); session.newSession({ parentSession: "parent.jsonl" }); const id = session.getSessionId(); expect(id).toBeDefined(); expect(id).not.toBe(""); expect(id).toMatch(UUID_V7_RE); }); it("includes the custom id in the session header", () => { const session = SessionManager.inMemory(); session.newSession({ id: "header-test-id" }); const header = session.getHeader(); expect(header).not.toBeNull(); expect(header!.id).toBe("header-test-id"); }); it("generates a UUIDv7 id when constructed without an explicit id", () => { const session = SessionManager.inMemory(); expect(session.getSessionId()).toMatch(UUID_V7_RE); expect(session.getHeader()!.id).toBe(session.getSessionId()); }); it("uses the provided id when creating a persisted session", () => { const tempDir = mkdtempSync(join(tmpdir(), "pi-session-manager-")); const session = SessionManager.create(tempDir, tempDir, { id: "created-session-id" }); expect(session.getSessionId()).toBe("created-session-id"); expect(session.getHeader()!.id).toBe("created-session-id"); const sessionFile = session.getSessionFile()!; expect(sessionFile).toContain("created-session-id"); expect(basename(sessionFile)).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z_created-session-id\.jsonl$/); expect(existsSync(sessionFile)).toBe(false); }); it("generates a UUIDv7 id when creating a branched session", () => { const session = SessionManager.inMemory(); const firstId = session.appendMessage({ role: "user", content: [{ type: "text", text: "hello" }], timestamp: Date.now(), }); session.createBranchedSession(firstId); expect(session.getSessionId()).toMatch(UUID_V7_RE); expect(session.getHeader()!.id).toBe(session.getSessionId()); }); it("generates a UUIDv7 id when forking from another session file", () => { const tempDir = mkdtempSync(join(tmpdir(), "pi-session-manager-")); const sourcePath = join(tempDir, "source.jsonl"); writeFileSync( sourcePath, `${[ JSON.stringify({ type: "session", version: 3, id: "legacy-session-id", timestamp: new Date().toISOString(), cwd: tempDir, }), JSON.stringify({ type: "message", id: "entry-1", parentId: null, timestamp: new Date().toISOString(), message: { role: "assistant", content: [{ type: "text", text: "hello" }], api: "openai-responses", provider: "openai", model: "gpt-5.4", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }, stopReason: "stop", timestamp: Date.now(), }, }), ].join("\n")} `, ); const forked = SessionManager.forkFrom(sourcePath, tempDir, tempDir); const header = forked.getHeader(); expect(header).not.toBeNull(); expect(header!.id).toMatch(UUID_V7_RE); expect(header!.parentSession).toBe(sourcePath); }); it("uses the provided id when forking from another session file", () => { const tempDir = mkdtempSync(join(tmpdir(), "pi-session-manager-")); const sourcePath = join(tempDir, "source.jsonl"); writeFileSync( sourcePath, `${JSON.stringify({ type: "session", version: 3, id: "source-session-id", timestamp: new Date().toISOString(), cwd: tempDir, })}\n`, ); const forked = SessionManager.forkFrom(sourcePath, tempDir, tempDir, { id: "forked-session-id" }); const header = forked.getHeader(); expect(header).not.toBeNull(); expect(header!.id).toBe("forked-session-id"); expect(header!.parentSession).toBe(sourcePath); const sessionFile = forked.getSessionFile()!; expect(sessionFile).toContain("forked-session-id"); expect(basename(sessionFile)).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z_forked-session-id\.jsonl$/); }); });