Files
claude/lib/tests/gstack-links.test.sh
T
bchanot 0efdff0d55 fix(portability): make test green on macOS, GNU-only idioms replaced
The suite and seven libs assumed a GNU userland: `cmd | grep -q` under
pipefail (grep exits at the first match, the producer takes SIGPIPE,
rc 141 → 15 false "merged into" FAILs in gitflow-test), `sed -i` with no
suffix, `wc -l` padding compared as a string, `stat -c`, `touch -d`,
`realpath -m` (gstack-links refusal never fired), bare `timeout` off the
sanitized PATH (design gate READY BUT UNVERIFIED), `grep -oP` (update-all
emptied the plugin list). Thirteen suites were red on this machine.

Portable forms on the native userland of both OS: producer captured out
of the pipeline, `sed -i.bak` in tests and a temp-sibling `sed_profile`
on the user dotfile, `tr -d ' '`, python3 perms, `touch -t`, a
`realpath -m` emulation that refuses `..`, perl `alarm` for the 15 s
bound, `sed -n` token extraction. Regression tests: gstack-links T4b,
doctrine-citers Alphabet/Alpha flip, profile-set-managed T18 (failing CLI
no longer aborts `set`), new portability-census suite over the tracked
shell files. Linux run deferred (see TODO).
2026-10-06 15:10:20 +02:00

110 lines
4.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# lib/tests/gstack-links.test.sh — lib/gstack-links.sh's
# link_gstack_helpers(): whole-class mirroring of a gstack skill dir
# (SKILL.md excluded), whole-dir symlink for a clean non-skill dir/file,
# skip-whole for a non-skill dir hiding a nested SKILL.md
# (browser-skills/, openclaw/-style), skip-by-name for `.git*` and
# `node_modules`, idempotent re-run (echoes 0, nothing changes), a stale
# dst-is-symlink-to-src planted by gstack ./setup (removed, dst becomes
# a real dir, nothing written into src), and a dst path that would
# resolve inside src (refused, rc 1, nothing created). Covers contract
# criterion 3 (whole class) and the r4 confirmation-pass fixtures.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
LIB="$ROOT/lib/gstack-links.sh"
pass=0; fail=0
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
SRC="$WORK/src"
DST="$WORK/dst"
# ── fixture src: one skill dir (browse), one nested skill-dir tree
# (review, with a sub-directory of its own), plain shared assets
# (bin/, ETHOS.md), the top-level gstack SKILL.md itself, vcs metadata,
# and the two non-skill-dir-hiding-a-nested-SKILL.md cases (other/deep,
# node_modules/pkg) ──
mkdir -p "$SRC/bin" "$SRC/browse/dist" "$SRC/review/specialists" \
"$SRC/.git" "$SRC/other/deep" "$SRC/node_modules/pkg"
echo x > "$SRC/bin/x"
echo ethos > "$SRC/ETHOS.md"
echo skill > "$SRC/SKILL.md"
echo browse-skill > "$SRC/browse/SKILL.md"
echo browse-bin > "$SRC/browse/dist/browse"
echo review-skill > "$SRC/review/SKILL.md"
echo checklist > "$SRC/review/checklist.md"
echo spec-a > "$SRC/review/specialists/a.md"
echo head > "$SRC/.git/HEAD"
echo nested > "$SRC/other/deep/SKILL.md"
echo pkg-skill > "$SRC/node_modules/pkg/SKILL.md"
# shellcheck source=../gstack-links.sh disable=SC1091
source "$LIB"
# ── T1: first run mirrors the whole class, exposes no SKILL.md ──
n1=$(link_gstack_helpers "$SRC" "$DST" 2>/dev/null)
check T1-bin-resolves \
"$([ -f "$DST/bin/x" ] && cat "$DST/bin/x" || echo missing)" x
check T1-ethos-resolves \
"$([ -f "$DST/ETHOS.md" ] && cat "$DST/ETHOS.md" || echo missing)" ethos
check T1-browse-dist-resolves \
"$([ -f "$DST/browse/dist/browse" ] && cat "$DST/browse/dist/browse" \
|| echo missing)" browse-bin
check T1-review-checklist-resolves \
"$([ -f "$DST/review/checklist.md" ] && cat "$DST/review/checklist.md" \
|| echo missing)" checklist
check T1-review-specialists-resolves \
"$([ -f "$DST/review/specialists/a.md" ] \
&& cat "$DST/review/specialists/a.md" || echo missing)" spec-a
check T1-no-skillmd-anywhere \
"$(find -L "$DST" -name SKILL.md 2>/dev/null | wc -l | tr -d ' ')" 0
check T1-no-dotgit "$([ -e "$DST/.git" ] && echo present || echo absent)" \
absent
check T1-no-other "$([ -e "$DST/other" ] && echo present || echo absent)" \
absent
check T1-no-node-modules \
"$([ -e "$DST/node_modules" ] && echo present || echo absent)" absent
check T1-count-positive "$([ "$n1" -gt 0 ] && echo yes || echo no)" yes
# ── T2: idempotent re-run — echoes 0, tree unchanged ──
n2=$(link_gstack_helpers "$SRC" "$DST" 2>/dev/null)
check T2-echoes-zero "$n2" 0
check T2-bin-still-resolves \
"$([ -f "$DST/bin/x" ] && cat "$DST/bin/x" || echo missing)" x
# ── T3: dst is a symlink to src (gstack ./setup's stale-link case) —
# removed, dst becomes a real dir, nothing written into src ──
DST3="$WORK/dst-symlinked"
ln -s "$SRC" "$DST3"
n3=$(link_gstack_helpers "$SRC" "$DST3" 2>/dev/null)
check T3-dst-is-real-dir "$([ -d "$DST3" ] && [ ! -L "$DST3" ] \
&& echo yes || echo no)" yes
check T3-bin-resolves \
"$([ -f "$DST3/bin/x" ] && cat "$DST3/bin/x" || echo missing)" x
check T3-nothing-written-in-src \
"$(find "$SRC" -type l 2>/dev/null | wc -l | tr -d ' ')" 0
check T3-count-positive "$([ "$n3" -gt 0 ] && echo yes || echo no)" yes
# ── T4: dst path resolves inside src — refused, rc 1, nothing created ──
DST4="$SRC/helpers"
out4=$(link_gstack_helpers "$SRC" "$DST4" 2>&1 >/dev/null)
rc4=$?
n4=$(link_gstack_helpers "$SRC" "$DST4" 2>/dev/null)
check T4-rc "$rc4" 1
check T4-echoes-zero "$n4" 0
check T4-nothing-created "$([ -e "$DST4" ] && echo present || echo absent)" \
absent
check T4-warns "$(printf '%s' "$out4" | grep -qi 'refusing' \
&& echo yes || echo no)" yes
# ── T4b: dst under src with a missing parent — refused, nothing created ──
DST4B="$SRC/missing/x"
link_gstack_helpers "$SRC" "$DST4B" >/dev/null 2>&1
rc4b=$?
check T4b-rc "$rc4b" 1
check T4b-nothing-created \
"$([ -e "$SRC/missing" ] && echo present || echo absent)" absent
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]