Files
openclaw/src/cli/completion-fish.test.ts
2026-07-31 08:59:11 -07:00

174 lines
5.6 KiB
TypeScript

// Fish completion tests cover fish shell completion script generation.
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
import {
buildFishOptionCompletionLine,
buildFishSubcommandCompletionLine,
} from "./completion-fish.js";
describe("completion-fish helpers", () => {
it("builds a subcommand completion line", () => {
const line = buildFishSubcommandCompletionLine({
rootCmd: "openclaw",
condition: "__fish_use_subcommand",
name: "plugins",
description: "Manage Bob's plugins",
});
expect(line).toBe(
`complete -c openclaw -n "__fish_use_subcommand" -a "plugins" -d 'Manage Bob'\\''s plugins'\n`,
);
});
it("builds option line with short and long flags", () => {
const line = buildFishOptionCompletionLine({
rootCmd: "openclaw",
condition: "__fish_use_subcommand",
flags: ["-s", "--shell"],
description: "Shell target",
});
expect(line).toBe(
`complete -c openclaw -n "__fish_use_subcommand" -s s -l shell -d 'Shell target'\n`,
);
});
it("builds option line with long-only flags", () => {
const line = buildFishOptionCompletionLine({
rootCmd: "openclaw",
condition: "__fish_seen_subcommand_from completion",
flags: ["--write-state"],
description: "Write cache",
});
expect(line).toBe(
`complete -c openclaw -n "__fish_seen_subcommand_from completion" -l write-state -d 'Write cache'\n`,
);
});
it("builds option line with two long aliases", () => {
const line = buildFishOptionCompletionLine({
rootCmd: "openclaw",
condition: "__fish_use_subcommand",
flags: ["--ws", "--workspace"],
description: "Workspace",
});
expect(line).toBe(
`complete -c openclaw -n "__fish_use_subcommand" -l ws -l workspace -d 'Workspace'\n`,
);
});
it("preserves required Commander option values and constrained choices", () => {
const line = buildFishOptionCompletionLine({
rootCmd: "openclaw",
condition: "__fish_seen_subcommand_from completion",
flags: ["-s", "--shell"],
description: "Shell target",
requiresValue: true,
choices: ["zsh", "bash", "powershell", "fish"],
});
const quotedChoices = ["zsh", "bash", "powershell", "fish"]
.map((choice) => `'${choice}'`)
.join(" ");
expect(line).toBe(
`complete -c openclaw -n "__fish_seen_subcommand_from completion" -s s -l shell -r -f -a "${quotedChoices}" -d 'Shell target'\n`,
);
});
it("preserves optional Commander option values without requiring an argument", () => {
const line = buildFishOptionCompletionLine({
rootCmd: "openclaw",
condition: "__fish_use_subcommand",
flags: ["--color"],
description: "Color output",
choices: ["always", "never"],
});
expect(line).toContain(` -l color -f -a "'always' 'never'" `);
expect(line).not.toContain(" -r ");
expect(line).toContain(
`complete -c openclaw -n "__fish_use_subcommand; and contains -- (commandline -opc)[-1] --color" -f -a "'always' 'never'" -d 'Color output'`,
);
});
it("preserves whitespace within each Commander choice", () => {
const line = buildFishOptionCompletionLine({
rootCmd: "openclaw",
condition: "__fish_use_subcommand",
flags: ["--theme"],
description: "Theme",
requiresValue: true,
choices: ["light blue", "dark"],
});
expect(line).toContain(` -r -f -a "'light blue' 'dark'" `);
});
it.skipIf(process.platform === "win32")(
"preserves apostrophes and backslashes through both Fish quoting layers",
() => {
const line = buildFishOptionCompletionLine({
rootCmd: "openclaw",
condition: "true",
flags: ["--theme"],
description: "Theme",
requiresValue: true,
choices: ["Bob's green", "path\\name", "ends\\"],
});
const capture = spawnSync(
"bash",
[
"--noprofile",
"--norc",
"-c",
`complete() { while (( $# )); do if [[ "$1" == "-a" ]]; then printf '%s' "$2"; return; fi; shift; done; }\n${line}`,
],
{ encoding: "utf8" },
);
expect(capture.status).toBe(0);
expect(capture.stderr).toBe("");
expect(capture.stdout).toBe("'Bob\\'s green' 'path\\\\name' 'ends\\\\'");
},
);
it.skipIf(process.platform === "win32")(
"keeps command-shaped Commander choices inert during completion expression parsing",
() => {
const marker = "OPENCLAW_FISH_CHOICE_MUST_NOT_EXECUTE";
const line = buildFishOptionCompletionLine({
rootCmd: "openclaw",
condition: "true",
flags: ["--channel"],
description: "Channel",
requiresValue: true,
choices: ["(stable)", `$(printf ${marker} >&2)`, "light blue"],
});
const capture = spawnSync(
"bash",
[
"--noprofile",
"--norc",
"-c",
`complete() { while (( $# )); do if [[ "$1" == "-a" ]]; then printf '%s' "$2"; return; fi; shift; done; }\n${line}`,
],
{ encoding: "utf8" },
);
expect(capture.status).toBe(0);
expect(capture.stderr).toBe("");
const evaluation = spawnSync(
"bash",
["--noprofile", "--norc", "-c", `set -- ${capture.stdout}; printf '%s\\n' "$@"`],
{ encoding: "utf8" },
);
expect(evaluation.status).toBe(0);
expect(evaluation.stderr).toBe("");
expect(evaluation.stdout.split("\n").filter(Boolean)).toEqual([
"(stable)",
`$(printf ${marker} >&2)`,
"light blue",
]);
},
);
});