repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/nu-bang/__tests__/real-world-full.test.ts
Rawimport { readdirSync } from "node:fs";
import {
chmod,
mkdir,
mkdtemp,
readFile,
rm,
stat,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import {
createNuBashOperations,
executeNu,
nuCompletions,
parseCompletionsOutput,
wrapNuAutocompleteProvider,
} from "../index.js";
let dir = "";
const supportsControlCharPaths = process.platform !== "win32";
const directoryCompletion =
process.platform === "win32" ? "`my dir`" : "`my dir/`";
async function seedFiles(): Promise<void> {
await writeFile(
join(dir, "people.json"),
JSON.stringify([
{ name: "Ada", role: "compiler", score: 99 },
{ name: "Grace", role: "systems", score: 95 },
{ name: "Linus", role: "kernel", score: 88 },
]),
"utf8",
);
await writeFile(
join(dir, "logs.csv"),
"service,level,count\napi,info,3\napi,error,1\nweb,info,5\n",
"utf8",
);
await writeFile(
join(dir, "config.nuon"),
"{host: localhost, ports: [8080, 8081], enabled: true}",
"utf8",
);
await writeFile(
join(dir, "alpha.nu"),
"export def greet [name: string] { $'hello ($name)' }",
"utf8",
);
await writeFile(join(dir, "beta.txt"), "hello\nworld\n", "utf8");
await writeFile(join(dir, "my file.txt"), "space path\n", "utf8");
await writeFile(join(dir, "hash#file.txt"), "hash path\n", "utf8");
await writeFile(join(dir, "semi;file.txt"), "semi path\n", "utf8");
await writeFile(join(dir, "paren(file).txt"), "paren path\n", "utf8");
await writeFile(join(dir, "brack[file].txt"), "brack path\n", "utf8");
await mkdir(join(dir, "my dir"));
await mkdir(join(dir, "src"));
await writeFile(join(dir, "script.nu"), "let x = 1\n$x + 1\n", "utf8");
}
function tmpBangDirs(): string[] {
return readdirSync(tmpdir()).filter(
(name) =>
name.startsWith("pi-nu-bang-") &&
!name.startsWith("pi-nu-bang-test-") &&
!name.startsWith("pi-nu-bang-output-"),
);
}
function run(command: string): ReturnType<typeof executeNu> {
return executeNu(command, dir);
}
// One shared seeded cwd avoids fixture churn; tests stay sequential so Nushell
// subprocess timings do not balloon under full-suite process contention.
beforeAll(async () => {
dir = await mkdtemp(join(tmpdir(), "pi-nu-bang-test-"));
await seedFiles();
});
afterAll(async () => {
await rm(dir, {
recursive: true,
force: true,
maxRetries: 20,
retryDelay: 50,
});
});
describe.sequential("nu-bang real-world execution", () => {
test("covers common Nushell data pipelines in one process", async () => {
const result = await run(String.raw`
[
{name: "ls", value: (ls | sort-by name | first | get name)}
{name: "json", value: (open people.json | where score > 90 | get name | str join ",")}
{name: "csv", value: (open logs.csv | group-by service | transpose service rows | insert total {|r| $r.rows.count | math sum } | get total | math sum | into string)}
{name: "nuon", value: (open config.nuon | get ports | math max | into string)}
{name: "path-parse", value: ("themes/nu-themes/tokyo-night.nu" | path parse | get stem)}
{name: "path-exists", value: (["alpha.nu" "missing.nu" "beta.txt"] | where ($it | str ends-with ".nu") and ($it | path exists) | str join ",")}
{name: "update", value: (open people.json | update name { str uppercase } | first | get name)}
{name: "insert", value: (open people.json | insert senior {|r| $r.score >= 95 } | where senior == true | get name | str join ":")}
{name: "reduce", value: (open people.json | get score | reduce {|n acc| $acc + $n } | into string)}
{name: "each", value: ([1 2 3] | each {|n| {n: $n, double: ($n * 2)} } | where double == 6 | length | into string)}
{name: "par-each", value: ([1 2 3 4] | par-each {|n| $n * $n } | sort | to json --raw)}
{name: "match", value: (match "prod" { "dev" => "loose", "prod" => "strict", _ => "unknown" })}
{name: "def", value: (do { def slug [s: string] { $s | str lowercase | str replace --all " " "-" }; slug "Hello Nu World" })}
{name: "module", value: (do { use ./alpha.nu greet; greet Ada })}
{name: "from-json", value: ('{"items":[{"id":1},{"id":2}]}' | from json | get items.id | math sum | into string)}
{name: "markdown", value: (open people.json | select name score | first 2 | to md | str contains "| Ada | 99 |" | into string)}
] | to json --raw
`);
expect(result.exitCode).toBe(0);
const cases = new Map(
(JSON.parse(result.output) as Array<{ name: string; value: string }>).map(
(item) => [item.name, item.value],
),
);
expect(Object.fromEntries(cases)).toMatchObject({
ls: "alpha.nu",
json: "Ada,Grace",
csv: "9",
nuon: "8081",
"path-parse": "tokyo-night",
"path-exists": "alpha.nu",
update: "ADA",
insert: "Ada:Grace",
reduce: "282",
each: "1",
"par-each": "[1,4,9,16]",
match: "strict",
def: "hello-nu-world",
module: "hello Ada",
"from-json": "3",
markdown: "true",
});
});
test("uses complete to preserve stderr and exit code", async () => {
const result = await run(
'^bash -lc "echo out; echo err >&2; exit 7" | complete',
);
expect(result.exitCode).toBe(0);
expect(result.output).toContain("stdout");
expect(result.output).toContain("stderr");
expect(result.output).toContain("exit_code");
expect(result.output).toContain("7");
});
test("propagates external exit code without complete", async () => {
const result = await run('^bash -lc "echo out; echo err >&2; exit 7"');
expect(result.exitCode).toBe(7);
expect(result.output).toContain("out");
expect(result.output).toContain("err");
});
test("preserves interleaved stdout and stderr", async () => {
// Short sleeps force ordering without making subprocess timing dominate.
const result = await run(
'^bash -lc "printf out1; sleep 0.01; printf err1 >&2; sleep 0.01; printf out2; sleep 0.01; printf err2 >&2"',
);
expect(result.exitCode).toBe(0);
expect(result.output).toBe("out1err1out2err2");
});
test("cancels long-running external commands", async () => {
const controller = new AbortController();
const started = Date.now();
setTimeout(() => controller.abort(), 50);
const result = await executeNu(
'^bash -lc "sleep 5"',
dir,
controller.signal,
);
expect(Date.now() - started).toBeLessThan(2000);
expect(result.cancelled).toBe(true);
});
test("streams output through bash operations before command exit", async () => {
const controller = new AbortController();
const operations = createNuBashOperations(
'^bash -lc "echo first; sleep 30; echo second"',
);
let done = false;
let seen = "";
let resolveFirst: () => void = () => {};
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
const firstChunk = new Promise<void>((resolve) => {
resolveFirst = resolve;
});
const timeout = new Promise<never>((_, reject) => {
timeoutTimer = setTimeout(
() => reject(new Error("timed out waiting for streamed output")),
10_000,
);
});
const exec = operations
.exec("ignored", dir, {
onData(chunk) {
seen += chunk.toString("utf8");
if (seen.includes("first")) resolveFirst();
},
signal: controller.signal,
})
.finally(() => {
done = true;
});
try {
await Promise.race([firstChunk, timeout]);
} finally {
if (timeoutTimer) clearTimeout(timeoutTimer);
}
expect(seen).toContain("first");
expect(done).toBe(false);
controller.abort();
await exec;
});
test("does not wait for background children that inherit stdio", async () => {
const started = Date.now();
const result = await run('^bash -lc "sleep 4 & echo done"');
expect(Date.now() - started).toBeLessThan(3500);
expect(result.exitCode).toBe(0);
expect(result.output).toBe("done\n");
});
test("supports save side effects in cwd", async () => {
const result = await run('"created" | save generated.txt --force');
expect(result.exitCode).toBe(0);
expect(await readFile(join(dir, "generated.txt"), "utf8")).toBe("created");
});
test("supports globbing", async () => {
const result = await run('ls **/*.nu | get name | sort | str join ","');
expect(result.output).toContain("alpha.nu");
expect(result.output).toContain("script.nu");
});
test("supports try/catch for recoverable errors", async () => {
const result = await run(
'try { open missing.json } catch {|err| $"caught: ($err.msg | str substring 0..10)" }',
);
expect(result.exitCode).toBe(0);
expect(result.output).toContain("caught:");
});
test("supports metadata command", async () => {
const result = await run(
'open people.json | first 1 | metadata | columns | sort | str join ","',
);
expect(result.exitCode).toBe(0);
expect(result.output.length).toBeGreaterThan(0);
});
test("nu emits plain errors when spawned with pipes", async () => {
const result = await run("open missing.json");
expect(result.exitCode).not.toBe(0);
expect(result.output).toContain("Error:");
expect(result.output).not.toContain("\u001b[");
});
test("strips terminal control sequences from returned output", async () => {
const result = await run(
String.raw`^bash -lc "printf $'plain\\033[31mred\\033[0m\\nosc\\033]0;title\\007done\\nctrl\\001x\\n'"`,
);
expect(result.exitCode).toBe(0);
expect(result.output).toContain("plainred");
expect(result.output).toContain("oscdone");
expect(result.output).toContain("ctrlx");
expect(result.output).not.toContain("\u001b");
expect(result.output).not.toContain("\u0007");
expect(result.output).not.toContain("\u0001");
expect(result.output).not.toContain("]0;title");
});
test("strips carriage returns and C1 terminal controls from returned output", async () => {
const result = await run(
String.raw`^bash -lc 'printf "cr\rkeep\nc1\302\23331mred\302\2330m\n"'`,
);
expect(result.exitCode).toBe(0);
expect(result.output).toContain("crkeep");
expect(result.output).toContain("c1red");
expect(result.output).not.toContain("\r");
expect(result.output).not.toContain("\u009b");
expect(result.output).not.toContain("31m");
expect(result.output).not.toContain("0m");
});
test("strips unterminated terminal string controls from returned output", async () => {
const result = await run(
String.raw`^bash -lc 'printf "ok\n\033]0;title\nvisible\n\033Psecret\nnext\n\302\235c1title\nlast\n"'`,
);
expect(result.exitCode).toBe(0);
expect(result.output).toContain("ok\n");
expect(result.output).toContain("visible\n");
expect(result.output).toContain("next\n");
expect(result.output).toContain("last\n");
expect(result.output).not.toContain("title");
expect(result.output).not.toContain("secret");
expect(result.output).not.toContain("\u001b");
expect(result.output).not.toContain("\u009d");
});
test("strips terminated DCS controls without dropping following text", async () => {
const result = await run(
String.raw`^python3 -c 'import sys; sys.stdout.buffer.write(b"before\x1bPsecret\x1b\\after\n")'`,
);
expect(result.exitCode).toBe(0);
expect(result.output).toBe("beforeafter\n");
expect(result.output).not.toContain("secret");
expect(result.output).not.toContain("\u001b");
});
test("shows nu's native parser diagnostics", async () => {
const result = await run("ls | where");
expect(result.exitCode).toBe(1);
expect(result.output).toContain("Missing required positional argument");
expect(result.output).toContain("source:1:");
});
test("shows nu's native delimiter diagnostics", async () => {
const result = await run("[1 2 3");
expect(result.exitCode).toBe(1);
expect(result.output).toMatch(/nu::parser::[a-z_]+/);
expect(result.output).toMatch(/expected .*\]/);
});
test("reports a missing working directory clearly", async () => {
const missing = join(dir, "missing-cwd");
const result = await executeNu("pwd", missing);
expect(result.exitCode).toBe(1);
expect(result.output).toContain("Working directory does not exist");
expect(result.output).toContain(missing);
expect(result.output).not.toContain("posix_spawn");
expect(result.output).not.toContain("spawn nu");
});
test("escapes controls in execute working directory errors", async () => {
const unsafeMissing = join(dir, "missing-\u001b[31mred");
const missingResult = await executeNu("pwd", unsafeMissing);
expect(missingResult.output).toContain("Working directory does not exist");
expect(missingResult.output).toContain("missing-\\u001b[31mred");
expect(missingResult.output).not.toContain("\u001b");
if (supportsControlCharPaths) {
const unsafeFile = join(dir, "file-\u001b[32mgreen");
await writeFile(unsafeFile, "not a directory", "utf8");
const fileResult = await executeNu("pwd", unsafeFile);
expect(fileResult.output).toContain(
"Working directory is not a directory",
);
expect(fileResult.output).toContain("file-\\u001b[32mgreen");
expect(fileResult.output).not.toContain("\u001b");
}
});
test.sequential("does not leave pi-nu-bang temp dirs after execution", async () => {
const before = tmpBangDirs();
await run("ls | first 1");
const after = tmpBangDirs();
expect(after.sort()).toEqual(before.sort());
});
test("truncates huge output and exposes readable full output", async () => {
const result = await run(
"1..2500 | each {|n| $'line-($n)' } | str join (char nl)",
);
expect(result.truncated).toBe(true);
expect(result.output).toContain("truncated");
expect(result.output).toContain("line-2500");
expect(typeof result.fullOutputPath).toBe("string");
if (!result.fullOutputPath) throw new Error("expected full output path");
try {
const fullOutputStat = await stat(result.fullOutputPath);
if (process.platform !== "win32")
expect(fullOutputStat.mode & 0o777).toBe(0o600);
const fullOutput = await readFile(result.fullOutputPath, "utf8");
expect(fullOutput).toContain("line-1");
expect(fullOutput).toContain("line-2500");
expect(fullOutput.length).toBeGreaterThan(result.output.length);
} finally {
await rm(result.fullOutputPath, { force: true });
}
});
});
describe.sequential("nu-bang real-world completions", () => {
test("completes pipeline commands", async () => {
const completions = await nuCompletions("ls | wh", 7, dir);
expect(completions).toContain("where");
expect(completions).toContain("which");
});
test("completes paths for open", async () => {
const completions = await nuCompletions("open ", 5, dir);
expect(completions).toContain("people.json");
expect(completions).toContain("logs.csv");
});
test("escapes terminal controls in parsed completion output", () => {
expect(
parseCompletionsOutput(
JSON.stringify({
completions: [
"unsafe-\u001b[31mred\u0007.txt",
"c1-\u009b31mred.txt",
],
}),
),
).toEqual(["unsafe-\\u001b[31mred\\u0007.txt", "c1-\\u009b31mred.txt"]);
});
test.skipIf(!supportsControlCharPaths)(
"does not expose terminal controls in path completions",
async () => {
await writeFile(
join(dir, "unsafe-\u001b[31mred\u0007.txt"),
"unsafe",
"utf8",
);
const completions = await nuCompletions("open unsafe-", 12, dir);
const unsafeCompletion = completions.find((completion) =>
completion.includes("unsafe-"),
);
expect(typeof unsafeCompletion).toBe("string");
expect(unsafeCompletion).toContain("unsafe-\\u001b[31mred\\u0007.txt");
expect(unsafeCompletion).not.toContain("\u001b");
expect(unsafeCompletion).not.toContain("\u0007");
},
);
test("filters escaped-space path suggestions to the typed prefix", async () => {
const provider = wrapNuAutocompleteProvider(
{
async getSuggestions() {
return null;
},
applyCompletion() {
throw new Error("not used");
},
},
dir,
);
const [broadSuggestions, suggestions, directorySuggestions] =
await Promise.all([
provider.getSuggestions(["!open my\\ "], 0, 10, {
signal: new AbortController().signal,
force: true,
}),
provider.getSuggestions(["!open my\\ f"], 0, 11, {
signal: new AbortController().signal,
force: true,
}),
provider.getSuggestions(["!open my\\ d"], 0, 11, {
signal: new AbortController().signal,
force: true,
}),
]);
expect(broadSuggestions?.prefix).toBe("my\\ ");
expect(broadSuggestions?.items.map((item) => item.value)).toEqual([
directoryCompletion,
"`my file.txt`",
]);
expect(suggestions?.prefix).toBe("my\\ f");
expect(suggestions?.items.map((item) => item.value)).toEqual([
"`my file.txt`",
]);
expect(directorySuggestions?.prefix).toBe("my\\ d");
expect(directorySuggestions?.items.map((item) => item.value)).toEqual([
directoryCompletion,
]);
});
test("completes escaped metacharacter paths instead of command suggestions", async () => {
const provider = wrapNuAutocompleteProvider(
{
async getSuggestions() {
return null;
},
applyCompletion() {
throw new Error("not used");
},
},
dir,
);
const cases = [
["!open hash\\#", "hash\\#", "`hash#file.txt`"],
["!open semi\\;", "semi\\;", "`semi;file.txt`"],
["!open paren\\(", "paren\\(", "`paren(file).txt`"],
["!open brack\\[", "brack\\[", "'brack[file].txt'"],
] as const;
const results = await Promise.all(
cases.map(async ([line]) =>
provider.getSuggestions([line], 0, line.length, {
signal: new AbortController().signal,
force: true,
}),
),
);
for (const [index, [, prefix, value]] of cases.entries()) {
const suggestions = results[index];
expect(suggestions?.prefix).toBe(prefix);
expect(suggestions?.items.map((item) => item.value)).toEqual([value]);
}
});
test("completes env record cells", async () => {
const completions = await nuCompletions("$env.", 5, dir);
expect(completions).toContain("PATH");
});
test("completes flags", async () => {
const completions = await nuCompletions("ls --", 5, dir);
expect(
completions.some((c) => c.includes("help") || c.includes("long")),
).toBe(true);
});
test("reports missing completion working directory clearly", async () => {
const missing = join(dir, "missing-completion-cwd");
await expect(nuCompletions("open ", 5, missing)).rejects.toThrow(
`Working directory does not exist: ${missing}`,
);
});
test("reports file completion working directory clearly", async () => {
const fileCwd = join(dir, "not-a-directory.txt");
await writeFile(fileCwd, "not a directory", "utf8");
await expect(nuCompletions("open ", 5, fileCwd)).rejects.toThrow(
`Working directory is not a directory: ${fileCwd}`,
);
});
test("escapes controls in completion working directory errors", async () => {
const unsafeMissing = join(dir, "missing-\u001b]0;title\u0007-cwd");
try {
await nuCompletions("open ", 5, unsafeMissing);
throw new Error("expected nuCompletions to reject");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
expect(message).toContain("Working directory does not exist");
expect(message).toContain("missing-\\u001b]0;title\\u0007-cwd");
expect(message).not.toContain("\u001b");
expect(message).not.toContain("\u0007");
}
if (!supportsControlCharPaths) return;
const unsafeFile = join(dir, "file-\u001b[33myellow");
await writeFile(unsafeFile, "not a directory", "utf8");
try {
await nuCompletions("open ", 5, unsafeFile);
throw new Error("expected nuCompletions to reject");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
expect(message).toContain("Working directory is not a directory");
expect(message).toContain("file-\\u001b[33myellow");
expect(message).not.toContain("\u001b");
}
const unsafeParent = join(dir, "parent-\u001b[36mcyan");
const unsafeChild = join(unsafeParent, "child-\u001b]0;title\u0007-cwd");
await writeFile(unsafeParent, "not a directory", "utf8");
try {
await nuCompletions("open ", 5, unsafeChild);
throw new Error("expected nuCompletions to reject");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
expect(message).toContain("Working directory cannot be checked");
expect(message).toContain("parent-\\u001b[36mcyan");
expect(message).toContain("child-\\u001b]0;title\\u0007-cwd");
expect(message).not.toContain("\u001b");
expect(message).not.toContain("\u0007");
}
});
test.skipIf(process.platform === "win32")(
"reports inaccessible working directories clearly",
async () => {
const unsafeDir = join(dir, "deny-\u001b[35msecret");
await mkdir(unsafeDir);
await chmod(unsafeDir, 0o000);
try {
const result = await executeNu("pwd", unsafeDir);
expect(result.output).toContain("Working directory is not accessible");
expect(result.output).toContain("deny-\\u001b[35msecret");
expect(result.output).not.toContain("posix_spawn");
expect(result.output).not.toContain("spawn nu");
expect(result.output).not.toContain("\u001b");
try {
await nuCompletions("open ", 5, unsafeDir);
throw new Error("expected nuCompletions to reject");
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
expect(message).toContain("Working directory is not accessible");
expect(message).toContain("deny-\\u001b[35msecret");
expect(message).not.toContain("posix_spawn");
expect(message).not.toContain("spawn nu");
expect(message).not.toContain("\u001b");
}
} finally {
await chmod(unsafeDir, 0o700).catch(() => {});
}
},
);
test.sequential("completion temp dirs are cleaned", async () => {
const before = tmpBangDirs();
await nuCompletions("open ", 5, dir);
const after = tmpBangDirs();
expect(after.sort()).toEqual(before.sort());
});
});