Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/klaus/__tests__/debug.test.ts

Raw
import {
	mkdir,
	mkdtemp,
	readdir,
	readFile,
	rm,
	stat,
	writeFile,
} from "node:fs/promises";
import { dirname, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

let root = "";
const original = {
	home: process.env.HOME,
	klausDebug: process.env.PI_KLAUS_DEBUG,
	legacyDebug: process.env.KLAUS_DEBUG_LOG,
	localAppData: process.env.LOCALAPPDATA,
	stateHome: process.env.XDG_STATE_HOME,
};

function restore(name: keyof typeof original, value: string | undefined): void {
	const key =
		name === "home"
			? "HOME"
			: name === "klausDebug"
				? "PI_KLAUS_DEBUG"
				: name === "legacyDebug"
					? "KLAUS_DEBUG_LOG"
					: name === "localAppData"
						? "LOCALAPPDATA"
						: "XDG_STATE_HOME";
	if (value === undefined) delete process.env[key];
	else process.env[key] = value;
}

async function writeSettings(debug: unknown): Promise<void> {
	const agentDirectory = join(root, ".pi", "agent");
	await mkdir(agentDirectory, { recursive: true });
	await writeFile(
		join(agentDirectory, "settings.json"),
		JSON.stringify({ "pi-ext": { debug } }),
	);
}

async function tracePaths(directory = root): Promise<string[]> {
	const paths: string[] = [];
	for (const entry of await readdir(directory, { withFileTypes: true })) {
		const path = join(directory, entry.name);
		if (entry.isDirectory()) paths.push(...(await tracePaths(path)));
		else if (entry.isFile() && entry.name.endsWith(".jsonl")) paths.push(path);
	}
	return paths;
}

async function tracePath(): Promise<string> {
	const paths = await tracePaths();
	expect(paths).toHaveLength(1);
	return paths[0];
}

beforeEach(async () => {
	vi.resetModules();
	vi.doUnmock("node:fs");
	root = await mkdtemp(join(process.cwd(), ".klaus-debug-test-"));
	process.env.HOME = root;
	process.env.XDG_STATE_HOME = join(root, "state");
	process.env.LOCALAPPDATA = join(root, "local-app-data");
	delete process.env.PI_KLAUS_DEBUG;
	delete process.env.KLAUS_DEBUG_LOG;
});

afterEach(async () => {
	vi.doUnmock("node:fs");
	for (const [name, value] of Object.entries(original))
		restore(name as keyof typeof original, value);
	if (root) await rm(root, { recursive: true, force: true });
	root = "";
});

describe("dbg", () => {
	it("selects Klaus through global settings and writes private safe JSONL", async () => {
		await writeSettings(["other", "klaus"]);
		const { dbg } = await import("../src/pi-ext-debug.js");

		dbg?.("test.event", {
			count: 2,
			modelId: "claude-sonnet-5",
			message: "secret prompt",
			payload: { value: "private-data" },
		});

		const path = await tracePath();
		const record = JSON.parse((await readFile(path, "utf8")).trim());
		expect(record).toMatchObject({
			extension: "klaus",
			pid: process.pid,
			event: "test.event",
			count: 2,
			modelId: "claude-sonnet-5",
		});
		expect(record.timestamp).toEqual(expect.any(String));
		expect(JSON.stringify(record)).not.toContain("secret");
		if (process.platform !== "win32") {
			expect((await stat(path)).mode & 0o777).toBe(0o600);
			expect((await stat(dirname(path))).mode & 0o777).toBe(0o700);
		}
	});

	it("lets PI_KLAUS_DEBUG override global selection and ignores legacy paths", async () => {
		await writeSettings(true);
		process.env.PI_KLAUS_DEBUG = "0";
		let debug = await import("../src/pi-ext-debug.js");
		expect(debug.dbg).toBeUndefined();

		vi.resetModules();
		await writeSettings(["other"]);
		process.env.PI_KLAUS_DEBUG = "1";
		process.env.KLAUS_DEBUG_LOG = join(root, "legacy.jsonl");
		debug = await import("../src/pi-ext-debug.js");
		debug.dbg?.("test.env");

		expect(await tracePath()).toMatch(/\.jsonl$/);
		await expect(stat(process.env.KLAUS_DEBUG_LOG)).rejects.toMatchObject({
			code: "ENOENT",
		});
	});

	it("does no debug work when disabled", async () => {
		await writeSettings(["other"]);
		const { dbg } = await import("../src/pi-ext-debug.js");
		let evaluated = false;

		dbg?.(
			"test.disabled",
			(() => {
				evaluated = true;
				return {};
			})(),
		);

		expect(dbg).toBeUndefined();
		expect(evaluated).toBe(false);
		expect(await tracePaths()).toEqual([]);
	});

	it("records unique monotonic spans with one completion each", async () => {
		await writeSettings("klaus");
		const { span: debugSpan } = await import("../src/pi-ext-debug.js");
		const first = debugSpan?.("test.operation", { modelId: "claude-opus-5" });
		const second = debugSpan?.("test.operation");
		await new Promise((resolve) => setTimeout(resolve, 1));
		first?.();
		first?.("error");
		second?.("error");

		const records = (await readFile(await tracePath(), "utf8"))
			.trim()
			.split("\n")
			.map((line) => JSON.parse(line));
		const starts = records.filter((record) => record.event.endsWith(".start"));
		const completions = records.filter(
			(record) =>
				record.event.endsWith(".finish") || record.event.endsWith(".error"),
		);
		expect(starts).toHaveLength(2);
		expect(completions).toHaveLength(2);
		expect(starts[0].spanId).not.toBe(starts[1].spanId);
		expect(completions[0]).toMatchObject({
			event: "test.operation.finish",
			spanId: starts[0].spanId,
		});
		expect(completions[0].durationMs).toBeGreaterThanOrEqual(0);
	});

	it("permanently stops after a write failure", async () => {
		await writeSettings(true);
		const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
		const writeSync = vi.fn(() => {
			throw new Error("write failed");
		});
		vi.doMock("node:fs", () => ({ ...actual, writeSync }));
		const { dbg, closeDebug } = await import("../src/pi-ext-debug.js");

		dbg?.("test.failure");
		closeDebug();
		dbg?.("test.failure");

		expect(writeSync).toHaveBeenCalledOnce();
		expect(await tracePath()).toBeTruthy();
	});

	it("classifies diagnostics without retaining diagnostic text", async () => {
		const { diagnosticKind } = await import("../src/debug.js");

		expect(
			diagnosticKind(new Error("No message found with message.uuid of: x")),
		).toBe("missing-resume-uuid");
		expect(
			diagnosticKind(new Error("OAuth access token has been revoked")),
		).toBe("revoked-oauth");
		expect(diagnosticKind({ toString: () => "private detail" })).toBe(
			"redacted",
		);
	});

	it("caps each process file and writes one terminal record", async () => {
		process.env.PI_KLAUS_DEBUG = "1";
		const { dbg, closeDebug } = await import("../src/pi-ext-debug.js");
		const event = `test.limit.${"x".repeat(100)}`;
		const modelId = "m".repeat(128);
		for (let index = 0; index < 45_000; index += 1)
			dbg?.(event, { index, modelId });
		closeDebug();
		dbg?.("test.event");
		const path = await tracePath();
		const size = (await stat(path)).size;
		const records = (await readFile(path, "utf8"))
			.trim()
			.split("\n")
			.map((line) => JSON.parse(line));

		expect(size).toBeLessThanOrEqual(8 * 1024 * 1024);
		expect(
			records.filter((record) => record.event === "debug.limit"),
		).toHaveLength(1);
	});
});