repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/klaus/__tests__/debug.test.ts
Rawimport {
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);
});
});