repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/nushell/__tests__/harness.test.ts
Rawimport { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { initTheme } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createTestSession, type TestSession } from "../../../test/harness";
import { findNu } from "../runtime.ts";
const extensionPath = path.resolve(import.meta.dirname, "../index.ts");
describe("nushell pi-test-harness", () => {
let t: TestSession | undefined;
let root: string | undefined;
afterEach(() => {
t?.dispose();
t = undefined;
if (root) rmSync(root, { recursive: true, force: true });
root = undefined;
});
it("registers inert tools and toggles without changing unrelated tools", async () => {
t = await createTestSession({ extensions: [extensionPath] });
const runner = t.session.extensionRunner;
const command = runner.getCommand("nushell");
const ctx = runner.createCommandContext();
expect(runner.extensions[0]?.tools.has("nu")).toBe(true);
expect(runner.extensions[0]?.tools.has("nu_session")).toBe(true);
expect(t.session.getActiveToolNames()).not.toContain("nu");
expect(command?.getArgumentCompletions?.("re")).toEqual([
{ value: "reset", label: "reset" },
]);
const initial = t.session.getActiveToolNames();
await command?.handler("on", ctx);
const enabled = t.session.getActiveToolNames();
if (!findNu()) {
expect(enabled).toEqual(initial);
return;
}
expect(enabled).toContain("nu");
expect(enabled).toContain("nu_session");
expect(enabled).not.toContain("bash");
expect(enabled).not.toContain("powershell");
for (const name of initial.filter(
(name) => name !== "bash" && name !== "powershell",
))
expect(enabled).toContain(name);
await command?.handler("off", ctx);
expect([...t.session.getActiveToolNames()].sort()).toEqual(
[...initial].sort(),
);
});
it("keeps shell tools active and warns when PATH has no executable Nu", async () => {
t = await createTestSession({ extensions: [extensionPath] });
root = mkdtempSync(path.join(tmpdir(), "pi-ext-nushell-path-"));
mkdirSync(path.join(root, process.platform === "win32" ? "nu.exe" : "nu"));
t.env.PATH = root;
const runner = t.session.extensionRunner;
const initial = t.session.getActiveToolNames();
runner.setFlagValue("nushell", true);
await runner.emit({ type: "session_start", reason: "startup" });
expect(t.session.getActiveToolNames()).toEqual(initial);
expect(t.events.uiCallsFor("notify").at(-1)?.args).toEqual([
"Nushell executable not found on PATH; tools unchanged",
"warning",
]);
await runner
.getCommand("nushell")
?.handler("on", runner.createCommandContext());
expect(t.session.getActiveToolNames()).toEqual(initial);
expect(t.events.uiCallsFor("notify").at(-1)?.args).toEqual([
"Nushell executable not found on PATH; tools unchanged",
"warning",
]);
});
it("registers --nushell to enable both tools on session start", async () => {
t = await createTestSession({ extensions: [extensionPath] });
const runner = t.session.extensionRunner;
expect(runner.getFlags().has("nushell")).toBe(true);
runner.setFlagValue("nushell", true);
await runner.emit({ type: "session_start", reason: "startup" });
const active = t.session.getActiveToolNames();
if (!findNu()) {
expect(active).not.toContain("nu");
return;
}
expect(active).toContain("nu");
expect(active).toContain("nu_session");
expect(active).not.toContain("bash");
expect(active).not.toContain("powershell");
});
it.runIf(Boolean(findNu()))(
"does not claim activation when Nu tools are excluded",
async () => {
t = await createTestSession({ extensions: [extensionPath] });
const runner = t.session.extensionRunner;
const initial = t.session.getActiveToolNames();
const tools = t.session.getAllTools();
vi.spyOn(t.session, "getAllTools").mockReturnValue(
tools.filter((tool) => tool.name !== "nu_session"),
);
runner.setFlagValue("nushell", true);
await runner.emit({ type: "session_start", reason: "startup" });
expect(t.session.getActiveToolNames()).toEqual(initial);
expect(t.events.uiCallsFor("notify").at(-1)?.args).toEqual([
expect.stringContaining("nu_session"),
"warning",
]);
},
);
it.runIf(Boolean(findNu()))(
"renders both Nu commands with syntax highlighting and shell-style results",
async () => {
t = await createTestSession({ extensions: [extensionPath] });
initTheme("default", false);
const tools = t.session.extensionRunner.extensions[0]?.tools;
const theme = {
fg: (color: string, value: string) =>
value ? `<${color}>${value}</${color}>` : "",
bold: (value: string) => value,
} as never;
for (const name of ["nu", "nu_session"]) {
const tool = tools?.get(name)?.definition;
const icon = name === "nu" ? "" : "";
expect(tool?.label).toBe(
`${icon} ${name === "nu" ? "nu" : "nu session"}`,
);
const context = {
state: {},
executionStarted: false,
argsComplete: false,
invalidate: vi.fn(),
} as never;
const partial = tool?.renderCall?.(
{ command: "let value" },
theme,
context,
);
expect(partial?.render(1000).join("\n")).not.toContain(
"<syntaxVariable>",
);
context.argsComplete = true;
context.executionStarted = true;
const command = 'let value = "safe"';
const call = tool?.renderCall?.({ command }, theme, context);
const lines = call?.render(1000).join("\n") ?? "";
expect(lines.replace(/<\/?[a-zA-Z]+>/g, "")).toContain(command);
expect(lines).not.toContain("<syntaxVariable>");
await vi.waitFor(() =>
expect(context.invalidate).toHaveBeenCalledTimes(1),
);
const highlighted =
tool
?.renderCall?.({ command }, theme, context)
.render(1000)
.join("\n") ?? "";
expect(highlighted).toContain("<syntaxVariable>value</syntaxVariable>");
expect(highlighted).toContain('<syntaxString>"safe"</syntaxString>');
expect(highlighted).not.toMatch(/\x1b\[/);
const paletteCommand =
'if true { let value = "safe"; $value | str uppercase; 2 > 1 }';
tool?.renderCall?.({ command: paletteCommand }, theme, context);
await vi.waitFor(() =>
expect(context.invalidate).toHaveBeenCalledTimes(2),
);
const palette =
tool
?.renderCall?.({ command: paletteCommand }, theme, context)
.render(2000)
.join("\n") ?? "";
for (const token of [
"<syntaxFunction>if</syntaxFunction>",
"<syntaxKeyword>true</syntaxKeyword>",
"<syntaxPunctuation>{</syntaxPunctuation>",
"<syntaxVariable>$value</syntaxVariable>",
"<syntaxPunctuation>|</syntaxPunctuation>",
"<syntaxOperator>></syntaxOperator>",
'<syntaxString>"safe"</syntaxString>',
])
expect(palette).toContain(token);
expect(lines).toContain(
`${icon} ${name === "nu" ? "nu>" : "nu session>"}`,
);
const result = tool?.renderResult?.(
{
content: [
{ type: "text", text: "one\ntwo\nthree\nfour\nfive\nsix" },
],
details: undefined,
},
{ expanded: false, isPartial: false },
theme,
{ ...context, showImages: false, isError: false },
);
expect(result?.render(120).join("\n")).toContain("to expand");
}
},
);
it.runIf(Boolean(findNu()))(
"keeps session state across calls but isolates fresh calls",
async () => {
t = await createTestSession({ extensions: [extensionPath] });
const runner = t.session.extensionRunner;
const ctx = runner.createCommandContext();
const tool = (name: string) =>
runner.extensions[0]?.tools.get(name)?.definition;
const run = (name: string, command: string) =>
tool(name)?.execute("call", { command }, undefined, undefined, ctx);
const fresh = await run("nu", "1 + 2");
expect(fresh?.content[0]).toMatchObject({ type: "text", text: "3\n" });
await run("nu_session", "let value = 41");
const persistent = await run("nu_session", "$value + 1");
expect(persistent?.content[0]).toMatchObject({
type: "text",
text: expect.stringContaining("output:42"),
});
await expect(run("nu", "$value + 1")).rejects.toThrow();
await expect(run("nu_session", "1 / 0")).rejects.toThrow(
"Division by zero",
);
await expect(
run("nu_session", 'error make {msg: "timeout from user"}'),
).rejects.toThrow("timeout from user");
const afterError = await run("nu_session", "$value");
expect(afterError?.content[0]).toMatchObject({
text: expect.stringContaining("output:41"),
});
const external = await run("nu_session", "^false");
expect(external?.content[0]).toMatchObject({
text: expect.stringContaining('output:""'),
});
await runner.getCommand("nushell")?.handler("reset", ctx);
await expect(run("nu_session", "$value")).rejects.toThrow();
await run("nu_session", "let value = 42");
const controller = new AbortController();
const pending = tool("nu_session")?.execute(
"abort-call",
{ command: "sleep 2sec" },
controller.signal,
undefined,
ctx,
);
const abortTimer = setTimeout(() => controller.abort(), 50);
try {
await expect(pending).rejects.toThrow("state reset");
} finally {
clearTimeout(abortTimer);
}
await expect(run("nu_session", "$value")).rejects.toThrow();
await runner.getCommand("nushell")?.handler("off", ctx);
},
);
});