import assert from "node:assert/strict"; import { CURRENT_SESSION_VERSION, SessionManager } from "@earendil-works/pi-coding-agent"; import { existsSync } from "node:fs"; import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import test from "node:test"; import { prepareSessionForCwd, switchToCwd } from "../src/session.ts"; interface FakeContext { sessionManager: { getSessionFile(): string | undefined }; } function fakeCtx(getSessionFile: () => string | undefined): FakeContext { return { sessionManager: { getSessionFile } }; } test("prepareSessionForCwd forks an existing session file into the target cwd", async (t) => { const agentDir = await mkdtemp(join(tmpdir(), "pi-worktree-agent-")); const previous = process.env.PI_CODING_AGENT_DIR; process.env.PI_CODING_AGENT_DIR = agentDir; t.after(() => { if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR; else process.env.PI_CODING_AGENT_DIR = previous; }); const root = await mkdtemp(join(tmpdir(), "pi-worktree-session-")); const sourceCwd = join(root, "source"); const targetCwd = join(root, "target"); await mkdir(sourceCwd, { recursive: true }); await mkdir(targetCwd, { recursive: true }); // Hand-written session file: header plus one user message entry. const sessionDir = join(root, "sessions"); await mkdir(sessionDir, { recursive: true }); const file = join(sessionDir, "source-session.jsonl"); const header = { type: "session", version: CURRENT_SESSION_VERSION, id: "11111111-1111-4111-8111-111111111111", timestamp: new Date().toISOString(), cwd: sourceCwd, }; const message = { type: "message", id: "22222222-2222-4222-8222-222222222222", parentId: null, timestamp: new Date().toISOString(), message: { role: "user", content: [{ type: "text", text: "hello" }], timestamp: Date.now(), }, }; await writeFile(file, `${JSON.stringify(header)}\n${JSON.stringify(message)}\n`, "utf8"); const newFile = prepareSessionForCwd(fakeCtx(() => file), targetCwd); assert.ok(existsSync(newFile)); assert.notEqual(newFile, file); const opened = SessionManager.open(newFile); assert.equal(opened.getCwd(), resolve(targetCwd)); const entries = opened.getEntries(); assert.ok(entries.some((e) => e.type === "message"), "forked session keeps the history"); }); test("prepareSessionForCwd writes a minimal session file when none exists yet", async (t) => { const agentDir = await mkdtemp(join(tmpdir(), "pi-worktree-agent-")); const previous = process.env.PI_CODING_AGENT_DIR; process.env.PI_CODING_AGENT_DIR = agentDir; t.after(() => { if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR; else process.env.PI_CODING_AGENT_DIR = previous; }); const targetCwd = await mkdtemp(join(tmpdir(), "pi-worktree-target-")); const newFile = prepareSessionForCwd(fakeCtx(() => undefined), targetCwd); assert.ok(existsSync(newFile)); assert.equal(SessionManager.open(newFile).getCwd(), resolve(targetCwd)); }); test("switchToCwd appends a cwd-change notice before running the callback", async (t) => { const agentDir = await mkdtemp(join(tmpdir(), "pi-worktree-agent-")); const previous = process.env.PI_CODING_AGENT_DIR; process.env.PI_CODING_AGENT_DIR = agentDir; t.after(() => { if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR; else process.env.PI_CODING_AGENT_DIR = previous; }); const targetCwd = await mkdtemp(join(tmpdir(), "pi-worktree-target-")); const order: string[] = []; const sent: { message: unknown; options: unknown }[] = []; const replacement = { sendMessage: async (message: unknown, options: unknown) => { order.push("sendMessage"); sent.push({ message, options }); }, }; const ctx = { cwd: "/a", waitForIdle: async () => undefined, sessionManager: { getSessionFile: () => undefined }, switchSession: async (_file: string, opts: { withSession: (c: unknown) => Promise }) => { await opts.withSession(replacement); return { cancelled: false }; }, }; const switched = await switchToCwd(ctx as never, targetCwd, async () => { order.push("withSession"); }); assert.equal(switched.cancelled, false); assert.deepEqual(order, ["sendMessage", "withSession"]); assert.equal(sent.length, 1); const { message, options } = sent[0] as { message: { customType?: string; content?: string; display?: boolean }; options: unknown }; assert.equal(message.customType, "pi-worktree"); assert.equal(message.display, true); assert.ok(message.content?.includes("/a"), "notice names the previous cwd"); assert.ok(message.content?.includes(targetCwd), "notice names the new cwd"); assert.equal(options, undefined); }); test("switchToCwd skips the notice when the cwd did not change", async (t) => { const agentDir = await mkdtemp(join(tmpdir(), "pi-worktree-agent-")); const previous = process.env.PI_CODING_AGENT_DIR; process.env.PI_CODING_AGENT_DIR = agentDir; t.after(() => { if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR; else process.env.PI_CODING_AGENT_DIR = previous; }); const targetCwd = await mkdtemp(join(tmpdir(), "pi-worktree-target-")); let sent = 0; const ctx = { cwd: targetCwd, waitForIdle: async () => undefined, sessionManager: { getSessionFile: () => undefined }, switchSession: async (_file: string, opts: { withSession: (c: unknown) => Promise }) => { await opts.withSession({ sendMessage: async () => sent++ }); return { cancelled: false }; }, }; await switchToCwd(ctx as never, targetCwd, async () => undefined); assert.equal(sent, 0); });