Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/nu-bang/__tests__/real-world-full.test.ts

Raw
import { 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());
	});
});