Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/bak/__tests__/debug.test.ts

Raw
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<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 },
		);
	});
});