import { readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createPiSession, type PiSession, } from "../../../test/fixture/create-pi-session.ts"; const extensionPath = fileURLToPath(new URL("../index.ts", import.meta.url)); // The fixture inherits the full ambient env; clear every bak variable so the // sandbox is a clean per-test slate. const CLEAR_BAK_ENV: Record = { PI_BAK_S3_ENDPOINT: undefined, PI_BAK_S3_BUCKET: undefined, PI_BAK_S3_REGION: undefined, PI_BAK_S3_ACCESS_KEY_ID: undefined, PI_BAK_S3_SECRET_ACCESS_KEY: undefined, PI_BAK_AUTO_BACKUP: undefined, PI_BAK_DEBUG: undefined, }; const debugEnv = (extra: Record = {}) => ({ ...CLEAR_BAK_ENV, PI_BAK_DEBUG: "1", ...extra, }); function debugDir(session: PiSession): string { if (!session.env["XDG_STATE_HOME"]) throw new Error("missing sandbox state home"); return path.join(session.env["XDG_STATE_HOME"], "pi-ext", "debug", "bak"); } function readEntries(directory: string): Array> { const [file] = readdirSync(directory); if (!file) throw new Error("no debug file yet"); return readFileSync(path.join(directory, file), "utf8") .trim() .split("\n") .map((line) => JSON.parse(line)); } describe("bak debug", () => { let session: PiSession | undefined; afterEach(async () => { await session?.dispose(); session = undefined; }); it("writes its lifecycle JSONL through the standalone factory", async () => { session = await createPiSession({ extensionPaths: [extensionPath], env: debugEnv(), }); await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); await session.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); const directory = debugDir(session); // Debug records flush asynchronously; poll until they appear. await vi.waitFor( () => { const events = readEntries(directory).map((entry) => entry["event"]); expect(events).toEqual( expect.arrayContaining(["session.start", "session.shutdown"]), ); }, { timeout: 5_000 }, ); }); it.each([ ["true", true], ["false", false], ])( "attempts a shutdown backup only when bak.autoBackup is %s", async (_name, autoBackup) => { session = await createPiSession({ extensionPaths: [extensionPath], userSettings: { bak: { autoBackup } }, env: debugEnv(), }); const runner = session.session.extensionRunner; await runner.emit({ type: "session_start", reason: "startup" }); await runner.emit({ type: "session_shutdown", reason: "quit" }); const directory = debugDir(session); // Automatic backup records are written before session.shutdown; poll for // the shutdown marker, then every flushed automatic attempt is present. await vi.waitFor( () => { const entries = readEntries(directory); if (!entries.some((entry) => entry["event"] === "session.shutdown")) throw new Error("shutdown not recorded yet"); const attempts = entries.filter( (entry) => entry["operation"] === "automatic-backup", ); // Every automatic attempt records an outcome, even with nothing to upload. expect(attempts.length > 0).toBe(autoBackup); }, { timeout: 5_000 }, ); }, ); it("records skipped, successful, and failed command outcomes", async () => { session = await createPiSession({ env: debugEnv() }); const ctx = session.session.extensionRunner.createContext(); // Env is applied to process.env before these imports, so the debug module // reads PI_BAK_DEBUG/XDG_STATE_HOME at import time. const { createBakRuntime } = await import("../implementation.ts"); const { closeDebug } = await 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(); const directory = debugDir(session); await vi.waitFor( () => { const outcomes = readEntries(directory).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", }), ]), ); }, { timeout: 5_000 }, ); }); });