t3-code-android-nightly/.repos/effect-smol/packages/effect/test/cli/completions/completions.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

915 lines
33 KiB
TypeScript

import { assert, describe, expect, it } from "@effect/vitest"
import { Argument, Command, Flag } from "effect/cli"
import * as Completions from "effect/cli/Completions"
import * as Bash from "effect/cli/internal/completions/bash"
import { fromCommand } from "effect/cli/internal/completions/descriptor"
import * as Fish from "effect/cli/internal/completions/fish"
import * as Zsh from "effect/cli/internal/completions/zsh"
import { ComprehensiveCli } from "../fixtures/ComprehensiveCli.ts"
// ---------------------------------------------------------------------------
// Shared test fixtures
// ---------------------------------------------------------------------------
const simpleCmd = Command.make("greet", {
name: Argument.String("name").pipe(
Argument.withDescription("Name to greet")
),
loud: Flag.Boolean("loud").pipe(
Flag.withAlias("l"),
Flag.withDescription("Shout the greeting")
),
times: Flag.Int("times").pipe(
Flag.withDescription("Repeat count"),
Flag.withDefault(1)
)
}).pipe(Command.withDescription("Greet someone"))
const withSubcommands = (() => {
const start = Command.make("start", {
port: Flag.Int("port").pipe(
Flag.withAlias("p"),
Flag.withDescription("Port number")
),
daemon: Flag.Boolean("daemon").pipe(
Flag.withDescription("Run as daemon")
)
}).pipe(Command.withDescription("Start the server"))
const stop = Command.make("stop", {
force: Flag.Boolean("force").pipe(
Flag.withAlias("f"),
Flag.withDescription("Force stop")
)
}).pipe(Command.withDescription("Stop the server"))
return Command.make("server", {
verbose: Flag.Boolean("verbose").pipe(Flag.withAlias("v")),
config: Flag.String("config")
}).pipe(
Command.withDescription("Server management"),
Command.withSubcommands([start, stop])
)
})()
const withChoices = Command.make("deploy", {
env: Flag.Literals("env", ["dev", "staging", "prod"]).pipe(
Flag.withDescription("Target environment")
),
region: Argument.Literals("region", ["us-east", "eu-west", "ap-south"]).pipe(
Argument.withDescription("Deployment region")
)
}).pipe(Command.withDescription("Deploy application"))
const trickyValues = [
"it's-fine",
"node:20",
"with space",
"(whoami)",
"#tag",
"$HOME",
"back\\slash",
`say"hi"`,
"a*b",
"a;b",
"~x",
"foo'",
"a!b",
"\u{1F680}"
]
const withTrickyChoices = Command.make("deploy", {
mode: Flag.Literals("mode", trickyValues).pipe(
Flag.withDescription("Deploy mode")
),
target: Argument.Literals("target", ["o'clock", "a:b", "{x,y}", "a\u{1F600}b"]).pipe(
Argument.withDescription("Deployment target")
)
}).pipe(Command.withDescription("Deploy application"))
const withPaths = Command.make("process", {
input: Flag.File("input").pipe(Flag.withDescription("Input file")),
outDir: Flag.Directory("output-dir").pipe(Flag.withDescription("Output directory")),
source: Argument.File("source", { mustExist: false }).pipe(
Argument.withDescription("Source file")
)
}).pipe(Command.withDescription("Process files"))
const withOptionalDirectoryAndSubcommands = Command.make("example", {
directory: Argument.Directory("directory").pipe(
Argument.withDescription("Directory to start in"),
Argument.optional
)
}).pipe(
Command.withSubcommands([
Command.make("serve").pipe(Command.withDescription("Start the server"))
])
)
const nested3Levels = (() => {
const leaf = Command.make("action", {
dryRun: Flag.Boolean("dry-run").pipe(Flag.withDescription("Dry run mode"))
}).pipe(Command.withDescription("Perform action"))
const mid = Command.make("sub").pipe(
Command.withSubcommands([leaf])
)
return Command.make("top").pipe(
Command.withSubcommands([mid])
)
})()
const emptyCmd = Command.make("noop").pipe(
Command.withDescription("Does nothing")
)
const choicesHelperSource = (script: string): string => {
const start = script.indexOf("_deploy--choices()")
const end = script.indexOf("\n}\n", start)
return script.slice(start, end + 2)
}
const linesWith = (script: string, needle: string): string =>
script.split("\n").filter((line) => line.includes(needle)).map((line) => line.trim()).join("\n")
// ---------------------------------------------------------------------------
// Bash completions
// ---------------------------------------------------------------------------
describe("Bash completions", () => {
it("completes the active positional argument instead of always using the first", () => {
const descriptor: Completions.CommandDescriptor = {
name: "tool",
description: undefined,
flags: [
{
name: "verbose",
aliases: ["v"],
description: undefined,
type: { _tag: "Boolean" }
},
{
name: "format",
aliases: ["f"],
description: undefined,
type: { _tag: "Choice", values: ["json", "text"] }
}
],
arguments: [
{
name: "source",
description: undefined,
required: true,
variadic: false,
type: { _tag: "Choice", values: ["one"] }
},
{
name: "target",
description: undefined,
required: true,
variadic: false,
type: { _tag: "Choice", values: ["two"] }
}
],
subcommands: []
}
const script = Bash.generate("tool", descriptor)
assert.include(script, `for ((i = _command_index + 1; i < cword; i++)); do`)
assert.include(script, `--verbose|-v|--no-verbose) ;;`)
assert.include(script, `--format|-f) _skip_next=1 ;;`)
assert.include(script, `--format=*|-f=*) ;;`)
assert.include(script, `0)\n _tool--choices "$cur" "$_comp_word" 'one'`)
assert.include(script, `1)\n _tool--choices "$cur" "$_comp_word" 'two'`)
})
it("generates completion function for root command", () => {
const desc = fromCommand(simpleCmd)
const script = Bash.generate("greet", desc)
assert.include(script, "_greet()")
assert.include(script, "complete -F _greet greet")
assert.include(script, `_init_completion -n "$COMP_WORDBREAKS" || return`)
})
it("includes subcommand names in word list", () => {
const desc = fromCommand(withSubcommands)
const script = Bash.generate("server", desc)
assert.include(script, "start)")
assert.include(script, "stop)")
})
it("does not dispatch subcommands from flag values", () => {
const desc = fromCommand(withSubcommands)
const script = Bash.generate("server", desc)
assert.include(
script,
`for ((i = _command_index + 1; i < cword; i++)); do
if (( _skip_next )); then
_skip_next=0
continue
fi
case "\${words[i]}" in
--config) _skip_next=1 ;;
--config=*) ;;
start)`
)
})
it("includes long flag names with -- prefix", () => {
const desc = fromCommand(simpleCmd)
const script = Bash.generate("greet", desc)
assert.include(script, "--loud")
assert.include(script, "--times")
})
it("includes short flag aliases", () => {
const desc = fromCommand(simpleCmd)
const script = Bash.generate("greet", desc)
assert.include(script, "-l")
})
it("generates --no-<flag> for boolean flags", () => {
const desc = fromCommand(simpleCmd)
const script = Bash.generate("greet", desc)
assert.include(script, "--no-loud")
})
it("uses compgen -f for file-type flags", () => {
const desc = fromCommand(withPaths)
const script = Bash.generate("process", desc)
assert.include(script, "compgen -f")
})
it("uses compgen -d for directory-type flags", () => {
const desc = fromCommand(withPaths)
const script = Bash.generate("process", desc)
assert.include(script, "compgen -d")
})
it("inlines choice values for choice flags", () => {
const desc = fromCommand(withChoices)
const script = Bash.generate("deploy", desc)
assert.include(script, `_deploy--choices "$cur" "$_comp_word" 'dev' 'staging' 'prod'`)
})
it("quotes choice values instead of exposing them to compgen -W re-expansion", () => {
const script = Bash.generate("deploy", fromCommand(withTrickyChoices))
expect(linesWith(script, `_deploy--choices "$cur"`)).toMatchInlineSnapshot(`
"_deploy--choices "$cur" "$_comp_word" 'it'\\''s-fine' 'node:20' 'with space' '(whoami)' '#tag' '$HOME' 'back\\slash' 'say"hi"' 'a*b' 'a;b' '~x' 'foo'\\''' 'a!b' '🚀'
_deploy--choices "$cur" "$_comp_word" 'o'\\''clock' 'a:b' '{x,y}' 'a😀b'"
`)
assert.notInclude(script, `compgen -W 'it`)
})
it("splits words on whitespace only, so a value holding a word-break character stays one word", () => {
const script = Bash.generate("deploy", fromCommand(withTrickyChoices))
assert.include(script, `_init_completion -n "$COMP_WORDBREAKS" || return`)
})
it("emits the choice helper", () => {
const script = Bash.generate("deploy", fromCommand(withTrickyChoices))
expect(choicesHelperSource(script)).toMatchInlineSnapshot(`
"_deploy--choices()
{
local _cur="$1" _word="$2"; shift 2
local _head="\${_cur%"$_word"}"
local _open=""
case "$_head" in
*\\') _open="'" ;;
*\\") _open='"' ;;
esac
local _prefix="$_cur" _committed="$_head"
_prefix=\${_prefix//\\\\/}; _prefix=\${_prefix//\\"/}; _prefix=\${_prefix//\\'/}
_committed=\${_committed//\\\\/}; _committed=\${_committed//\\"/}; _committed=\${_committed//\\'/}
COMPREPLY=()
local _choice _rest _match
for _choice in "$@"; do
[[ "$_choice" == "$_prefix"* ]] || continue
_rest="\${_choice#"$_committed"}"
case "$_open" in
"'")
if [[ "$_head" == "'" ]]; then
_match=\${_rest//\\'/\\'\\\\\\'\\'}
else
[[ "$_rest" == *\\'* ]] && continue
_match="$_rest"
fi
;;
'"')
_match="\${_rest//\\\\/\\\\\\\\}"
_match="\${_match//\\$/\\\\$}"
_match="\${_match//\\\`/\\\\\\\`}"
_match="\${_match//\\"/\\\\\\"}"
;;
*)
printf -v _match '%q' "$_rest"
[[ -z "$_head" && "$_match" == '~'* ]] && _match="\\\\$_match"
;;
esac
[[ -n "$_open" && "$_match" == *"$_open" ]] && _match+="$_open"
COMPREPLY+=("$_match")
done
}"
`)
})
it("generates separate functions for nested subcommands", () => {
const desc = fromCommand(withSubcommands)
const script = Bash.generate("server", desc)
assert.include(script, "_server()")
assert.include(script, "_server_start()")
assert.include(script, "_server_stop()")
assert.include(script, `_server_start "$i"`)
})
it("handles commands with no subcommands", () => {
const desc = fromCommand(emptyCmd)
const script = Bash.generate("noop", desc)
assert.include(script, "_noop()")
assert.include(script, "complete -F _noop noop")
})
it("generates deeply nested functions", () => {
const desc = fromCommand(nested3Levels)
const script = Bash.generate("top", desc)
assert.include(script, "_top()")
assert.include(script, "_top_sub()")
assert.include(script, "_top_sub_action()")
})
it("wraps script in begin/end markers", () => {
const desc = fromCommand(simpleCmd)
const script = Bash.generate("greet", desc)
assert.include(script, "###-begin-greet-completions-###")
assert.include(script, "###-end-greet-completions-###")
})
it("groups flag aliases for used-flag filtering", () => {
const desc = fromCommand(simpleCmd)
const script = Bash.generate("greet", desc)
assert.include(script, "--loud|-l|--no-loud) _used_0=1 ;;")
assert.include(script, "--times) _used_1=1 ;;")
assert.include(script, `[[ -n "$_used_0" ]] || _filtered_flags+=" --loud -l --no-loud"`)
assert.include(script, `[[ -n "$_used_1" ]] || _filtered_flags+=" --times"`)
// Uses _filtered_flags instead of a static word list
assert.include(script, "compgen -W \"$_filtered_flags\"")
})
it("does not generate flag groups for commands with no flags", () => {
const desc = fromCommand(emptyCmd)
const script = Bash.generate("noop", desc)
assert.notInclude(script, "_used_0")
assert.notInclude(script, "_filtered_flags")
})
it("uses no bash 4 syntax, so the script runs on the bash macOS ships", () => {
const script = Bash.generate("comprehensive", fromCommand(ComprehensiveCli))
assert.notInclude(script, "local -A")
assert.notInclude(script, "declare -A")
})
it("includes inline _init_completion fallback", () => {
const desc = fromCommand(simpleCmd)
const script = Bash.generate("greet", desc)
assert.include(script, "if ! type _init_completion &>/dev/null; then")
assert.include(script, "COMPREPLY=()")
assert.include(script, `if [[ "$_line" == [[:blank:]]* ]]; then`)
assert.include(script, "((_i == COMP_CWORD)) && cword=$_j")
assert.include(script, `cur="\${words[cword]}"`)
})
})
// ---------------------------------------------------------------------------
// Zsh completions
// ---------------------------------------------------------------------------
describe("Zsh completions", () => {
it("generates _arguments specs for flags", () => {
const desc = fromCommand(simpleCmd)
const script = Zsh.generate("greet", desc)
assert.include(script, "_arguments")
assert.include(script, "--loud")
assert.include(script, "--times")
})
it("includes flag descriptions in specs", () => {
const desc = fromCommand(simpleCmd)
const script = Zsh.generate("greet", desc)
assert.include(script, "Shout the greeting")
assert.include(script, "Repeat count")
})
it("includes subcommand descriptions with _describe", () => {
const desc = fromCommand(withSubcommands)
const script = Zsh.generate("server", desc)
assert.include(script, "_describe")
assert.include(script, "Start the server")
assert.include(script, "Stop the server")
})
it("generates --no-<flag> for boolean flags", () => {
const desc = fromCommand(simpleCmd)
const script = Zsh.generate("greet", desc)
assert.include(script, "--no-loud")
})
it("uses _files for file-type flags", () => {
const desc = fromCommand(withPaths)
const script = Zsh.generate("process", desc)
assert.include(script, "_files")
})
it("uses _directories for directory-type flags", () => {
const desc = fromCommand(withPaths)
const script = Zsh.generate("process", desc)
assert.include(script, "_directories")
})
it("inlines choice values with (val1 val2) syntax", () => {
const desc = fromCommand(withChoices)
const script = Zsh.generate("deploy", desc)
assert.include(script, "(dev staging prod)")
})
it("generates handler functions for nested subcommands", () => {
const desc = fromCommand(withSubcommands)
const script = Zsh.generate("server", desc)
assert.include(script, "_server()")
assert.include(script, "_server_start()")
assert.include(script, "_server_stop()")
})
it("generates argument specs for positional arguments", () => {
const desc = fromCommand(simpleCmd)
const script = Zsh.generate("greet", desc)
assert.include(script, "Name to greet")
})
it("generates choice argument completions", () => {
const desc = fromCommand(withChoices)
const script = Zsh.generate("deploy", desc)
assert.include(script, "(us-east eu-west ap-south)")
})
it("escapes choice values for both the spec quoting and the action list re-parse", () => {
const script = Zsh.generate("deploy", fromCommand(withTrickyChoices))
expect(linesWith(script, ":value:(")).toMatchInlineSnapshot(
`"'(--mode)--mode[Deploy mode]:value:(it\\'\\''s-fine node\\:20 with\\ space \\(whoami\\) \\#tag \\$HOME back\\\\slash say\\"hi\\" a\\*b a\\;b \\~x foo\\'\\'' a\\!b \\🚀)'"`
)
expect(linesWith(script, "Deployment target")).toMatchInlineSnapshot(
`"':Deployment target:(o\\'\\''clock a\\:b \\{x,y\\} a\\😀b)'"`
)
})
it("uses alternative argument sets for positional arguments and subcommands", () => {
const desc = fromCommand(withOptionalDirectoryAndSubcommands)
const script = Zsh.generate("example", desc)
assert.include(
script,
` -
parent-arguments
':Directory to start in:_directories'
-
subcommands
'1:command:->command'
'*::arg:->args'`
)
assert.notInclude(
script,
` ':Directory to start in:_directories'
'1:command:->command'`
)
})
it("starts with #compdef directive", () => {
const desc = fromCommand(simpleCmd)
const script = Zsh.generate("greet", desc)
assert.isTrue(script.startsWith("#compdef greet"))
})
it("wraps script in begin/end markers", () => {
const desc = fromCommand(simpleCmd)
const script = Zsh.generate("greet", desc)
assert.include(script, "###-begin-greet-completions-###")
assert.include(script, "###-end-greet-completions-###")
})
it("declares state machine locals for commands with subcommands", () => {
const desc = fromCommand(withSubcommands)
const script = Zsh.generate("server", desc)
assert.include(script, "local context state state_descr line")
assert.include(script, "typeset -A opt_args")
})
it("does not declare state machine locals for leaf commands", () => {
const desc = fromCommand(simpleCmd)
const script = Zsh.generate("greet", desc)
assert.notInclude(script, "local context state state_descr line")
assert.notInclude(script, "typeset -A opt_args")
})
it("uses specs array instead of line continuations", () => {
const desc = fromCommand(simpleCmd)
const script = Zsh.generate("greet", desc)
assert.include(script, "local -a specs")
assert.include(script, "specs=(")
assert.include(script, "_arguments \"${specs[@]}\"")
})
it("generates exclusion groups for flag aliases", () => {
const desc = fromCommand(simpleCmd)
const script = Zsh.generate("greet", desc)
// --loud, -l, and --no-loud should share an exclusion group
assert.include(script, "'(--loud -l --no-loud)--loud[Shout the greeting]'")
assert.include(script, "'(--loud -l --no-loud)-l[Shout the greeting]'")
assert.include(script, "'(--loud -l --no-loud)--no-loud[Disable loud]'")
})
it("generates exclusion group for flags without aliases", () => {
const desc = fromCommand(simpleCmd)
const script = Zsh.generate("greet", desc)
// --times has no alias, exclusion group is just (--times)
assert.include(script, "'(--times)--times[Repeat count]:integer:'")
})
it("uses Disable description for boolean negation", () => {
const desc = fromCommand(simpleCmd)
const script = Zsh.generate("greet", desc)
assert.include(script, "--no-loud[Disable loud]")
})
})
// ---------------------------------------------------------------------------
// Fish completions
// ---------------------------------------------------------------------------
describe("Fish completions", () => {
it("scopes nested completions by the full command path", () => {
const leaf = (name: string, flag: string): Completions.CommandDescriptor => ({
name,
description: undefined,
flags: [{ name: flag, aliases: [], description: undefined, type: { _tag: "Boolean" } }],
arguments: [],
subcommands: []
})
const descriptor: Completions.CommandDescriptor = {
name: "tool",
description: undefined,
flags: [],
arguments: [],
subcommands: [
{
name: "alpha",
description: undefined,
flags: [],
arguments: [],
subcommands: [leaf("common", "alpha-only")]
},
{ name: "beta", description: undefined, flags: [], arguments: [], subcommands: [leaf("common", "beta-only")] }
]
}
const lines = Fish.generate("tool", descriptor).split("\n")
const alphaOnly = lines.find((line) => line.includes("-l alpha-only"))!
const betaOnly = lines.find((line) => line.includes("-l beta-only"))!
assert.include(alphaOnly, "__fish_seen_subcommand_from alpha; and __fish_seen_subcommand_from common")
assert.include(betaOnly, "__fish_seen_subcommand_from beta; and __fish_seen_subcommand_from common")
})
it("generates complete commands for root subcommands", () => {
const desc = fromCommand(withSubcommands)
const script = Fish.generate("server", desc)
assert.include(script, "complete -c server")
assert.include(script, "-a 'start'")
assert.include(script, "-a 'stop'")
})
it("generates complete commands for flags with -l and -s", () => {
const desc = fromCommand(simpleCmd)
const script = Fish.generate("greet", desc)
assert.include(script, "-l loud")
assert.include(script, "-s l")
assert.include(script, "-l times")
})
it("generates --no-<flag> for boolean flags", () => {
const desc = fromCommand(simpleCmd)
const script = Fish.generate("greet", desc)
assert.include(script, "-l no-loud")
})
it("uses -r -F for file-type flags", () => {
const desc = fromCommand(withPaths)
const script = Fish.generate("process", desc)
assert.include(script, "-r -F")
})
it("uses -r -f -a for choice flags", () => {
const desc = fromCommand(withChoices)
const script = Fish.generate("deploy", desc)
assert.include(script, "-r -f -a 'dev staging prod'")
})
it("escapes choice values for both the string quoting and the expansion of the -a list", () => {
const script = Fish.generate("deploy", fromCommand(withTrickyChoices))
expect(linesWith(script, "-r -f -a")).toMatchInlineSnapshot(`
"complete -c deploy -n 'begin; not __fish_contains_opt mode; or contains -- (commandline -poc)[-1] --mode; end' -l mode -d 'Deploy mode' -r -f -a 'it\\\\\\'s-fine node\\\\:20 with\\\\ space \\\\(whoami\\\\) \\\\#tag \\\\$HOME back\\\\\\\\slash say\\\\"hi\\\\" a\\\\*b a\\\\;b \\\\~x foo\\\\\\' a\\\\!b \\\\🚀'
complete -c deploy -r -f -a 'o\\\\\\'clock a\\\\:b \\\\{x,y\\\\} a\\\\😀b' -d 'Deployment target'"
`)
})
it("escapes backslashes in descriptions before quotes", () => {
const trailingBackslash = Command.make("deploy", {
mode: Flag.Literals("mode", ["a"]).pipe(Flag.withDescription("Path like C:\\"))
})
const script = Fish.generate("deploy", fromCommand(trailingBackslash))
assert.include(script, `-d 'Path like C:\\\\'`)
})
it("uses -n conditions for nested subcommand flags", () => {
const desc = fromCommand(withSubcommands)
const script = Fish.generate("server", desc)
assert.include(script, "__fish_seen_subcommand_from start")
assert.include(script, "__fish_seen_subcommand_from stop")
})
it("includes descriptions with -d flag", () => {
const desc = fromCommand(withSubcommands)
const script = Fish.generate("server", desc)
assert.include(script, "-d 'Start the server'")
assert.include(script, "-d 'Stop the server'")
})
it("handles deeply nested command paths", () => {
const desc = fromCommand(nested3Levels)
const script = Fish.generate("top", desc)
assert.include(script, "__fish_seen_subcommand_from action")
assert.include(script, "-l dry-run")
})
it("handles commands with no flags", () => {
const desc = fromCommand(emptyCmd)
const script = Fish.generate("noop", desc)
assert.include(script, "###-begin-noop-completions-###")
assert.include(script, "###-end-noop-completions-###")
})
it("uses __fish_use_subcommand for root level", () => {
const desc = fromCommand(withSubcommands)
const script = Fish.generate("server", desc)
assert.include(script, "__fish_use_subcommand")
})
it("wraps script in begin/end markers", () => {
const desc = fromCommand(simpleCmd)
const script = Fish.generate("greet", desc)
assert.include(script, "###-begin-greet-completions-###")
assert.include(script, "###-end-greet-completions-###")
})
it("generates used-flag dedup conditions", () => {
const desc = fromCommand(simpleCmd)
const script = Fish.generate("greet", desc)
// --loud is boolean with alias -l — gets dedup condition on the -l entry
assert.include(script, "not __fish_contains_opt -s l loud no-loud")
const lines = script.split("\n")
const timesLongEntry = lines.find((l) => l.includes("-l times"))!
assert.include(
timesLongEntry,
"-n 'begin; not __fish_contains_opt times; or contains -- (commandline -poc)[-1] --times; end'"
)
const timesArgEntry = lines.find((l) => l.includes("-a '--times'"))!
assert.include(timesArgEntry, "not __fish_contains_opt times")
})
it("combines subcommand and dedup conditions", () => {
const desc = fromCommand(withSubcommands)
const script = Fish.generate("server", desc)
// daemon is boolean — gets subcommand + dedup condition on -l entry
assert.include(script, "__fish_seen_subcommand_from start; and not __fish_contains_opt daemon no-daemon")
const lines = script.split("\n")
const portLongEntry = lines.find((l) => l.includes("-l port"))!
assert.include(
portLongEntry,
"-n '__fish_seen_subcommand_from start; and begin; not __fish_contains_opt -s p port; or contains -- (commandline -poc)[-1] --port -p; end'"
)
const portArgEntry = lines.find((l) => l.includes("-a '--port'"))!
assert.include(portArgEntry, "not __fish_contains_opt -s p port")
})
it("root-level subcommands use __fish_use_subcommand without child guard", () => {
const desc = fromCommand(withSubcommands)
const script = Fish.generate("server", desc)
// Root-level subcommands only need __fish_use_subcommand (it already
// returns false once any subcommand is entered)
assert.include(script, "-n '__fish_use_subcommand' -f -a 'start'")
assert.include(script, "-n '__fish_use_subcommand' -f -a 'stop'")
})
it("guards nested child subcommands against re-offering", () => {
const desc = fromCommand(nested3Levels)
const script = Fish.generate("top", desc)
// sub's child "action" should be guarded
assert.include(
script,
"__fish_seen_subcommand_from sub; and not __fish_seen_subcommand_from action"
)
})
it("uses Disable description for boolean negation", () => {
const desc = fromCommand(simpleCmd)
const script = Fish.generate("greet", desc)
assert.include(script, "-l no-loud")
assert.include(script, "-d 'Disable loud'")
})
it("suppresses default file completion for commands without path arguments", () => {
const desc = fromCommand(withSubcommands)
const script = Fish.generate("server", desc)
// Root level: bare -f entry to suppress file listing
assert.include(script, "complete -c server -n '__fish_use_subcommand' -f")
// Leaf subcommand "start" has no path-type args — gets a bare -f entry
assert.include(script, "complete -c server -n '__fish_seen_subcommand_from start' -f")
})
it("adds -a entries for flags so they appear on bare TAB", () => {
const desc = fromCommand(simpleCmd)
const script = Fish.generate("greet", desc)
const lines = script.split("\n")
// --loud appears as an -a entry guarded by "not string match" and dedup
const loudArg = lines.find((l) => l.includes("-a '--loud'"))
assert.isDefined(loudArg)
assert.include(loudArg!, "not string match -q -- \"-*\" (commandline -ct)")
assert.include(loudArg!, "not __fish_contains_opt")
// --times also appears as an -a entry
const timesArg = lines.find((l) => l.includes("-a '--times'"))
assert.isDefined(timesArg)
assert.include(timesArg!, "not string match -q -- \"-*\" (commandline -ct)")
// Boolean negation also gets an -a entry
const noLoudArg = lines.find((l) => l.includes("-a '--no-loud'"))
assert.isDefined(noLoudArg)
})
it("bare-TAB -a entries use double quotes around glob pattern to avoid nested single-quote errors", () => {
const desc = fromCommand(simpleCmd)
const script = Fish.generate("greet", desc)
const lines = script.split("\n")
const argEntries = lines.filter((l) => /\s-a\s+'--/.test(l))
assert.isAbove(argEntries.length, 0)
for (const line of argEntries) {
// The -n condition must use double quotes around -* so it doesn't
// break the outer single-quoted string (Fish glob parse error).
assert.include(line, "\"-*\"", `bare-TAB entry should use double-quoted glob pattern: ${line}`)
assert.notInclude(line, "'-*'", `bare-TAB entry must NOT use single-quoted glob pattern: ${line}`)
}
})
it("does not suppress file completion for commands with path arguments", () => {
const desc = fromCommand(withPaths)
const script = Fish.generate("process", desc)
// "process" has a path-type positional arg — should NOT get a bare -f entry
const lines = script.split("\n")
const bareSuppression = lines.some((line) =>
// Match bare -f (file suppression) lines that don't have -l, -a, -r, -F
/^complete -c process( -n '[^']*')? -f$/.test(line)
)
assert.isFalse(bareSuppression, "Commands with path arguments should not suppress file completion")
})
})
// ---------------------------------------------------------------------------
// Completions dispatcher
// ---------------------------------------------------------------------------
describe("Completions", () => {
it("dispatches to bash generator", () => {
const desc = fromCommand(simpleCmd)
const script = Completions.generate("greet", "bash", desc)
assert.include(script, "complete -F _greet greet")
})
it("dispatches to zsh generator", () => {
const desc = fromCommand(simpleCmd)
const script = Completions.generate("greet", "zsh", desc)
assert.include(script, "#compdef greet")
})
it("dispatches to fish generator", () => {
const desc = fromCommand(simpleCmd)
const script = Completions.generate("greet", "fish", desc)
assert.include(script, "complete -c greet")
})
})
// ---------------------------------------------------------------------------
// Integration tests with ComprehensiveCli
// ---------------------------------------------------------------------------
describe("Completions integration", () => {
it("generates valid bash script for ComprehensiveCli", () => {
const desc = fromCommand(ComprehensiveCli)
const script = Bash.generate("mycli", desc)
// Root command
assert.include(script, "_mycli()")
assert.include(script, "complete -F _mycli mycli")
// Global flags
assert.include(script, "--debug")
assert.include(script, "-d")
assert.include(script, "--quiet")
assert.include(script, "-q")
assert.include(script, "--config")
assert.include(script, "--no-debug")
assert.include(script, "--no-quiet")
// Subcommands
assert.include(script, "_mycli_admin()")
assert.include(script, "_mycli_copy()")
assert.include(script, "_mycli_build()")
assert.include(script, "_mycli_git()")
// Nested subcommands
assert.include(script, "_mycli_admin_users()")
assert.include(script, "_mycli_admin_config()")
assert.include(script, "_mycli_git_clone()")
assert.include(script, "_mycli_git_add()")
assert.include(script, "_mycli_git_status()")
// Deeply nested
assert.include(script, "_mycli_admin_users_list()")
assert.include(script, "_mycli_admin_users_create()")
assert.include(script, "_mycli_admin_config_set()")
assert.include(script, "_mycli_admin_config_get()")
// File completion for copy command
assert.include(script, "compgen -f")
})
it("generates valid zsh script for ComprehensiveCli", () => {
const desc = fromCommand(ComprehensiveCli)
const script = Zsh.generate("mycli", desc)
// Zsh directives
assert.include(script, "#compdef mycli")
assert.include(script, "_arguments")
assert.include(script, "_describe")
// Root function
assert.include(script, "_mycli()")
// Global flags with descriptions
assert.include(script, "--debug")
assert.include(script, "Enable debug logging")
assert.include(script, "--quiet")
// Subcommand functions
assert.include(script, "_mycli_admin()")
assert.include(script, "_mycli_copy()")
assert.include(script, "_mycli_build()")
assert.include(script, "_mycli_git()")
// Nested subcommand functions
assert.include(script, "_mycli_admin_users()")
assert.include(script, "_mycli_git_clone()")
// File/directory completions
assert.include(script, "_files")
})
it("generates valid fish script for ComprehensiveCli", () => {
const desc = fromCommand(ComprehensiveCli)
const script = Fish.generate("mycli", desc)
// Fish complete commands
assert.include(script, "complete -c mycli")
// Root subcommands
assert.include(script, "-a 'admin'")
assert.include(script, "-a 'copy'")
assert.include(script, "-a 'build'")
assert.include(script, "-a 'git'")
// Root flags
assert.include(script, "-l debug")
assert.include(script, "-s d")
assert.include(script, "-l quiet")
assert.include(script, "-s q")
assert.include(script, "-l no-debug")
assert.include(script, "-l no-quiet")
// Descriptions
assert.include(script, "-d 'Administrative commands'")
assert.include(script, "-d 'Build the project'")
// Nested subcommand conditions
assert.include(script, "__fish_use_subcommand")
assert.include(script, "__fish_seen_subcommand_from admin")
assert.include(script, "__fish_seen_subcommand_from git")
})
})