From 51da3e9d83b426c98a9c784baec9bb2b557edb1a Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:57:07 -0400 Subject: [PATCH 01/10] test: add byte-identity harness for the shared palette Compares rendered stdout and stderr of 29 colour-bearing cases between a pristine baseline ref and the working tree, in isolated XDG_CONFIG_HOMEs that carry the gitignored fish_variables so the opinionated guards resolve. Two traps this harness exists to avoid, both of which silently produce a meaningless comparison rather than an error: - `git archive main` omits fish_variables because it is untracked. Without it __fish_config_op_enabled is unresolvable and every guarded function short-circuits, so all cases render empty and trivially "match". - `qc --help` shells out to aichat and never reaches its colour path unless aichat is on PATH; the harness stubs it. --- tests/palette-bytes.fish | 157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 tests/palette-bytes.fish diff --git a/tests/palette-bytes.fish b/tests/palette-bytes.fish new file mode 100644 index 0000000..b94a289 --- /dev/null +++ b/tests/palette-bytes.fish @@ -0,0 +1,157 @@ +#!/usr/bin/env fish +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Byte-identity harness for the shared output palette (__fish_palette). +# +# Compares rendered output of every colour-bearing function between a +# pristine checkout of a baseline git ref and the current working tree. +# This is a one-time acceptance harness, not part of run-tests.fish's +# permanent suite -- the permanent check lives in tests/functional.fish. +# +# Usage: +# fish tests/palette-bytes.fish [--baseline REF] byte-diff stdout+stderr +# fish tests/palette-bytes.fish --structural diff-shape assertion +# fish tests/palette-bytes.fish --startup startup medians +# +# Both sandboxes get an isolated XDG_CONFIG_HOME carrying a copy of the +# working tree's fish_variables. That file is gitignored, so `git archive` +# omits it; without it __fish_config_op_enabled is unresolvable and every +# opinionated-guarded function silently short-circuits. + +set -l repo (realpath (dirname (status filename))/..) +set -l mode bytes +set -l baseline main +for i in (seq (count $argv)) + switch $argv[$i] + case --structural; set mode structural + case --startup; set mode startup + case --baseline; set baseline $argv[(math $i + 1)] + end +end + +set -l tmp (mktemp -d) +function __pb_cleanup --on-event fish_exit --inherit-variable tmp + test -n "$tmp"; and rm -rf $tmp +end + +# ── Build the two sandboxes ──────────────────────────────────────────── +set -l A $tmp/base/fish # pristine baseline ref +set -l B $tmp/work/fish # current working tree +mkdir -p $A $B +git -C $repo archive $baseline | tar -x -C $A +or begin + echo "palette-bytes: cannot archive baseline ref '$baseline'" >&2 + exit 2 +end +for d in functions conf.d completions integrations themes data + test -d $repo/$d; and cp -r $repo/$d $B/ +end +cp $repo/config.fish $B/ 2>/dev/null +# fish_variables is gitignored -- copy it into BOTH sandboxes by hand. +for d in $A $B + cp $repo/fish_variables $d/ 2>/dev/null +end +# qc --help shells out to aichat; stub it so its colour path is reachable. +set -l stub $tmp/stub +mkdir -p $stub +printf '#!/bin/sh\necho "aichat stub"\n' >$stub/aichat +chmod +x $stub/aichat + +# ── The cases ────────────────────────────────────────────────────────── +# 25 --help paths (every converted function that has one) plus 4 error +# paths, two of which write to stderr. Side-effect-free by construction: +# --help returns before doing work, and each error path fails on argument +# validation. Do NOT add a case that mutates the filesystem. +set -l cases \ + "agents-init --help" "agents-vault --help" "auto-pull --help" \ + "config-settings --help" "config-update --help" "detach --help" \ + "dng2avif --help" "dockup --help" "edit --help" "jobrunner --help" \ + "kitty-logging --help" "logs --help" "mkcd --help" "open-url --help" \ + "p --help" "pkg --help" "play-media --help" "qc --help" \ + "rand_string --help" "replay --help" "repo-open --help" "scrub --help" \ + "smart_exit --help" "spark --help" "y --help" \ + "mkcd" "auto-pull remove __no_such_repo__" \ + "agents-init --no-such-flag" "pkg __no_such_subcommand__" + +function __pb_run --argument-names cfg stub cmd out + env XDG_CONFIG_HOME=(dirname $cfg) PATH="$stub:$PATH" TERM=xterm-256color \ + HOME=$HOME fish -c "$cmd" >$out.out 2>$out.err +end + +# ── Mode: bytes ──────────────────────────────────────────────────────── +if test $mode = bytes + echo "== palette byte-identity vs $baseline ==" + set -l failed 0 + set -l n 0 + for cmd in $cases + set n (math $n + 1) + __pb_run $A $stub "$cmd" $tmp/a$n + __pb_run $B $stub "$cmd" $tmp/b$n + set -l so ok + set -l se ok + cmp -s $tmp/a$n.out $tmp/b$n.out; or set so DIFF + cmp -s $tmp/a$n.err $tmp/b$n.err; or set se DIFF + if test $so = DIFF -o $se = DIFF + set failed (math $failed + 1) + printf ' FAIL %-34s stdout=%s stderr=%s\n' "$cmd" $so $se + test $so = DIFF; and diff -u (xxd $tmp/a$n.out | psub) (xxd $tmp/b$n.out | psub) | head -12 + test $se = DIFF; and diff -u (xxd $tmp/a$n.err | psub) (xxd $tmp/b$n.err | psub) | head -12 + else + printf ' ok %-34s (%s B out, %s B err)\n' "$cmd" (wc -c <$tmp/a$n.out | string trim) (wc -c <$tmp/a$n.err | string trim) + end + end + echo (math $n - $failed)"/$n cases byte-identical" + test $failed -eq 0 + exit $status +end + +# ── Mode: structural ─────────────────────────────────────────────────── +# For files converted WITHOUT drift renames, the whole diff must be +# declaration removals plus inserted __fish_palette calls. If that holds, +# the file's output strings are provably untouched. +if test $mode = structural + echo "== structural diff shape vs $baseline ==" + set -l bad 0 + for f in (git -C $repo diff --name-only $baseline -- functions/) + # fish_prompt.fish keeps its own hex palette -- see Task 10. + string match -q '*fish_prompt.fish' $f; and continue + set -l offenders + for line in (git -C $repo diff -U0 $baseline -- $f | string match -r '^[+-][^+-].*') + set -l body (string sub -s 2 -- $line) + string match -qr '^\s*set -l c_[a-z]+\s+\(set_color[^)]*\)\s*$' -- $body; and continue + string match -qr '^\s*__fish_palette\s*$' -- $body; and continue + set -a offenders $line + end + if test (count $offenders) -gt 0 + set bad (math $bad + 1) + echo " NOT PURELY STRUCTURAL $f" + printf ' %s\n' $offenders[1..3] + end + end + if test $bad -eq 0 + echo " all changed files are purely structural" + else + echo " $bad file(s) changed rendering text -- expected only for the drift-rename batch" + end + test $bad -eq 0 + exit $status +end + +# ── Mode: startup ────────────────────────────────────────────────────── +echo "== fish -c true, 31 interleaved pairs, median ==" +set -l ta +set -l tb +for i in (seq 31) + set -l s (date +%s%N) + env XDG_CONFIG_HOME=$tmp/base fish -c true >/dev/null 2>&1 + set -a ta (math "("(date +%s%N)" - $s) / 1000") + set s (date +%s%N) + env XDG_CONFIG_HOME=$tmp/work fish -c true >/dev/null 2>&1 + set -a tb (math "("(date +%s%N)" - $s) / 1000") +end +set -l sa (printf '%s\n' $ta | sort -n) +set -l sb (printf '%s\n' $tb | sort -n) +printf ' baseline median=%.2f ms p10=%.2f p90=%.2f\n' (math $sa[16]/1000) (math $sa[4]/1000) (math $sa[28]/1000) +printf ' working median=%.2f ms p10=%.2f p90=%.2f\n' (math $sb[16]/1000) (math $sb[4]/1000) (math $sb[28]/1000) +echo " (p10-p90 spread is ~15 ms; treat any delta inside it as noise)" -- 2.54.0 From a31a46bdc2933c866ae5e898528ac350857190b2 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 15:00:32 -0400 Subject: [PATCH 02/10] feat: add __fish_palette shared output palette helper Declared --no-scope-shadowing with a bare set, so the 12 colour roles land in the caller's scope under the same short names the consuming functions already interpolate. Keeping the names means the conversion never edits an output string in 33 of the 38 consumers. No consumer is converted yet; byte-identity harness still reports 29/29. --- functions/__fish_palette.fish | 63 +++++++++++++++++++++++++++++++++++ tests/functional.fish | 27 +++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 functions/__fish_palette.fish diff --git a/functions/__fish_palette.fish b/functions/__fish_palette.fish new file mode 100644 index 0000000..7097f39 --- /dev/null +++ b/functions/__fish_palette.fish @@ -0,0 +1,63 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# __fish_palette +# +# DESCRIPTION +# Defines the shared terminal-output colour roles used by the +# user-facing functions in this configuration. Declared +# --no-scope-shadowing and using a bare `set`, so the variables are +# created in the CALLER's scope -- a consumer just calls it and then +# interpolates $c_head, $c_err and friends exactly as it did when the +# declarations were inline. +# +# Call it where the local declarations used to sit, once per contiguous +# block that needs the palette. set_color runs at call time, so the +# values track $TERM exactly as inline declarations did. (Measured: +# set_color output is identical across every TERM tested except +# TERM=dumb, which yields empty strings, and is unaffected by whether +# stdout is a tty or a pipe.) +# +# A role is a semantic slot, not a colour. c_flag and c_warn are both +# yellow but stay separate, as do c_ok and c_accent (both green) -- +# merging either pair would foreclose ever restyling one without the +# other. c_accent is the command name in logs and smart_exit, which +# style it green where the rest of the config styles it bold. +# +# ARGUMENTS +# none +# +# EXIT STATUS +# 0 always +# +# EXAMPLE +# function mytool +# __fish_palette +# echo "$c_head""Usage:$c_reset $c_cmd""mytool$c_reset" +# end +# +# NOTES +# Calling this at top level (outside any function) creates GLOBAL +# variables. Every consumer calls it from inside a function, where the +# variables stay function-local and do not leak. +# +# functions/fish_prompt.fish deliberately does NOT use this palette. Its +# c_* values are Catppuccin hex strings passed as ARGUMENTS to set_color +# (`set_color --bold $c_green`), not captured escape sequences -- colour +# inputs rather than rendered output, a different concern. + +function __fish_palette --no-scope-shadowing --description 'Define the shared output colour palette in the caller scope' + set c_reset (set_color normal) + set c_head (set_color --bold cyan) + set c_cmd (set_color --bold) + set c_arg (set_color cyan) + set c_flag (set_color yellow) + set c_warn (set_color yellow) + set c_err (set_color red) + set c_ok (set_color green) + set c_accent (set_color green) + set c_dim (set_color brblack) + set c_sel (set_color --bold magenta) + set c_hi (set_color --bold white) +end diff --git a/tests/functional.fish b/tests/functional.fish index e4d1651..68c64e8 100644 --- a/tests/functional.fish +++ b/tests/functional.fish @@ -97,6 +97,33 @@ function test_vault_dir_honors_override test "$got" = /tmp/vault-override-check end +function test_palette_roles_defined + functions -q __fish_palette + or begin + echo " __fish_palette is not defined" + return 1 + end + # Called from inside a function, the palette must land in THIS scope. + __fish_palette + set -l missing + for role in c_reset c_head c_cmd c_arg c_flag c_warn c_err c_ok \ + c_accent c_dim c_sel c_hi + if not set -q $role; or test -z "$$role" + set -a missing $role + end + end + if test (count $missing) -gt 0 + echo " palette roles empty or unset: $missing" + return 1 + end + # Nothing may leak to global scope. + if set -q -g c_reset + echo " __fish_palette leaked c_reset into global scope" + return 1 + end + return 0 +end + function functional_test_main set -l names (functions -a | string match 'test_*' | sort) set -l failed 0 -- 2.54.0 From bca79d3c5a2077f8bd2640e44e57e1019eec0d46 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 20:02:09 -0400 Subject: [PATCH 03/10] fix(test): exclude __fish_palette.fish from the structural diff check The helper is a new file, so its diff is entirely additions and can never be "purely structural". It declares colours rather than rendering any, and tests/functional.fish asserts its 12 roles directly. --- tests/palette-bytes.fish | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/palette-bytes.fish b/tests/palette-bytes.fish index b94a289..9eb3a55 100644 --- a/tests/palette-bytes.fish +++ b/tests/palette-bytes.fish @@ -116,6 +116,11 @@ if test $mode = structural for f in (git -C $repo diff --name-only $baseline -- functions/) # fish_prompt.fish keeps its own hex palette -- see Task 10. string match -q '*fish_prompt.fish' $f; and continue + # __fish_palette.fish is the palette itself: a new file, so its diff + # is 100% additions and can never be "purely structural". Skipping it + # is not a loosening -- it declares the colours rather than rendering + # any, and tests/functional.fish asserts its 12 roles directly. + string match -q '*__fish_palette.fish' $f; and continue set -l offenders for line in (git -C $repo diff -U0 $baseline -- $f | string match -r '^[+-][^+-].*') set -l body (string sub -s 2 -- $line) -- 2.54.0 From e64e964666f6c209877f9660ca137cde6999397e Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 20:02:15 -0400 Subject: [PATCH 04/10] refactor: use __fish_palette in agents and auto-pull functions 34 duplicated declarations replaced by 5 calls. Output strings untouched; byte-identical across 29 harness cases. --- functions/_agents_init_ensure_gitignore.fish | 3 +-- functions/agents-init.fish | 9 +-------- functions/agents-vault.fish | 9 +-------- functions/auto-pull.fish | 9 +-------- functions/config-update.fish | 9 +-------- 5 files changed, 5 insertions(+), 34 deletions(-) diff --git a/functions/_agents_init_ensure_gitignore.fish b/functions/_agents_init_ensure_gitignore.fish index 45d4c9f..f8b9ee3 100644 --- a/functions/_agents_init_ensure_gitignore.fish +++ b/functions/_agents_init_ensure_gitignore.fish @@ -31,8 +31,7 @@ # EXAMPLE # _agents_init_ensure_gitignore /home/user/myproject "agents-init" "AGENTS/" "/AGENTS.md" function _agents_init_ensure_gitignore - set -l c_ok (set_color green) - set -l c_reset (set_color normal) + __fish_palette if test (count $argv) -lt 3 echo (set_color red)"_agents_init_ensure_gitignore: requires