// Completion CLI tests cover shell completion command generation and install output. import { spawnSync } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { Command, Option } from "commander"; import { describe, expect, it } from "vitest"; import { getCompletionScript, registerCompletionCli } from "./completion-cli.js"; import { createAliasedCompletionProgram, itWithFish, itWithPowerShell, runGeneratedBashCompletion, runGeneratedFishCompletion, runGeneratedPowerShellCompletion, } from "./completion-cli.test-support.js"; function createCompletionProgram(): Command { const program = new Command(); program.name("openclaw"); program.description("CLI root"); program.option("-v, --verbose", "Verbose output"); program.option( "--status-json", "Output JSON (alias for `models status --json`) in $OPENCLAW_STATE_DIR", ); const gateway = program.command("gateway").description("Gateway commands"); gateway.option("--force", "Force the action"); gateway.option("-t, --token ", "Gateway token"); gateway.command("status").description("Show gateway status").option("--json", "JSON output"); gateway.command("restart").description("Restart gateway"); program .command("agent") .description("Agent commands") .option("--verbose ", "Set verbosity"); const sessions = program.command("sessions").description("Session commands"); sessions.option("--verbose", "Verbose output"); sessions.command("cleanup").description("Clean sessions").option("--dry-run", "Preview cleanup"); return program; } function createDocumentedCompletionProgram(): Command { const program = createCompletionProgram(); registerCompletionCli(program); return program; } function createOptionalChoiceCompletionProgram(): Command { const program = new Command().name("openclaw"); program.addOption(new Option("--mode [mode]", "Mode").choices(["auto", "manual", "-legacy"])); program.option("--json", "JSON output"); return program; } describe("completion-cli", () => { it("generates zsh functions for nested subcommands", () => { const script = getCompletionScript("zsh", createCompletionProgram()); expect(script).toContain("_openclaw_gateway()"); expect(script).toContain("(status) _openclaw_gateway_status ;;"); expect(script).toContain("(restart) _openclaw_gateway_restart ;;"); expect(script).toContain("--force[Force the action]"); expect(script).toContain("\\`models status --json\\`"); expect(script).toContain("\\$OPENCLAW_STATE_DIR"); }); it("escapes zsh option descriptions for double-quoted arguments specs", () => { const program = new Command() .name("openclaw") .option("--literal", "Use $OPENCLAW_STATE_DIR with `model/list` and John's profile"); const script = getCompletionScript("zsh", program); expect(script).toContain( "--literal[Use \\$OPENCLAW_STATE_DIR with \\`model/list\\` and John's profile]", ); expect(script).not.toContain("John'\\''s"); }); it("marks zsh option arguments and completes validated shell choices", () => { const script = getCompletionScript("zsh", createDocumentedCompletionProgram()); expect(script).toContain('"[Gateway token]:token:"'); expect(script).toContain( '"[Shell to generate completion for (default: zsh)]:shell:(zsh bash powershell fish)"', ); }); it.skipIf(process.platform === "win32")( "keeps zsh completion choices literal and preserves candidate boundaries", () => { const program = new Command().name("openclaw"); program.addOption( new Option("--value ", "Value").choices([ "two words", 'say "hello"', "it's literal", "literal $(printf OPENCLAW_COMPLETION_VALUE_EXECUTED >&2)", "literal `printf OPENCLAW_COMPLETION_VALUE_EXECUTED >&2`", ]), ); const result = spawnSync( "zsh", [ "-fc", `${getCompletionScript("zsh", program)} _arguments() { printf '%s\\n' "$@"; } _openclaw_root_completion `, ], { encoding: "utf8" }, ); if (result.error) { if ("code" in result.error && result.error.code === "ENOENT") { return; } throw result.error; } expect(result.stderr).toBe(""); expect(result.status).toBe(0); expect(result.stdout).toContain("two\\ words"); expect(result.stdout).toContain('say\\ \\"hello\\"'); expect(result.stdout).toContain("OPENCLAW_COMPLETION_VALUE_EXECUTED"); }, ); it("defers zsh registration until compinit is available", async () => { if (process.platform === "win32") { return; } const probe = spawnSync("zsh", ["-fc", "exit 0"], { encoding: "utf8" }); if (probe.error) { if ( "code" in probe.error && (probe.error.code === "ENOENT" || probe.error.code === "EACCES") ) { return; } throw probe.error; } const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-zsh-completion-")); try { const scriptPath = path.join(tempDir, "openclaw.zsh"); await fs.writeFile(scriptPath, getCompletionScript("zsh", createCompletionProgram()), "utf8"); const result = spawnSync( "zsh", [ "-fc", ` source ${JSON.stringify(scriptPath)} [[ -z "\${_comps[openclaw]-}" ]] || exit 10 [[ "\${precmd_functions[(r)_openclaw_register_completion]}" = "_openclaw_register_completion" ]] || exit 11 autoload -Uz compinit compinit -C _openclaw_register_completion [[ -z "\${precmd_functions[(r)_openclaw_register_completion]}" ]] || exit 12 [[ "\${_comps[openclaw]-}" = "_openclaw_root_completion" ]] `, ], { encoding: "utf8", env: { ...process.env, HOME: tempDir, ZDOTDIR: tempDir, }, }, ); expect(result.stderr).not.toContain("command not found: compdef"); expect(result.status).toBe(0); } finally { await fs.rm(tempDir, { recursive: true, force: true }); } }); it("generates PowerShell command paths without the executable prefix", () => { const script = getCompletionScript("powershell", createCompletionProgram()); expect(script).toContain("if ($commandPath -eq 'gateway') {"); expect(script).toContain("if ($commandPath -eq 'gateway status') {"); expect(script).not.toContain("if ($commandPath -eq 'openclaw gateway') {"); expect(script).toContain("$completions = @('status','restart','--force','-t','--token')"); expect(script).not.toContain("'-t,'"); }); it("generates valid PowerShell root arrays when commands or options are empty", () => { const commandsOnly = new Command().name("openclaw"); commandsOnly.command("status"); const optionsOnly = new Command().name("openclaw").option("--json", "JSON output"); const empty = new Command().name("openclaw"); expect(getCompletionScript("powershell", commandsOnly)).toContain("$completions = @('status')"); expect(getCompletionScript("powershell", optionsOnly)).toContain("$completions = @('--json')"); expect(getCompletionScript("powershell", empty)).toContain("$completions = @()"); }); it("preserves documented short and long completion flags in PowerShell", () => { const script = getCompletionScript("powershell", createDocumentedCompletionProgram()); expect(script).toContain("'-v','--verbose'"); expect(script).toContain("'--force','-t','--token'"); expect(script).toContain("'-s','--shell','-i','--install','--write-state','-y','--yes'"); }); it("generates PowerShell value choices for both completion shell flags", () => { const script = getCompletionScript("powershell", createDocumentedCompletionProgram()); expect(script).toContain("if ($choiceFlag -in @('-s','--shell')) {"); expect(script).toContain("@('zsh','bash','powershell','fish')"); expect(script).toContain("'ParameterValue'"); }); it("escapes apostrophes in PowerShell completion choices", () => { const program = new Command().name("openclaw"); program.addOption(new Option("--profile ", "Profile").choices(["Jane's", "work"])); expect(getCompletionScript("powershell", program)).toContain("@('Jane''s','work')"); }); it("matches PowerShell value prefixes literally and case-insensitively", () => { const program = new Command().name("openclaw"); program.addOption(new Option("--value ", "Value").choices(["alpha", "a*literal"])); expect(getCompletionScript("powershell", program)).toContain( "StartsWith($choicePrefix, [StringComparison]::OrdinalIgnoreCase)", ); }); itWithPowerShell.each([ ["a long shell flag", "openclaw completion --shell f"], ["a short shell flag", "openclaw completion -s f"], ])("completes validated values in real PowerShell after %s", (_name, commandLine) => { expect( runGeneratedPowerShellCompletion(createDocumentedCompletionProgram(), commandLine), ).toEqual(["fish"]); }); itWithPowerShell.each([ { name: "an omitted optional value", commandLine: "openclaw --mode --j", expected: ["--json"], }, { name: "an inline optional value", commandLine: "openclaw --mode=a", expected: ["--mode=auto"], }, { name: "a hyphen-prefixed optional choice", commandLine: "openclaw --mode -l", expected: ["-legacy"], }, ])("preserves real PowerShell completion after $name", ({ commandLine, expected }) => { expect( runGeneratedPowerShellCompletion(createOptionalChoiceCompletionProgram(), commandLine), ).toEqual(expected); }); itWithPowerShell.each([ { name: "an ordinary prefix", commandLine: "openclaw --value al", expected: ["alpha"], }, { name: "a literal asterisk", commandLine: "openclaw --value a*", expected: ["'a*literal'"], }, { name: "a literal opening bracket", commandLine: "openclaw --value a[", expected: ["'a[bracket]'"], }, { name: "a case-insensitive literal asterisk", commandLine: "openclaw --value A*", expected: ["'a*literal'"], }, { name: "an inline literal asterisk", commandLine: "openclaw --value=a*", expected: ["--value='a*literal'"], }, ])("matches real PowerShell choices with $name", ({ commandLine, expected }) => { const program = new Command().name("openclaw"); program.addOption( new Option("--value ", "Value").choices(["alpha", "a*literal", "a[bracket]"]), ); expect(runGeneratedPowerShellCompletion(program, commandLine)).toEqual(expected); }); itWithPowerShell.each([ { name: "ordinary choices", value: "alpha", prefix: "al" }, { name: "whitespace", value: "two words", prefix: "tw" }, { name: "apostrophes", value: "Jane's", prefix: "Ja" }, { name: "literal command substitution", value: "literal $(Write-Error OPENCLAW_COMPLETION_VALUE_EXECUTED)", prefix: "literal", }, { name: "literal backtick metacharacters", value: "literal `$(Write-Error OPENCLAW_COMPLETION_VALUE_EXECUTED)", prefix: "literal", }, { name: "literal statement separators", value: "literal; Write-Error OPENCLAW_COMPLETION_VALUE_EXECUTED", prefix: "literal", }, ])("inserts PowerShell $name as one safe argument", ({ value, prefix }) => { const program = new Command().name("openclaw"); program.addOption(new Option("--value ", "Value").choices([value])); const safeValue = /^[A-Za-z0-9_./:+-]+$/.test(value) ? value : `'${value.replaceAll("'", "''")}'`; expect(runGeneratedPowerShellCompletion(program, `openclaw --value ${prefix}`)).toEqual([ safeValue, ]); expect(runGeneratedPowerShellCompletion(program, `openclaw --value=${prefix}`)).toEqual([ `--value=${safeValue}`, ]); }); itWithPowerShell("completes root short and long flags in real PowerShell", () => { const completions = runGeneratedPowerShellCompletion( createDocumentedCompletionProgram(), "openclaw -", ); expect(completions).toEqual(expect.arrayContaining(["-v", "--verbose", "--status-json"])); }); itWithPowerShell("completes documented nested short and long flags in real PowerShell", () => { const completions = runGeneratedPowerShellCompletion( createDocumentedCompletionProgram(), "openclaw completion -", ); expect(completions).toEqual( expect.arrayContaining(["-s", "--shell", "-i", "--install", "-y", "--yes", "--write-state"]), ); }); itWithPowerShell.each([ ["a long flag", "openclaw gateway --token secret st"], ["a short flag", "openclaw gateway -t secret st"], ["an inline long value", "openclaw gateway --token=secret st"], ["an inline short value", "openclaw gateway -t=secret st"], ["a preceding boolean flag", "openclaw gateway --force --token secret st"], ])("keeps real PowerShell nested completions after %s", (_name, commandLine) => { expect(runGeneratedPowerShellCompletion(createCompletionProgram(), commandLine)).toEqual([ "status", ]); }); itWithPowerShell.each([ ["-v", ["-v"]], ["--v", ["--verbose"]], ])("filters real PowerShell root flag aliases for %s", (prefix, expected) => { expect( runGeneratedPowerShellCompletion(createDocumentedCompletionProgram(), `openclaw ${prefix}`), ).toEqual(expected); }); it("generates fish completions for root and nested command contexts", () => { const script = getCompletionScript("fish", createCompletionProgram()); expect(script).toContain( 'complete -c openclaw -n "__openclaw_command_path_matches --" -a "gateway" -d \'Gateway commands\'', ); expect(script).toContain( 'complete -c openclaw -n "__openclaw_command_path_matches gateway -- -t --token" -a "status" -d \'Show gateway status\'', ); expect(script).toContain( "complete -c openclaw -n \"__openclaw_command_path_matches gateway -- -t --token\" -l force -d 'Force the action'", ); expect(script).toContain( "complete -c openclaw -n \"__openclaw_command_path_matches gateway status -- -t --token\" -l json -d 'JSON output'", ); expect(script).toContain("__openclaw_command_path_matches gateway -- -t --token"); expect(script).toContain("if contains -- $flag $value_options"); }); it("distinguishes Fish child command paths from positional arguments", () => { const script = getCompletionScript("fish", createCompletionProgram()); expect(script).toContain('switch "$candidate_path"'); expect(script).toContain("'gateway status'"); }); itWithFish.each([ ["a separate long root option", "openclaw --profile work g"], ["an inline long root option", "openclaw --profile=work g"], ["a separate short root option", "openclaw -p work g"], ["an inline short root option", "openclaw -p=work g"], ["an attached short root option", "openclaw -pwork g"], ["a separate log-level root option", "openclaw --log-level debug g"], ["an inline log-level root option", "openclaw --log-level=debug g"], ["a separate container root option", "openclaw --container local g"], ["an inline container root option", "openclaw --container=local g"], ["repeated root options", "openclaw --profile first --profile second g"], [ "mixed value-taking root options", "openclaw --profile work --log-level debug --container local g", ], ["a preceding boolean root option", "openclaw -v --profile work g"], ["a root option value named like a command", "openclaw --profile gateway g"], ])("completes root commands in real Fish after %s", (_name, commandLine) => { const program = createCompletionProgram() .option("-p, --profile ", "Profile") .option("--log-level ", "Log level") .option("--container ", "Container"); expect(runGeneratedFishCompletion(program, commandLine)).toContain("gateway"); }); itWithFish.each([ ["a separate long root option", "openclaw --profile work --p"], ["an inline long root option", "openclaw --profile=work --p"], ["a separate short root option", "openclaw -p work --p"], ["repeated root options", "openclaw --profile first --profile second --p"], ])("completes root options in real Fish after %s", (_name, commandLine) => { const program = createCompletionProgram().option("-p, --profile ", "Profile"); expect(runGeneratedFishCompletion(program, commandLine)).toContain("--profile"); }); itWithFish.each([ ["the exact nested command", "openclaw gateway status -"], ["a separate long option value", "openclaw gateway --token secret status -"], ["a separate short option value", "openclaw gateway -t secret status -"], ["an inline long option value", "openclaw gateway --token=secret status -"], ["an inline short option value", "openclaw gateway -t=secret status -"], ["a parent boolean option", "openclaw gateway --force status -"], ])("keeps real Fish completions scoped after %s", (_name, commandLine) => { expect(runGeneratedFishCompletion(createCompletionProgram(), commandLine)).toEqual(["--json"]); }); itWithFish.each([ ["a positional argument", "openclaw gateway status query -"], ["multiple positional arguments", "openclaw gateway status first second -"], ["a positional argument named like a sibling", "openclaw gateway status restart -"], ["a long option and positional argument", "openclaw gateway --token secret status query -"], ["an inline option and positional argument", "openclaw gateway --token=secret status query -"], ])("keeps real Fish leaf options after %s", (_name, commandLine) => { const program = createCompletionProgram(); const gateway = program.commands.find((command) => command.name() === "gateway"); const status = gateway?.commands.find((command) => command.name() === "status"); if (!status) { throw new Error("Gateway status command is unavailable"); } status.argument("[query...]", "Search query"); expect(runGeneratedFishCompletion(program, commandLine)).toEqual(["--json"]); }); itWithFish("preserves documented short and long completion flags in real Fish", () => { expect( runGeneratedFishCompletion(createDocumentedCompletionProgram(), "openclaw completion -"), ).toEqual( expect.arrayContaining(["-s", "--shell", "-i", "--install", "-y", "--yes", "--write-state"]), ); }); itWithFish.each([ ["a separated long optional value", "openclaw --color a", "always"], ["a separated short optional value", "openclaw -c n", "never"], ["an attached long optional value", "openclaw --color=a", "--color=always"], ])("completes real Fish Commander choices after %s", (_name, commandLine, expected) => { const program = new Command() .name("openclaw") .addOption(new Option("-c, --color [when]").choices(["always", "never"])); expect(runGeneratedFishCompletion(program, commandLine)).toContain(expected); }); itWithFish.each([ ["a long shell flag", "openclaw completion --shell f"], ["a short shell flag", "openclaw completion -s f"], ])("completes validated values in real Fish after %s", (_name, commandLine) => { expect(runGeneratedFishCompletion(createDocumentedCompletionProgram(), commandLine)).toEqual([ "fish", ]); }); it("registers validated Fish option choices without filesystem fallback", () => { const script = getCompletionScript("fish", createDocumentedCompletionProgram()); expect(script).toContain(" -s s -l shell -r -f -a "); expect(script).toContain("'zsh' 'bash' 'powershell' 'fish'"); }); itWithFish.each([ { name: "whitespace", value: "two words", prefix: "tw" }, { name: "double quotes", value: 'say "hello"', prefix: "sa" }, { name: "apostrophes", value: "it's literal", prefix: "it" }, { name: "literal command substitution", value: "literal $(printf OPENCLAW_COMPLETION_VALUE_EXECUTED >&2)", prefix: "literal", }, { name: "literal backtick substitution", value: "literal `printf OPENCLAW_COMPLETION_VALUE_EXECUTED >&2`", prefix: "literal", }, ])("preserves Fish choice $name as one inert candidate", ({ value, prefix }) => { const program = new Command().name("openclaw"); program.addOption(new Option("--value ", "Value").choices([value])); expect(runGeneratedFishCompletion(program, `openclaw --value ${prefix}`)).toEqual([value]); }); it("does not require optional Fish option choices", () => { const program = new Command().name("openclaw"); program.addOption(new Option("--mode [mode]", "Mode").choices(["auto", "manual"])); const optionLine = getCompletionScript("fish", program) .split("\n") .find((line) => line.includes(" -l mode ")); expect(optionLine).toContain(" -f -a "); expect(optionLine).not.toContain(" -r "); expect(optionLine).toContain("'auto' 'manual'"); }); it("scopes fish value-taking option skips to the active command path", () => { const script = getCompletionScript("fish", createCompletionProgram()); expect(script).toContain("__openclaw_command_path_matches agent -- --verbose"); expect(script).toContain("__openclaw_command_path_matches sessions cleanup --"); expect(script).not.toContain("__openclaw_command_path_matches sessions cleanup -- --verbose"); expect(script).toContain( "complete -c openclaw -n \"__openclaw_command_path_matches sessions cleanup --\" -l dry-run -d 'Preview cleanup'", ); }); it("uses Commander's parsed flags instead of value placeholder syntax", () => { const program = new Command() .name("openclaw") .option("--trigger-script ", "Condition script file, or - for stdin") .option("--ws, --workspace ", "Workspace"); const fishScript = getCompletionScript("fish", program); expect(fishScript).toContain( "complete -c openclaw -n \"__openclaw_command_path_matches -- --trigger-script --ws --workspace\" -l trigger-script -r -d 'Condition script file, or - for stdin'", ); expect(fishScript).not.toContain(" -s > "); expect(fishScript).toContain(" -l ws -l workspace -r -d 'Workspace'"); expect(getCompletionScript("bash", program)).not.toContain("--trigger-script ->"); expect(getCompletionScript("zsh", program)).not.toContain("{--trigger-script,->}"); }); it("generates Bash completions without comma-suffixed short flags", () => { const script = getCompletionScript("bash", createCompletionProgram()); expect(script).toContain("--token"); expect(script).not.toContain("-t,"); }); it.skipIf(process.platform === "win32")( "completes both root short flags and their long aliases in real Bash", () => { const completions = runGeneratedBashCompletion(createDocumentedCompletionProgram(), [ "openclaw", "-", ]); expect(completions).toEqual(expect.arrayContaining(["-v", "--verbose", "--status-json"])); expect(completions.some((flag) => flag.endsWith(","))).toBe(false); }, ); it.skipIf(process.platform === "win32")( "completes every documented completion short flag in real Bash", () => { const completions = runGeneratedBashCompletion(createDocumentedCompletionProgram(), [ "openclaw", "completion", "-", ]); expect(completions).toEqual( expect.arrayContaining([ "-s", "--shell", "-i", "--install", "-y", "--yes", "--write-state", ]), ); expect(completions.some((flag) => flag.endsWith(","))).toBe(false); }, ); it.skipIf(process.platform === "win32")( "completes both nested value-taking flag aliases in real Bash", () => { const completions = runGeneratedBashCompletion(createDocumentedCompletionProgram(), [ "openclaw", "gateway", "-", ]); expect(completions).toEqual(expect.arrayContaining(["-t", "--token", "--force"])); expect(completions.some((flag) => flag.endsWith(","))).toBe(false); }, ); it.skipIf(process.platform === "win32")( "filters short and long aliases independently in real Bash", () => { const program = createDocumentedCompletionProgram(); expect(runGeneratedBashCompletion(program, ["openclaw", "completion", "-s"])).toEqual(["-s"]); expect(runGeneratedBashCompletion(program, ["openclaw", "completion", "--s"])).toEqual([ "--shell", ]); }, ); it.skipIf(process.platform === "win32").each([ { name: "a long shell flag", words: ["openclaw", "completion", "--shell", "f"], expected: ["fish"], }, { name: "a short shell flag", words: ["openclaw", "completion", "-s", "f"], expected: ["fish"], }, { name: "an inline long shell flag", words: ["openclaw", "completion", "--shell=f"], expected: ["--shell=fish"], }, { name: "an inline short shell flag", words: ["openclaw", "completion", "-s=f"], expected: ["-s=fish"], }, ])("completes validated values in real Bash after $name", ({ words, expected }) => { expect(runGeneratedBashCompletion(createDocumentedCompletionProgram(), words)).toEqual( expected, ); }); it.skipIf(process.platform === "win32").each([ { name: "an omitted optional value", words: ["openclaw", "--mode", "--j"], expected: ["--json"], }, { name: "a separate optional value", words: ["openclaw", "--mode", "a"], expected: ["auto"], }, { name: "an inline optional value", words: ["openclaw", "--mode=a"], expected: ["--mode=auto"], }, { name: "a hyphen-prefixed optional choice", words: ["openclaw", "--mode", "-l"], expected: ["-legacy"], }, ])("preserves real Bash completion after $name", ({ words, expected }) => { expect(runGeneratedBashCompletion(createOptionalChoiceCompletionProgram(), words)).toEqual( expected, ); }); it.skipIf(process.platform === "win32").each([ { name: "whitespace", value: "two words", prefix: "two ", }, { name: "double quotes", value: 'say "hello"', prefix: 'say "', }, { name: "apostrophes", value: "it's literal", prefix: "it's", }, { name: "literal command substitution", value: "$(printf OPENCLAW_COMPLETION_VALUE_EXECUTED >&2)", prefix: "$(", }, { name: "literal backtick substitution", value: "`printf OPENCLAW_COMPLETION_VALUE_EXECUTED >&2`", prefix: "`", }, ])("keeps Bash choice $name literal without executing it", ({ value, prefix }) => { const program = new Command().name("openclaw"); program.addOption(new Option("--value ", "Value").choices([value])); expect(runGeneratedBashCompletion(program, ["openclaw", "--value", prefix])).toEqual([value]); expect(runGeneratedBashCompletion(program, ["openclaw", `--value=${prefix}`])).toEqual([ `--value=${value}`, ]); }); it.skipIf(process.platform === "win32").each([ { name: "a root option", words: ["openclaw", "--channel", "b"], expected: ["beta"], }, { name: "an inherited parent option", words: ["openclaw", "cron", "create", "--channel", "pre"], expected: ["preview"], }, { name: "an inline inherited parent option", words: ["openclaw", "cron", "create", "--channel=pre"], expected: ["--channel=preview"], }, { name: "a differently prefixed inherited parent choice", words: ["openclaw", "cron", "create", "--channel", "pro"], expected: ["production"], }, ])("uses the nearest validated Bash choices for $name", ({ words, expected }) => { const program = createAliasedCompletionProgram(); program.addOption( new Option("--channel ", "Update channel").choices(["stable", "beta"]), ); const cron = program.commands.find((command) => command.name() === "cron"); if (!cron) { throw new Error("Cron command is unavailable"); } cron.addOption( new Option("--channel ", "Cron channel").choices(["production", "preview"]), ); expect(runGeneratedBashCompletion(program, words)).toEqual(expected); }); it("preserves documented short and long completion flags in Fish and Zsh", () => { const program = createDocumentedCompletionProgram(); const fishScript = getCompletionScript("fish", program); const zshScript = getCompletionScript("zsh", program); expect(fishScript).toContain(" -s s -l shell "); expect(fishScript).toContain(" -s i -l install "); expect(fishScript).toContain(" -s y -l yes "); expect(zshScript).toContain("{--shell,-s}"); expect(zshScript).toContain("{--install,-i}"); expect(zshScript).toContain("{--yes,-y}"); }); it("preserves required shell values and their Commander choices in Fish and Zsh", () => { const program = createDocumentedCompletionProgram(); const fishScript = getCompletionScript("fish", program); const zshScript = getCompletionScript("zsh", program); expect(fishScript).toContain(" -s s -l shell -r -f -a "); expect(fishScript).toContain(`"'zsh' 'bash' 'powershell' 'fish'"`); expect(zshScript).toContain( `{--shell,-s}"[Shell to generate completion for (default: zsh)]:shell:(zsh bash powershell fish)"`, ); expect(zshScript).toContain('{--token,-t}"[Gateway token]:token:"'); }); it.skipIf(process.platform === "win32")( "completes Commander option choices instead of commands in real Bash", () => { const program = createDocumentedCompletionProgram(); expect( runGeneratedBashCompletion(program, ["openclaw", "completion", "--shell", ""]), ).toEqual(["zsh", "bash", "powershell", "fish"]); expect(runGeneratedBashCompletion(program, ["openclaw", "completion", "-s", "f"])).toEqual([ "fish", ]); expect(runGeneratedBashCompletion(program, ["openclaw", "completion", "--shell=f"])).toEqual([ "--shell=fish", ]); expect( runGeneratedBashCompletion(program, ["openclaw", "completion", "--shell", "=", "f"]), ).toEqual(["fish"]); expect(runGeneratedBashCompletion(program, ["openclaw", "completion", "-sf"])).toEqual([ "-sfish", ]); expect(runGeneratedBashCompletion(program, ["openclaw", "completion", "-ysf"])).toEqual([ "-ysfish", ]); expect(runGeneratedBashCompletion(program, ["openclaw", "completion", "-ys", "f"])).toEqual([ "fish", ]); expect(runGeneratedBashCompletion(program, ["openclaw", "completion", "-ys"])).toEqual([ "-yszsh", "-ysbash", "-yspowershell", "-ysfish", ]); }, ); it.skipIf(process.platform === "win32")( "keeps optional choice values from consuming the following option in real Bash", () => { const program = new Command() .name("openclaw") .addOption(new Option("-c, --color [when]").choices(["always", "never"])) .option("-v, --verbose", "Verbose output"); expect(runGeneratedBashCompletion(program, ["openclaw", "--color", "a"])).toEqual(["always"]); expect(runGeneratedBashCompletion(program, ["openclaw", "--color", "--v"])).toEqual([ "--verbose", ]); expect(runGeneratedBashCompletion(program, ["openclaw", "-ca"])).toEqual(["-calways"]); expect(runGeneratedBashCompletion(program, ["openclaw", "-vca"])).toEqual(["-vcalways"]); }, ); it("includes Commander option choices in the PowerShell argument completer", () => { const script = getCompletionScript("powershell", createDocumentedCompletionProgram()); expect(script).toContain("switch ($candidatePath)"); expect(script).toContain("$choiceFlag -in @('-s','--shell')"); expect(script).toContain("$wordToComplete -match '^(--[^=]+)=(.*)$'"); expect(script).toContain("$wordToComplete -match '^-[^-].+$'"); expect(script).toContain("StartsWith($choicePrefix, [StringComparison]::OrdinalIgnoreCase)"); expect(script).toContain("@('zsh','bash','powershell','fish')"); }); it("omits empty PowerShell command-path switches for root-only programs", () => { const program = new Command() .name("openclaw") .addOption(new Option("--theme ").choices(["light", "dark"])); expect(getCompletionScript("powershell", program)).not.toContain("switch ($candidatePath)"); }); it("quotes PowerShell choice completion text while preserving its display value", () => { const program = new Command() .name("openclaw") .addOption(new Option("--theme ").choices(["light blue", "Bob's green", "path`name"])); const script = getCompletionScript("powershell", program); expect(script).toContain('$completionText = "$choiceCompletionPrefix$choiceValue"'); expect(script).toContain('$_.Replace("\'", "\'\'")'); expect(script).toContain( "[System.Management.Automation.CompletionResult]::new($completionText, $_, 'ParameterValue', $_)", ); }); itWithPowerShell.each([ ["a separate option value", "openclaw completion --shell f", "fish"], ["an attached option value", "openclaw completion --shell=f", "--shell=fish"], ["an attached short option value", "openclaw completion -sf", "-sfish"], ["a short-option cluster value", "openclaw completion -ysf", "-ysfish"], ["a separated short-option cluster value", "openclaw completion -ys f", "fish"], ])("completes PowerShell Commander choices after %s", (_name, commandLine, expected) => { expect( runGeneratedPowerShellCompletion(createDocumentedCompletionProgram(), commandLine), ).toEqual([expected]); }); itWithPowerShell("completes an empty attached short-option cluster value", () => { expect( runGeneratedPowerShellCompletion( createDocumentedCompletionProgram(), "openclaw completion -ys", ), ).toEqual(["-yszsh", "-ysbash", "-yspowershell", "-ysfish"]); }); itWithPowerShell.each([ ["a spaced choice", "openclaw --theme l", "'light blue'"], ["an attached spaced choice", "openclaw --theme=l", "--theme='light blue'"], ["an apostrophe", "openclaw --theme Bob", "'Bob''s green'"], ["a backtick", "openclaw --theme p", "'path`name'"], ])("quotes %s in real PowerShell completion text", (_name, commandLine, expected) => { const program = new Command() .name("openclaw") .addOption(new Option("--theme ").choices(["light blue", "Bob's green", "path`name"])); expect(runGeneratedPowerShellCompletion(program, commandLine)).toEqual([expected]); }); itWithPowerShell( "keeps optional choice values from consuming the following PowerShell option", () => { const program = new Command() .name("openclaw") .addOption(new Option("-c, --color [when]").choices(["always", "never"])) .option("-v, --verbose", "Verbose output"); expect(runGeneratedPowerShellCompletion(program, "openclaw --color a")).toEqual(["always"]); expect(runGeneratedPowerShellCompletion(program, "openclaw --color --v")).toEqual([ "--verbose", ]); }, ); it.skipIf(process.platform === "win32")( "preserves spaces and apostrophes inside individual Commander choices", () => { const program = new Command() .name("openclaw") .addOption( new Option("--theme ", "Color theme").choices([ "light blue", "dark", "Bob's green", ]), ); expect(runGeneratedBashCompletion(program, ["openclaw", "--theme", "l"])).toEqual([ "light blue", ]); expect(runGeneratedBashCompletion(program, ["openclaw", "--theme", "Bob"])).toEqual([ "Bob's green", ]); expect(getCompletionScript("fish", program)).toContain(`"'light blue' 'dark'`); expect(getCompletionScript("zsh", program)).toContain(":theme:(light"); }, ); it("generates valid Bash completion without subcommands", () => { if (process.platform === "win32") { return; } const script = getCompletionScript("bash", new Command().name("openclaw")); const result = spawnSync("bash", ["--noprofile", "--norc", "-n"], { encoding: "utf8", input: script, }); expect(result.stderr).toBe(""); expect(result.status).toBe(0); }); });