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