Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/ask/__tests__/debug.test.ts

Raw
import { mkdtempSync, readdirSync, readFileSync, rmSync } 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("ask 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-ask-debug-"));
		const env = sandboxEnv(root, { env: { PI_ASK_DEBUG: "1" } });
		vi.resetModules();
		const factory = await withProcessEnv(
			env,
			async () => (await import("../index.ts")).default,
		);
		session = await createTestSession({
			cwd: root,
			extensionFactories: [factory],
			env: { PI_ASK_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", "ask");
		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("keeps declared fields and rejects unknown events, values, and accessors", async () => {
		root = mkdtempSync(path.join(tmpdir(), "pi-ext-ask-schema-"));
		const env = sandboxEnv(root, { env: { PI_ASK_DEBUG: "1" } });
		vi.resetModules();
		await withProcessEnv(env, async () => {
			const { defineDebug } = await import("../src/pi-ext-debug.ts");
			const { dbg, span, closeDebug } = defineDebug({
				events: ["request.done"],
				spans: ["request"],
				fields: {
					retryCount: "number",
					hasToken: "boolean",
					outcome: ["ok", "timeout"],
				},
			} as const);
			const hostile = Object.defineProperties(
				{ retryCount: 2, hasToken: true, outcome: "ok", payload: "secret" },
				{
					secret: {
						get: () => {
							throw new Error("must not read undeclared metadata");
						},
					},
				},
			);
			dbg?.("request.done", hostile);
			(dbg as ((event: string, details?: unknown) => void) | undefined)?.(
				"token-shaped-secret",
				{ retryCount: 3 },
			);
			const end = span?.("request", { retryCount: 4 });
			end?.("finish", { outcome: "timeout" });
			(dbg as ((event: string, details?: unknown) => void) | undefined)?.(
				"request.done",
				{ outcome: "sk-test-ABC123", retryCount: Number.POSITIVE_INFINITY },
			);
			const accessor = Object.defineProperty({}, "retryCount", {
				get: () => {
					throw new Error("must not inspect declared accessors");
				},
			});
			(dbg as ((event: string, details?: unknown) => void) | undefined)?.(
				"request.done",
				accessor,
			);
			expect(
				defineDebug({
					events: ["invalid"],
					spans: [],
					fields: { pid: "number" },
				}).dbg,
			).toBeUndefined();
			closeDebug();
		});

		if (!env.XDG_STATE_HOME) throw new Error("missing sandbox state home");
		const directory = path.join(env.XDG_STATE_HOME, "pi-ext", "debug", "ask");
		const [file] = readdirSync(directory);
		const records = readFileSync(path.join(directory, file), "utf8")
			.trim()
			.split("\n")
			.map((line) => JSON.parse(line));
		expect(records).toEqual([
			expect.objectContaining({
				event: "request.done",
				retryCount: 2,
				hasToken: true,
				outcome: "ok",
			}),
			expect.objectContaining({
				event: "request.start",
				retryCount: 4,
				spanId: expect.any(String),
			}),
			expect.objectContaining({
				event: "request.finish",
				outcome: "timeout",
				durationMs: expect.any(Number),
			}),
			expect.objectContaining({ event: "request.done" }),
			expect.objectContaining({ event: "request.done" }),
		]);
		expect(JSON.stringify(records)).not.toMatch(/secret|payload|Infinity/);
		expect(records[3]).not.toHaveProperty("outcome");
		expect(records[3]).not.toHaveProperty("retryCount");
		expect(records[4]).not.toHaveProperty("retryCount");
	});

	it("continues in a new private file after a clean close", async () => {
		root = mkdtempSync(path.join(tmpdir(), "pi-ext-ask-segments-"));
		const env = sandboxEnv(root, { env: { PI_ASK_DEBUG: "1" } });
		await withProcessEnv(env, async () => {
			vi.resetModules();
			const { defineDebug } = await import("../src/pi-ext-debug.ts");
			const { dbg, span, closeDebug } = defineDebug({
				events: ["session.start"],
				spans: ["operation"],
				fields: {},
			} as const);
			const end = span?.("operation");
			closeDebug();
			closeDebug();
			end?.();
			dbg?.("session.start");
			closeDebug();
		});
		if (!env.XDG_STATE_HOME) throw new Error("missing sandbox state home");
		const directory = path.join(env.XDG_STATE_HOME, "pi-ext", "debug", "ask");
		const segments = readdirSync(directory).map((file) =>
			readFileSync(path.join(directory, file), "utf8")
				.trim()
				.split("\n")
				.map((line) => JSON.parse(line)),
		);
		expect(segments).toHaveLength(2);
		const starts = segments
			.flat()
			.filter((row) => row.event === "operation.start");
		const finishes = segments
			.flat()
			.filter((row) => row.event === "operation.finish");
		expect(starts).toHaveLength(1);
		expect(finishes).toEqual([
			expect.objectContaining({
				spanId: starts[0].spanId,
				durationMs: expect.any(Number),
			}),
		]);
		expect(segments.flat().map((row) => row.event)).toContain("session.start");
	});
});