repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/nushell/__tests__/debug.test.ts
Rawimport {
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} 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";
function tracePaths(directory: string): string[] {
if (!pathExists(directory)) return [];
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const candidate = path.join(directory, entry.name);
if (entry.isDirectory()) return tracePaths(candidate);
return entry.isFile() && entry.name.endsWith(".jsonl") ? [candidate] : [];
});
}
function pathExists(candidate: string): boolean {
try {
readdirSync(candidate);
return true;
} catch {
return false;
}
}
async function extensionFactory(env: NodeJS.ProcessEnv) {
vi.resetModules();
return await withProcessEnv(
env,
async () => (await import("../index.ts")).default,
);
}
function stateHome(env: NodeJS.ProcessEnv): string {
if (!env.XDG_STATE_HOME) throw new Error("sandbox has no XDG state home");
return env.XDG_STATE_HOME;
}
describe("Nushell opt-in debug logging", () => {
let session: TestSession | undefined;
let root: string | undefined;
afterEach(() => {
session?.dispose();
session = undefined;
vi.doUnmock("node:fs");
if (root) rmSync(root, { recursive: true, force: true });
root = undefined;
});
it("writes safe lifecycle events and pairs operation spans", async () => {
root = mkdtempSync(path.join(tmpdir(), "pi-ext-nushell-debug-"));
const env = sandboxEnv(root, { env: { PI_NUSHELL_DEBUG: "1" } });
const factory = await extensionFactory(env);
session = await createTestSession({
cwd: root,
extensionFactories: [factory],
env: { PI_NUSHELL_DEBUG: "1" },
});
const runner = session.session.extensionRunner;
await runner.emit({ type: "session_start", reason: "startup" });
const { freshOperations, NuSession } = await withProcessEnv(
session.env,
async () => await import("../runtime.ts"),
);
await freshOperations(process.execPath).exec("trace-secret-command", root, {
onData: () => {},
});
const worker = new NuSession();
await expect(
worker.evaluate(
path.join(root, "missing-nu"),
root,
"trace-secret-request",
),
).rejects.toThrow();
await worker.reset();
await runner.emit({ type: "session_shutdown", reason: "quit" });
const paths = tracePaths(stateHome(env));
expect(paths).toHaveLength(1);
const [trace] = paths;
if (!trace) throw new Error("missing debug trace");
const text = readFileSync(trace, "utf8");
const records = text
.trim()
.split("\n")
.map((line) => JSON.parse(line) as Record<string, unknown>);
expect(records.map((record) => record.event)).toEqual(
expect.arrayContaining(["session.start", "session.shutdown"]),
);
for (const event of ["fresh", "session.connect", "session.request"]) {
const start = records.find((record) => record.event === `${event}.start`);
expect(start).toBeDefined();
const terminal = records.filter(
(record) =>
(record.event === `${event}.finish` ||
record.event === `${event}.error`) &&
record.spanId === start?.spanId,
);
expect(terminal).toHaveLength(1);
expect(terminal[0]?.durationMs).toEqual(expect.any(Number));
}
expect(text).not.toContain("trace-secret-command");
expect(text).not.toContain("trace-secret-request");
});
it("lets PI_NUSHELL_DEBUG=0 override global pi-ext selection", async () => {
root = mkdtempSync(path.join(tmpdir(), "pi-ext-nushell-debug-"));
const agentDirectory = path.join(root, ".test-home/.pi/agent");
mkdirSync(agentDirectory, { recursive: true });
writeFileSync(
path.join(agentDirectory, "settings.json"),
JSON.stringify({ "pi-ext": { debug: "nushell" } }),
);
const env = sandboxEnv(root, { env: { PI_NUSHELL_DEBUG: "0" } });
const factory = await extensionFactory(env);
session = await createTestSession({
cwd: root,
extensionFactories: [factory],
env: { PI_NUSHELL_DEBUG: "0" },
});
const runner = session.session.extensionRunner;
await runner.emit({ type: "session_start", reason: "startup" });
await runner.emit({ type: "session_shutdown", reason: "quit" });
expect(tracePaths(stateHome(env))).toEqual([]);
});
it("enables through global pi-ext selection", async () => {
root = mkdtempSync(path.join(tmpdir(), "pi-ext-nushell-debug-"));
const agentDirectory = path.join(root, ".test-home/.pi/agent");
mkdirSync(agentDirectory, { recursive: true });
writeFileSync(
path.join(agentDirectory, "settings.json"),
JSON.stringify({ "pi-ext": { debug: ["other", "nushell"] } }),
);
const env = sandboxEnv(root);
const factory = await extensionFactory(env);
session = await createTestSession({
cwd: root,
extensionFactories: [factory],
});
const runner = session.session.extensionRunner;
await runner.emit({ type: "session_start", reason: "startup" });
await runner.emit({ type: "session_shutdown", reason: "quit" });
expect(tracePaths(stateHome(env))).toHaveLength(1);
});
it("keeps extension work usable after a debug write failure", async () => {
root = mkdtempSync(path.join(tmpdir(), "pi-ext-nushell-debug-"));
const env = sandboxEnv(root, { env: { PI_NUSHELL_DEBUG: "1" } });
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
const writeSync = vi.fn(() => {
throw new Error("debug write failed");
});
vi.doMock("node:fs", () => ({ ...actual, writeSync }));
const factory = await extensionFactory(env);
session = await createTestSession({
cwd: root,
extensionFactories: [factory],
env: { PI_NUSHELL_DEBUG: "1" },
});
await session.session.extensionRunner.emit({
type: "session_start",
reason: "startup",
});
expect(session.session.extensionRunner.getCommand("nushell")).toBeDefined();
expect(writeSync).toHaveBeenCalledOnce();
});
});