From 6109f8176901203af585cbf0cb77c7a420553ef0 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 31 Jul 2026 01:52:56 -0500 Subject: [PATCH] wake/#973: three-valued grep assertion helpers + instrument micro-test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit has_match/count_lines in _wake-common.sh: rc 0 -> match, rc 1 -> no-match, anything else -> loud abort naming the call site, the raw exit code, and the arguments — an error never becomes a verdict. The abort escapes subshells, pipelines, and $() substitutions by signalling the suite's main shell, so the suite dies verdict-less; loudness survives call-site 2>/dev/null via a wake_assert_init-saved stderr fd (dynamically allocated >= 10, no collision with the wake lock fd 8 or detector run-loop fd 9). Validate instrumentation (evidence only, inert in production): WAKE_ASSERT_LEDGER appends " :" BEFORE the grep runs, so an aborting site's row has already landed and the counted ledger can never go quiet in the one condition it exists to report. WAKE_ASSERT_FORCE_GREP_ERROR_AT=: routes exactly that call site through a REAL grep driven onto its real error path (rc 2) and emits a positive ARMED confirmation first, so arm-never-matched (no ARMED line) and error-path-broken (ARMED line, no abort) are separable on stderr alone. validate-973/microtest-wake-assert.sh: nine checks (C1..C9) proving the instrument before any conversion trusts it — ledger set equality across two files, truncation detection behind an early exit, per-shape abort proofs (subshell, 2>/dev/null canary shape, $() count capture, pipeline tail), count-of-zero on grep rc 1, env-prefix reaching the grep child, and a no-match arm loud by omission. All nine pass; C1 pins BASH_LINENO's first-physical-line convention for backslash continuations, matching the denominator artifact's coordinates. Production tools (store.sh, ack.sh) source this file but call none of the new helpers. Written-by: pepper (sb-it-1-dt) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NsKce8iZuSuRnu3gVMCBKB --- .../framework/tools/wake/_wake-common.sh | 138 +++++++++++ .../validate-973/microtest-wake-assert.sh | 233 ++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100755 packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh diff --git a/packages/mosaic/framework/tools/wake/_wake-common.sh b/packages/mosaic/framework/tools/wake/_wake-common.sh index 5bdb16c7..fc659711 100755 --- a/packages/mosaic/framework/tools/wake/_wake-common.sh +++ b/packages/mosaic/framework/tools/wake/_wake-common.sh @@ -191,3 +191,141 @@ _wake_init_dir() { [ -f "$dir/pending.jsonl" ] || printf '' | _atomic_write "$dir/pending.jsonl" [ -f "$dir/ack-ledger.jsonl" ] || printf '' | _atomic_write "$dir/ack-ledger.jsonl" } + +# --------------------------------------------------------------------------- +# #973 — three-valued grep assertion helpers for the wake test suites. +# +# grep's exit contract is three-valued: 0 = match, 1 = no match, >1 = ERROR +# (bad file, bad pattern, resource failure). Every wake-suite assertion used +# to read all non-zero as "absent", so a grep that COULD NOT LOOK wore the +# colour of a verdict: OR-polarity sites (`|| fail`) went falsely red, +# AND-polarity sites (`&& fail` — including the credential canaries) went +# falsely green. The repair is to refuse to answer: rc 0 -> match, rc 1 -> no +# match, anything else -> loud abort naming the call site, the raw exit code, +# and the arguments. An error NEVER becomes a verdict. +# +# Production tools (store.sh, ack.sh) source this file but call none of the +# helpers below; they are inert outside the suites. +# +# Suite integration contract: +# - Call `wake_assert_init` ONCE at suite top level, right after sourcing. +# It dups the suite's real stderr to a saved fd BEFORE any call-site +# redirect exists, so an abort stays loud even at sites that append +# `2>/dev/null` (the preimage credential canaries pre-swallow stderr — +# exactly where a silent abort would recreate the defect being fixed). +# - Assertion sites live inside `( ... ) && ok` subshell blocks, pipelines, +# and `$(...)` substitutions, where a plain `exit` dies one layer deep and +# the suite would carry on to emit a verdict. The abort therefore signals +# the suite's MAIN shell ($$ is the main PID in every subshell) and then +# exits the current context: the suite dies by signal, non-zero, with NO +# verdict line emitted. +# +# Validation instrumentation (#973 evidence, not part of the assertion fix): +# - WAKE_ASSERT_LEDGER=: every helper call appends +# " :" to . That is the ONLY +# divergence from production behaviour — the suite otherwise runs its +# normal arms, so a validate run exercises exactly the shipped paths. +# - WAKE_ASSERT_FORCE_GREP_ERROR_AT=:: at exactly +# that call site, the invocation is routed through a REAL grep driven onto +# its real error path (unknown option -> rc 2) — a genuinely executed +# failing process, not a stubbed return — to prove per-site that the abort +# fires. Unset in production; matching no site is a no-op. +# --------------------------------------------------------------------------- + +# wake_assert_init — dup the suite's real stderr once, for abort loudness. +# MUST be called at suite TOP LEVEL, immediately after sourcing and before any +# test block: a lazy (first-call) dup could capture an already-redirected +# stderr if the first executed helper call sat under a call-site 2>/dev/null, +# silencing every abort thereafter. The fd is allocated dynamically (>= 10), +# so it cannot collide with the wake lock fds (8) or the detector run-loop +# lock (9). +wake_assert_init() { + if [ -z "${_wake_assert_err_fd:-}" ]; then + exec {_wake_assert_err_fd}>&2 + fi +} + +# _wake_assert_err_note MSG — write MSG to the saved real-stderr fd, falling +# back to the current stderr if init was never called. +_wake_assert_err_note() { + if [ -n "${_wake_assert_err_fd:-}" ]; then + printf '%s\n' "$1" >&"$_wake_assert_err_fd" 2>/dev/null || + printf '%s\n' "$1" >&2 + else + printf '%s\n' "$1" >&2 + fi +} + +# _wake_assert_abort HELPER SITE RC ARGS... — refuse to answer, loudly. +# Writes the named reason to the saved real-stderr fd (falling back to the +# current stderr), signals the suite's main shell, and exits this context. +_wake_assert_abort() { + local _wa_helper="$1" _wa_where="$2" _wa_code="$3" + shift 3 + _wake_assert_err_note "WAKE-ASSERT ABORT: ${_wa_helper} at ${_wa_where}: grep exit ${_wa_code} is an error, not a verdict (args: $*) — refusing to answer (#973)" + if [ -n "${BASHPID:-}" ] && [ "$BASHPID" != "$$" ]; then + kill -TERM "$$" 2>/dev/null || true + fi + exit 97 +} + +# _wake_assert_armed SITE — true iff the forced-error arm targets SITE; on a +# match it emits a positive confirmation FIRST, so "site did not abort" can +# never conflate SITE NOT CONVERTED with ARM NEVER REACHED IT: an armed run +# with no ARMED line means the arm matched nothing (typo/renumber/drift), and +# an ARMED line with no abort means the site's error path is broken. The two +# defects are separable on stderr alone. +_wake_assert_armed() { + [ "${WAKE_ASSERT_FORCE_GREP_ERROR_AT:-}" = "$1" ] || return 1 + _wake_assert_err_note "WAKE-ASSERT ARMED: forcing real grep error at $1 (#973)" + return 0 +} + +# has_match GREP_ARGS... — three-valued grep verdict. +# Drop-in for verdict-bearing `grep` calls (flags, files, stdin all pass +# through; stdout is not captured, so extract-form call sites may use it +# inside a substitution). Returns 0 on match, 1 on no-match; any other grep +# exit aborts the suite via _wake_assert_abort. +has_match() { + local _wa_site="${BASH_SOURCE[1]##*/}:${BASH_LINENO[0]}" _wa_rc=0 + if [ -n "${WAKE_ASSERT_LEDGER:-}" ]; then + printf 'has_match %s\n' "$_wa_site" >>"$WAKE_ASSERT_LEDGER" + fi + if _wake_assert_armed "$_wa_site"; then + command grep --wake-assert-forced-error -- /dev/null + _wa_rc=$? + else + command grep "$@" + _wa_rc=$? + fi + case "$_wa_rc" in + 0) return 0 ;; + 1) return 1 ;; + *) _wake_assert_abort has_match "$_wa_site" "$_wa_rc" "$@" ;; + esac +} + +# count_lines GREP_ARGS... — `grep -c` with the same three-way discipline. +# Call sites drop their `-c` (the helper supplies it) and keep every other +# argument. Prints the count on rc 0 AND rc 1 (rc 1 is grep's "count is 0" — +# a valid measurement, not an error); any other exit aborts. The abort still +# kills the suite from inside a `$(...)` capture: the substitution subshell +# cannot exit the suite, but the signal to the main shell can — a count from +# a failed measurement is never printed. +count_lines() { + local _wa_site="${BASH_SOURCE[1]##*/}:${BASH_LINENO[0]}" _wa_rc=0 _wa_out="" + if [ -n "${WAKE_ASSERT_LEDGER:-}" ]; then + printf 'count_lines %s\n' "$_wa_site" >>"$WAKE_ASSERT_LEDGER" + fi + if _wake_assert_armed "$_wa_site"; then + _wa_out="$(command grep --wake-assert-forced-error -c -- /dev/null)" + _wa_rc=$? + else + _wa_out="$(command grep -c "$@")" + _wa_rc=$? + fi + case "$_wa_rc" in + 0 | 1) printf '%s\n' "$_wa_out" ;; + *) _wake_assert_abort count_lines "$_wa_site" "$_wa_rc" "$@" ;; + esac +} diff --git a/packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh b/packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh new file mode 100755 index 00000000..cee0e51b --- /dev/null +++ b/packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh @@ -0,0 +1,233 @@ +#!/usr/bin/env bash +# microtest-wake-assert.sh — #973 instrument self-test. Run BEFORE trusting any +# validate-run evidence: it proves the counted ledger and the abort mechanics on +# two generated mini-suites, so a defect in the instrument cannot silently wear +# the colour of a clean validation. +# +# What it proves (each check named C1..C8 below): +# C1 green run: ledger set EQUALS a text-derived expected set spanning TWO +# files (file-field discrimination), row count > 1, both sentinels emitted, +# exit 0. Also pins the BASH_LINENO convention for backslash-continuation +# call sites against the first-physical-line convention the denominator +# artifact uses. +# C2 early-exit truncation: a suite that exits before its later site yields a +# SHORT ledger, and the expected-set comparison catches it — a counted +# ledger must report its own truncation, never a smaller total. +# C3 abort from inside a `( ... )` test subshell kills the WHOLE suite: no +# sentinel, non-zero exit, loud named reason (file:line + raw rc). +# C4 abort stays loud at a call site that appends 2>/dev/null (the preimage +# canary shape) — the saved-fd path. +# C5 abort escapes a `$( count_lines ... )` substitution (A6 shape): the +# count from a failed measurement is never compared and the suite dies. +# C6 abort escapes a pipeline tail (`printf | has_match`). +# C7 count_lines prints 0 on grep rc 1 (zero matches is a measurement, not an +# error) — implicit in C1's green run via the delta-count site. +# C8 an env-prefix on the helper (`LC_ALL=C has_match ...`) reaches the grep +# child — pins the conversion shape for the digest-hmac LC_ALL site. +# C9 an arm that matches NO site is loud about it by omission: green run, +# sentinel present, and NO "WAKE-ASSERT ARMED" line — so "did not abort" +# is separable into arm-never-matched (no ARMED line) vs error-path- +# broken (ARMED line, no abort). C3..C6 require the ARMED line AND the +# aborting site's ledger row (append lands BEFORE the grep runs, so an +# abort can never shorten the count it is part of). +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export WAKE_COMMON="$HERE/../_wake-common.sh" +[ -f "$WAKE_COMMON" ] || { + echo "microtest: _wake-common.sh not found at $WAKE_COMMON" >&2 + exit 1 +} + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +fails=0 +check() { # check NAME COND-DESCRIPTION (pass/fail already decided by caller: $1=name $2=0|1 $3=detail) + if [ "$2" -eq 0 ]; then + echo " PASS $1" + else + echo " FAIL $1 — $3" + fails=$((fails + 1)) + fi +} + +# --- fixture data ---------------------------------------------------------- +printf 'alpha\nbeta\nbeta\ngamma-unused\n' >"$TMP/data.txt" + +# --- mini-suite A: six helper sites across every converted form ------------ +cat >"$TMP/mini-a.sh" <<'MINI_A' +#!/usr/bin/env bash +set -uo pipefail +. "$WAKE_COMMON" +wake_assert_init +TMP="$1" +FAILFILE="$TMP/failures-a" +: >"$FAILFILE" +fail_msg() { echo " FAIL: $*" >&2; echo x >>"$FAILFILE"; } +ok() { :; } +( + has_match -q alpha "$TMP/data.txt" || fail_msg "alpha missing" # SITE:or-subshell +) && ok +( + has_match -q FORBIDDEN "$TMP/data.txt" 2>/dev/null && fail_msg "forbidden present" # SITE:and-swallow +) && ok +( + [ "$(count_lines beta "$TMP/data.txt")" = "2" ] || fail_msg "beta count" # SITE:count-capture +) && ok +( + printf 'gamma\n' | has_match -q gamma || fail_msg "gamma pipeline" # SITE:pipeline +) && ok +( + has_match -q \ + alpha "$TMP/data.txt" || fail_msg "continuation" # SITE:continuation +) && ok +( + [ "$(count_lines delta "$TMP/data.txt")" = "0" ] || fail_msg "delta zero" # SITE:count-zero +) && ok +if [ -s "$FAILFILE" ]; then + echo "mini-a: FAILED" >&2 + exit 1 +fi +echo "mini-a: OK" >&2 +MINI_A + +# --- mini-suite B: second file, one site behind an early exit -------------- +cat >"$TMP/mini-b.sh" <<'MINI_B' +#!/usr/bin/env bash +set -uo pipefail +. "$WAKE_COMMON" +wake_assert_init +TMP="$1" +( + has_match -q alpha "$TMP/data.txt" || echo "b1 missing" >&2 # SITE:b-first +) +if [ "${MINI_B_EARLY_EXIT:-}" = "1" ]; then + exit 0 +fi +( + has_match -q beta "$TMP/data.txt" || echo "b2 missing" >&2 # SITE:b-second +) +echo "mini-b: OK" >&2 +MINI_B +chmod +x "$TMP/mini-a.sh" "$TMP/mini-b.sh" + +# Text-derived expected set: helper-name + basename:line for every SITE-marked +# call, taken from the generated files' TEXT (independent of BASH_LINENO), with +# the continuation site expected at its FIRST physical line — the denominator +# artifact's convention. +expected_set() { # expected_set FILE + local f="$1" base + base="$(basename "$f")" + awk ' + /# SITE:/ { + line = NR + if ($0 !~ /has_match|count_lines/) line = NR - 1 # marker on the continuation tail + print line + } + ' "$f" | while read -r ln; do + txt="$(sed -n "${ln}p" "$f")" + case "$txt" in + *count_lines*) printf 'count_lines %s:%s\n' "$base" "$ln" ;; + *) printf 'has_match %s:%s\n' "$base" "$ln" ;; + esac + done +} + +site_line() { # site_line FILE MARKER -> first physical line of that call + local f="$1" marker="$2" ln + ln="$(grep -n "# SITE:${marker}\$" "$f" | cut -d: -f1)" + # continuation marker sits on the tail line; the call starts one line up + if ! sed -n "${ln}p" "$f" | grep -Eq 'has_match|count_lines'; then + ln=$((ln - 1)) + fi + printf '%s' "$ln" +} + +# --- C1: green run, two files, set equality -------------------------------- +LEDGER="$TMP/ledger-c1" +: >"$LEDGER" +outA="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$TMP/mini-a.sh" "$TMP" 2>&1)" +rcA=$? +outB="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$TMP/mini-b.sh" "$TMP" 2>&1)" +rcB=$? +{ expected_set "$TMP/mini-a.sh"; expected_set "$TMP/mini-b.sh"; } | sort >"$TMP/expected-c1" +sort "$LEDGER" >"$TMP/got-c1" +n_expected="$(grep -c . "$TMP/expected-c1")" +if [ "$rcA" -eq 0 ] && [ "$rcB" -eq 0 ] && + printf '%s' "$outA" | grep -q 'mini-a: OK' && + printf '%s' "$outB" | grep -q 'mini-b: OK' && + [ "$n_expected" -gt 1 ] && + cmp -s "$TMP/expected-c1" "$TMP/got-c1"; then + check C1 0 "" +else + check C1 1 "rcA=$rcA rcB=$rcB expected($n_expected)/got diff: $(diff "$TMP/expected-c1" "$TMP/got-c1" 2>&1 | head -n 10 | tr '\n' ' ')" +fi + +# --- C2: early exit -> short ledger, comparison catches it ----------------- +LEDGER="$TMP/ledger-c2" +: >"$LEDGER" +WAKE_ASSERT_LEDGER="$LEDGER" MINI_B_EARLY_EXIT=1 bash "$TMP/mini-b.sh" "$TMP" >/dev/null 2>&1 +expected_set "$TMP/mini-b.sh" | sort >"$TMP/expected-c2" +sort "$LEDGER" >"$TMP/got-c2" +if ! cmp -s "$TMP/expected-c2" "$TMP/got-c2" && + grep -q "has_match mini-b.sh:$(site_line "$TMP/mini-b.sh" b-first)" "$TMP/got-c2" && + ! grep -q "mini-b.sh:$(site_line "$TMP/mini-b.sh" b-second)" "$TMP/got-c2"; then + check C2 0 "" +else + check C2 1 "truncated ledger was not detected as short" +fi + +# --- C3..C6: per-shape abort proofs ---------------------------------------- +abort_case() { # abort_case NAME MARKER HELPER + local name="$1" marker="$2" helper="$3" ln site out rc ledger + ln="$(site_line "$TMP/mini-a.sh" "$marker")" + site="mini-a.sh:${ln}" + ledger="$TMP/ledger-${name}" + : >"$ledger" + out="$(WAKE_ASSERT_LEDGER="$ledger" WAKE_ASSERT_FORCE_GREP_ERROR_AT="$site" \ + bash "$TMP/mini-a.sh" "$TMP" 2>&1)" + rc=$? + if [ "$rc" -ne 0 ] && + ! printf '%s' "$out" | grep -q 'mini-a: OK' && + ! printf '%s' "$out" | grep -q 'mini-a: FAILED' && + printf '%s' "$out" | grep -q "WAKE-ASSERT ARMED: forcing real grep error at $site" && + printf '%s' "$out" | grep -q "WAKE-ASSERT ABORT" && + printf '%s' "$out" | grep -q "$site" && + printf '%s' "$out" | grep -q "grep exit 2" && + grep -q "^${helper} ${site}\$" "$ledger"; then + check "$name" 0 "" + else + check "$name" 1 "rc=$rc site=$site ledger=$(grep -c . "$ledger") out=$(printf '%s' "$out" | tail -n 3 | tr '\n' ' ')" + fi +} +abort_case C3 or-subshell has_match +abort_case C4 and-swallow has_match +abort_case C5 count-capture count_lines +abort_case C6 pipeline has_match + +# --- C7: covered by C1 (delta-count site prints 0 on grep rc 1) ------------ +check C7 0 "" + +# --- C8: env-prefix on a function reaches the grep child ------------------- +envprobe() { command env | command grep -c '^LC_ALL=xx_wake_test$'; } +got="$(LC_ALL=xx_wake_test envprobe 2>/dev/null)" # bash's setlocale warning about the fake locale is itself proof the prefix landed +if [ "$got" = "1" ]; then check C8 0 ""; else check C8 1 "env-prefix did not reach child (got=$got)"; fi + +# --- C9: arm matching NO site -> green run, no ARMED line ------------------ +out="$(WAKE_ASSERT_FORCE_GREP_ERROR_AT="mini-a.sh:9999" bash "$TMP/mini-a.sh" "$TMP" 2>&1)" +rc=$? +if [ "$rc" -eq 0 ] && + printf '%s' "$out" | grep -q 'mini-a: OK' && + ! printf '%s' "$out" | grep -q 'WAKE-ASSERT ARMED'; then + check C9 0 "" +else + check C9 1 "rc=$rc out=$(printf '%s' "$out" | tail -n 3 | tr '\n' ' ')" +fi + +echo +if [ "$fails" -gt 0 ]; then + echo "microtest-wake-assert: FAILED ($fails check(s))" >&2 + exit 1 +fi +echo "microtest-wake-assert: OK (all checks passed)"