import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it, vi } from "vitest"; import { CURRENT_SESSION_VERSION, SessionManager } from "../../src/core/session-manager.js"; import { resolveSessionPath, SessionSelectorAmbiguousError, SessionSelectorNotFoundError, } from "../../src/core/session-resolver.js"; import { assistantMsg, userMsg } from "../utilities.js"; function createSession(cwd: string, sessionDir: string, prompt: string): SessionManager { const session = SessionManager.create(cwd, sessionDir); session.appendMessage(userMsg(prompt)); session.appendMessage(assistantMsg(`${prompt} answer`)); return session; } /** Header-only transcript, so decoy ids stay independent of session-id generation. */ function writeHeaderOnlySession(sessionDir: string, id: string, cwd: string): string { mkdirSync(sessionDir, { recursive: true }); const path = join(sessionDir, `${id}.jsonl`); const header = { type: "session", version: CURRENT_SESSION_VERSION, id, timestamp: new Date().toISOString(), cwd }; writeFileSync(path, `${JSON.stringify(header)}\n`); return path; } describe("resolveSessionPath", () => { it("resolves a session id from the directory listing without scanning the catalog", async () => { const tempDir = mkdtempSync(join(tmpdir(), "session-selector-")); try { const sessionDir = join(tempDir, "sessions"); const cwd = join(tempDir, "project"); const target = createSession(cwd, sessionDir, "target"); writeHeaderOnlySession(sessionDir, "decoy-0000", join(tempDir, "other")); const listSpy = vi.spyOn(SessionManager, "list"); const listAllSpy = vi.spyOn(SessionManager, "listAll"); const resolved = await resolveSessionPath(target.getSessionId(), cwd, sessionDir); expect(resolved).toEqual({ type: "local", path: target.getSessionFile() }); expect(listSpy).not.toHaveBeenCalled(); expect(listAllSpy).not.toHaveBeenCalled(); listSpy.mockRestore(); listAllSpy.mockRestore(); } finally { rmSync(tempDir, { recursive: true, force: true }); } }); it("resolves an id prefix and a display-id suffix without scanning the catalog", async () => { const tempDir = mkdtempSync(join(tmpdir(), "session-selector-")); try { const sessionDir = join(tempDir, "sessions"); const cwd = join(tempDir, "project"); const target = createSession(cwd, sessionDir, "target"); writeHeaderOnlySession(sessionDir, "decoy-0000", join(tempDir, "other")); const listSpy = vi.spyOn(SessionManager, "list"); const listAllSpy = vi.spyOn(SessionManager, "listAll"); const byPrefix = await resolveSessionPath(target.getSessionId().slice(0, 8), cwd, sessionDir); const bySuffix = await resolveSessionPath(target.getSessionId().slice(-12), cwd, sessionDir); expect(byPrefix).toEqual({ type: "local", path: target.getSessionFile() }); expect(bySuffix).toEqual({ type: "local", path: target.getSessionFile() }); expect(listSpy).not.toHaveBeenCalled(); expect(listAllSpy).not.toHaveBeenCalled(); listSpy.mockRestore(); listAllSpy.mockRestore(); } finally { rmSync(tempDir, { recursive: true, force: true }); } }); it("reports a session from another project as global with its saved cwd", async () => { const tempDir = mkdtempSync(join(tmpdir(), "session-selector-")); try { const sessionDir = join(tempDir, "sessions"); const otherCwd = join(tempDir, "other"); const target = createSession(otherCwd, sessionDir, "target"); const resolved = await resolveSessionPath(target.getSessionId(), join(tempDir, "project"), sessionDir); expect(resolved).toEqual({ type: "global", path: target.getSessionFile(), cwd: otherCwd }); } finally { rmSync(tempDir, { recursive: true, force: true }); } }); it("falls back to the scanning resolver for ambiguous and unknown selectors", async () => { const tempDir = mkdtempSync(join(tmpdir(), "session-selector-")); try { const sessionDir = join(tempDir, "sessions"); const cwd = join(tempDir, "project"); writeHeaderOnlySession(sessionDir, "kolbr-aaa", cwd); writeHeaderOnlySession(sessionDir, "kolbr-bbb", cwd); await expect(resolveSessionPath("kolbr", cwd, sessionDir)).rejects.toBeInstanceOf( SessionSelectorAmbiguousError, ); await expect(resolveSessionPath("zzzznope", cwd, sessionDir)).rejects.toBeInstanceOf( SessionSelectorNotFoundError, ); } finally { rmSync(tempDir, { recursive: true, force: true }); } }); it("does not resolve a transcript whose header id disagrees with its file name", async () => { const tempDir = mkdtempSync(join(tmpdir(), "session-selector-")); try { const sessionDir = join(tempDir, "sessions"); const cwd = join(tempDir, "project"); const target = createSession(cwd, sessionDir, "target"); writeFileSync(join(sessionDir, "renamed.jsonl"), readFileSync(target.getSessionFile()!)); await expect(resolveSessionPath("renamed", cwd, sessionDir)).rejects.toBeInstanceOf( SessionSelectorNotFoundError, ); // The header id still resolves through the scanning fallback. const byHeaderId = await resolveSessionPath(target.getSessionId(), cwd, sessionDir); expect(byHeaderId.path).toBe(target.getSessionFile()); } finally { rmSync(tempDir, { recursive: true, force: true }); } }); });