Merge bugfix/macos-gnu-coreutils into develop
This commit is contained in:
+33
-1
@@ -30,6 +30,13 @@
|
|||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
TIMEOUT="${GATES_TIMEOUT:-120}"
|
TIMEOUT="${GATES_TIMEOUT:-120}"
|
||||||
|
# GNU coreutils' `timeout` ships on Linux but NOT on a stock macOS (Homebrew
|
||||||
|
# installs it as both `timeout` and `gtimeout`). Resolve it once: without it
|
||||||
|
# every check exits 127 and reports NOT-MET whatever the check actually did.
|
||||||
|
# Overridable, and with `-` not `:-` so an explicitly EMPTY value forces the
|
||||||
|
# pure-bash path — that is how the fallback gets exercised on a machine that
|
||||||
|
# does have the binary.
|
||||||
|
GATES_TIMEOUT_BIN="${GATES_TIMEOUT_BIN-$(command -v timeout || command -v gtimeout || true)}"
|
||||||
EVIDENCE_CAP=140
|
EVIDENCE_CAP=140
|
||||||
|
|
||||||
# Module-level parse tables, index-aligned. Bash has no record type; threading
|
# Module-level parse tables, index-aligned. Bash has no record type; threading
|
||||||
@@ -165,9 +172,34 @@ _decisive() { # _decisive <combined-output>
|
|||||||
|
|
||||||
# Fail-closed: exit 0 AND the marker. A nonzero process never passes because
|
# Fail-closed: exit 0 AND the marker. A nonzero process never passes because
|
||||||
# its error text happens to contain the expected token.
|
# its error text happens to contain the expected token.
|
||||||
|
# Same contract as `timeout`: run the command, return 124 if it outruns <secs>.
|
||||||
|
# Pure-bash stand-in for a platform shipping neither binary, so the deadline
|
||||||
|
# stays real instead of silently degrading into "every gate NOT-MET".
|
||||||
|
_gates_timeout() { # _gates_timeout <secs> <cmd>...
|
||||||
|
local secs="$1"; shift
|
||||||
|
[ -n "$GATES_TIMEOUT_BIN" ] && { "$GATES_TIMEOUT_BIN" "$secs" "$@"; return $?; }
|
||||||
|
local waited=0 pid
|
||||||
|
"$@" &
|
||||||
|
pid=$!
|
||||||
|
while kill -0 "$pid" 2>/dev/null; do
|
||||||
|
if [ "$waited" -ge "$secs" ]; then
|
||||||
|
# Park the shell's stderr: bash announces a signal-killed job on ITS
|
||||||
|
# stderr, which the caller captures with 2>&1 and would read as output.
|
||||||
|
exec 3>&2 2>/dev/null
|
||||||
|
kill -TERM "$pid" 2>/dev/null || true
|
||||||
|
wait "$pid" 2>/dev/null || true
|
||||||
|
exec 2>&3 3>&-
|
||||||
|
return 124
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
waited=$((waited + 1))
|
||||||
|
done
|
||||||
|
wait "$pid"
|
||||||
|
}
|
||||||
|
|
||||||
_run_one() { # _run_one <idx>
|
_run_one() { # _run_one <idx>
|
||||||
local i="$1" out rc
|
local i="$1" out rc
|
||||||
out="$(timeout "$TIMEOUT" bash -c "${_CHECK[i]}" 2>&1)"
|
out="$(_gates_timeout "$TIMEOUT" bash -c "${_CHECK[i]}" 2>&1)"
|
||||||
rc=$?
|
rc=$?
|
||||||
_STATUS[i]="NOT-MET"
|
_STATUS[i]="NOT-MET"
|
||||||
if [ "$rc" -eq 124 ]; then
|
if [ "$rc" -eq 124 ]; then
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ no() { echo " FAIL $1 — $2"; FAIL=$((FAIL+1)); }
|
|||||||
# assert stdout of a command contains / omits a fixed string
|
# assert stdout of a command contains / omits a fixed string
|
||||||
has() { if printf '%s' "$2" | grep -qF -- "$3"; then ok "$1"; else no "$1" "missing: $3"; fi; }
|
has() { if printf '%s' "$2" | grep -qF -- "$3"; then ok "$1"; else no "$1" "missing: $3"; fi; }
|
||||||
hasnt(){ if printf '%s' "$2" | grep -qF -- "$3"; then no "$1" "forbidden: $3"; else ok "$1"; fi; }
|
hasnt(){ if printf '%s' "$2" | grep -qF -- "$3"; then no "$1" "forbidden: $3"; else ok "$1"; fi; }
|
||||||
|
# Octal permission bits. GNU stat spells it -c %a, BSD stat (macOS) -f %OLp;
|
||||||
|
# neither accepts the other's flag, so try one then the other.
|
||||||
|
perm() { stat -c '%a' "$1" 2>/dev/null || stat -f '%OLp' "$1"; }
|
||||||
|
|
||||||
echo "── tokenstore ──"
|
echo "── tokenstore ──"
|
||||||
TMP="$(mktemp -d)"; STORE="$TMP/tokens.json"
|
TMP="$(mktemp -d)"; STORE="$TMP/tokens.json"
|
||||||
@@ -23,9 +26,9 @@ 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")"
|
PERM="$(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="$(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 +139,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="$(grep -E "^(import|from) " "$SD/safe_fetch.py" | grep -cvE "gzip|http\.client|ipaddress|socket|ssl|urllib\.parse" | tr -d ' ')"
|
||||||
[ "$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 +480,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="$(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 \
|
||||||
|
|||||||
@@ -36,17 +36,20 @@ 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 -d '10 days ago'` is GNU-only; BSD touch (macOS) wants -t with an
|
||||||
|
# absolute stamp. perl's utime is the one spelling both platforms ship.
|
||||||
|
perl -e 'my $t = time - 10*86400; utime $t, $t, $ARGV[0]' \
|
||||||
|
"$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 ]
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ 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
|
# awk, not `sed -i`: BSD sed (macOS) needs a suffix argument after -i AND
|
||||||
|
# does not expand \n in the replacement — the edit silently did nothing
|
||||||
|
# there, so the CHANGELOG assertion below failed for the wrong reason.
|
||||||
|
awk '{ print; if ($0 == "## [Unreleased]") { print ""; print "## [4.0.0] — 2026-06-30" } }' \
|
||||||
|
CHANGELOG.md > CHANGELOG.tmp && cat CHANGELOG.tmp > CHANGELOG.md && rm -f CHANGELOG.tmp
|
||||||
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user