repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/firefox-bidi/__tests__/debug.test.ts
Rawimport { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
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";
const extensionPath = fileURLToPath(new URL("../index.ts", import.meta.url));
function envValue(env: NodeJS.ProcessEnv, key: string): string {
const value = env[key];
if (!value) throw new Error(`missing sandbox environment variable: ${key}`);
return value;
}
function directory(env: NodeJS.ProcessEnv): string {
const home = envValue(env, "HOME");
if (process.platform === "darwin")
return path.join(home, "Library", "Logs", "pi-ext", "firefox-bidi");
if (process.platform === "win32")
return path.join(
env["LOCALAPPDATA"] ?? path.join(home, "AppData", "Local"),
"pi-ext",
"debug",
"firefox-bidi",
);
return path.join(
envValue(env, "XDG_STATE_HOME"),
"pi-ext",
"debug",
"firefox-bidi",
);
}
function records(env: NodeJS.ProcessEnv): Record<string, unknown>[] {
try {
const files = readdirSync(directory(env)).filter((file) =>
file.endsWith(".jsonl"),
);
return files.flatMap((file) =>
readFileSync(path.join(directory(env), file), "utf8")
.trim()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line) as Record<string, unknown>),
);
} catch {
return [];
}
}
function sandboxEnv(root: string, debug?: "0" | "1"): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
...process.env,
HOME: path.join(root, ".test-home"),
USERPROFILE: path.join(root, ".test-home"),
XDG_STATE_HOME: path.join(root, ".test-state"),
APPDATA: path.join(root, ".test-config"),
LOCALAPPDATA: path.join(root, ".test-state"),
};
delete env["PI_CODING_AGENT_DIR"];
if (debug !== undefined) env["PI_FIREFOX_BIDI_DEBUG"] = debug;
return env;
}
async function withProcessEnv<T>(
env: NodeJS.ProcessEnv,
fn: () => T | Promise<T>,
): Promise<T> {
const original = { ...process.env };
for (const key of Object.keys(process.env)) delete process.env[key];
Object.assign(process.env, env);
try {
return await fn();
} finally {
for (const key of Object.keys(process.env)) delete process.env[key];
Object.assign(process.env, original);
}
}
async function writeSettings(
env: NodeJS.ProcessEnv,
debug: unknown,
): Promise<void> {
const agentDirectory = path.join(envValue(env, "HOME"), ".pi", "agent");
await mkdir(agentDirectory, { recursive: true });
await writeFile(
path.join(agentDirectory, "settings.json"),
JSON.stringify({ "pi-ext": { debug } }),
);
}
describe("firefox-bidi debug", () => {
let root: string | undefined;
let session: PiSession | undefined;
afterEach(async () => {
vi.doUnmock("node:fs");
vi.resetModules();
await session?.dispose();
if (root) rmSync(root, { recursive: true, force: true });
session = undefined;
root = undefined;
});
it("writes lifecycle events and pairs command spans", async () => {
root = mkdtempSync(path.join(tmpdir(), "pi-ext-firefox-bidi-debug-"));
session = await createPiSession({
cwd: root,
extensionPaths: [extensionPath],
env: { PI_FIREFOX_BIDI_DEBUG: "1" },
});
const runner = session.session.extensionRunner;
await runner.emit({ type: "session_start", reason: "startup" });
await runner
.getCommand("firefox")
?.handler("close", runner.createCommandContext());
await runner.emit({ type: "session_shutdown", reason: "quit" });
const sandbox = session.env;
await vi.waitFor(() => {
const events = records(sandbox).map((record) => record["event"]);
expect(events).toEqual(
expect.arrayContaining([
"session.start",
"session.shutdown",
"firefox.command.start",
"firefox.command.finish",
]),
);
});
const trace = records(session.env);
const start = trace.find(
(record) => record["event"] === "firefox.command.start",
);
const finish = trace.find(
(record) => record["event"] === "firefox.command.finish",
);
expect(finish).toMatchObject({
spanId: start?.["spanId"],
durationMs: expect.any(Number),
});
expect(finish?.["durationMs"]).toBeGreaterThanOrEqual(0);
});
it("uses user-level extension selection", async () => {
root = mkdtempSync(path.join(tmpdir(), "pi-ext-firefox-bidi-debug-"));
const env = sandboxEnv(root);
await writeSettings(env, "firefox-bidi");
vi.resetModules();
const debug = await withProcessEnv(
env,
() => import("../src/pi-ext-debug.ts"),
);
await withProcessEnv(env, () => {
debug.dbg?.("test.selection", { count: 1, payload: "secret" });
debug.closeDebug();
});
const [record] = records(env);
expect(record).toMatchObject({
extension: "firefox-bidi",
event: "test.selection",
count: 1,
});
expect(JSON.stringify(record)).not.toContain("secret");
});
it("does no debug work when explicit disable overrides settings", async () => {
root = mkdtempSync(path.join(tmpdir(), "pi-ext-firefox-bidi-debug-"));
const env = sandboxEnv(root, "0");
await writeSettings(env, true);
vi.resetModules();
const debug = await withProcessEnv(
env,
() => import("../src/pi-ext-debug.ts"),
);
let evaluated = false;
debug.dbg?.(
"test.disabled",
(() => {
evaluated = true;
return {};
})(),
);
expect(debug.dbg).toBeUndefined();
expect(evaluated).toBe(false);
expect(records(env)).toEqual([]);
});
it("does not throw when a debug write fails", async () => {
root = mkdtempSync(path.join(tmpdir(), "pi-ext-firefox-bidi-debug-"));
const env = sandboxEnv(root, "1");
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 }));
vi.resetModules();
const debug = await withProcessEnv(
env,
() => import("../src/pi-ext-debug.ts"),
);
await expect(
withProcessEnv(env, () => {
debug.dbg?.("test.failure");
debug.dbg?.("test.failure");
debug.closeDebug();
}),
).resolves.toBeUndefined();
expect(writeSync).toHaveBeenCalledOnce();
});
});