repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/ask/__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";
describe("ask debug", () => {
let session: PiSession | undefined;
afterEach(async () => {
await session?.dispose();
session = undefined;
vi.unstubAllEnvs();
});
it("writes its lifecycle JSONL through the standalone factory", async () => {
session = await createPiSession({
extensionPaths: [fileURLToPath(new URL("../index.ts", import.meta.url))],
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",
});
const stateHome = session.env["XDG_STATE_HOME"];
if (!stateHome) throw new Error("missing sandbox state home");
const directory = path.join(stateHome, "pi-ext", "debug", "ask");
const events = await vi.waitFor(() => {
const [file] = readdirSync(directory);
if (!file) throw new Error("no debug file yet");
const parsed = readFileSync(path.join(directory, file), "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line).event);
if (
!parsed.includes("session.start") ||
!parsed.includes("session.shutdown")
)
throw new Error("lifecycle events not flushed yet");
return parsed;
});
expect(events).toEqual(
expect.arrayContaining(["session.start", "session.shutdown"]),
);
});
it("keeps declared fields and rejects unknown events, values, and accessors", async () => {
session = await createPiSession({ env: { PI_ASK_DEBUG: "1" } });
vi.resetModules();
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 as Parameters<NonNullable<typeof dbg>>[1]);
(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();
const stateHome = session.env["XDG_STATE_HOME"];
if (!stateHome) throw new Error("missing sandbox state home");
const directory = path.join(stateHome, "pi-ext", "debug", "ask");
const [file] = readdirSync(directory);
if (!file) throw new Error("no debug file");
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 () => {
session = await createPiSession({ env: { PI_ASK_DEBUG: "1" } });
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();
const stateHome = session.env["XDG_STATE_HOME"];
if (!stateHome) throw new Error("missing sandbox state home");
const directory = path.join(stateHome, "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");
});
});