Files
fish-config/tests/functional.fish
T
rootiest f6ead99f48 feat(help): header-driven --help for development functions, plus the wiring guard
bd-pull, branch, check_fish_deps, claude-docs, claude-pr, fast, poke, qr
and parur take the standard call site. fish-deps instead routes -h/--help
to its existing __fish_deps_help, which previously answered only via the
unknown-subcommand path, printing "Unknown subcommand: --help" and
exiting 1.

This completes the 40 conversions and lands
test_every_user_facing_function_has_help -- acceptance criterion 6, the
check that fails when a new user-facing function ships without help. It
was developed red against the pre-conversion tree and reported exactly
the 40 names of the design's CONVERT table:

    bd-pull bkg branch check_fish_deps claude-docs claude-pr cleanup
    fast fc fish-deps fzf-update gip gip4 gip6 hist lD ld limine-edit
    lock lsr lss lstree lt ltr lx parur poke ports qr sbver screensleep
    split spwin steam-dl sudo-toggle swapstat tab tmux-clean upgrade
    wake-lock

each as "<name>: no -h/--help handling and not in $__help_exempt", with
the guard exiting 1. The count fell 40 -> 32 -> 24 -> 17 -> 10 -> 0
across the conversion commits. It is committed green so every commit on
this branch passes the suite.

$__help_exempt is the single machine-readable exemption record; a second
assertion fails if an exempt name stops being a published function, so a
rename or deletion upstream cannot silently exempt nothing.
2026-09-07 20:09:30 -04:00

373 lines
13 KiB
Fish

# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Functional checks for foundational config behavior. Sourced inside a
# fully-loaded, sandboxed interactive fish session by tests/run-tests.fish
# -- see that file for the sandbox setup. Each test_* function returns 0
# on pass, non-zero on fail; functional_test_main collects and runs them.
function test_xdg_defaults
test -n "$XDG_CONFIG_HOME" -a -n "$XDG_CACHE_HOME" \
-a -n "$XDG_DATA_HOME" -a -n "$XDG_STATE_HOME"
end
function test_path_additions
contains -- "$HOME/.local/bin" $PATH
end
function test_cdpath
contains -- "$HOME/projects" $CDPATH
end
function test_vi_key_bindings
test "$fish_key_bindings" = fish_vi_key_bindings
end
function test_abbreviations_loaded
abbr -q n
end
function test_core_functions_defined
for f in cat logs config-help fish-deps check_fish_deps config-settings
if not functions -q $f
echo " missing function: $f"
return 1
end
end
end
function test_exit_rewired
functions -q exit
and functions exit | string match -q '*smart_exit*'
end
function test_op_registry_lookup
functions -q __fish_config_op_registry_lookup
or return 1
set -l tags (__fish_config_op_registry_lookup config cdpath)
test $status -eq 0 -a (count $tags) -gt 0
end
function test_privacy_variables
test "$DO_NOT_TRACK" = "1" -a "$DISABLE_TELEMETRY" = "1"
end
function test_privacy_op_registry_lookup
functions -q __fish_config_op_registry_lookup
or return 1
set -l tags (__fish_config_op_registry_lookup config privacy)
test $status -eq 0 -a "$tags" = "overrides/privacy"
end
function test_op_enabled_fail_open
# An identity/site pair with no registry entry must resolve to
# enabled -- the documented fail-open default.
__fish_config_op_enabled __fish_config_test_never_registered somesite
end
function test_greeting_function_defined
functions -q fish_greeting
end
function test_agents_vault_defined
for f in agents-vault _agents_vault_dir _agents_repo_slug \
_agents_repo_ensure_symlink _agents_repo_sync \
_agents_repo_install_tools
if not functions -q $f
echo " missing function: $f"
return 1
end
end
end
function test_wrappers_call_agents_vault
functions -q claude; or return 1
functions claude | string match -q '*agents-vault*'; or return 1
functions -q agy; or return 1
functions agy | string match -q '*agents-vault*'
end
function test_vault_dir_honors_override
set -l saved
set -q __fish_agent_vault_dir; and set saved $__fish_agent_vault_dir
set -g __fish_agent_vault_dir /tmp/vault-override-check
set -l got (_agents_vault_dir)
set -e __fish_agent_vault_dir
test (count $saved) -gt 0; and set -g __fish_agent_vault_dir $saved
test "$got" = /tmp/vault-override-check
end
# ── Header-driven --help ─────────────────────────────────────────────
# Helper: run `<fn> $argv` in a throwaway fish that can see both $dir and
# the loaded session's function path, so a fixture function can call the
# real __fish_help_header. Paths here are mktemp -d output, never spaced.
function _help_probe --argument-names dir
env TERM=dumb fish --no-config -c \
"set -g fish_function_path $dir $fish_function_path; $argv[2..]"
end
function test_help_renderer
set -l tmp (mktemp -d)
printf '%s\n' \
'# Copyright (C) 2026 Rootiest' \
'' \
'# CATEGORY' \
'# 99-fixture' \
'#' \
'# SYNOPSIS' \
'# fixturefn [options]' \
'#' \
'# DESCRIPTION' \
'# First paragraph.' \
'#' \
'# Second paragraph.' \
'#' \
'# ARGUMENTS' \
'# -x Do the thing' \
'# more Indented continuation' \
'#' \
'# EXAMPLE' \
'# fixturefn -x' \
'function fixturefn' \
' __fish_help_header (status current-function) $argv; and return 0' \
' echo RAN-BODY' \
'end' >$tmp/fixturefn.fish
set -l out (_help_probe $tmp 'fixturefn --help')
set -l code $status
set -l text (string join \n $out)
rm -rf $tmp
set -l failed 0
if test $code -ne 0
echo " renderer exited $code, expected 0"
set failed 1
end
if contains -- RAN-BODY $out
echo " body executed despite --help"
set failed 1
end
if not contains -- USAGE $out
echo " missing USAGE heading (SYNOPSIS should render as USAGE)"
set failed 1
end
if contains -- CATEGORY $out
echo " CATEGORY leaked into the menu"
set failed 1
end
if not string match -q '* more Indented continuation*' -- $text
echo " nested ARGUMENTS indentation lost"
set failed 1
end
# Index-based, not a glob: fish's `string match` glob `*` does not
# span newlines, so a pattern straddling two lines silently never
# matches and the assertion would pass for the wrong reason.
set -l i (contains -i -- " First paragraph." $out)
if test -z "$i"
echo " DESCRIPTION body missing entirely"
set failed 1
else
# Indices hoisted: a command substitution inside a quoted index
# ("$out[(math ...)]") is a fish parse error, not an expansion.
set -l gap (math $i + 1)
set -l nxt (math $i + 2)
if test -n "$out[$gap]"
echo " multi-paragraph DESCRIPTION lost its blank line"
set failed 1
else if test "$out[$nxt]" != " Second paragraph."
echo " second paragraph missing after the blank"
set failed 1
end
end
test $failed -eq 0
end
function test_help_renderer_degrades_safely
# The renderer must return 1 ONLY when argv[1] is not a help flag.
# A missing or label-less header must still print and exit 0, because
# returning 1 hands control back to the caller's body -- and the body
# of upgrade(1) is `paru -Syu --noconfirm`.
set -l tmp (mktemp -d)
printf '%s\n' \
'function headerless' \
' __fish_help_header (status current-function) $argv; and return 0' \
" touch $tmp/BODY-RAN" \
'end' >$tmp/headerless.fish
# A comment run carrying no `# LABEL` line at all.
printf '%s\n' \
'# just an ordinary comment, no labels here' \
'function malformed' \
' __fish_help_header (status current-function) $argv; and return 0' \
" touch $tmp/BODY-RAN" \
'end' >$tmp/malformed.fish
set -l failed 0
for fn in headerless malformed
set -l out (_help_probe $tmp "$fn --help")
set -l code $status
if test $code -ne 0
echo " $fn --help exited $code, expected 0"
set failed 1
end
if test (count $out) -eq 0
echo " $fn --help printed nothing"
set failed 1
end
if not contains -- $fn $out
echo " $fn --help did not name the function"
set failed 1
end
if test -e $tmp/BODY-RAN
echo " $fn executed its body despite --help"
set failed 1
rm -f $tmp/BODY-RAN
end
end
# The inverse: no help flag must return 1 and let the body run.
_help_probe $tmp headerless >/dev/null 2>&1
if not test -e $tmp/BODY-RAN
echo " body did NOT run when no help flag was passed"
set failed 1
end
rm -rf $tmp
# Explicit, never a trailing `if`: standing gotcha #5 -- an if with no
# branch taken resolves $status to 0 and the test would pass silently.
test $failed -eq 0
end
function test_help_never_executes_destructive_path
# These eight ignore $argv entirely, so before the header-driven help
# landed, `upgrade --help` ran `paru -Syu --noconfirm`. The check has
# to prove --help does NOT reach the destructive path *without* ever
# running it: every external binary the eight can reach is shadowed by
# a recording stub on PATH, and the recorder must stay empty.
#
# WARNING: a silent pass here means a MISSING STUB, not success. If a
# function shows neither an EXECUTED line nor its own help, its
# command is absent from the stub list below -- add it. A test that
# cannot fail proves nothing about a body that runs sudo pacman -Rns.
set -l root (realpath (dirname (status filename))/..)
set -l tmp (mktemp -d)
mkdir -p $tmp/bin
set -l log $tmp/invoked.log
touch $log
for b in sudo pacman paru yay loginctl busctl tmux systemd-inhibit \
sudoedit limine-enroll-config limine-mkinitcpio sbctl git fzf steam
printf '#!/bin/sh\necho "$(basename "$0") $*" >> %s\n' $log >$tmp/bin/$b
chmod +x $tmp/bin/$b
end
set -l failed 0
for fn in cleanup fzf-update limine-edit lock screensleep sudo-toggle \
tmux-clean upgrade
set -l out (env TERM=dumb PATH="$tmp/bin:$PATH" HOME=$tmp \
fish --no-config -c \
"set -g fish_function_path $root/functions $fish_function_path
$fn --help" 2>/dev/null)
set -l code $status
if test $code -ne 0
echo " $fn --help exited $code, expected 0"
set failed 1
end
if not contains -- $fn $out
echo " $fn --help did not print its own help"
set failed 1
end
set -l ran (string trim -- (command cat $log))
if test -n "$ran"
echo " $fn --help EXECUTED: $ran"
set failed 1
end
echo -n "" >$log
end
rm -rf $tmp
test $failed -eq 0
end
# Functions published in the manual that are exempt from the -h/--help
# rule. Rationale per entry: AGENTS/specs/2026-09-07-header-driven-help-design.md
# §4. This array is the ONLY machine-readable copy of the exempt set.
#
# EXEMPT-A -- shadows a same-named binary, or forwards $argv to one named
# tool that owns its own --help. Intercepting would hide that tool's help,
# and for the C1-guarded shadows it also breaks the disabled-fallback
# contract, where the bare tool is supposed to answer.
set -g __help_exempt \
agy antigravity-ide bash cat cdi cffetch cheat claude clone clonet \
config-toggle copy docker du dusize fast-cli ffetch gitui gitup jr \
joplin less ls mkdir mv paste ping rawfish rg rm search ssh top \
view yt-dlp
# EXEMPT-B -- invoked by fish, never typed by a user.
set -a __help_exempt fish_prompt fish_right_prompt fish_mode_prompt \
sponge_filter_secrets
function test_every_user_facing_function_has_help
set -l root (realpath (dirname (status filename))/..)
set -l failed 0
set -l published
for f in $root/functions/*.fish
set -l lines (string split \n -- (command cat $f))
# Published == carries a `# CATEGORY` block, matching
# manualtools.parse_functions.
contains -- "# CATEGORY" (string trim -- $lines); or continue
# Resolve the real defined name; the file stem can disagree
# (dops.fish defines `docker` -- see JOB-BRIEF-FINDINGS.md §1).
set -l name (string match -rg '^\s*function\s+(\S+)' -- $lines)[1]
test -n "$name"; or continue
set name (string trim -c "'\"" -- $name)
string match -q '_*' -- $name; and continue
set -a published $name
contains -- $name $__help_exempt; and continue
# Body == everything from the `function` line down, comment lines
# dropped, so a header that merely mentions --help cannot pass.
set -l body
set -l in_body 0
for l in $lines
test $in_body -eq 1; or string match -qr '^\s*function\s' -- $l; and set in_body 1
test $in_body -eq 1; or continue
string match -qr '^\s*#' -- $l; and continue
set -a body $l
end
if not string match -qr -- '__fish_help_header|_flag_help|h/help|--help' \
(string join \n -- $body)
echo " $name: no -h/--help handling and not in \$__help_exempt"
set failed 1
end
end
# Guard against a stale exempt list: every exempt name must still be a
# published function. Catches renames and deletions.
for e in $__help_exempt
if not contains -- $e $published
echo " \$__help_exempt lists '$e', which is no longer published"
set failed 1
end
end
test $failed -eq 0
end
function functional_test_main
set -l names (functions -a | string match 'test_*' | sort)
set -l failed 0
for name in $names
if $name
echo " PASS $name"
else
echo " FAIL $name"
set failed (math $failed + 1)
end
end
echo ""
echo (math (count $names) - $failed)"/"(count $names)" passed"
return $failed
end