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).
This commit is contained in:
+29
-6
@@ -471,7 +471,8 @@ echo ""
|
|||||||
install_plugin() {
|
install_plugin() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local source="$2"
|
local source="$2"
|
||||||
if claude plugin list 2>/dev/null | grep -qi "$name"; then
|
# CLI call inside the condition: no pipe (SIGPIPE on macOS), no errexit abort
|
||||||
|
if grep -qi "$name" <<<"$(claude plugin list 2>/dev/null)"; then
|
||||||
ok "$name (already installed)"
|
ok "$name (already installed)"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -1140,7 +1141,8 @@ apply_effort_pins "$REPO" || warn "effort pins: map lines rejected — fix lib/e
|
|||||||
# the session. Mirrors the ctx7 auth block (Step 6), stdin-only test included.
|
# the session. Mirrors the ctx7 auth block (Step 6), stdin-only test included.
|
||||||
if command -v 21st &>/dev/null; then
|
if command -v 21st &>/dev/null; then
|
||||||
# `whoami` is a local token read (no network): "Logged in as <user> (saved …)."
|
# `whoami` is a local token read (no network): "Logged in as <user> (saved …)."
|
||||||
TFD_WHO="$(21st whoami 2>/dev/null | head -1)"
|
TFD_WHO="$(21st whoami 2>/dev/null)" || true
|
||||||
|
TFD_WHO="${TFD_WHO%%$'\n'*}" # first line, no head(1) in a pipeline
|
||||||
if [[ "$TFD_WHO" == "Logged in as "* ]]; then
|
if [[ "$TFD_WHO" == "Logged in as "* ]]; then
|
||||||
ok "21st: ${TFD_WHO%.}"
|
ok "21st: ${TFD_WHO%.}"
|
||||||
elif [ -t 0 ]; then
|
elif [ -t 0 ]; then
|
||||||
@@ -1196,16 +1198,37 @@ fi
|
|||||||
# Remove obsolete effort config — effort is now set in settings.json
|
# Remove obsolete effort config — effort is now set in settings.json
|
||||||
# ("effortLevel"), which supersedes both the old CLAUDE_EFFORT env var and the
|
# ("effortLevel"), which supersedes both the old CLAUDE_EFFORT env var and the
|
||||||
# `claude --effort max` alias (the alias would even override settings.json).
|
# `claude --effort max` alias (the alias would even override settings.json).
|
||||||
|
# sed_profile <expr> — in-place sed on $SHELL_PROFILE. BSD in-place sed needs
|
||||||
|
# a suffix, and -i.bak would clobber a hand-made .bak, so write a unique
|
||||||
|
# sibling temp and copy it back. rc 1 leaves the profile as it was.
|
||||||
|
sed_profile() {
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp "$SHELL_PROFILE.XXXXXX")" || return 1
|
||||||
|
# the profile is truncated by the copy-back: past that point $tmp is the
|
||||||
|
# only complete copy, so a failed write keeps it
|
||||||
|
sed "$1" "$SHELL_PROFILE" >"$tmp" || { rm -f "$tmp"; return 1; }
|
||||||
|
if ! cat "$tmp" >"$SHELL_PROFILE"; then
|
||||||
|
warn "profile write failed — full copy kept at $tmp" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f "$tmp"
|
||||||
|
}
|
||||||
EFFORT_CLEANED=0
|
EFFORT_CLEANED=0
|
||||||
if grep -qF 'export CLAUDE_EFFORT=max' "$SHELL_PROFILE" 2>/dev/null; then
|
if grep -qF 'export CLAUDE_EFFORT=max' "$SHELL_PROFILE" 2>/dev/null; then
|
||||||
sed -i '/export CLAUDE_EFFORT=max/d' "$SHELL_PROFILE"; EFFORT_CLEANED=1
|
if sed_profile '/export CLAUDE_EFFORT=max/d'; then
|
||||||
|
EFFORT_CLEANED=1
|
||||||
|
else
|
||||||
|
warn "could not remove CLAUDE_EFFORT from $SHELL_PROFILE"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if grep -qF "alias claude='claude --effort max'" "$SHELL_PROFILE" 2>/dev/null; then
|
if grep -qF "alias claude='claude --effort max'" "$SHELL_PROFILE" 2>/dev/null; then
|
||||||
sed -i "\#alias claude='claude --effort max'#d" "$SHELL_PROFILE"; EFFORT_CLEANED=1
|
if sed_profile "\#alias claude='claude --effort max'#d"; then
|
||||||
|
EFFORT_CLEANED=1
|
||||||
|
else
|
||||||
|
warn "could not remove the claude effort alias from $SHELL_PROFILE"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if [ "$EFFORT_CLEANED" -eq 1 ]; then
|
if [ "$EFFORT_CLEANED" -eq 1 ]; then
|
||||||
# Remove orphaned comment lines left before the deleted entries
|
|
||||||
sed -i '/^# Claude Code — added by install-plugins.sh$/{ N; /^\n$/d; }' "$SHELL_PROFILE"
|
|
||||||
info "Removed obsolete effort alias/env from $SHELL_PROFILE (effort set in settings.json)"
|
info "Removed obsolete effort alias/env from $SHELL_PROFILE (effort set in settings.json)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+21
-7
@@ -70,6 +70,7 @@ ensure_claude_on_path() {
|
|||||||
for cand in \
|
for cand in \
|
||||||
"$HOME/.claude/local/claude" \
|
"$HOME/.claude/local/claude" \
|
||||||
"$HOME/.local/bin/claude" \
|
"$HOME/.local/bin/claude" \
|
||||||
|
/opt/homebrew/bin/claude \
|
||||||
/usr/local/bin/claude; do
|
/usr/local/bin/claude; do
|
||||||
[ -x "$cand" ] && { PATH="$(dirname "$cand"):$PATH"; return; }
|
[ -x "$cand" ] && { PATH="$(dirname "$cand"):$PATH"; return; }
|
||||||
done
|
done
|
||||||
@@ -94,6 +95,7 @@ ensure_21st_on_path() {
|
|||||||
local cand
|
local cand
|
||||||
for cand in \
|
for cand in \
|
||||||
"$HOME/.local/bin/21st" \
|
"$HOME/.local/bin/21st" \
|
||||||
|
/opt/homebrew/bin/21st \
|
||||||
/usr/local/bin/21st; do
|
/usr/local/bin/21st; do
|
||||||
[ -x "$cand" ] && { PATH="$(dirname "$cand"):$PATH"; return; }
|
[ -x "$cand" ] && { PATH="$(dirname "$cand"):$PATH"; return; }
|
||||||
done
|
done
|
||||||
@@ -113,7 +115,8 @@ ensure_21st_on_path
|
|||||||
# out; the FIRST LINE of stdout does. A token env, when already exported by
|
# out; the FIRST LINE of stdout does. A token env, when already exported by
|
||||||
# the user's shell profile, wins without a CLI call (never requested here:
|
# the user's shell profile, wins without a CLI call (never requested here:
|
||||||
# tool calls don't share a shell, and a secret doesn't belong in a comment
|
# tool calls don't share a shell, and a secret doesn't belong in a comment
|
||||||
# or the transcript). `timeout 15` bounds a hung CLI; stdin is closed so a
|
# or the transcript). A perl `alarm` of 15 s bounds a hung CLI (GNU
|
||||||
|
# `timeout` is absent from the macOS system PATH); stdin is closed so a
|
||||||
# CLI that reads stdin can't eat the gate's own `read` loop; stderr never
|
# CLI that reads stdin can't eat the gate's own `read` loop; stderr never
|
||||||
# enters the match (stdout only). Echoes: in | out | unknown:<diagnostic>.
|
# enters the match (stdout only). Echoes: in | out | unknown:<diagnostic>.
|
||||||
twentyfirst_auth_state() {
|
twentyfirst_auth_state() {
|
||||||
@@ -122,11 +125,13 @@ twentyfirst_auth_state() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
local line rc
|
local line rc
|
||||||
if line="$(timeout 15 21st whoami 2>/dev/null </dev/null | head -1)"; then
|
if line="$(perl -e 'alarm shift; exec @ARGV' 15 21st whoami \
|
||||||
|
2>/dev/null </dev/null)"; then
|
||||||
rc=0
|
rc=0
|
||||||
else
|
else
|
||||||
rc=$?
|
rc=$?
|
||||||
fi
|
fi
|
||||||
|
line="${line%%$'\n'*}" # first line, without head(1) in a pipeline
|
||||||
if [ "$rc" -eq 0 ]; then
|
if [ "$rc" -eq 0 ]; then
|
||||||
case "$line" in
|
case "$line" in
|
||||||
"Logged in as "*) echo in; return ;;
|
"Logged in as "*) echo in; return ;;
|
||||||
@@ -160,14 +165,22 @@ tool_active() {
|
|||||||
;;
|
;;
|
||||||
plugin)
|
plugin)
|
||||||
if ! command -v "$CLAUDE_BIN" >/dev/null 2>&1; then echo unknown; return; fi
|
if ! command -v "$CLAUDE_BIN" >/dev/null 2>&1; then echo unknown; return; fi
|
||||||
if "$CLAUDE_BIN" plugin list 2>/dev/null \
|
# capture first, then match: an early-exit awk/grep -q in a pipe
|
||||||
| awk -v p="^[[:space:]]*❯ ${name}@" '$0 ~ p {f=1; next} f && /Status:/ {print; exit}' \
|
# SIGPIPEs the producer (rc 141 under pipefail on macOS)
|
||||||
| grep -q "✔ enabled"
|
local plist
|
||||||
|
plist="$("$CLAUDE_BIN" plugin list 2>/dev/null)" || true
|
||||||
|
if grep -q "✔ enabled" < <(awk -v p="^[[:space:]]*❯ ${name}@" \
|
||||||
|
'$0 ~ p {f=1; next} f && /Status:/ {print; exit}' <<<"$plist")
|
||||||
then echo active; else echo inactive; fi
|
then echo active; else echo inactive; fi
|
||||||
;;
|
;;
|
||||||
mcp)
|
mcp)
|
||||||
if ! command -v "$CLAUDE_BIN" >/dev/null 2>&1; then echo unknown; return; fi
|
if ! command -v "$CLAUDE_BIN" >/dev/null 2>&1; then echo unknown; return; fi
|
||||||
if "$CLAUDE_BIN" mcp list 2>/dev/null | grep -q "^${name}"; then echo active; else echo inactive; fi
|
if grep -q "^${name}" \
|
||||||
|
<<<"$("$CLAUDE_BIN" mcp list 2>/dev/null)"; then
|
||||||
|
echo active
|
||||||
|
else
|
||||||
|
echo inactive
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
cli)
|
cli)
|
||||||
command -v "$name" >/dev/null 2>&1 || { echo inactive; return; }
|
command -v "$name" >/dev/null 2>&1 || { echo inactive; return; }
|
||||||
@@ -222,7 +235,8 @@ print_unverified() {
|
|||||||
echo " also unverified (claude CLI unreachable): ${unverified[*]}"
|
echo " also unverified (claude CLI unreachable): ${unverified[*]}"
|
||||||
fi
|
fi
|
||||||
local entry name diag
|
local entry name diag
|
||||||
for entry in "${unverified_cli[@]}"; do
|
# bash 3.2 (macOS /bin/bash) errors on an empty array under set -u
|
||||||
|
for entry in ${unverified_cli[@]+"${unverified_cli[@]}"}; do
|
||||||
name="${entry%% (*}"
|
name="${entry%% (*}"
|
||||||
diag="${entry#*\(}"; diag="${diag%\)}"
|
diag="${entry#*\(}"; diag="${diag%\)}"
|
||||||
echo " $name could not answer: $diag —" \
|
echo " $name could not answer: $diag —" \
|
||||||
|
|||||||
+2
-1
@@ -67,7 +67,8 @@ _path_exceeds_reason() {
|
|||||||
printf 'new/untracked doc (a creation, not a MINOR drift-patch): %s\n' "$p"
|
printf 'new/untracked doc (a creation, not a MINOR drift-patch): %s\n' "$p"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
if git diff HEAD -- "$p" | grep -Eq '^\+#{1,6}[ \t]'; then
|
# producer out of the pipe: grep -q would SIGPIPE git (fails open on macOS)
|
||||||
|
if grep -Eq '^\+#{1,6}[ \t]' < <(git diff HEAD -- "$p"); then
|
||||||
printf 'adds a section heading (structural change, not a factual tweak): %s\n' "$p"
|
printf 'adds a section heading (structural change, not a factual tweak): %s\n' "$p"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|||||||
+26
-26
@@ -48,7 +48,7 @@ chk "socle: re-ignore PENDING" 'grep -qxF ".claude/deploy/PENDING.json" .gitigno
|
|||||||
chk "hook installed" '[ -x .githooks/pre-commit ] && [ "$(git config core.hooksPath)" = .githooks ]'
|
chk "hook installed" '[ -x .githooks/pre-commit ] && [ "$(git config core.hooksPath)" = .githooks ]'
|
||||||
chk "tree CLEAN after init" '[ -z "$(git status --porcelain)" ]'
|
chk "tree CLEAN after init" '[ -z "$(git status --porcelain)" ]'
|
||||||
chk "hook TRACKED in commit" 'git ls-files --error-unmatch .githooks/pre-commit >/dev/null 2>&1'
|
chk "hook TRACKED in commit" 'git ls-files --error-unmatch .githooks/pre-commit >/dev/null 2>&1'
|
||||||
chk "socle IN root commit" 'git show HEAD:.gitignore | grep -qxF ".claude/deploy/PENDING.json"'
|
chk "socle IN root commit" 'grep -qxF ".claude/deploy/PENDING.json" < <(git show HEAD:.gitignore)'
|
||||||
|
|
||||||
echo "T2b — init existing (master→main rename + adoption via chore/gitflow-adopt merge)"
|
echo "T2b — init existing (master→main rename + adoption via chore/gitflow-adopt merge)"
|
||||||
newrepo existing
|
newrepo existing
|
||||||
@@ -59,10 +59,10 @@ hookon
|
|||||||
gitflow_init >/dev/null 2>&1
|
gitflow_init >/dev/null 2>&1
|
||||||
chk "master→main renamed" 'git rev-parse --verify -q refs/heads/main >/dev/null && ! git rev-parse --verify -q refs/heads/master >/dev/null'
|
chk "master→main renamed" 'git rev-parse --verify -q refs/heads/main >/dev/null && ! git rev-parse --verify -q refs/heads/master >/dev/null'
|
||||||
chk "develop created" 'git rev-parse --verify -q refs/heads/develop >/dev/null'
|
chk "develop created" 'git rev-parse --verify -q refs/heads/develop >/dev/null'
|
||||||
chk "adoption commit" 'git log main --oneline | grep -q "adopt gitflow"'
|
chk "adoption commit" 'grep -q "adopt gitflow" < <(git log main --oneline)'
|
||||||
chk "existing tree CLEAN" '[ -z "$(git status --porcelain)" ]'
|
chk "existing tree CLEAN" '[ -z "$(git status --porcelain)" ]'
|
||||||
chk "existing hook tracked" 'git ls-files --error-unmatch .githooks/pre-commit >/dev/null 2>&1'
|
chk "existing hook tracked" 'git ls-files --error-unmatch .githooks/pre-commit >/dev/null 2>&1'
|
||||||
chk "kept project rule" 'git show HEAD:.gitignore | grep -qxF "node_modules/"'
|
chk "kept project rule" 'grep -qxF "node_modules/" < <(git show HEAD:.gitignore)'
|
||||||
|
|
||||||
echo "T2c — init existing under a LIVE pre-commit (global hooks simulated): socle lands via merge"
|
echo "T2c — init existing under a LIVE pre-commit (global hooks simulated): socle lands via merge"
|
||||||
newrepo live; git symbolic-ref HEAD refs/heads/master
|
newrepo live; git symbolic-ref HEAD refs/heads/master
|
||||||
@@ -73,9 +73,9 @@ git config core.hooksPath "$WORK/globalhooks" # stands in for git's GLOBAL
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
live_rc=0; GITFLOW_NO_PUSH=1 gitflow_init >/dev/null 2>&1 || live_rc=$?
|
live_rc=0; GITFLOW_NO_PUSH=1 gitflow_init >/dev/null 2>&1 || live_rc=$?
|
||||||
chk "T2c init succeeds under the live hook (rc 0)" "[ $live_rc -eq 0 ]"
|
chk "T2c init succeeds under the live hook (rc 0)" "[ $live_rc -eq 0 ]"
|
||||||
chk "T2c socle reached main via a merge commit" 'git log main --oneline -1 | grep -q "Merge chore/gitflow-adopt"'
|
chk "T2c socle reached main via a merge commit" 'grep -q "Merge chore/gitflow-adopt" < <(git log main --oneline -1)'
|
||||||
chk "T2c .gitignore socle on main" 'git show main:.gitignore | grep -qxF ".claude/deploy/PENDING.json"'
|
chk "T2c .gitignore socle on main" 'grep -qxF ".claude/deploy/PENDING.json" < <(git show main:.gitignore)'
|
||||||
chk "T2c hooks tracked on main" 'git ls-tree -r main --name-only | grep -q "^.githooks/pre-commit$"'
|
chk "T2c hooks tracked on main" 'grep -q "^.githooks/pre-commit$" < <(git ls-tree -r main --name-only)'
|
||||||
chk "T2c adoption branch deleted" '! git rev-parse --verify -q refs/heads/chore/gitflow-adopt >/dev/null'
|
chk "T2c adoption branch deleted" '! git rev-parse --verify -q refs/heads/chore/gitflow-adopt >/dev/null'
|
||||||
chk "T2c develop created from main" '[ "$(git rev-parse develop)" = "$(git rev-parse main)" ]'
|
chk "T2c develop created from main" '[ "$(git rev-parse develop)" = "$(git rev-parse main)" ]'
|
||||||
chk "T2c repo hook active afterwards" '[ "$(git config core.hooksPath)" = .githooks ]'
|
chk "T2c repo hook active afterwards" '[ "$(git config core.hooksPath)" = .githooks ]'
|
||||||
@@ -107,7 +107,7 @@ newrepo finfeat; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
|||||||
gitflow_start feature f1 >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m w
|
gitflow_start feature f1 >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m w
|
||||||
main_before="$(git rev-parse main)"
|
main_before="$(git rev-parse main)"
|
||||||
gitflow_finish >/dev/null 2>&1
|
gitflow_finish >/dev/null 2>&1
|
||||||
chk "merged into develop" 'git log develop --oneline | grep -q "Merge feature/f1 into develop"'
|
chk "merged into develop" 'grep -q "Merge feature/f1 into develop" < <(git log develop --oneline)'
|
||||||
chk "main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
|
chk "main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
|
||||||
chk "branch deleted" '! git rev-parse --verify -q refs/heads/feature/f1 >/dev/null'
|
chk "branch deleted" '! git rev-parse --verify -q refs/heads/feature/f1 >/dev/null'
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ gitflow_start chore c1 >/dev/null 2>&1
|
|||||||
mkdir -p .claude/memory; echo m>.claude/memory/x.md; git add -A; git commit -q -m "chore(memory)"
|
mkdir -p .claude/memory; echo m>.claude/memory/x.md; git add -A; git commit -q -m "chore(memory)"
|
||||||
main_before="$(git rev-parse main)"
|
main_before="$(git rev-parse main)"
|
||||||
gitflow_finish >/dev/null 2>&1
|
gitflow_finish >/dev/null 2>&1
|
||||||
chk "chore merged into develop" 'git log develop --oneline | grep -q "Merge chore/c1 into develop"'
|
chk "chore merged into develop" 'grep -q "Merge chore/c1 into develop" < <(git log develop --oneline)'
|
||||||
chk "chore main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
|
chk "chore main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
|
||||||
chk "chore branch deleted" '! git rev-parse --verify -q refs/heads/chore/c1 >/dev/null'
|
chk "chore branch deleted" '! git rev-parse --verify -q refs/heads/chore/c1 >/dev/null'
|
||||||
|
|
||||||
@@ -125,8 +125,8 @@ echo "T7 — finish hotfix → main + develop fan-out"
|
|||||||
newrepo finhot; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
newrepo finhot; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||||
gitflow_start hotfix h1 >/dev/null 2>&1; echo p>patch.txt; git add patch.txt; git commit -q -m patch
|
gitflow_start hotfix h1 >/dev/null 2>&1; echo p>patch.txt; git add patch.txt; git commit -q -m patch
|
||||||
gitflow_finish >/dev/null 2>&1
|
gitflow_finish >/dev/null 2>&1
|
||||||
chk "hotfix in main" 'git log main --oneline | grep -q "Merge hotfix/h1 into main"'
|
chk "hotfix in main" 'grep -q "Merge hotfix/h1 into main" < <(git log main --oneline)'
|
||||||
chk "hotfix in develop" 'git log develop --oneline | grep -q "Merge hotfix/h1 into develop"'
|
chk "hotfix in develop" 'grep -q "Merge hotfix/h1 into develop" < <(git log develop --oneline)'
|
||||||
chk "hotfix branch gone" '! git rev-parse --verify -q refs/heads/hotfix/h1 >/dev/null'
|
chk "hotfix branch gone" '! git rev-parse --verify -q refs/heads/hotfix/h1 >/dev/null'
|
||||||
|
|
||||||
echo "T8 — finish hotfix also lands in OPEN release"
|
echo "T8 — finish hotfix also lands in OPEN release"
|
||||||
@@ -134,7 +134,7 @@ newrepo finhotrel; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
|||||||
gitflow_start release 1.0 >/dev/null 2>&1; echo r>rel.txt; git add rel.txt; git commit -q -m relwork
|
gitflow_start release 1.0 >/dev/null 2>&1; echo r>rel.txt; git add rel.txt; git commit -q -m relwork
|
||||||
gitflow_start hotfix h2 >/dev/null 2>&1; echo p>p2.txt; git add p2.txt; git commit -q -m patch2
|
gitflow_start hotfix h2 >/dev/null 2>&1; echo p>p2.txt; git add p2.txt; git commit -q -m patch2
|
||||||
gitflow_finish >/dev/null 2>&1
|
gitflow_finish >/dev/null 2>&1
|
||||||
chk "hotfix in open release" 'git log release/1.0 --oneline | grep -q "Merge hotfix/h2 into release/1.0"'
|
chk "hotfix in open release" 'grep -q "Merge hotfix/h2 into release/1.0" < <(git log release/1.0 --oneline)'
|
||||||
|
|
||||||
echo "T9 — reconcile is additive + idempotent + preserves project rules"
|
echo "T9 — reconcile is additive + idempotent + preserves project rules"
|
||||||
newrepo recon; echo a>a; git add a; git commit -q -m a
|
newrepo recon; echo a>a; git add a; git commit -q -m a
|
||||||
@@ -181,11 +181,11 @@ mism_out="$(gitflow_finish bugfix other 2>&1)"; mism_rc=$?
|
|||||||
chk "arg-mismatch → nonzero rc" "[ $mism_rc -ne 0 ]"
|
chk "arg-mismatch → nonzero rc" "[ $mism_rc -ne 0 ]"
|
||||||
chk "arg-mismatch → HEAD untouched" '[ "$(git symbolic-ref --short HEAD)" = feature/standon ]'
|
chk "arg-mismatch → HEAD untouched" '[ "$(git symbolic-ref --short HEAD)" = feature/standon ]'
|
||||||
chk "arg-mismatch → branch kept" 'git rev-parse --verify -q refs/heads/feature/standon >/dev/null'
|
chk "arg-mismatch → branch kept" 'git rev-parse --verify -q refs/heads/feature/standon >/dev/null'
|
||||||
chk "arg-mismatch → develop NOT merged" '! git log develop --oneline | grep -q "Merge feature/standon into develop"'
|
chk "arg-mismatch → develop NOT merged" '! grep -q "Merge feature/standon into develop" < <(git log develop --oneline)'
|
||||||
chk "arg-mismatch → message names both" 'printf "%s" "$mism_out" | grep -q "current branch" && printf "%s" "$mism_out" | grep -q "bugfix/other"'
|
chk "arg-mismatch → message names both" 'printf "%s" "$mism_out" | grep -q "current branch" && printf "%s" "$mism_out" | grep -q "bugfix/other"'
|
||||||
# match: naming the current branch explicitly finishes exactly like the no-arg path
|
# match: naming the current branch explicitly finishes exactly like the no-arg path
|
||||||
gitflow_finish feature standon >/dev/null 2>&1
|
gitflow_finish feature standon >/dev/null 2>&1
|
||||||
chk "arg-match → merged into develop" 'git log develop --oneline | grep -q "Merge feature/standon into develop"'
|
chk "arg-match → merged into develop" 'grep -q "Merge feature/standon into develop" < <(git log develop --oneline)'
|
||||||
chk "arg-match → branch deleted" '! git rev-parse --verify -q refs/heads/feature/standon >/dev/null'
|
chk "arg-match → branch deleted" '! git rev-parse --verify -q refs/heads/feature/standon >/dev/null'
|
||||||
|
|
||||||
echo "T13 — finish release fan-out (main+develop+delete), 2 open releases + bugfix→develop-only"
|
echo "T13 — finish release fan-out (main+develop+delete), 2 open releases + bugfix→develop-only"
|
||||||
@@ -193,8 +193,8 @@ newrepo finrel; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
|||||||
gitflow_start release 9.9.9 >/dev/null 2>&1; echo v>VERSION; git add VERSION; git commit -q -m "bump 9.9.9"
|
gitflow_start release 9.9.9 >/dev/null 2>&1; echo v>VERSION; git add VERSION; git commit -q -m "bump 9.9.9"
|
||||||
finish_rc=0; gitflow_finish >/dev/null 2>&1 || finish_rc=$?
|
finish_rc=0; gitflow_finish >/dev/null 2>&1 || finish_rc=$?
|
||||||
chk "T13a finish rc 0" "[ $finish_rc -eq 0 ]"
|
chk "T13a finish rc 0" "[ $finish_rc -eq 0 ]"
|
||||||
chk "T13a main has release commit" 'git log main --oneline | grep -q "bump 9.9.9"'
|
chk "T13a main has release commit" 'grep -q "bump 9.9.9" < <(git log main --oneline)'
|
||||||
chk "T13a develop has release commit" 'git log develop --oneline | grep -q "bump 9.9.9"'
|
chk "T13a develop has release commit" 'grep -q "bump 9.9.9" < <(git log develop --oneline)'
|
||||||
chk "T13a release branch deleted" '! git rev-parse --verify -q refs/heads/release/9.9.9 >/dev/null'
|
chk "T13a release branch deleted" '! git rev-parse --verify -q refs/heads/release/9.9.9 >/dev/null'
|
||||||
|
|
||||||
newrepo finrel2; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
newrepo finrel2; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||||
@@ -202,14 +202,14 @@ gitflow_start release 1.0 >/dev/null 2>&1; echo r1>r1; git add r1; git commit -q
|
|||||||
gitflow_start release 2.0 >/dev/null 2>&1; echo r2>r2; git add r2; git commit -q -m rel2
|
gitflow_start release 2.0 >/dev/null 2>&1; echo r2>r2; git add r2; git commit -q -m rel2
|
||||||
gitflow_start hotfix hboth >/dev/null 2>&1; echo p>p; git add p; git commit -q -m hotfixboth
|
gitflow_start hotfix hboth >/dev/null 2>&1; echo p>p; git add p; git commit -q -m hotfixboth
|
||||||
gitflow_finish >/dev/null 2>&1
|
gitflow_finish >/dev/null 2>&1
|
||||||
chk "T13b hotfix in release/1.0" 'git log release/1.0 --oneline | grep -q "Merge hotfix/hboth into release/1.0"'
|
chk "T13b hotfix in release/1.0" 'grep -q "Merge hotfix/hboth into release/1.0" < <(git log release/1.0 --oneline)'
|
||||||
chk "T13b hotfix in release/2.0" 'git log release/2.0 --oneline | grep -q "Merge hotfix/hboth into release/2.0"'
|
chk "T13b hotfix in release/2.0" 'grep -q "Merge hotfix/hboth into release/2.0" < <(git log release/2.0 --oneline)'
|
||||||
|
|
||||||
newrepo finbugfix; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
newrepo finbugfix; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||||
gitflow_start bugfix bx >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m bugfixwork
|
gitflow_start bugfix bx >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m bugfixwork
|
||||||
main_before="$(git rev-parse main)"
|
main_before="$(git rev-parse main)"
|
||||||
gitflow_finish >/dev/null 2>&1
|
gitflow_finish >/dev/null 2>&1
|
||||||
chk "T13c develop has bugfix commit" 'git log develop --oneline | grep -q "Merge bugfix/bx into develop"'
|
chk "T13c develop has bugfix commit" 'grep -q "Merge bugfix/bx into develop" < <(git log develop --oneline)'
|
||||||
chk "T13c main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
|
chk "T13c main untouched" "[ \"\$(git rev-parse main)\" = \"$main_before\" ]"
|
||||||
chk "T13c bugfix branch deleted" '! git rev-parse --verify -q refs/heads/bugfix/bx >/dev/null'
|
chk "T13c bugfix branch deleted" '! git rev-parse --verify -q refs/heads/bugfix/bx >/dev/null'
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ git add secret.txt
|
|||||||
gl_out="$(git commit -q -m "add secret" 2>&1)"; gl_rc=$?
|
gl_out="$(git commit -q -m "add secret" 2>&1)"; gl_rc=$?
|
||||||
chk "T16a fake secret on feature branch → blocked" "[ $gl_rc -ne 0 ]"
|
chk "T16a fake secret on feature branch → blocked" "[ $gl_rc -ne 0 ]"
|
||||||
chk "T16a message mentions gitleaks" 'printf "%s" "$gl_out" | grep -qi gitleaks'
|
chk "T16a message mentions gitleaks" 'printf "%s" "$gl_out" | grep -qi gitleaks'
|
||||||
chk "T16a nothing committed" '! git log --oneline 2>/dev/null | grep -q "add secret"'
|
chk "T16a nothing committed" '! grep -q "add secret" < <(git log --oneline 2>/dev/null)'
|
||||||
git restore --staged secret.txt 2>/dev/null || true; rm -f secret.txt
|
git restore --staged secret.txt 2>/dev/null || true; rm -f secret.txt
|
||||||
|
|
||||||
# T16b — a clean commit is unaffected
|
# T16b — a clean commit is unaffected
|
||||||
@@ -295,19 +295,19 @@ gitflow_finish >/dev/null 2>&1
|
|||||||
# <sha>:path` proves BDR-065's "git history = the archive" recovery.
|
# <sha>:path` proves BDR-065's "git history = the archive" recovery.
|
||||||
# shellcheck disable=SC2034 # pf_add_sha is used in the deferred chk eval string
|
# shellcheck disable=SC2034 # pf_add_sha is used in the deferred chk eval string
|
||||||
pf_add_sha="$(git log develop --full-history --format=%H -- docs/superpowers/specs/s.md | tail -1)"
|
pf_add_sha="$(git log develop --full-history --format=%H -- docs/superpowers/specs/s.md | tail -1)"
|
||||||
chk "T17a merged into develop" 'git log develop --oneline | grep -q "Merge feature/pf into develop"'
|
chk "T17a merged into develop" 'grep -q "Merge feature/pf into develop" < <(git log develop --oneline)'
|
||||||
chk "T17a develop TIP has no transient" '[ -z "$(git ls-tree -r develop --name-only -- docs/superpowers)" ]'
|
chk "T17a develop TIP has no transient" '[ -z "$(git ls-tree -r develop --name-only -- docs/superpowers)" ]'
|
||||||
chk "T17a purge commit on record" 'git log develop --oneline | grep -q "purge transient planning artifacts"'
|
chk "T17a purge commit on record" 'grep -q "purge transient planning artifacts" < <(git log develop --oneline)'
|
||||||
chk "T17a artifact recoverable from history" '[ "$(git show "$pf_add_sha":docs/superpowers/specs/s.md 2>/dev/null)" = spec ]'
|
chk "T17a artifact recoverable from history" '[ "$(git show "$pf_add_sha":docs/superpowers/specs/s.md 2>/dev/null)" = spec ]'
|
||||||
chk "T17a non-transient code survives" 'git ls-tree -r develop --name-only | grep -qx feat.txt'
|
chk "T17a non-transient code survives" 'grep -qx feat.txt < <(git ls-tree -r develop --name-only)'
|
||||||
chk "T17a feature branch deleted" '! git rev-parse --verify -q refs/heads/feature/pf >/dev/null'
|
chk "T17a feature branch deleted" '! git rev-parse --verify -q refs/heads/feature/pf >/dev/null'
|
||||||
|
|
||||||
# T17b — no artifacts → purge is a silent no-op, no spurious commit
|
# T17b — no artifacts → purge is a silent no-op, no spurious commit
|
||||||
newrepo purgenone; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
newrepo purgenone; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||||
gitflow_start feature pn >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m w
|
gitflow_start feature pn >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m w
|
||||||
gitflow_finish >/dev/null 2>&1
|
gitflow_finish >/dev/null 2>&1
|
||||||
chk "T17b merged into develop" 'git log develop --oneline | grep -q "Merge feature/pn into develop"'
|
chk "T17b merged into develop" 'grep -q "Merge feature/pn into develop" < <(git log develop --oneline)'
|
||||||
chk "T17b no purge commit created" '! git log develop --oneline | grep -q "purge transient"'
|
chk "T17b no purge commit created" '! grep -q "purge transient" < <(git log develop --oneline)'
|
||||||
|
|
||||||
# T17c — opt-out (GITFLOW_PURGE_TRANSIENT=0) keeps the artifacts on develop
|
# T17c — opt-out (GITFLOW_PURGE_TRANSIENT=0) keeps the artifacts on develop
|
||||||
newrepo purgeoff; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
newrepo purgeoff; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||||
@@ -330,7 +330,7 @@ newrepo pushsrc; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
|||||||
bare="$WORK/pushsrc.git"; git init -q --bare "$bare"; git remote add origin "$bare"
|
bare="$WORK/pushsrc.git"; git init -q --bare "$bare"; git remote add origin "$bare"
|
||||||
git push -q origin main develop 2>/dev/null
|
git push -q origin main develop 2>/dev/null
|
||||||
gitflow_start feature ap >/dev/null 2>&1
|
gitflow_start feature ap >/dev/null 2>&1
|
||||||
chk "T18a start pushed the branch" 'git ls-remote --heads origin feature/ap | grep -q feature/ap'
|
chk "T18a start pushed the branch" 'grep -q feature/ap < <(git ls-remote --heads origin feature/ap)'
|
||||||
echo w>w; git add w; git commit -q -m w 2>/dev/null
|
echo w>w; git add w; git commit -q -m w 2>/dev/null
|
||||||
chk "T18b commit pushed by post-commit" '[ "$(git rev-parse HEAD)" = "$(git -C "$bare" rev-parse feature/ap)" ]'
|
chk "T18b commit pushed by post-commit" '[ "$(git rev-parse HEAD)" = "$(git -C "$bare" rev-parse feature/ap)" ]'
|
||||||
echo w2>>w; git add w; GITFLOW_NO_PUSH=1 git commit -q -m w2 2>/dev/null
|
echo w2>>w; git add w; GITFLOW_NO_PUSH=1 git commit -q -m w2 2>/dev/null
|
||||||
@@ -381,7 +381,7 @@ chk "T20b pre-commit rewritten == emitted" 'diff -q <(_gitflow_emit_pre_commit)
|
|||||||
chk "T20c post-commit restored" '[ -x .githooks/post-commit ]'
|
chk "T20c post-commit restored" '[ -x .githooks/post-commit ]'
|
||||||
chk "T20d second run is silent" '[ -z "$(gitflow_reconcile_hooks 2>/dev/null)" ]'
|
chk "T20d second run is silent" '[ -z "$(gitflow_reconcile_hooks 2>/dev/null)" ]'
|
||||||
mkdir -p sub; cd sub || exit 1; echo "# stale" >> ../.githooks/post-merge
|
mkdir -p sub; cd sub || exit 1; echo "# stale" >> ../.githooks/post-merge
|
||||||
chk "T20e works from a subdirectory" 'gitflow_reconcile_hooks 2>/dev/null | grep -q post-merge'
|
chk "T20e works from a subdirectory" 'grep -q post-merge < <(gitflow_reconcile_hooks 2>/dev/null)'
|
||||||
cd .. || exit 1
|
cd .. || exit 1
|
||||||
newrepo plain; echo a>a; git add a; git commit -q -m a
|
newrepo plain; echo a>a; git add a; git commit -q -m a
|
||||||
chk "T20f non-gitflow repo → silent, no .githooks created" '[ -z "$(gitflow_reconcile_hooks 2>/dev/null)" ] && [ ! -d .githooks ]'
|
chk "T20f non-gitflow repo → silent, no .githooks created" '[ -z "$(gitflow_reconcile_hooks 2>/dev/null)" ] && [ ! -d .githooks ]'
|
||||||
|
|||||||
+20
-1
@@ -49,6 +49,25 @@ if ! declare -F info >/dev/null 2>&1; then
|
|||||||
info() { echo -e "${BLUE}→${NC} $1"; }
|
info() { echo -e "${BLUE}→${NC} $1"; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# _gstack_links_realpath_m <path> — prints <path> resolved through its
|
||||||
|
# nearest existing ancestor (BSD realpath has no -m). rc 1 + warning when
|
||||||
|
# the path holds a `..` component: it cannot be resolved lexically.
|
||||||
|
_gstack_links_realpath_m() {
|
||||||
|
local path="${1%/}" rest="" dir base
|
||||||
|
case "/$path/" in
|
||||||
|
*/../*) warn "refusing path with '..': $1" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
dir="$path"
|
||||||
|
while [ -n "$dir" ] && [ ! -d "$dir" ]; do
|
||||||
|
base="${dir##*/}"
|
||||||
|
rest="/$base$rest"
|
||||||
|
case "$dir" in */*) dir="${dir%/*}" ;; *) dir=. ;; esac
|
||||||
|
done
|
||||||
|
[ -n "$dir" ] || dir=/
|
||||||
|
dir="$(CDPATH='' cd -P -- "$dir" && pwd -P)" || return 1
|
||||||
|
printf '%s%s\n' "${dir%/}" "$rest"
|
||||||
|
}
|
||||||
|
|
||||||
# _gstack_links_guard_dst <src> <dst> — removes a stale <dst> symlink
|
# _gstack_links_guard_dst <src> <dst> — removes a stale <dst> symlink
|
||||||
# (gstack ./setup plants `skills/gstack -> skills-external/gstack` when
|
# (gstack ./setup plants `skills/gstack -> skills-external/gstack` when
|
||||||
# the dir is absent), refuses ever writing INTO <src> (dst resolving
|
# the dir is absent), refuses ever writing INTO <src> (dst resolving
|
||||||
@@ -61,7 +80,7 @@ _gstack_links_guard_dst() {
|
|||||||
rm -f "$dst"
|
rm -f "$dst"
|
||||||
fi
|
fi
|
||||||
real_src="$(realpath "$src")"
|
real_src="$(realpath "$src")"
|
||||||
real_dst="$(realpath -m "$dst")"
|
real_dst="$(_gstack_links_realpath_m "$dst")" || return 1
|
||||||
case "$real_dst" in
|
case "$real_dst" in
|
||||||
"$real_src"/*|"$real_src")
|
"$real_src"/*|"$real_src")
|
||||||
warn "refusing to write into the gstack submodule: $dst" >&2
|
warn "refusing to write into the gstack submodule: $dst" >&2
|
||||||
|
|||||||
+14
-8
@@ -265,13 +265,15 @@ skill_status() {
|
|||||||
# `claude plugin list` is the source of truth — settings.json may be
|
# `claude plugin list` is the source of truth — settings.json may be
|
||||||
# ahead of or behind reality if the user toggled outside this tool.
|
# ahead of or behind reality if the user toggled outside this tool.
|
||||||
if command -v "$CLAUDE_BIN" >/dev/null 2>&1; then
|
if command -v "$CLAUDE_BIN" >/dev/null 2>&1; then
|
||||||
# Match the plugin block by name then check Status line
|
# Match the plugin block by name then check Status line. List is
|
||||||
if "$CLAUDE_BIN" plugin list 2>/dev/null \
|
# captured first: an early-exit awk/grep -q in a pipe SIGPIPEs the
|
||||||
| awk -v p="$skill" '
|
# producer (rc 141 under pipefail on macOS).
|
||||||
|
local plist
|
||||||
|
plist="$("$CLAUDE_BIN" plugin list 2>/dev/null)" || true
|
||||||
|
if grep -q "✔ enabled" < <(awk -v p="$skill" '
|
||||||
/^[[:space:]]*❯ '"$skill"'@/ { found=1; next }
|
/^[[:space:]]*❯ '"$skill"'@/ { found=1; next }
|
||||||
found && /Status:/ { print; exit }
|
found && /Status:/ { print; exit }
|
||||||
' \
|
' <<<"$plist"); then
|
||||||
| grep -q "✔ enabled"; then
|
|
||||||
echo "enabled"
|
echo "enabled"
|
||||||
else
|
else
|
||||||
echo "disabled"
|
echo "disabled"
|
||||||
@@ -282,7 +284,7 @@ skill_status() {
|
|||||||
;;
|
;;
|
||||||
mcp)
|
mcp)
|
||||||
if command -v "$CLAUDE_BIN" >/dev/null 2>&1 && \
|
if command -v "$CLAUDE_BIN" >/dev/null 2>&1 && \
|
||||||
"$CLAUDE_BIN" mcp list 2>/dev/null | grep -q "^${skill}"; then
|
grep -q "^${skill}" <<<"$("$CLAUDE_BIN" mcp list 2>/dev/null)"; then
|
||||||
echo "enabled"
|
echo "enabled"
|
||||||
else
|
else
|
||||||
echo "disabled"
|
echo "disabled"
|
||||||
@@ -371,7 +373,10 @@ enable_skill() {
|
|||||||
if [ "$(skill_status "$skill" "$type")" = "enabled" ]; then
|
if [ "$(skill_status "$skill" "$type")" = "enabled" ]; then
|
||||||
: # already on
|
: # already on
|
||||||
elif command -v "$CLAUDE_BIN" >/dev/null 2>&1; then
|
elif command -v "$CLAUDE_BIN" >/dev/null 2>&1; then
|
||||||
if "$CLAUDE_BIN" plugin enable "${skill}@${marketplace}" 2>&1 | grep -qiE "enabled|already"; then
|
# CLI call stays inside the condition: a failing CLI must not abort
|
||||||
|
if grep -qiE "enabled|already" \
|
||||||
|
<<<"$("$CLAUDE_BIN" plugin enable \
|
||||||
|
"${skill}@${marketplace}" 2>&1)"; then
|
||||||
ok "enabled plugin: ${skill}@${marketplace}"
|
ok "enabled plugin: ${skill}@${marketplace}"
|
||||||
else
|
else
|
||||||
warn "could not enable plugin: ${skill}@${marketplace}"
|
warn "could not enable plugin: ${skill}@${marketplace}"
|
||||||
@@ -441,7 +446,8 @@ disable_skill() {
|
|||||||
if [ "$(skill_status "$skill" "$type")" = "disabled" ]; then
|
if [ "$(skill_status "$skill" "$type")" = "disabled" ]; then
|
||||||
: # already off
|
: # already off
|
||||||
elif command -v "$CLAUDE_BIN" >/dev/null 2>&1; then
|
elif command -v "$CLAUDE_BIN" >/dev/null 2>&1; then
|
||||||
if "$CLAUDE_BIN" plugin disable "$key" 2>&1 | grep -qiE "disabled|already"; then
|
if grep -qiE "disabled|already" \
|
||||||
|
<<<"$("$CLAUDE_BIN" plugin disable "$key" 2>&1)"; then
|
||||||
ok "disabled plugin: $key"
|
ok "disabled plugin: $key"
|
||||||
else
|
else
|
||||||
warn "could not disable plugin: $key"
|
warn "could not disable plugin: $key"
|
||||||
|
|||||||
@@ -23,9 +23,13 @@ has "list shows client-a" "$LIST" '"client-a"'
|
|||||||
has "list shows client-b" "$LIST" '"client-b"'
|
has "list shows client-b" "$LIST" '"client-b"'
|
||||||
has "list shows a property" "$LIST" 'sc-domain:a.com'
|
has "list shows a property" "$LIST" 'sc-domain:a.com'
|
||||||
hasnt "list redacts refresh tokens" "$LIST" 'RT_AAA'
|
hasnt "list redacts refresh tokens" "$LIST" 'RT_AAA'
|
||||||
PERM="$(stat -c '%a' "$STORE")"
|
# stat -c is GNU only; python3 gives the same octal mode on BSD and GNU
|
||||||
|
octal_perm() {
|
||||||
|
python3 -I -c 'import os,stat,sys; print(oct(stat.S_IMODE(os.stat(sys.argv[1]).st_mode))[2:])' "$1"
|
||||||
|
}
|
||||||
|
PERM="$(octal_perm "$STORE")"
|
||||||
[ "$PERM" = "600" ] && ok "store file is 0600" || no "store file 0600" "got $PERM"
|
[ "$PERM" = "600" ] && ok "store file is 0600" || no "store file 0600" "got $PERM"
|
||||||
DPERM="$(stat -c '%a' "$(dirname "$STORE")")"
|
DPERM="$(octal_perm "$(dirname "$STORE")")"
|
||||||
[ "$DPERM" = "700" ] && ok "store dir is 0700" || no "store dir 0700" "got $DPERM"
|
[ "$DPERM" = "700" ] && ok "store dir is 0700" || no "store dir 0700" "got $DPERM"
|
||||||
rm -rf "$TMP"
|
rm -rf "$TMP"
|
||||||
|
|
||||||
@@ -136,7 +140,7 @@ import safe_fetch as sf
|
|||||||
try: sf.safe_fetch("file:///etc/passwd"); print("OK")
|
try: sf.safe_fetch("file:///etc/passwd"); print("OK")
|
||||||
except sf.UnsafeTarget: print("REFUSED")')"
|
except sf.UnsafeTarget: print("REFUSED")')"
|
||||||
has "non-http scheme refused" "$SCHEME" 'REFUSED'
|
has "non-http scheme refused" "$SCHEME" 'REFUSED'
|
||||||
IMP="$(/bin/grep -E "^(import|from) " "$SD/safe_fetch.py" | /bin/grep -cvE "gzip|http\.client|ipaddress|socket|ssl|urllib\.parse")"
|
IMP="$(/usr/bin/grep -E "^(import|from) " "$SD/safe_fetch.py" | /usr/bin/grep -cvE "gzip|http\.client|ipaddress|socket|ssl|urllib\.parse")"
|
||||||
[ "$IMP" = "0" ] && ok "safe_fetch is stdlib-only" || no "safe_fetch is stdlib-only" "$IMP non-stdlib imports"
|
[ "$IMP" = "0" ] && ok "safe_fetch is stdlib-only" || no "safe_fetch is stdlib-only" "$IMP non-stdlib imports"
|
||||||
hasnt "no requests dependency" "$(cat "$SD/safe_fetch.py")" 'import requests'
|
hasnt "no requests dependency" "$(cat "$SD/safe_fetch.py")" 'import requests'
|
||||||
|
|
||||||
@@ -477,7 +481,7 @@ has "clear reports ok" "$CL" '"status": "ok"'
|
|||||||
has "clear reports count" "$CL" '"cleared": 1'
|
has "clear reports count" "$CL" '"cleared": 1'
|
||||||
L7="$(python3 "$SD/tokenstore.py" list --file "$S6")"
|
L7="$(python3 "$SD/tokenstore.py" list --file "$S6")"
|
||||||
has "clear empties store" "$L7" '"accounts": []'
|
has "clear empties store" "$L7" '"accounts": []'
|
||||||
PERM6="$(stat -c '%a' "$S6")"
|
PERM6="$(octal_perm "$S6")"
|
||||||
[ "$PERM6" = "600" ] && ok "store stays 0600 after clear" || no "store 0600 after clear" "got $PERM6"
|
[ "$PERM6" = "600" ] && ok "store stays 0600 after clear" || no "store 0600 after clear" "got $PERM6"
|
||||||
# via the real fetch.sh dispatch layer
|
# via the real fetch.sh dispatch layer
|
||||||
python3 "$SD/tokenstore.py" set --file "$S6" --label back --refresh-token RT_BACK \
|
python3 "$SD/tokenstore.py" set --file "$S6" --label back --refresh-token RT_BACK \
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ bad() { echo "FAIL $1 — $2"; FAIL=$((FAIL + 1)); }
|
|||||||
# own probes must never resolve a REAL 21st — else CLI_ABSENT_10 (and every
|
# own probes must never resolve a REAL 21st — else CLI_ABSENT_10 (and every
|
||||||
# other case) would silently exercise this machine's CLI instead of the stub.
|
# other case) would silently exercise this machine's CLI instead of the stub.
|
||||||
if PATH=/usr/bin:/bin command -v 21st >/dev/null 2>&1 \
|
if PATH=/usr/bin:/bin command -v 21st >/dev/null 2>&1 \
|
||||||
|| [ -e /usr/local/bin/21st ]; then
|
|| [ -e /usr/local/bin/21st ] || [ -e /opt/homebrew/bin/21st ]; then
|
||||||
echo "FAIL precondition: system-wide 21st present," \
|
echo "FAIL precondition: system-wide 21st present," \
|
||||||
"CLI_ABSENT case not hermetic"
|
"CLI_ABSENT case not hermetic"
|
||||||
FAIL=$((FAIL + 1))
|
FAIL=$((FAIL + 1))
|
||||||
|
|||||||
@@ -16,16 +16,27 @@ check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
|
|||||||
|
|
||||||
# _citers_extract <file>… → "file:line:name" per cited section (quoted) or label (§)
|
# _citers_extract <file>… → "file:line:name" per cited section (quoted) or label (§)
|
||||||
_citers_extract() {
|
_citers_extract() {
|
||||||
/usr/bin/grep -nHoE 'CLAUDE(\.global)?\.md[^"“]{0,12}["“][^"”]{2,60}["”]' "$@" 2>/dev/null \
|
/usr/bin/grep -nHoE 'CLAUDE(\.global)?\.md[^"“]{0,12}["“][^"”[:space:]][^"”]{1,59}["”]' "$@" 2>/dev/null \
|
||||||
| sed -E 's/^([^:]+:[0-9]+):.*["“]([^"”]+)["”]$/\1:\2/'
|
| sed -E 's/^([^:]+:[0-9]+):.*["“]([^"”]+)["”]$/\1:\2/'
|
||||||
/usr/bin/grep -nHoE 'CLAUDE(\.global)?\.md[^§]{0,60}§ ?[A-Z][A-Za-z][A-Za-z -]{1,40}' "$@" 2>/dev/null \
|
/usr/bin/grep -nHoE 'CLAUDE(\.global)?\.md[^§]{0,60}§ ?[A-Z][A-Za-z][A-Za-z -]{1,40}' "$@" 2>/dev/null \
|
||||||
| sed -E 's/^([^:]+:[0-9]+):.*§ ?([A-Za-z][A-Za-z -]+)$/\1:\2/; s/[[:space:]]+$//'
|
| sed -E 's/^([^:]+:[0-9]+):.*§ ?([A-Za-z][A-Za-z -]+)$/\1:\2/; s/[[:space:]]+$//'
|
||||||
}
|
}
|
||||||
|
|
||||||
# _citers_resolve <doctrine> <name> → rc 0 when a heading or a bold label starts with <name>
|
# _citers_resolve <doctrine> <name> → rc 0 when a heading starts with <name>
|
||||||
|
# (then end, space, `:`, `(` or `—`) or a bold label `**<name>` appears.
|
||||||
|
# Fixed-string awk: the name is never read as a regex (BSD grep -E rejects
|
||||||
|
# some, and "Alpha" must not resolve against "## Alphabet").
|
||||||
_citers_resolve() {
|
_citers_resolve() {
|
||||||
/usr/bin/grep -qE "^#+ ${2}( |$|:|\(|—)" "$1" && return 0
|
awk -v n="$2" '
|
||||||
/usr/bin/grep -qF -- "**${2}" "$1"
|
/^#+ / {
|
||||||
|
t = $0; sub(/^#+ /, "", t)
|
||||||
|
if (substr(t, 1, length(n)) == n) {
|
||||||
|
r = substr(t, length(n) + 1)
|
||||||
|
if (r == "" || r ~ /^[ :(]/ || index(r, "—") == 1) found = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index($0, "**" n) { found = 1 }
|
||||||
|
END { exit !found }' "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# citers_check <doctrine> <file>… → prints DANGLING lines; rc = their count (capped 99)
|
# citers_check <doctrine> <file>… → prints DANGLING lines; rc = their count (capped 99)
|
||||||
@@ -45,6 +56,10 @@ FIX="$(mktemp -d)"; trap 'rm -rf "$FIX"' EXIT
|
|||||||
printf '## Alpha\n\n**Always English, always caveman**: rule.\n\n## Memory registries (`x`)\n' > "$FIX/doctrine.md"
|
printf '## Alpha\n\n**Always English, always caveman**: rule.\n\n## Memory registries (`x`)\n' > "$FIX/doctrine.md"
|
||||||
printf 'ok: see CLAUDE.md "Alpha" and CLAUDE.md "Memory registries" (Always English, always caveman)\n' > "$FIX/good.md"
|
printf 'ok: see CLAUDE.md "Alpha" and CLAUDE.md "Memory registries" (Always English, always caveman)\n' > "$FIX/good.md"
|
||||||
printf 'bad: (see CLAUDE.md "Memory registries" § Language) and CLAUDE.md "Beta"\n' > "$FIX/bad.md"
|
printf 'bad: (see CLAUDE.md "Memory registries" § Language) and CLAUDE.md "Beta"\n' > "$FIX/bad.md"
|
||||||
|
printf '## Alphabet\n' > "$FIX/prefix.md"
|
||||||
|
printf 'prefix: see CLAUDE.md "Alpha"\n' > "$FIX/alpha.md"
|
||||||
|
citers_check "$FIX/prefix.md" "$FIX/alpha.md" >/dev/null
|
||||||
|
check T2d-name-prefix-of-heading-stays-dangling "$?" 1
|
||||||
citers_check "$FIX/doctrine.md" "$FIX/good.md" >/dev/null; check T1-resolving-citations-pass "$?" 0
|
citers_check "$FIX/doctrine.md" "$FIX/good.md" >/dev/null; check T1-resolving-citations-pass "$?" 0
|
||||||
out=$(citers_check "$FIX/doctrine.md" "$FIX/bad.md"); rc=$?
|
out=$(citers_check "$FIX/doctrine.md" "$FIX/bad.md"); rc=$?
|
||||||
check T2-dangling-section-and-label-caught "$rc" 2
|
check T2-dangling-section-and-label-caught "$rc" 2
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ check T6b-no-name-still-frontmatter "$(fm_effort "$EXT/noname/SKILL.md")" "low"
|
|||||||
snap="$(cat "$EXT"/*/SKILL.md)"
|
snap="$(cat "$EXT"/*/SKILL.md)"
|
||||||
bash "$LIB" "$REPO" >/dev/null 2>&1
|
bash "$LIB" "$REPO" >/dev/null 2>&1
|
||||||
check T7-idempotent "$(cat "$EXT"/*/SKILL.md)" "$snap"
|
check T7-idempotent "$(cat "$EXT"/*/SKILL.md)" "$snap"
|
||||||
check T7b-no-tmp-left "$(find "$EXT" -name '*.tmp' | wc -l)" 0
|
check T7b-no-tmp-left "$(find "$EXT" -name '*.tmp' | wc -l | tr -d ' ')" 0
|
||||||
|
|
||||||
# rejections: nothing written, rc 1
|
# rejections: nothing written, rc 1
|
||||||
for bad in 'alpha turbo' '../evil high' 'alpha high extra'; do
|
for bad in 'alpha turbo' '../evil high' 'alpha high extra'; do
|
||||||
@@ -96,7 +96,7 @@ else
|
|||||||
printf 'ro high\n' > "$WORK/h14/lib/effort-pins.txt"
|
printf 'ro high\n' > "$WORK/h14/lib/effort-pins.txt"
|
||||||
chmod 555 "$d14"; out="$(bash "$LIB" "$WORK/h14" 2>&1)"; rc=$?; chmod 755 "$d14"
|
chmod 555 "$d14"; out="$(bash "$LIB" "$WORK/h14" 2>&1)"; rc=$?; chmod 755 "$d14"
|
||||||
check T14-write-failure-no-temp \
|
check T14-write-failure-no-temp \
|
||||||
"$rc|$(printf '%s' "$out" | grep -c 'ERR ')|$(find "$d14" -name 'SKILL.md.*' | wc -l)" "1|1|0"
|
"$rc|$(printf '%s' "$out" | grep -c 'ERR ')|$(find "$d14" -name 'SKILL.md.*' | wc -l | tr -d ' ')" "1|1|0"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# T15: SIGINT during the awk write removes the temp sibling, exit 130
|
# T15: SIGINT during the awk write removes the temp sibling, exit 130
|
||||||
@@ -106,7 +106,7 @@ bash -c 'source "$1"; awk() { kill -INT $$; sleep 2; }
|
|||||||
_effort_pin_write "$2" sig high' _ "$LIB" "$d15/SKILL.md" >/dev/null 2>&1
|
_effort_pin_write "$2" sig high' _ "$LIB" "$d15/SKILL.md" >/dev/null 2>&1
|
||||||
rc=$?
|
rc=$?
|
||||||
check T15-sigint-removes-temp \
|
check T15-sigint-removes-temp \
|
||||||
"$rc|$(find "$d15" -name 'SKILL.md.*' | wc -l)" "130|0"
|
"$rc|$(find "$d15" -name 'SKILL.md.*' | wc -l | tr -d ' ')" "130|0"
|
||||||
|
|
||||||
# T15b: previous INT trap restored on a normal return, no EXIT trap set
|
# T15b: previous INT trap restored on a normal return, no EXIT trap set
|
||||||
mkrepo h15b tr; d15b="$WORK/h15b/skills-external/tr"
|
mkrepo h15b tr; d15b="$WORK/h15b/skills-external/tr"
|
||||||
|
|||||||
@@ -36,17 +36,17 @@ check T4-none "$(bash "$L" detect "$tmp/cpp" >/dev/null 2>&1; echo $?)" 1
|
|||||||
check T5-missing "$(bash "$L" cache-status "$tmp/js" || true)" missing
|
check T5-missing "$(bash "$L" cache-status "$tmp/js" || true)" missing
|
||||||
mkdir -p "$tmp/js/.ctx7-cache"; touch "$tmp/js/.ctx7-cache/react-core.md"
|
mkdir -p "$tmp/js/.ctx7-cache"; touch "$tmp/js/.ctx7-cache/react-core.md"
|
||||||
check T6-fresh "$(bash "$L" cache-status "$tmp/js")" fresh
|
check T6-fresh "$(bash "$L" cache-status "$tmp/js")" fresh
|
||||||
touch -d '10 days ago' "$tmp/js/.ctx7-cache/react-core.md"
|
touch -t 200001010000 "$tmp/js/.ctx7-cache/react-core.md"
|
||||||
check T7-stale "$(bash "$L" cache-status "$tmp/js" || true)" stale
|
check T7-stale "$(bash "$L" cache-status "$tmp/js" || true)" stale
|
||||||
|
|
||||||
# --- hook: fires once per session, silent on stable projects ---
|
# --- hook: fires once per session, silent on stable projects ---
|
||||||
hook() { printf '{"prompt":"add a hook","session_id":"%s","cwd":"%s"}' \
|
hook() { printf '{"prompt":"add a hook","session_id":"%s","cwd":"%s"}' \
|
||||||
"$1" "$2" | TMPDIR="$tmp" bash "$H"; }
|
"$1" "$2" | TMPDIR="$tmp" bash "$H"; }
|
||||||
check H1-fires "$(hook s1 "$tmp/js" | grep -c 'Fast-moving')" 1
|
check H1-fires "$(hook s1 "$tmp/js" | grep -c 'Fast-moving')" 1
|
||||||
check H2-once "$(hook s1 "$tmp/js" | wc -l)" 0
|
check H2-once "$(hook s1 "$tmp/js" | wc -l | tr -d ' ')" 0
|
||||||
check H3-cpp-quiet "$(hook s2 "$tmp/cpp" | wc -l)" 0
|
check H3-cpp-quiet "$(hook s2 "$tmp/cpp" | wc -l | tr -d ' ')" 0
|
||||||
check H4-notif-quiet \
|
check H4-notif-quiet \
|
||||||
"$(printf '{"prompt":"<task-notification>x","session_id":"s3","cwd":"%s"}' \
|
"$(printf '{"prompt":"<task-notification>x","session_id":"s3","cwd":"%s"}' \
|
||||||
"$tmp/js" | TMPDIR="$tmp" bash "$H" | wc -l)" 0
|
"$tmp/js" | TMPDIR="$tmp" bash "$H" | wc -l | tr -d ' ')" 0
|
||||||
|
|
||||||
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ check_kind STUB "$rc" 2 "$out" 'FLOOR STUB'
|
|||||||
|
|
||||||
# ── THRESHOLD_DOWN ────────────────────────────────────────────────────────
|
# ── THRESHOLD_DOWN ────────────────────────────────────────────────────────
|
||||||
d=$(mk_repo threshold); base=$(git -C "$d" rev-parse HEAD)
|
d=$(mk_repo threshold); base=$(git -C "$d" rev-parse HEAD)
|
||||||
sed -i 's/lines: 80/lines: 60/' "$d/vitest.config.ts"
|
# BSD sed -i needs a suffix argument
|
||||||
|
sed -i.bak 's/lines: 80/lines: 60/' "$d/vitest.config.ts" && rm -f "$d/vitest.config.ts.bak"
|
||||||
out=$(cd "$d" && bash "$LIB" "$base" 2>&1); rc=$?
|
out=$(cd "$d" && bash "$LIB" "$base" 2>&1); rc=$?
|
||||||
check_kind THRESHOLD_DOWN "$rc" 2 "$out" 'FLOOR THRESHOLD_DOWN'
|
check_kind THRESHOLD_DOWN "$rc" 2 "$out" 'FLOOR THRESHOLD_DOWN'
|
||||||
|
|
||||||
|
|||||||
@@ -98,4 +98,12 @@ check T4-nothing-created "$([ -e "$DST4" ] && echo present || echo absent)" \
|
|||||||
check T4-warns "$(printf '%s' "$out4" | grep -qi 'refusing' \
|
check T4-warns "$(printf '%s' "$out4" | grep -qi 'refusing' \
|
||||||
&& echo yes || echo no)" yes
|
&& 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 ]
|
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
||||||
|
|||||||
@@ -123,8 +123,9 @@ check T7-update-conflict-nondestructive "$t7_state" "1:Y:Y"
|
|||||||
|
|
||||||
# ── T8 — no destructive command anywhere in the lib source ───────────────
|
# ── T8 — no destructive command anywhere in the lib source ───────────────
|
||||||
d8=OK
|
d8=OK
|
||||||
sed 's/#.*//' "$L" | grep -qE 'git [^|;]*(checkout|reset|clean|stash)' && d8=BAD
|
# producer out of the pipe: grep -q SIGPIPEs it under pipefail on BSD
|
||||||
sed 's/#.*//' "$L" | grep -qwE '(rm|rmdir|unlink|truncate|mv)' && d8=BAD
|
grep -qE 'git [^|;]*(checkout|reset|clean|stash)' < <(sed 's/#.*//' "$L") && d8=BAD
|
||||||
|
grep -qwE '(rm|rmdir|unlink|truncate|mv)' < <(sed 's/#.*//' "$L") && d8=BAD
|
||||||
check T8-no-destructive-command "$d8" OK
|
check T8-no-destructive-command "$d8" OK
|
||||||
|
|
||||||
# ── T9-T14 — browsers-report, fixture cache + playwright-core installs ───
|
# ── T9-T14 — browsers-report, fixture cache + playwright-core installs ───
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/tests/portability-census.test.sh — regression guard for GNU-only shell
|
||||||
|
# idioms that break on macOS (BSD userland). Deterministic idioms only:
|
||||||
|
# sed -i with no suffix, stat -c, realpath -m, touch -d, grep -P,
|
||||||
|
# a bare /bin/grep (LRN-074: pin /usr/bin/grep).
|
||||||
|
# Not covered on purpose: `cmd | grep -q` under pipefail (not decidable by
|
||||||
|
# text; fixed structurally by taking the producer out of the pipe).
|
||||||
|
# Usage: portability-census.test.sh [file…]
|
||||||
|
# no args: flip-test, then scan tracked *.sh + hooks/*
|
||||||
|
# args : scan only those files; exit 2 on any hit
|
||||||
|
set -u
|
||||||
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
|
||||||
|
RE="sed -i ['\"]|stat -c|realpath -m|touch -d|grep -[A-Za-z]*P([^A-Za-z]|$)"
|
||||||
|
RE="$RE|(^|[^a-z])/bin/grep"
|
||||||
|
|
||||||
|
# file:line (or file:*) exempt from the scan, each with its reason.
|
||||||
|
ALLOW=(
|
||||||
|
"lib/tests/guard-bash.test.sh:221" # deny fixture: GNU spelling
|
||||||
|
"lib/tests/guard-bash.test.sh:225" # deny fixture: GNU spelling
|
||||||
|
"lib/tests/portability-census.test.sh:*" # spells the idioms
|
||||||
|
)
|
||||||
|
|
||||||
|
_allowed() {
|
||||||
|
local entry
|
||||||
|
for entry in "${ALLOW[@]}"; do
|
||||||
|
entry="${entry%% #*}"
|
||||||
|
[ "$entry" = "$1:$2" ] || [ "$entry" = "$1:*" ] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# scan <file>… → prints file:line:text per hit, rc 2 when any
|
||||||
|
scan() {
|
||||||
|
local f rel line lno text hits=0
|
||||||
|
for f in "$@"; do
|
||||||
|
rel="${f#"$ROOT"/}"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
lno="${line%%:*}"; text="${line#*:}"
|
||||||
|
case "$text" in [[:space:]]*"#"*|"#"*) continue ;; esac
|
||||||
|
_allowed "$rel" "$lno" && continue
|
||||||
|
printf 'GNU-ONLY: %s:%s:%s\n' "$rel" "$lno" "$text"; hits=$((hits+1))
|
||||||
|
done < <(grep -nE -- "$RE" "$f" 2>/dev/null)
|
||||||
|
done
|
||||||
|
[ "$hits" -eq 0 ] || return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$#" -gt 0 ]; then scan "$@"; exit $?; fi
|
||||||
|
|
||||||
|
# flip-test: a planted GNU idiom must be caught before the real census runs
|
||||||
|
PLANT="$(mktemp -d)" || exit 1; trap 'rm -rf "$PLANT"' EXIT
|
||||||
|
printf '#!/usr/bin/env bash\nsed -i '"'"'s/a/b/'"'"' x\n' > "$PLANT/planted.sh"
|
||||||
|
scan "$PLANT/planted.sh" >/dev/null; flip=$?
|
||||||
|
if [ "$flip" -ne 2 ]; then echo "FAIL flip: plant not caught"; exit 1; fi
|
||||||
|
|
||||||
|
mapfile -t FILES < <(cd "$ROOT" && git ls-files '*.sh' 'hooks/*' \
|
||||||
|
| sed "s#^#$ROOT/#")
|
||||||
|
scan "${FILES[@]}"; rc=$?
|
||||||
|
[ "$rc" -eq 0 ] && echo "PASS portability census (${#FILES[@]} files)"
|
||||||
|
exit "$rc"
|
||||||
@@ -178,7 +178,8 @@ run_mutant T3-mutant-removed "$M1" 'REMOVED_LISTED:qa:ship' \
|
|||||||
|
|
||||||
# Mutant 2: the superset profile drops a name full carries.
|
# Mutant 2: the superset profile drops a name full carries.
|
||||||
M2="$WORK/mutant-superset"; cp -r "$BASE" "$M2"
|
M2="$WORK/mutant-superset"; cp -r "$BASE" "$M2"
|
||||||
sed -i '/^beta$/d' "$M2/max.profile"
|
# BSD sed -i needs a suffix argument
|
||||||
|
sed -i.bak '/^beta$/d' "$M2/max.profile" && rm -f "$M2/max.profile.bak"
|
||||||
run_mutant T4-mutant-superset "$M2" 'SUPERSET_GAP:beta' \
|
run_mutant T4-mutant-superset "$M2" 'SUPERSET_GAP:beta' \
|
||||||
FIXTURE_SUPERSET_DETECTED
|
FIXTURE_SUPERSET_DETECTED
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,8 @@ printf ' none \r' > "$FX/.active-profile"
|
|||||||
out="$(statusline)"
|
out="$(statusline)"
|
||||||
check_has T10-full "$out" "profile: full"
|
check_has T10-full "$out" "profile: full"
|
||||||
|
|
||||||
sed -i 's/^DEFAULT_PROFILE="full"/DEFAULT_PROFILE="otherish"/' "$FX/lib/profile.sh"
|
# BSD sed -i needs a suffix argument
|
||||||
|
sed -i.bak 's/^DEFAULT_PROFILE="full"/DEFAULT_PROFILE="otherish"/' "$FX/lib/profile.sh" && rm -f "$FX/lib/profile.sh.bak"
|
||||||
rm -f "$FX/.active-profile"
|
rm -f "$FX/.active-profile"
|
||||||
out="$(statusline)"
|
out="$(statusline)"
|
||||||
check_has T11-otherish "$out" "profile: otherish"
|
check_has T11-otherish "$out" "profile: otherish"
|
||||||
|
|||||||
@@ -80,4 +80,25 @@ check T16b-obs-back "$([ -e "$FX/skills/observability-and-instrumentation" ] &&
|
|||||||
check T16c-obs-park-gone "$([ -e "$FX/skills-disabled/observability-and-instrumentation" ] && echo p || echo n)" n
|
check T16c-obs-park-gone "$([ -e "$FX/skills-disabled/observability-and-instrumentation" ] && echo p || echo n)" n
|
||||||
check T17-no-mcp-ever "$(grep -c '^mcp ' "$FX/claude-calls.log" || true)" 0
|
check T17-no-mcp-ever "$(grep -c '^mcp ' "$FX/claude-calls.log" || true)" 0
|
||||||
|
|
||||||
|
# --- a failing `claude plugin enable` must warn, never abort `set` ---
|
||||||
|
# Guards the capture-inside-the-condition form: a bare out="$(claude …)"
|
||||||
|
# would trip errexit and skip every entry after the plugin.
|
||||||
|
mkdir -p "$FX/failbin"
|
||||||
|
cat > "$FX/failbin/claude" <<'EOF'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
[ "$1 $2" = "plugin enable" ] && exit 1
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$FX/failbin/claude"
|
||||||
|
cat > "$FX/lib/profiles/pluginish.profile" <<'EOF'
|
||||||
|
fake-plug plugin@fake-market
|
||||||
|
gs-a
|
||||||
|
EOF
|
||||||
|
rm -f "$FX/skills/gs-a"
|
||||||
|
PATH="$FX/failbin:$PATH" PROFILE_REPO_OVERRIDE="$FX" \
|
||||||
|
TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX" bash "$FX/lib/profile.sh" set pluginish \
|
||||||
|
>/dev/null 2>&1
|
||||||
|
check T18-failing-plugin-enable-keeps-going \
|
||||||
|
"$([ -e "$FX/skills/gs-a" ] && echo on || echo off)" on
|
||||||
|
|
||||||
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ printf ' err: %s\n' "$(printf '%s' "$ERR" | grep -i decisions | head -1)"
|
|||||||
if [ "$RC" -eq 4 ]; then ok "mixed → exit 4"; else ko "expected 4, got $RC"; fi
|
if [ "$RC" -eq 4 ]; then ok "mixed → exit 4"; else ko "expected 4, got $RC"; fi
|
||||||
if [ "$(git -C "$R" rev-parse HEAD)" = "$BEFORE" ]; then ok "NOTHING committed (README not half-committed)"; else ko "a commit slipped through"; fi
|
if [ "$(git -C "$R" rev-parse HEAD)" = "$BEFORE" ]; then ok "NOTHING committed (README not half-committed)"; else ko "a commit slipped through"; fi
|
||||||
if printf '%s' "$ERR" | grep -q '.claude/memory/decisions.md'; then ok "stderr names the offender"; else ko "offender not named"; fi
|
if printf '%s' "$ERR" | grep -q '.claude/memory/decisions.md'; then ok "stderr names the offender"; else ko "offender not named"; fi
|
||||||
if git -C "$R" status --porcelain | grep -q ' M README.md'; then ok "README left dirty (not embarked)"; else ko "README state wrong"; fi
|
if grep -q ' M README.md' < <(git -C "$R" status --porcelain); then ok "README left dirty (not embarked)"; else ko "README state wrong"; fi
|
||||||
rm -rf "$R"
|
rm -rf "$R"
|
||||||
|
|
||||||
echo "T2 — dynamic pathspec: clean passed path filtered, no abort"
|
echo "T2 — dynamic pathspec: clean passed path filtered, no abort"
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ echo
|
|||||||
( cd "$WORK" || exit 1
|
( cd "$WORK" || exit 1
|
||||||
bash "$GITFLOW" start release 4.0.0 >/dev/null # base develop → release/4.0.0 (lib L49/L71)
|
bash "$GITFLOW" start release 4.0.0 >/dev/null # base develop → release/4.0.0 (lib L49/L71)
|
||||||
printf '4.0.0\n' > version.txt # prep: version bump
|
printf '4.0.0\n' > version.txt # prep: version bump
|
||||||
sed -i 's/## \[Unreleased\]/## [Unreleased]\n\n## [4.0.0] — 2026-06-30/' CHANGELOG.md
|
# BSD sed -i needs a suffix argument: -i.bak then drop the backup
|
||||||
|
sed -i.bak 's/## \[Unreleased\]/## [Unreleased]\n\n## [4.0.0] — 2026-06-30/' CHANGELOG.md && rm -f CHANGELOG.md.bak
|
||||||
git commit -qam "chore(release): 4.0.0 — version.txt + CHANGELOG"
|
git commit -qam "chore(release): 4.0.0 — version.txt + CHANGELOG"
|
||||||
bash "$GITFLOW" finish >/dev/null # fan-out main+develop+delete (lib L108-111)
|
bash "$GITFLOW" finish >/dev/null # fan-out main+develop+delete (lib L108-111)
|
||||||
# TAG = the gap. Lives in the SKILL (lib untouched). RED skips it, GREEN does it.
|
# TAG = the gap. Lives in the SKILL (lib untouched). RED skips it, GREEN does it.
|
||||||
@@ -52,7 +53,7 @@ echo "=== assertions (RC_TAG=$RC_TAG) ==="
|
|||||||
if [ "$(git -C "$WORK" show main:version.txt 2>/dev/null)" = "4.0.0" ]; then ok "fan-out: main carries the release (version.txt 4.0.0)"; else no "fan-out: main version.txt != 4.0.0"; fi
|
if [ "$(git -C "$WORK" show main:version.txt 2>/dev/null)" = "4.0.0" ]; then ok "fan-out: main carries the release (version.txt 4.0.0)"; else no "fan-out: main version.txt != 4.0.0"; fi
|
||||||
if [ "$(git -C "$WORK" show develop:version.txt 2>/dev/null)" = "4.0.0" ]; then ok "merge-back: develop carries 4.0.0"; else no "merge-back failed"; fi
|
if [ "$(git -C "$WORK" show develop:version.txt 2>/dev/null)" = "4.0.0" ]; then ok "merge-back: develop carries 4.0.0"; else no "merge-back failed"; fi
|
||||||
if git -C "$WORK" show-ref --verify -q refs/heads/release/4.0.0; then no "release/4.0.0 NOT deleted"; else ok "release/4.0.0 branch deleted"; fi
|
if git -C "$WORK" show-ref --verify -q refs/heads/release/4.0.0; then no "release/4.0.0 NOT deleted"; else ok "release/4.0.0 branch deleted"; fi
|
||||||
if git -C "$WORK" show main:CHANGELOG.md | $GREP -q '## \[4.0.0\]'; then ok "CHANGELOG [4.0.0] on main"; else no "CHANGELOG not finalized"; fi
|
if $GREP -q '## \[4.0.0\]' < <(git -C "$WORK" show main:CHANGELOG.md); then ok "CHANGELOG [4.0.0] on main"; else no "CHANGELOG not finalized"; fi
|
||||||
if git -C "$WORK" rev-parse -q --verify refs/tags/v4.0.0 >/dev/null; then
|
if git -C "$WORK" rev-parse -q --verify refs/tags/v4.0.0 >/dev/null; then
|
||||||
if [ "$(git -C "$WORK" rev-list -n1 v4.0.0)" = "$(git -C "$WORK" rev-parse main)" ]; then ok "tag v4.0.0 on main's release-merge commit"; else no "tag v4.0.0 exists but not on main HEAD"; fi
|
if [ "$(git -C "$WORK" rev-list -n1 v4.0.0)" = "$(git -C "$WORK" rev-parse main)" ]; then ok "tag v4.0.0 on main's release-merge commit"; else no "tag v4.0.0 exists but not on main HEAD"; fi
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ case "$G" in *"*"*) check C2-grep-has-no-glob "has-glob" ok ;;
|
|||||||
*) check C2-grep-has-no-glob ok ok ;; esac
|
*) check C2-grep-has-no-glob ok ok ;; esac
|
||||||
|
|
||||||
# --- findargs: one token per line, 3 tokens per dir ---
|
# --- findargs: one token per line, 3 tokens per dir ---
|
||||||
N="$(cd "$TMP/plain" && bash "$S" findargs | wc -l)"
|
N="$(cd "$TMP/plain" && bash "$S" findargs | wc -l | tr -d ' ')"
|
||||||
D="$(cd "$TMP/plain" && bash "$S" list | wc -l)"
|
D="$(cd "$TMP/plain" && bash "$S" list | wc -l | tr -d ' ')"
|
||||||
check D1-findargs-3-tokens-per-dir "$N" "$((D * 3))"
|
check D1-findargs-3-tokens-per-dir "$N" "$((D * 3))"
|
||||||
check D2-findargs-first-token "$(cd "$TMP/plain" && bash "$S" findargs | head -1)" '!'
|
check D2-findargs-first-token "$(cd "$TMP/plain" && bash "$S" findargs | head -1)" '!'
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ check E1-excludes-dist "$(find . "${FEXCL[@]}" -name 'a.png' | grep -c '/dist/'
|
|||||||
check E2-keeps-src "$(find . "${FEXCL[@]}" -name 'a.png' | grep -c '/src/')" 1
|
check E2-keeps-src "$(find . "${FEXCL[@]}" -name 'a.png' | grep -c '/src/')" 1
|
||||||
check E3-excludes-nodem "$(find . "${FEXCL[@]}" -name '*.png' | grep -c 'node_modules')" 0
|
check E3-excludes-nodem "$(find . "${FEXCL[@]}" -name '*.png' | grep -c 'node_modules')" 0
|
||||||
# public/ survives: the audit's own resource checks live there
|
# public/ survives: the audit's own resource checks live there
|
||||||
check E4-keeps-public "$(find . "${FEXCL[@]}" -name 'favicon.ico' | wc -l)" 1
|
check E4-keeps-public "$(find . "${FEXCL[@]}" -name 'favicon.ico' | wc -l | tr -d ' ')" 1
|
||||||
cd / || exit 1
|
cd / || exit 1
|
||||||
|
|
||||||
# --- usage ---
|
# --- usage ---
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ pack_hints() {
|
|||||||
21st)
|
21st)
|
||||||
if ! command -v 21st >/dev/null 2>&1; then
|
if ! command -v 21st >/dev/null 2>&1; then
|
||||||
warn "the \`21st\` CLI is not on PATH — install it: npm i -g @21st-dev/cli"
|
warn "the \`21st\` CLI is not on PATH — install it: npm i -g @21st-dev/cli"
|
||||||
elif ! 21st whoami 2>/dev/null | grep -q '^Logged in as '; then
|
elif ! grep -q '^Logged in as ' <<<"$(21st whoami 2>/dev/null)"; then
|
||||||
warn "not signed in to 21st — component retrieval and 21st AI need: 21st login"
|
warn "not signed in to 21st — component retrieval and 21st AI need: 21st login"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|||||||
+2
-1
@@ -603,8 +603,9 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo "── Updating marketplace plugins..."
|
echo "── Updating marketplace plugins..."
|
||||||
if command -v claude &>/dev/null; then
|
if command -v claude &>/dev/null; then
|
||||||
|
# sed -n: BSD grep has no PCRE mode (rc 2 emptied the list)
|
||||||
_plugins=$(claude plugin list 2>/dev/null \
|
_plugins=$(claude plugin list 2>/dev/null \
|
||||||
| grep -oP '(?<=❯ )\S+' || true)
|
| sed -n 's/.*❯ \([^[:space:]][^[:space:]]*\).*/\1/p' || true)
|
||||||
if [ -n "$_plugins" ]; then
|
if [ -n "$_plugins" ]; then
|
||||||
while IFS= read -r _p; do
|
while IFS= read -r _p; do
|
||||||
_name="${_p%%@*}"
|
_name="${_p%%@*}"
|
||||||
|
|||||||
Reference in New Issue
Block a user