import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestSession, sandboxEnv, type TestSession, withProcessEnv, } from "../../../test/harness"; describe("bak debug", () => { let root: string | undefined; let session: TestSession | undefined; afterEach(() => { session?.dispose(); if (root) rmSync(root, { recursive: true, force: true }); session = undefined; root = undefined; }); it("writes its lifecycle JSONL through the standalone factory", async () => { root = mkdtempSync(path.join(tmpdir(), "pi-ext-bak-debug-")); const env = sandboxEnv(root, { env: { PI_BAK_DEBUG: "1" } }); vi.resetModules(); const factory = await withProcessEnv( env, async () => (await import("../index.ts")).default, ); session = await createTestSession({ cwd: root, extensionFactories: [factory], env: { PI_BAK_DEBUG: "1" }, }); await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); await session.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); if (!env.XDG_STATE_HOME) throw new Error("missing sandbox state home"); const directory = path.join(env.XDG_STATE_HOME, "pi-ext", "debug", "bak"); const [file] = readdirSync(directory); const events = readFileSync(path.join(directory, file), "utf8") .trim() .split("\n") .map((line) => JSON.parse(line).event); expect(events).toEqual( expect.arrayContaining(["session.start", "session.shutdown"]), ); }); it.each([ ["true", true], ["false", false], ])( "attempts a shutdown backup only when bak.autoBackup is %s", async (_name, autoBackup) => { root = mkdtempSync(path.join(tmpdir(), "pi-ext-bak-debug-")); const env = sandboxEnv(root, { env: { PI_BAK_DEBUG: "1" } }); const agentDir = path.join(env.HOME ?? "", ".pi", "agent"); mkdirSync(agentDir, { recursive: true }); writeFileSync( path.join(agentDir, "settings.json"), JSON.stringify({ bak: { autoBackup } }), ); vi.resetModules(); const factory = await withProcessEnv( env, async () => (await import("../index.ts")).default, ); session = await createTestSession({ cwd: root, extensionFactories: [factory], env: { PI_BAK_DEBUG: "1" }, }); const runner = session.session.extensionRunner; await runner.emit({ type: "session_start", reason: "startup" }); await runner.emit({ type: "session_shutdown", reason: "quit" }); if (!env.XDG_STATE_HOME) throw new Error("missing sandbox state home"); const directory = path.join(env.XDG_STATE_HOME, "pi-ext", "debug", "bak"); const [file] = readdirSync(directory); // Every automatic attempt records an outcome, even when it has nothing to upload. const attempts = readFileSync(path.join(directory, file), "utf8") .trim() .split("\n") .map((line) => JSON.parse(line)) .filter((entry) => entry.operation === "automatic-backup"); expect(attempts.length > 0).toBe(autoBackup); }, ); it("records skipped, successful, and failed command outcomes", async () => { root = mkdtempSync(path.join(tmpdir(), "pi-ext-bak-debug-")); const env = sandboxEnv(root, { env: { PI_BAK_DEBUG: "1" } }); vi.resetModules(); session = await createTestSession({ cwd: root, env: { PI_BAK_DEBUG: "1" }, }); const ctx = session.session.extensionRunner.createContext(); const { createBakRuntime } = await withProcessEnv( env, () => import("../implementation.ts"), ); const { closeDebug } = await withProcessEnv( env, () => import("../src/debug.ts"), ); const disabled = createBakRuntime({ missing: ["PI_BAK_S3_ENDPOINT"], }); disabled.sessionStart(ctx); await disabled.command.handler("backup", ctx); const configured = createBakRuntime({ missing: [], config: { endpoint: "https://example.invalid", bucket: "test", region: "auto", accessKeyId: "id", secretAccessKey: "secret", }, }); configured.sessionStart(ctx); await configured.command.handler("status", ctx); await configured.command.handler("init", ctx); closeDebug(); if (!env.XDG_STATE_HOME) throw new Error("missing sandbox state home"); const directory = path.join(env.XDG_STATE_HOME, "pi-ext", "debug", "bak"); const [file] = readdirSync(directory); const outcomes = readFileSync(path.join(directory, file), "utf8") .trim() .split("\n") .map((line) => JSON.parse(line)) .filter((entry) => entry.event === "operation.finish"); expect(outcomes).toEqual( expect.arrayContaining([ expect.objectContaining({ operation: "command", subcommand: "backup", outcome: "skipped", }), expect.objectContaining({ operation: "command", subcommand: "status", outcome: "success", }), expect.objectContaining({ operation: "command", subcommand: "init", outcome: "failed", }), ]), ); }); });