repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/bak/__tests__/debug.test.ts
Rawimport { 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<string, string | undefined> = {
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<string, string | undefined> = {}) => ({
...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<Record<string, unknown>> {
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 },
);
});
});