From 51da3e9d83b426c98a9c784baec9bb2b557edb1a Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:57:07 -0400 Subject: [PATCH] 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)"