import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; import { createPiSession, type PiSession, } from "../../../test/fixture/create-pi-session.ts"; describe("nu-bang contract", () => { let session: PiSession | undefined; afterEach(async () => { await session?.dispose(); session = undefined; }); it("writes enabled lifecycle debug records in an isolated sandbox", async () => { const root = mkdtempSync(join(tmpdir(), "pi-ext-nu-bang-debug-")); try { session = await createPiSession({ cwd: root, env: { PI_NU_BANG_DEBUG: "1", XDG_STATE_HOME: join(root, "state"), }, extensionPaths: [ fileURLToPath(new URL("../index.ts", import.meta.url)), ], }); await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); await session.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); const events = readdirSync( join(root, "state", "pi-ext", "debug", "nu-bang"), ).flatMap((file) => readFileSync( join(root, "state", "pi-ext", "debug", "nu-bang", file), "utf8", ) .trim() .split("\n") .map((line) => JSON.parse(line)), ); expect(events).toEqual( expect.arrayContaining([ expect.objectContaining({ extension: "nu-bang", event: "session.start", }), expect.objectContaining({ extension: "nu-bang", event: "session.shutdown", }), ]), ); } finally { await session?.dispose(); session = undefined; rmSync(root, { recursive: true, force: true }); } }); it("loads in real Pi extension runtime", async () => { session = await createPiSession({ extensionPaths: [fileURLToPath(new URL("../index.ts", import.meta.url))], }); expect(session.extensions).toHaveLength(1); }); it("streams ordinary bang commands and refuses interactive ones outside the TUI", async () => { session = await createPiSession({ extensionPaths: [fileURLToPath(new URL("../index.ts", import.meta.url))], }); const runner = session.session.extensionRunner; const streamed = await runner.emitUserBash({ type: "user_bash", command: "[1 2 3] | math sum", excludeFromContext: false, cwd: session.cwd, }); if (!streamed?.operations) throw new Error("expected bash operations"); const executed = await session.session.executeBash( "[1 2 3] | math sum", undefined, { operations: streamed.operations }, ); expect(executed.exitCode).toBe(0); expect(executed.output.trim()).toBe("6"); const interactive = await runner.emitUserBash({ type: "user_bash", command: "vim notes.md", excludeFromContext: false, cwd: session.cwd, }); expect(interactive?.result).toMatchObject({ exitCode: 1, cancelled: false, }); expect(interactive?.result?.output).toContain("require Pi's TUI"); }); });