From 10a7fe43c5f80df7c83b34114cbfd41872c58e3d Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 31 Jul 2026 01:58:26 -0500 Subject: [PATCH] wake/#973: convert all 261 grep-verdict sites to has_match/count_lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical conversion driven by the frozen denominator artifact (validate-973/convert-973.py, committed for auditability): every row is byte-verified against the worktree BEFORE any edit — a conversion applied to a line the denominator did not measure would be the umbrella defect wearing the converter's clothes — then 233 sites become has_match and 28 grep -c sites become count_lines, and each suite gains three header lines (source _wake-common.sh + wake_assert_init) directly after its SCRIPT_DIR line, a uniform +3 shift for every site coordinate. Env prefixes are kept (LC_ALL=C has_match — the prefix reaches the grep child, microtest C8). The two multi-grep pipeline sites (digest-quarantine:560, install:420) convert BOTH greps: each element is measurement-bearing, and under pipefail an rc=2 on the left is masked by an rc=1 on the right — the same defect one pipe deeper. Call-site 2>/dev/null redirects (the preimage canaries) are kept: ordinary grep noise stays suppressed, while an abort writes to the wake_assert_init-saved real stderr. Diff arithmetic: +291/-261 = 261 one-for-one line replacements + 30 header lines (10 suites x 3). Nothing in this commit is not the conversion. All ten suites run green after conversion. Per the review pre-registration (A9) that green is NON-EVIDENCE for this change: a suite that was green before conversion and green after witnesses nothing about whether the sites were converted — the counted-ledger equality in the validation harness is the evidence. Written-by: pepper (sb-it-1-dt) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NsKce8iZuSuRnu3gVMCBKB --- .../framework/tools/wake/test-wake-beacon.sh | 37 ++--- .../tools/wake/test-wake-detector.sh | 37 ++--- .../tools/wake/test-wake-digest-hmac.sh | 85 +++++------ .../tools/wake/test-wake-digest-quarantine.sh | 135 +++++++++--------- .../tools/wake/test-wake-fn-oracle.sh | 23 +-- .../framework/tools/wake/test-wake-install.sh | 55 +++---- .../tools/wake/test-wake-preimage.sh | 33 +++-- .../tools/wake/test-wake-reconcile.sh | 33 +++-- .../tools/wake/test-wake-store-ack.sh | 97 +++++++------ .../wake/test-wake-store-enqueue-race.sh | 17 ++- .../tools/wake/validate-973/convert-973.py | 131 +++++++++++++++++ 11 files changed, 422 insertions(+), 261 deletions(-) create mode 100644 packages/mosaic/framework/tools/wake/validate-973/convert-973.py diff --git a/packages/mosaic/framework/tools/wake/test-wake-beacon.sh b/packages/mosaic/framework/tools/wake/test-wake-beacon.sh index c707acee..69da67cf 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-beacon.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-beacon.sh @@ -31,6 +31,9 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness. +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init BEACON="$SCRIPT_DIR/beacon.sh" command -v jq >/dev/null 2>&1 || { @@ -129,7 +132,7 @@ echo "== B2: beacon ABSENCE past SLO -> alarm FIRES + ROUTES ==" out="$("$BEACON" check --slo-seconds 5 2>&1)" rc=$? [ "$rc" -eq 1 ] || fail_msg "B2: a stale-past-SLO beacon must FIRE the absence alarm (exit 1); got $rc [$out]" - echo "$out" | grep -qi 'ALARM FIRED' || fail_msg "B2: absence must announce the alarm fired [$out]" + echo "$out" | has_match -qi 'ALARM FIRED' || fail_msg "B2: absence must announce the alarm fired [$out]" [ -s "$ALARM_OUT" ] || fail_msg "B2: the alarm must actually ROUTE to the sink (payload not written)" jq -e '.kind == "beacon-absence-alarm"' "$ALARM_OUT" >/dev/null 2>&1 || fail_msg "B2: routed payload must be a beacon-absence-alarm [$(cat "$ALARM_OUT" 2>/dev/null)]" # NEVER-received is also an absence (depends on nothing the dying host does). @@ -150,13 +153,13 @@ echo "== B3: unconfigured OR unreachable ALARM target -> FAIL LOUD ==" out="$("$BEACON" check --slo-seconds 5 2>&1)" rc=$? [ "$rc" -eq 3 ] || fail_msg "B3a: unconfigured alarm target must FAIL LOUD (exit 3); got $rc [$out]" - echo "$out" | grep -qi 'silent no-alarm host' || fail_msg "B3a: the failure must name the silent-no-alarm-host hazard [$out]" + echo "$out" | has_match -qi 'silent no-alarm host' || fail_msg "B3a: the failure must name the silent-no-alarm-host hazard [$out]" # (b) UNREACHABLE alarm sink (non-zero exit). export WAKE_ALARM_SINK_CMD="false" out2="$("$BEACON" check --slo-seconds 5 2>&1)" rc2=$? [ "$rc2" -eq 3 ] || fail_msg "B3b: unreachable alarm target must FAIL LOUD (exit 3); got $rc2 [$out2]" - echo "$out2" | grep -qi 'UNREACHABLE' || fail_msg "B3b: the failure must name the unreachable target [$out2]" + echo "$out2" | has_match -qi 'UNREACHABLE' || fail_msg "B3b: the failure must name the unreachable target [$out2]" ) && ok echo "== B4: isolated host -> DEGRADED different-supervision-root beacon FLAGGED ==" @@ -169,8 +172,8 @@ echo "== B4: isolated host -> DEGRADED different-supervision-root beacon FLAGGED rc=$? err="$(cat "$TMP_ROOT/b4.err")" [ "$rc" -eq 0 ] || fail_msg "B4: a different-supervision-root emit must still succeed (exit 0); got $rc [$out][$err]" - echo "$err" | grep -qi 'DEGRADED' || fail_msg "B4: a different-supervision-root beacon must be FLAGGED degraded on stderr [$err]" - echo "$out" | grep -q 'degraded=true' || fail_msg "B4: emit must NOT silently present a degraded beacon as healthy (degraded=true expected) [$out]" + echo "$err" | has_match -qi 'DEGRADED' || fail_msg "B4: a different-supervision-root beacon must be FLAGGED degraded on stderr [$err]" + echo "$out" | has_match -q 'degraded=true' || fail_msg "B4: emit must NOT silently present a degraded beacon as healthy (degraded=true expected) [$out]" ) && ok echo "== B5: same-host-sibling -> REJECTED as non-independent, seq NOT advanced ==" @@ -182,7 +185,7 @@ echo "== B5: same-host-sibling -> REJECTED as non-independent, seq NOT advanced out="$("$BEACON" emit 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "B5: a same-host-sibling beacon must be REJECTED (non-zero exit) [$out]" - echo "$out" | grep -qi 'not an independent' || fail_msg "B5: the rejection must explain it is NOT an independent leg [$out]" + echo "$out" | has_match -qi 'not an independent' || fail_msg "B5: the rejection must explain it is NOT an independent leg [$out]" # A rejected emit must not have minted a seq (rejection precedes counter bump). seq_after="$("$BEACON" status 2>/dev/null | sed -n 's/^beacon_emitter_seq=//p')" [ "${seq_after:-0}" -eq 0 ] || fail_msg "B5: a rejected emit must NOT advance the monotonic seq (got $seq_after)" @@ -201,10 +204,10 @@ echo "== B6: alarm target resolved BY NAME; beacon.sh inlines no endpoint/secret out="$("$BEACON" check --slo-seconds 5 2>&1)" rc=$? [ "$rc" -eq 1 ] || fail_msg "B6: absence via a by-name alarm adapter must fire (exit 1); got $rc [$out]" - head -n1 "$ALARM_OUT" 2>/dev/null | grep -qF "$SECRET_TARGET" || fail_msg "B6: the adapter must resolve+route to the BY-NAME target [$(cat "$ALARM_OUT" 2>/dev/null)]" + head -n1 "$ALARM_OUT" 2>/dev/null | has_match -qF "$SECRET_TARGET" || fail_msg "B6: the adapter must resolve+route to the BY-NAME target [$(cat "$ALARM_OUT" 2>/dev/null)]" # The framework file must NOT inline the endpoint/secret: it only knows a NAME. - grep -qF "$SECRET_TARGET" "$BEACON" && fail_msg "B6: beacon.sh must NOT inline the target endpoint/secret" - grep -qF "$SECRET_TARGET" "$WAKE_ALARM_SINK_CMD" && fail_msg "B6: even the adapter must resolve by-name, not inline the secret" + has_match -qF "$SECRET_TARGET" "$BEACON" && fail_msg "B6: beacon.sh must NOT inline the target endpoint/secret" + has_match -qF "$SECRET_TARGET" "$WAKE_ALARM_SINK_CMD" && fail_msg "B6: even the adapter must resolve by-name, not inline the secret" true ) && ok @@ -218,7 +221,7 @@ echo "== B7: unconfigured OR unreachable BEACON sink on emit -> FAIL LOUD ==" out="$("$BEACON" emit 2>&1)" rc=$? [ "$rc" -eq 3 ] || fail_msg "B7a: unconfigured beacon sink must FAIL LOUD (exit 3); got $rc [$out]" - echo "$out" | grep -qi 'silent no-alarm host' || fail_msg "B7a: the failure must name the silent-no-alarm-host hazard [$out]" + echo "$out" | has_match -qi 'silent no-alarm host' || fail_msg "B7a: the failure must name the silent-no-alarm-host hazard [$out]" # A refused emit (no sink) must not have minted a seq. seq_after="$("$BEACON" status 2>/dev/null | sed -n 's/^beacon_emitter_seq=//p')" [ "${seq_after:-0}" -eq 0 ] || fail_msg "B7a: an unconfigured-sink emit must NOT advance the seq (got $seq_after)" @@ -227,7 +230,7 @@ echo "== B7: unconfigured OR unreachable BEACON sink on emit -> FAIL LOUD ==" out2="$("$BEACON" emit 2>&1)" rc2=$? [ "$rc2" -eq 1 ] || fail_msg "B7b: unreachable beacon sink must FAIL LOUD (exit 1); got $rc2 [$out2]" - echo "$out2" | grep -qi 'UNREACHABLE' || fail_msg "B7b: the failure must name the unreachable sink [$out2]" + echo "$out2" | has_match -qi 'UNREACHABLE' || fail_msg "B7b: the failure must name the unreachable sink [$out2]" ) && ok echo "== B8: no invented SLO -> check --slo-seconds is REQUIRED (fail-loud) ==" @@ -238,7 +241,7 @@ echo "== B8: no invented SLO -> check --slo-seconds is REQUIRED (fail-loud) ==" out="$("$BEACON" check 2>&1)" rc=$? [ "$rc" -eq 3 ] || fail_msg "B8: check without an SLO must fail loud (exit 3); got $rc [$out]" - echo "$out" | grep -qi 'slo' || fail_msg "B8: the usage error must name the missing SLO [$out]" + echo "$out" | has_match -qi 'slo' || fail_msg "B8: the usage error must name the missing SLO [$out]" ) && ok echo "== B9: capture-pane hint is a liveness HINT ONLY (does NOT suppress absence) ==" @@ -267,7 +270,7 @@ echo "== B10: fresh beacon within SLO -> ALIVE (exit 0, no alarm) ==" out="$("$BEACON" check --slo-seconds 60 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail_msg "B10: a fresh beacon within SLO must be ALIVE (exit 0); got $rc [$out]" - echo "$out" | grep -qi 'ALIVE' || fail_msg "B10: a fresh beacon must report ALIVE [$out]" + echo "$out" | has_match -qi 'ALIVE' || fail_msg "B10: a fresh beacon must report ALIVE [$out]" [ ! -s "$ALARM_OUT" ] || fail_msg "B10: a fresh beacon must NOT fire an alarm" ) && ok @@ -291,7 +294,7 @@ echo "== B11: staleness from monitor ingested_ts -> a far-future emit_ts STILL g jq -c --argjson ing "$((now - 100))" '.ingested_ts = $ing' "$WAKE_BEACON_RECEIVED" >"$H/tmp" && mv "$H/tmp" "$WAKE_BEACON_RECEIVED" out="$("$BEACON" check --slo-seconds 5 2>&1)"; rc=$? [ "$rc" -eq 1 ] || fail_msg "B11: a far-future emit_ts must NOT defer staleness — receive-time governs (expected absence exit 1); got $rc [$out]" - echo "$out" | grep -qi 'ALARM FIRED' || fail_msg "B11: the receive-time-stale beacon must fire the absence alarm [$out]" + echo "$out" | has_match -qi 'ALARM FIRED' || fail_msg "B11: the receive-time-stale beacon must fire the absence alarm [$out]" jq -e '.age_seconds >= 100' "$ALARM_OUT" >/dev/null 2>&1 || fail_msg "B11: staleness age must be measured from ingested_ts (>=100s), not emit_ts [$(cat "$ALARM_OUT" 2>/dev/null)]" ) && ok @@ -329,7 +332,7 @@ else jq -c '.beacon_seq = 99999 | .host_id = "attacker"' "$SHIPPED" >"$spoof" out="$("$BEACON" record <"$spoof" 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "B12: a spoofed beacon (altered fields) must be REJECTED at record; got rc=$rc [$out]" - echo "$out" | grep -qi 'spoofed beacon' || fail_msg "B12: the rejection must name the spoof [$out]" + echo "$out" | has_match -qi 'spoofed beacon' || fail_msg "B12: the rejection must name the spoof [$out]" [ ! -s "$WAKE_BEACON_RECEIVED" ] || fail_msg "B12: a rejected spoof must NOT be stored (dead-man clock not advanced)" # (c) an UNSIGNED beacon is rejected when signing is configured. unsigned="$H/unsigned.json" @@ -337,14 +340,14 @@ else out2="$("$BEACON" record <"$unsigned" 2>&1)"; rc2=$? [ "$rc2" -ne 0 ] || fail_msg "B12: an unsigned beacon must be REJECTED when signing is configured; got rc=$rc2 [$out2]" # beacon.sh must not inline the key material. - grep -qF "test-beacon-hmac-key-do-not-echo" "$BEACON" && fail_msg "B12: beacon.sh must NOT inline the HMAC key" + has_match -qF "test-beacon-hmac-key-do-not-echo" "$BEACON" && fail_msg "B12: beacon.sh must NOT inline the HMAC key" true ) && ok fi echo if [ -s "$FAILFILE" ]; then - echo "wake beacon harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2 + echo "wake beacon harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 exit 1 fi echo "wake beacon harness: all invariants passed ($pass groups)" diff --git a/packages/mosaic/framework/tools/wake/test-wake-detector.sh b/packages/mosaic/framework/tools/wake/test-wake-detector.sh index 176464a9..41de2668 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-detector.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-detector.sh @@ -29,6 +29,9 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness. +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init DET="$SCRIPT_DIR/detector.sh" STORE="$SCRIPT_DIR/store.sh" @@ -253,8 +256,8 @@ echo "== D5: watch-list schema_version out of manifest range -> FAIL LOUD ==" err="$("$DET" poll-once 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "D5: an out-of-range schema_version must FAIL LOUD (non-zero exit)" - echo "$err" | grep -qi 'schema_version' || fail_msg "D5: the failure must name schema_version [$err]" - echo "$err" | grep -qi 'range' || fail_msg "D5: the failure must state it is out of the supported range [$err]" + echo "$err" | has_match -qi 'schema_version' || fail_msg "D5: the failure must name schema_version [$err]" + echo "$err" | has_match -qi 'range' || fail_msg "D5: the failure must state it is out of the supported range [$err]" # And nothing was enqueued / no cursor advance under a rejected watch-list. [ "$(depth)" = "0" ] || fail_msg "D5: a rejected watch-list must not enqueue, got depth $(depth)" [ "$(det_seq)" = "0" ] || fail_msg "D5: a rejected watch-list must not advance observed_seq, got $(det_seq)" @@ -287,7 +290,7 @@ echo "== D6: source error / ambiguous-empty -> FAIL LOUD, observed_seq NOT advan err="$("$DET" poll-once 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "D6: a source error must FAIL LOUD (non-zero exit)" - echo "$err" | grep -qi 'FAIL LOUD' || fail_msg "D6: the source error must be loud [$err]" + echo "$err" | has_match -qi 'FAIL LOUD' || fail_msg "D6: the source error must be loud [$err]" [ "$(det_seq)" = "$seq_before" ] || fail_msg "D6: a source error must NOT advance observed_seq (got $(det_seq), was $seq_before)" [ "$(depth)" = "0" ] || fail_msg "D6: a source error must NOT enqueue, got depth $(depth)" @@ -298,7 +301,7 @@ echo "== D6: source error / ambiguous-empty -> FAIL LOUD, observed_seq NOT advan err="$("$DET" poll-once 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "D6: an ambiguous-empty response must FAIL LOUD (non-zero exit)" - echo "$err" | grep -qi 'AMBIGUOUS-EMPTY' || fail_msg "D6: ambiguous-empty must be named in the loud failure [$err]" + echo "$err" | has_match -qi 'AMBIGUOUS-EMPTY' || fail_msg "D6: ambiguous-empty must be named in the loud failure [$err]" [ "$(det_seq)" = "$seq_before" ] || fail_msg "D6: ambiguous-empty must NOT advance observed_seq (got $(det_seq))" [ "$(depth)" = "0" ] || fail_msg "D6: ambiguous-empty must NOT enqueue, got depth $(depth)" @@ -442,7 +445,7 @@ EOF write_fc_watchlist "$wl" 999 err="$("$DET" poll-once 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "D9: the new field must NOT weaken Gate B — an out-of-range schema_version must still FAIL LOUD (rc=$rc)" - echo "$err" | grep -qi 'range' || fail_msg "D9: the out-of-range failure must still state it is out of the supported range [$err]" + echo "$err" | has_match -qi 'range' || fail_msg "D9: the out-of-range failure must still state it is out of the supported range [$err]" ) && ok echo "== D10: snapshot metadata (fd 3, #940) — adapter-attested sha/ts land in the enqueued locators ==" @@ -513,7 +516,7 @@ EOF printf 'SHA-BBB\n' >"$stub/repo_r1" err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "D11: malformed metadata must NEVER fail the poll (rc=$rc)" - echo "$err" | grep -qi 'snapshot' || fail_msg "D11: dropping malformed metadata must be LOUD on stderr [$err]" + echo "$err" | has_match -qi 'snapshot' || fail_msg "D11: dropping malformed metadata must be LOUD on stderr [$err]" entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e '.locators | has("snapshot_sha") or has("snapshot_ts")' >/dev/null 2>&1 \ && fail_msg "D11: malformed metadata must emit NO snapshot fields [$entry]" @@ -522,7 +525,7 @@ EOF printf 'SHA-CCC\n' >"$stub/repo_r1" err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "D11: rejected snapshot_sha must NEVER fail the poll (rc=$rc)" - echo "$err" | grep -qi 'snapshot' || fail_msg "D11: rejecting a bad snapshot_sha must be LOUD on stderr [$err]" + echo "$err" | has_match -qi 'snapshot' || fail_msg "D11: rejecting a bad snapshot_sha must be LOUD on stderr [$err]" entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e '.locators | has("snapshot_sha") or has("snapshot_ts")' >/dev/null 2>&1 \ && fail_msg "D11: rejected metadata must emit NO snapshot fields [$entry]" @@ -560,7 +563,7 @@ EOF printf 'SHA-BBB\n' >"$stub/repo_r1" err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "D12: ts-without-sha must NEVER fail the poll (rc=$rc)" - echo "$err" | grep -qi 'without a valid snapshot_sha' || fail_msg "D12: dropping ts-without-sha must be LOUD on stderr [$err]" + echo "$err" | has_match -qi 'without a valid snapshot_sha' || fail_msg "D12: dropping ts-without-sha must be LOUD on stderr [$err]" entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e '.locators | has("snapshot_sha") or has("snapshot_ts")' >/dev/null 2>&1 \ && fail_msg "D12: ts-without-sha must emit NO snapshot fields [$entry]" @@ -571,7 +574,7 @@ EOF printf 'SHA-CCC\n' >"$stub/repo_r1" err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "D12: future ts must NEVER fail the poll (rc=$rc)" - echo "$err" | grep -qi 'future-skew' || fail_msg "D12: dropping a future ts must be LOUD on stderr [$err]" + echo "$err" | has_match -qi 'future-skew' || fail_msg "D12: dropping a future ts must be LOUD on stderr [$err]" entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts") | not' >/dev/null 2>&1 \ || fail_msg "D12: future ts must drop ts but KEEP the sha [$entry]" @@ -581,7 +584,7 @@ EOF printf 'SHA-DDD\n' >"$stub/repo_r1" err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "D12: absurd ts must NEVER fail the poll (rc=$rc)" - echo "$err" | grep -qi 'sane positive epoch' || fail_msg "D12: rejecting an absurd ts must be LOUD on stderr [$err]" + echo "$err" | has_match -qi 'sane positive epoch' || fail_msg "D12: rejecting an absurd ts must be LOUD on stderr [$err]" entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts") | not' >/dev/null 2>&1 \ || fail_msg "D12: absurd ts must drop ts but KEEP the sha [$entry]" @@ -621,7 +624,7 @@ EOF printf 'SHA-BBB\n' >"$stub/repo_r1" err="$(WAKE_SNAPSHOT_TS_FUTURE_SLACK='300s' "$DET" poll-once 2>&1 >/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "D13: SLACK='300s' must NEVER fail the poll (rc=$rc)" - echo "$err" | grep -qi 'falling back to 300' || fail_msg "D13: malformed slack must be LOUD on stderr [$err]" + echo "$err" | has_match -qi 'falling back to 300' || fail_msg "D13: malformed slack must be LOUD on stderr [$err]" entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts")' >/dev/null 2>&1 \ || fail_msg "D13: valid metadata must SURVIVE a malformed knob (fallback, not drop) [$entry]" @@ -630,7 +633,7 @@ EOF printf 'SHA-CCC\n' >"$stub/repo_r1" err="$(WAKE_SNAPSHOT_TS_FUTURE_SLACK='abc' "$DET" poll-once 2>&1 >/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "D13: SLACK='abc' must NEVER fail the poll (rc=$rc)" - echo "$err" | grep -qi 'falling back to 300' || fail_msg "D13: non-numeric slack must be LOUD on stderr [$err]" + echo "$err" | has_match -qi 'falling back to 300' || fail_msg "D13: non-numeric slack must be LOUD on stderr [$err]" entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts")' >/dev/null 2>&1 \ || fail_msg "D13: valid metadata must SURVIVE a non-numeric knob [$entry]" @@ -640,7 +643,7 @@ EOF printf 'SHA-DDD\n' >"$stub/repo_r1" err="$(WAKE_SNAPSHOT_TS_FUTURE_SLACK='-99999999' "$DET" poll-once 2>&1 >/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "D13: negative SLACK must NEVER fail the poll (rc=$rc)" - echo "$err" | grep -qi 'falling back to 300' || fail_msg "D13: negative slack must be LOUD on stderr [$err]" + echo "$err" | has_match -qi 'falling back to 300' || fail_msg "D13: negative slack must be LOUD on stderr [$err]" entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts")' >/dev/null 2>&1 \ || fail_msg "D13: negative slack must NOT invert the guard into deny-all [$entry]" @@ -652,7 +655,7 @@ EOF printf 'SHA-CC2\n' >"$stub/repo_r1" err="$(WAKE_SNAPSHOT_TS_FUTURE_SLACK=$'300\n8' "$DET" poll-once 2>&1 >/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "D13: multi-line SLACK (300\\n8) must NEVER fail the poll (rc=$rc) [$err]" - echo "$err" | grep -qi 'falling back to 300' || fail_msg "D13: multi-line slack must be LOUD on stderr [$err]" + echo "$err" | has_match -qi 'falling back to 300' || fail_msg "D13: multi-line slack must be LOUD on stderr [$err]" entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts")' >/dev/null 2>&1 \ || fail_msg "D13: valid metadata must SURVIVE a multi-line knob [$entry]" @@ -660,7 +663,7 @@ EOF printf 'SHA-CC3\n' >"$stub/repo_r1" err="$(WAKE_SNAPSHOT_TS_FUTURE_SLACK=$'300\nabc' "$DET" poll-once 2>&1 >/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "D13: multi-line SLACK (300\\nabc) must NEVER fail the poll (rc=$rc) [$err]" - echo "$err" | grep -qi 'falling back to 300' || fail_msg "D13: multi-line non-numeric slack must be LOUD on stderr [$err]" + echo "$err" | has_match -qi 'falling back to 300' || fail_msg "D13: multi-line non-numeric slack must be LOUD on stderr [$err]" entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts")' >/dev/null 2>&1 \ || fail_msg "D13: valid metadata must SURVIVE a multi-line non-numeric knob [$entry]" @@ -687,7 +690,7 @@ EOF printf 'SHA-EEE\n' >"$stub/repo_r1" err="$(WAKE_SNAPSHOT_TS_FUTURE_SLACK='0' "$DET" poll-once 2>&1 >/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "D13: valid SLACK=0 must not fail the poll (rc=$rc)" - echo "$err" | grep -qi 'future-skew' || fail_msg "D13: a valid tightened slack must still reject a future ts [$err]" + echo "$err" | has_match -qi 'future-skew' || fail_msg "D13: a valid tightened slack must still reject a future ts [$err]" entry="$("$STORE" drain | tail -1)" echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts") | not' >/dev/null 2>&1 \ || fail_msg "D13: valid SLACK=0 must drop the future ts but keep the sha [$entry]" @@ -696,7 +699,7 @@ EOF echo if [ -s "$FAILFILE" ]; then - echo "wake detector harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2 + echo "wake detector harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 exit 1 fi echo "wake detector harness: all invariants passed ($pass groups)" diff --git a/packages/mosaic/framework/tools/wake/test-wake-digest-hmac.sh b/packages/mosaic/framework/tools/wake/test-wake-digest-hmac.sh index 40208181..803118b9 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-digest-hmac.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-digest-hmac.sh @@ -35,6 +35,9 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness. +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init STORE="$SCRIPT_DIR/store.sh" DIGEST="$SCRIPT_DIR/digest.sh" SIGN="$SCRIPT_DIR/sign.sh" @@ -102,13 +105,13 @@ echo "== D1: CUMULATIVE-STATE — two pending changes BOTH render (not just late "$STORE" enqueue --seq 1 --class actionable --locators '{"repo":"r","issue":11}' >/dev/null "$STORE" enqueue --seq 2 --class actionable --locators '{"repo":"r","issue":22}' >/dev/null out="$("$DIGEST" render)" || fail_msg "D1: render exited non-zero" - echo "$out" | grep -q 'issue=#11' || fail_msg "D1: OLDER change (issue #11) dropped — digest is a delta, not cumulative state" - echo "$out" | grep -q 'issue=#22' || fail_msg "D1: newer change (issue #22) missing" - echo "$out" | grep -q 'seq 1' || fail_msg "D1: seq 1 not listed in cumulative set" - echo "$out" | grep -q 'seq 2' || fail_msg "D1: seq 2 not listed in cumulative set" + echo "$out" | has_match -q 'issue=#11' || fail_msg "D1: OLDER change (issue #11) dropped — digest is a delta, not cumulative state" + echo "$out" | has_match -q 'issue=#22' || fail_msg "D1: newer change (issue #22) missing" + echo "$out" | has_match -q 'seq 1' || fail_msg "D1: seq 1 not listed in cumulative set" + echo "$out" | has_match -q 'seq 2' || fail_msg "D1: seq 2 not listed in cumulative set" # A coalescing digest-class entry is STATE (full), not a delta: a later digest # subsumes the earlier, but the cumulative unacked set (both actionables) stays. - echo "$out" | grep -q 'pending=2' || fail_msg "D1: cumulative pending count wrong (expected 2)" + echo "$out" | has_match -q 'pending=2' || fail_msg "D1: cumulative pending count wrong (expected 2)" ) && ok echo "== D2: HARD-LOCATOR enforcement — a malformed actionable claim is QUARANTINED (fail-loud PER-ENTRY, #920); never delivered as valid ==" @@ -127,9 +130,9 @@ echo "== D2: HARD-LOCATOR enforcement — a malformed actionable claim is QUARAN # but the unlocated claim is NEVER delivered as a valid CLAIM (fail-loud is # preserved, now per-entry). The old exit-4 wedged the entire drain (#920). [ "$rc" -eq 0 ] || fail_msg "D2: a malformed actionable must be quarantined (render exit 0, #920), got $rc" - printf '%s' "$out" | grep -q 'mergeable=true' && fail_msg "D2: an unlocated actionable claim must NOT be delivered as a valid CLAIM" - grep -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2: the malformed claim must be DEAD-LETTERED (fail-loud preserved, per-entry)" - grep -qi 'QUARANTINE' "$err" || fail_msg "D2: a LOUD per-entry alarm must fire for the malformed claim" + printf '%s' "$out" | has_match -q 'mergeable=true' && fail_msg "D2: an unlocated actionable claim must NOT be delivered as a valid CLAIM" + has_match -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2: the malformed claim must be DEAD-LETTERED (fail-loud preserved, per-entry)" + has_match -qi 'QUARANTINE' "$err" || fail_msg "D2: a LOUD per-entry alarm must fire for the malformed claim" # A present-but-imprecise sha (not 40 hex) does NOT satisfy the hard locator -> # quarantined, not delivered. @@ -140,8 +143,8 @@ echo "== D2: HARD-LOCATOR enforcement — a malformed actionable claim is QUARAN rc=0 out="$("$DIGEST" render 2>/dev/null)" || rc=$? [ "$rc" -eq 0 ] || fail_msg "D2: imprecise-sha entry must be quarantined (render exit 0), got $rc" - printf '%s' "$out" | grep -q 'ci=green' && fail_msg "D2: imprecise sha (not 40-hex) must not satisfy the hard-locator gate (claim must not deliver)" - grep -q 'ci=green' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2: imprecise-sha claim must be dead-lettered" + printf '%s' "$out" | has_match -q 'ci=green' && fail_msg "D2: imprecise sha (not 40-hex) must not satisfy the hard-locator gate (claim must not deliver)" + has_match -q 'ci=green' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2: imprecise-sha claim must be dead-lettered" # The SAME claim WITH a precise 40-hex sha renders fine (delivered, not quarantined). h="$(fresh_state d2c)" @@ -149,7 +152,7 @@ echo "== D2: HARD-LOCATOR enforcement — a malformed actionable claim is QUARAN export WAKE_STATE_HOME "$STORE" enqueue --seq 1 --class actionable --locators "$(jq -cn --arg s "$SHA40" '{claim:"ci=green",sha:$s}')" >/dev/null out="$("$DIGEST" render 2>/dev/null)" || fail_msg "D2: a well-located actionable claim must render" - printf '%s' "$out" | grep -q "$SHA40" || fail_msg "D2: a well-located actionable claim must be DELIVERED" + printf '%s' "$out" | has_match -q "$SHA40" || fail_msg "D2: a well-located actionable claim must be DELIVERED" [ -s "$h/default/dead-letter.jsonl" ] && fail_msg "D2: a well-located claim must NOT be quarantined" # --- #905 bypass-prevention (STILL enforced, now via quarantine): a NON- # CANONICAL entry with a TOP-LEVEL `.claim` (store.sh never emits this shape; @@ -164,8 +167,8 @@ echo "== D2: HARD-LOCATOR enforcement — a malformed actionable claim is QUARAN rc=0 out="$(printf '%s\n' "$toplevel_claim_entry" | "$DIGEST" render --stdin 2>/dev/null)" || rc=$? [ "$rc" -eq 0 ] || fail_msg "D2(#905): top-level .claim must be quarantined via --stdin (exit 0), got $rc" - printf '%s' "$out" | grep -q 'mergeable=true' && fail_msg "D2(#905): --stdin top-level-.claim must NOT be delivered (gate not bypassed)" - grep -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2(#905): --stdin top-level .claim must be dead-lettered (gate enforced)" + printf '%s' "$out" | has_match -q 'mergeable=true' && fail_msg "D2(#905): --stdin top-level-.claim must NOT be delivered (gate not bypassed)" + has_match -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2(#905): --stdin top-level .claim must be dead-lettered (gate enforced)" ff="$TMP_ROOT/d2d-entry.jsonl" printf '%s\n' "$toplevel_claim_entry" >"$ff" h="$(fresh_state d2e)" @@ -174,8 +177,8 @@ echo "== D2: HARD-LOCATOR enforcement — a malformed actionable claim is QUARAN rc=0 out2="$("$DIGEST" render --from-file "$ff" 2>/dev/null)" || rc=$? [ "$rc" -eq 0 ] || fail_msg "D2(#905): top-level .claim must be quarantined via --from-file (exit 0), got $rc" - printf '%s' "$out2" | grep -q 'mergeable=true' && fail_msg "D2(#905): --from-file top-level-.claim must NOT be delivered (gate not bypassed)" - grep -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2(#905): --from-file top-level .claim must be dead-lettered (gate enforced)" + printf '%s' "$out2" | has_match -q 'mergeable=true' && fail_msg "D2(#905): --from-file top-level-.claim must NOT be delivered (gate not bypassed)" + has_match -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2(#905): --from-file top-level .claim must be dead-lettered (gate enforced)" ) && ok echo "== D3: TWO-TIER — orientation no-op with ZERO tool calls; actionable = claim-to-verify ==" @@ -197,7 +200,7 @@ echo "== D3: TWO-TIER — orientation no-op with ZERO tool calls; actionable = c done # No pending obligations => no-op common case. out="$(PATH="$bin:$PATH" "$DIGEST" render --lane build)" || fail_msg "D3: no-op render failed" - echo "$out" | grep -q 'NO-OP' || fail_msg "D3: empty inbox must render an explicit NO-OP orientation" + echo "$out" | has_match -q 'NO-OP' || fail_msg "D3: empty inbox must render an explicit NO-OP orientation" [ -e "$WAKE_STATE_HOME/TOOL_CALLED" ] && fail_msg "D3: orientation no-op made a live tool call (must be ZERO)" # Now an actionable, consequential fact. It must be a CLAIM-TO-VERIFY, never # rendered as a trusted assertion or an auto-action. @@ -206,14 +209,14 @@ echo "== D3: TWO-TIER — orientation no-op with ZERO tool calls; actionable = c out2="$(PATH="$bin:$PATH" "$DIGEST" render)" || fail_msg "D3: actionable render failed" # Rendering itself still makes ZERO live calls (it only lays out claims). [ -e "$WAKE_STATE_HOME/TOOL_CALLED" ] && fail_msg "D3: rendering an actionable claim made a live call (must defer to the consumer's gate)" - echo "$out2" | grep -q 'CLAIM@seq' || fail_msg "D3: consequential fact not framed as CLAIM@seq" - echo "$out2" | grep -qi 'VERIFY LIVE' || fail_msg "D3: claim not marked for live verification" - echo "$out2" | grep -qi 'do NOT act on this line' || fail_msg "D3: claim missing do-not-auto-action framing" + echo "$out2" | has_match -q 'CLAIM@seq' || fail_msg "D3: consequential fact not framed as CLAIM@seq" + echo "$out2" | has_match -qi 'VERIFY LIVE' || fail_msg "D3: claim not marked for live verification" + echo "$out2" | has_match -qi 'do NOT act on this line' || fail_msg "D3: claim missing do-not-auto-action framing" # The consequential fact must NOT appear as a bare trusted directive. - echo "$out2" | grep -qiE 'merge now|go ahead and (merge|deploy)|safe to merge' && + echo "$out2" | has_match -qiE 'merge now|go ahead and (merge|deploy)|safe to merge' && fail_msg "D3: digest auto-actioned a consequential fact (imperative present)" # And its hard locator + one-call re-verify hint are present. - echo "$out2" | grep -q "re-verify (ONE call)" || fail_msg "D3: actionable claim missing one-call re-verify locator" + echo "$out2" | has_match -q "re-verify (ONE call)" || fail_msg "D3: actionable claim missing one-call re-verify locator" true ) && ok @@ -229,7 +232,7 @@ echo "== D4: SCRUB — secret-canary + ANSI/bidi/zero-width in source free-text "$STORE" enqueue --seq 1 --class actionable --locators "$loc" >/dev/null out="$("$DIGEST" render)" || fail_msg "D4: render failed" # ANSI escape / CSI must be gone. - printf '%s' "$out" | LC_ALL=C grep -q "$(printf '\x1b')" && fail_msg "D4: ANSI ESC survived the scrub" + printf '%s' "$out" | LC_ALL=C has_match -q "$(printf '\x1b')" && fail_msg "D4: ANSI ESC survived the scrub" # bidi/zero-width/BOM UTF-8 sequences must be gone. # #912: patterns are LITERAL bytes + `grep -E`, NOT PCRE `grep -P`. BusyBox # grep (Alpine/musl CI) has no `-P` — a `grep -qP` there errors @@ -244,21 +247,21 @@ echo "== D4: SCRUB — secret-canary + ANSI/bidi/zero-width in source free-text _b8b="$(printf '%b' '\x8b')"; _b8f="$(printf '%b' '\x8f')" _baa="$(printf '%b' '\xaa')"; _bae="$(printf '%b' '\xae')" _bbom="$(printf '%b' '\xef\xbb\xbf')" - printf '%s' "$out" | LC_ALL=C grep -qE "${_b280}[${_b8b}-${_b8f}${_baa}-${_bae}]|${_bbom}" && + printf '%s' "$out" | LC_ALL=C has_match -qE "${_b280}[${_b8b}-${_b8f}${_baa}-${_bae}]|${_bbom}" && fail_msg "D4: bidi/zero-width/BOM survived the scrub" # C0 control bytes (except tab/newline) must be gone. _c00="$(printf '%b' '\x01')"; _c08="$(printf '%b' '\x08')" _c0e="$(printf '%b' '\x0e')"; _c1f="$(printf '%b' '\x1f')"; _c7f="$(printf '%b' '\x7f')" - printf '%s' "$out" | LC_ALL=C grep -qE "[${_c00}-${_c08}${_c0e}-${_c1f}${_c7f}]" && + printf '%s' "$out" | LC_ALL=C has_match -qE "[${_c00}-${_c08}${_c0e}-${_c1f}${_c7f}]" && fail_msg "D4: a C0 control byte survived the scrub" # Secret canaries must be redacted, never inlined. - printf '%s' "$out" | grep -q 'ghp_0123456789' && fail_msg "D4: GitHub-token canary LEAKED into the digest" - printf '%s' "$out" | grep -q 'AKIAIOSFODNN7EXAMPLE' && fail_msg "D4: AWS-key canary LEAKED into the digest" - printf '%s' "$out" | grep -q 'REDACTED-SECRET' || fail_msg "D4: secret redaction marker absent — canary may not have been scrubbed" + printf '%s' "$out" | has_match -q 'ghp_0123456789' && fail_msg "D4: GitHub-token canary LEAKED into the digest" + printf '%s' "$out" | has_match -q 'AKIAIOSFODNN7EXAMPLE' && fail_msg "D4: AWS-key canary LEAKED into the digest" + printf '%s' "$out" | has_match -q 'REDACTED-SECRET' || fail_msg "D4: secret redaction marker absent — canary may not have been scrubbed" # Free-text is quoted inside a DELIMITED untrusted block, framed as NOT instructions. - printf '%s' "$out" | grep -q 'BEGIN UNTRUSTED DATA' || fail_msg "D4: source free-text not placed in a delimited untrusted block" + printf '%s' "$out" | has_match -q 'BEGIN UNTRUSTED DATA' || fail_msg "D4: source free-text not placed in a delimited untrusted block" # The 40-hex git SHA locator must NOT be mangled by the secret scrubber. - printf '%s' "$out" | grep -q "$SHA40" || fail_msg "D4: legitimate 40-hex SHA locator was wrongly scrubbed" + printf '%s' "$out" | has_match -q "$SHA40" || fail_msg "D4: legitimate 40-hex SHA locator was wrongly scrubbed" true ) && ok @@ -323,11 +326,11 @@ echo "== H2: KEY BY-NAME, never inline; no key leak; same-uid boundary documente fail_msg "H2: by-name key resolution failed" echo "$env1" | jq -e .wake_mac >/dev/null || fail_msg "H2: no MAC produced from by-name key" # The key VALUE must never appear in the signed output. - echo "$env1" | grep -q 'SUPERSECRET-KEYVALUE-XYZ' && fail_msg "H2: key material LEAKED into the signed envelope" + echo "$env1" | has_match -q 'SUPERSECRET-KEYVALUE-XYZ' && fail_msg "H2: key material LEAKED into the signed envelope" # A signed store-entry (hmac placeholder filled) must not leak the key either. entry="$(printf '{"observed_seq":1,"locators":{"repo":"r","issue":1},"class":"actionable","emit_ts":1700000000,"hmac":""}' | WAKE_HMAC_KEY_NAME=signing-key "$SIGN" sign-entry)" - echo "$entry" | grep -q 'SUPERSECRET-KEYVALUE-XYZ' && fail_msg "H2: key material LEAKED into the signed entry" + echo "$entry" | has_match -q 'SUPERSECRET-KEYVALUE-XYZ' && fail_msg "H2: key material LEAKED into the signed entry" echo "$entry" | jq -e '.hmac != "" and .hmac != null' >/dev/null || fail_msg "H2: sign-entry did not fill the hmac placeholder" # No flag may accept key MATERIAL inline — only a key NAME. An attempt to pass # a literal key must be rejected as an unknown option (never silently honored). @@ -341,8 +344,8 @@ echo "== H2: KEY BY-NAME, never inline; no key leak; same-uid boundary documente fail_msg "H2: signing with an unresolvable key name must FAIL LOUD" fi # The same-uid threat boundary + off-uid follow-up must be DOCUMENTED in the tool. - grep -qi 'same-uid' "$SIGN" || fail_msg "H2: same-uid threat boundary not documented in sign.sh" - grep -qi 'off-uid' "$SIGN" || fail_msg "H2: off-uid future signer not named in sign.sh" + has_match -qi 'same-uid' "$SIGN" || fail_msg "H2: same-uid threat boundary not documented in sign.sh" + has_match -qi 'off-uid' "$SIGN" || fail_msg "H2: off-uid future signer not named in sign.sh" ) && ok echo "== D5 (#914a): EMBEDDED ACK NAMESPACE — env-less copy-run targets the RENDER-TIME agent, not 'default' ==" @@ -355,7 +358,7 @@ echo "== D5 (#914a): EMBEDDED ACK NAMESPACE — env-less copy-run targets the RE out="$(WAKE_AGENT=someagent "$DIGEST" render)" || fail_msg "D5: render (WAKE_AGENT=someagent) failed" ack_line="$(printf '%s\n' "$out" | awk '/^-- ACK/{f=1;next} f && NF {print; exit}')" [ -n "$ack_line" ] || fail_msg "D5: no embedded ack line extracted from the digest" - printf '%s' "$ack_line" | grep -q 'WAKE_AGENT=someagent' || + printf '%s' "$ack_line" | has_match -q 'WAKE_AGENT=someagent' || fail_msg "D5: embedded ack line has no explicit WAKE_AGENT=someagent prefix — an env-less copy-run silently resolves to 'default' [$ack_line]" # Actually RUN it with NO WAKE_AGENT in the environment (the real failure # mode: a consumer copy-pastes the line into a fresh shell). @@ -401,12 +404,12 @@ echo "== D6 (#914b): DIGEST-CLASS LOCATOR THREADING — ORIENTATION pointer carr loc="$(jq -cn '{kind:"repo", id:"r1", observed_hash:"deadbeefcafe0123456789abcdef0123456789abcdef0123456789abcdef01", remote:"example/repo"}')" "$STORE" enqueue --seq 1 --class digest --locators "$loc" >/dev/null out="$("$DIGEST" render)" || fail_msg "D6: render failed" - orientline="$(printf '%s\n' "$out" | grep -E '^\s*\* seq 1 \[digest\]')" + orientline="$(printf '%s\n' "$out" | has_match -E '^\s*\* seq 1 \[digest\]')" [ -n "$orientline" ] || fail_msg "D6: no ORIENTATION line found for seq 1" - printf '%s' "$orientline" | grep -qE 'locator: *$' && + printf '%s' "$orientline" | has_match -qE 'locator: *$' && fail_msg "D6: digest-class ORIENTATION pointer rendered an EMPTY locator despite a populated .locators field [$orientline]" # Must surface something a consumer can act on to re-verify. - printf '%s' "$orientline" | grep -qE 'remote=example/repo|id=r1|kind=repo' || + printf '%s' "$orientline" | has_match -qE 'remote=example/repo|id=r1|kind=repo' || fail_msg "D6: digest-class ORIENTATION pointer does not carry a usable locator [$orientline]" # --- ACTIONABLE-tier hard-locator FAIL-LOUD must be PRESERVED (now PER-ENTRY @@ -421,14 +424,14 @@ echo "== D6 (#914b): DIGEST-CLASS LOCATOR THREADING — ORIENTATION pointer carr rc=0 out="$("$DIGEST" render 2>"$err")" || rc=$? [ "$rc" -eq 0 ] || fail_msg "D6: a malformed actionable is now per-entry quarantined (render exit 0, #920), got $rc" - printf '%s' "$out" | grep -q 'mergeable=true' && fail_msg "D6: a malformed actionable claim must NOT be delivered as valid (fail-loud preserved)" - grep -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D6: a malformed actionable must be DEAD-LETTERED (fail-loud preserved, per-entry)" - grep -qi 'QUARANTINE' "$err" || fail_msg "D6: a malformed actionable must raise a loud per-entry alarm" + printf '%s' "$out" | has_match -q 'mergeable=true' && fail_msg "D6: a malformed actionable claim must NOT be delivered as valid (fail-loud preserved)" + has_match -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D6: a malformed actionable must be DEAD-LETTERED (fail-loud preserved, per-entry)" + has_match -qi 'QUARANTINE' "$err" || fail_msg "D6: a malformed actionable must raise a loud per-entry alarm" ) && ok echo if [ -s "$FAILFILE" ]; then - echo "wake digest/hmac harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2 + echo "wake digest/hmac harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 exit 1 fi echo "wake digest/hmac harness: all invariants passed ($pass groups)" diff --git a/packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh b/packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh index d0e14370..481782b6 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-digest-quarantine.sh @@ -117,6 +117,9 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness. +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init DIGEST="$SCRIPT_DIR/digest.sh" command -v jq >/dev/null 2>&1 || { @@ -176,12 +179,12 @@ echo "== Q1 (a): malformed address-free {kind,id,observed_hash} QUARANTINES; cle out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")" rc=$? [ "$rc" -eq 0 ] || fail_msg "Q1: render must EXIT 0 (per-entry quarantine, not whole-digest exit-4), got rc=$rc" - printf '%s' "$out" | grep -q "$SHA40" || fail_msg "Q1: the clean sibling (sha $SHA40) must STILL be delivered in the same drain [head-of-line block]" - printf '%s' "$out" | grep -q 'MALFORMED-Q' && fail_msg "Q1: the quarantined entry must be EXCLUDED from the rendered digest" + printf '%s' "$out" | has_match -q "$SHA40" || fail_msg "Q1: the clean sibling (sha $SHA40) must STILL be delivered in the same drain [head-of-line block]" + printf '%s' "$out" | has_match -q 'MALFORMED-Q' && fail_msg "Q1: the quarantined entry must be EXCLUDED from the rendered digest" [ -f "$(dlq "$home")" ] || fail_msg "Q1: a durable dead-letter file must be written" - grep -q 'MALFORMED-Q' "$(dlq "$home")" 2>/dev/null || fail_msg "Q1: the malformed entry must be DEAD-LETTERED (accounted-for, not silently dropped)" - grep -qi 'QUARANTINE' "$err" || fail_msg "Q1: a LOUD per-entry alarm must fire on stderr" - grep -q 'observed_seq=1' "$err" || fail_msg "Q1: the alarm must identify the offending entry (observed_seq=1)" + has_match -q 'MALFORMED-Q' "$(dlq "$home")" 2>/dev/null || fail_msg "Q1: the malformed entry must be DEAD-LETTERED (accounted-for, not silently dropped)" + has_match -qi 'QUARANTINE' "$err" || fail_msg "Q1: a LOUD per-entry alarm must fire on stderr" + has_match -q 'observed_seq=1' "$err" || fail_msg "Q1: the alarm must identify the offending entry (observed_seq=1)" ) && ok echo "== Q2 (b): reconciler enumeration (reconciled:true) renders ORIENTATION-tier, NO exit-4 ==" @@ -200,8 +203,8 @@ echo "== Q2 (b): reconciler enumeration (reconciled:true) renders ORIENTATION-ti out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")" rc=$? [ "$rc" -eq 0 ] || fail_msg "Q2: a reconciler enumeration must NOT exit-4 (it is ORIENTATION-tier), got rc=$rc" - printf '%s' "$out" | grep -q 'id=ENUM-B' || fail_msg "Q2: the enumeration must render as an ORIENTATION pointer (id=ENUM-B via _locator_line)" - printf '%s' "$out" | grep -q 'CLAIM@seq' && fail_msg "Q2: an enumeration must NOT render as an ACTIONABLE CLAIM@seq" + printf '%s' "$out" | has_match -q 'id=ENUM-B' || fail_msg "Q2: the enumeration must render as an ORIENTATION pointer (id=ENUM-B via _locator_line)" + printf '%s' "$out" | has_match -q 'CLAIM@seq' && fail_msg "Q2: an enumeration must NOT render as an ACTIONABLE CLAIM@seq" [ -s "$(dlq "$home")" ] && fail_msg "Q2: an ORIENTATION-tier enumeration must NOT be quarantined/dead-lettered" true ) && ok @@ -220,10 +223,10 @@ echo "== Q3 (c): TWO DISTINCT enumerations BOTH survive as SEPARATE orientation out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")" rc=$? [ "$rc" -eq 0 ] || fail_msg "Q3: two enumerations must render (exit 0), got rc=$rc" - n="$(printf '%s\n' "$out" | grep -c 'id=ENUM-C[12]' || true)" + n="$(printf '%s\n' "$out" | count_lines 'id=ENUM-C[12]' || true)" [ "$n" = "2" ] || fail_msg "Q3: BOTH distinct enumerations must survive as SEPARATE orientation pointers (expected 2, got $n) — neither coalesced away" - printf '%s' "$out" | grep -q 'id=ENUM-C1' || fail_msg "Q3: enumeration ENUM-C1 must be present" - printf '%s' "$out" | grep -q 'id=ENUM-C2' || fail_msg "Q3: enumeration ENUM-C2 must be present" + printf '%s' "$out" | has_match -q 'id=ENUM-C1' || fail_msg "Q3: enumeration ENUM-C1 must be present" + printf '%s' "$out" | has_match -q 'id=ENUM-C2' || fail_msg "Q3: enumeration ENUM-C2 must be present" [ -s "$(dlq "$home")" ] && fail_msg "Q3: enumerations must NOT be quarantined" true ) && ok @@ -239,10 +242,10 @@ echo "== Q4: PRESERVED — a genuine malformed ACTIONABLE claim is STILL loud (d out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")" rc=$? [ "$rc" -eq 0 ] || fail_msg "Q4: render must exit 0 (per-entry quarantine), got rc=$rc" - printf '%s' "$out" | grep -q 'CLAIM-KEEP' && fail_msg "Q4: a malformed actionable must NOT be delivered as if valid" - printf '%s' "$out" | grep -q '(none) — no consequential claims pending' || fail_msg "Q4: with the only claim quarantined, the ACTIONABLE section must show (none)" - grep -q 'CLAIM-KEEP' "$(dlq "$home")" 2>/dev/null || fail_msg "Q4: the malformed actionable must be DEAD-LETTERED (loudly surfaced, not silent)" - grep -qi 'QUARANTINE' "$err" || fail_msg "Q4: the malformed actionable must raise a LOUD alarm" + printf '%s' "$out" | has_match -q 'CLAIM-KEEP' && fail_msg "Q4: a malformed actionable must NOT be delivered as if valid" + printf '%s' "$out" | has_match -q '(none) — no consequential claims pending' || fail_msg "Q4: with the only claim quarantined, the ACTIONABLE section must show (none)" + has_match -q 'CLAIM-KEEP' "$(dlq "$home")" 2>/dev/null || fail_msg "Q4: the malformed actionable must be DEAD-LETTERED (loudly surfaced, not silent)" + has_match -qi 'QUARANTINE' "$err" || fail_msg "Q4: the malformed actionable must raise a LOUD alarm" ) && ok echo "== Q5: claim-precedence gated — a non-actionable-class entry carrying a claim (no hard locator) is STILL quarantined ==" @@ -258,8 +261,8 @@ echo "== Q5: claim-precedence gated — a non-actionable-class entry carrying a out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")" rc=$? [ "$rc" -eq 0 ] || fail_msg "Q5: render must exit 0, got rc=$rc" - grep -q 'CLAIMY' "$(dlq "$home")" 2>/dev/null || fail_msg "Q5: a claim-carrying entry with no hard locator must be quarantined (claim precedence, §2.1)" - printf '%s' "$out" | grep -q 'CI is green' && fail_msg "Q5: the un-verifiable claim must NOT be delivered" + has_match -q 'CLAIMY' "$(dlq "$home")" 2>/dev/null || fail_msg "Q5: a claim-carrying entry with no hard locator must be quarantined (claim precedence, §2.1)" + printf '%s' "$out" | has_match -q 'CI is green' && fail_msg "Q5: the un-verifiable claim must NOT be delivered" true ) && ok @@ -278,11 +281,11 @@ echo "== Q6 (a): dead-lettered entry routes EXACTLY ONE off-host alarm (payload out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")" rc=$? [ "$rc" -eq 0 ] || fail_msg "Q6: render must still EXIT 0 (per-entry quarantine, not whole-drain wedge), got rc=$rc" - n="$(grep -c . "$ALARM_OUT" 2>/dev/null || true)" + n="$(count_lines . "$ALARM_OUT" 2>/dev/null || true)" [ "$n" = "1" ] || fail_msg "Q6: EXACTLY ONE off-host alarm must route for the dead-lettered entry (got $n) [$(cat "$ALARM_OUT" 2>/dev/null)]" - grep -q '"observed_seq":21' "$ALARM_OUT" 2>/dev/null || fail_msg "Q6: the routed alarm payload must name the entry's observed_seq (21)" - grep -qi 'QUARANTINE' "$err" || fail_msg "Q6: the existing #920 stderr diagnostic must STILL fire (local + off-host, not either/or)" - printf '%s' "$out" | grep -q 'DLQ-Q6' && fail_msg "Q6: the quarantined entry must still be EXCLUDED from the rendered digest" + has_match -q '"observed_seq":21' "$ALARM_OUT" 2>/dev/null || fail_msg "Q6: the routed alarm payload must name the entry's observed_seq (21)" + has_match -qi 'QUARANTINE' "$err" || fail_msg "Q6: the existing #920 stderr diagnostic must STILL fire (local + off-host, not either/or)" + printf '%s' "$out" | has_match -q 'DLQ-Q6' && fail_msg "Q6: the quarantined entry must still be EXCLUDED from the rendered digest" true ) && ok @@ -300,9 +303,9 @@ echo "== Q7 (b): re-draining the SAME still-dead-lettered entry N times routes Z for i in 1 2 3 4; do "$DIGEST" render --from-file "$f" --agent default >/dev/null 2>"$TMP_ROOT/q7.err.$i" done - n="$(grep -c . "$ALARM_OUT" 2>/dev/null || true)" + n="$(count_lines . "$ALARM_OUT" 2>/dev/null || true)" [ "$n" = "1" ] || fail_msg "Q7: re-draining the SAME dead-lettered entry 4x must route ONLY ONE off-host alarm total (durable dedup by observed_seq); got $n [$(cat "$ALARM_OUT" 2>/dev/null)]" - grep -qi 'QUARANTINE' "$TMP_ROOT/q7.err.4" || fail_msg "Q7: the #920 stderr diagnostic must STILL fire on every re-drain (only the off-host route is deduped)" + has_match -qi 'QUARANTINE' "$TMP_ROOT/q7.err.4" || fail_msg "Q7: the #920 stderr diagnostic must STILL fire on every re-drain (only the off-host route is deduped)" ) && ok echo "== Q8 (c): a NEW distinct dead-lettered entry routes its OWN one alarm (dedup is per-entry, not a global latch) ==" @@ -321,10 +324,10 @@ echo "== Q8 (c): a NEW distinct dead-lettered entry routes its OWN one alarm (de "$DIGEST" render --from-file "$f1" --agent default >/dev/null 2>/dev/null "$DIGEST" render --from-file "$f1" --agent default >/dev/null 2>/dev/null # re-drain seq 31 -> must NOT re-alarm "$DIGEST" render --from-file "$f2" --agent default >/dev/null 2>/dev/null # NEW distinct seq 32 -> its own alarm - n="$(grep -c . "$ALARM_OUT" 2>/dev/null || true)" + n="$(count_lines . "$ALARM_OUT" 2>/dev/null || true)" [ "$n" = "2" ] || fail_msg "Q8: two DISTINCT dead-lettered entries must together route exactly 2 off-host alarms total (got $n) [$(cat "$ALARM_OUT" 2>/dev/null)]" - grep -q '"observed_seq":31' "$ALARM_OUT" 2>/dev/null || fail_msg "Q8: seq 31's alarm must be present" - grep -q '"observed_seq":32' "$ALARM_OUT" 2>/dev/null || fail_msg "Q8: seq 32's (the new distinct entry's) OWN alarm must be present" + has_match -q '"observed_seq":31' "$ALARM_OUT" 2>/dev/null || fail_msg "Q8: seq 31's alarm must be present" + has_match -q '"observed_seq":32' "$ALARM_OUT" 2>/dev/null || fail_msg "Q8: seq 32's (the new distinct entry's) OWN alarm must be present" ) && ok echo "== Q9 (d): WAKE_ALARM_SINK_CMD unconfigured OR unreachable -> FAIL LOUD (never silent no-alarm); per-entry, render still exits 0 ==" @@ -340,9 +343,9 @@ echo "== Q9 (d): WAKE_ALARM_SINK_CMD unconfigured OR unreachable -> FAIL LOUD (n out_a="$("$DIGEST" render --from-file "$f" --agent default 2>"$err_a")" rc_a=$? [ "$rc_a" -eq 0 ] || fail_msg "Q9a: per-entry quarantine must still exit 0 even when the off-host alarm sink is unconfigured (no whole-drain wedge), got rc=$rc_a" - grep -qi 'FAIL LOUD' "$err_a" || fail_msg "Q9a: an unconfigured off-host alarm target must FAIL LOUD on stderr [$(cat "$err_a")]" - grep -Eqi 'silent no-alarm|silent-miss|PERMANENTLY miss|permanent silent miss' "$err_a" || fail_msg "Q9a: the diagnostic must name the silent-miss hazard (G2a), mirroring beacon.sh's fail-closed wording [$(cat "$err_a")]" - printf '%s' "$out_a" | grep -q 'DLQ-Q9' && fail_msg "Q9a: the quarantined entry must still be EXCLUDED from the rendered digest" + has_match -qi 'FAIL LOUD' "$err_a" || fail_msg "Q9a: an unconfigured off-host alarm target must FAIL LOUD on stderr [$(cat "$err_a")]" + has_match -Eqi 'silent no-alarm|silent-miss|PERMANENTLY miss|permanent silent miss' "$err_a" || fail_msg "Q9a: the diagnostic must name the silent-miss hazard (G2a), mirroring beacon.sh's fail-closed wording [$(cat "$err_a")]" + printf '%s' "$out_a" | has_match -q 'DLQ-Q9' && fail_msg "Q9a: the quarantined entry must still be EXCLUDED from the rendered digest" true ) && ok ( @@ -355,9 +358,9 @@ echo "== Q9 (d): WAKE_ALARM_SINK_CMD unconfigured OR unreachable -> FAIL LOUD (n out_b="$("$DIGEST" render --from-file "$f" --agent default 2>"$err_b")" rc_b=$? [ "$rc_b" -eq 0 ] || fail_msg "Q9b: per-entry quarantine must still exit 0 even when the off-host alarm sink is unreachable, got rc=$rc_b" - grep -qi 'FAIL LOUD' "$err_b" || fail_msg "Q9b: an unreachable off-host alarm target must FAIL LOUD on stderr [$(cat "$err_b")]" - grep -qi 'UNREACHABLE' "$err_b" || fail_msg "Q9b: the diagnostic must name the unreachable target [$(cat "$err_b")]" - printf '%s' "$out_b" | grep -q 'DLQ-Q9' && fail_msg "Q9b: the quarantined entry must still be EXCLUDED from the rendered digest" + has_match -qi 'FAIL LOUD' "$err_b" || fail_msg "Q9b: an unreachable off-host alarm target must FAIL LOUD on stderr [$(cat "$err_b")]" + has_match -qi 'UNREACHABLE' "$err_b" || fail_msg "Q9b: the diagnostic must name the unreachable target [$(cat "$err_b")]" + printf '%s' "$out_b" | has_match -q 'DLQ-Q9' && fail_msg "Q9b: the quarantined entry must still be EXCLUDED from the rendered digest" true ) && ok @@ -383,16 +386,16 @@ echo "== Q10: snapshot metadata (#940) — snapshot_sha/snapshot_ts render on th out="$("$DIGEST" render --from-file "$f" --agent default 2>/dev/null)" rc=$? [ "$rc" -eq 0 ] || fail_msg "Q10: render must succeed (rc=$rc)" - snapa_line="$(printf '%s\n' "$out" | grep 'id=SNAP-A' || true)" - printf '%s' "$snapa_line" | grep -q 'snapshot_sha=0123abc4567890def0123abc4567890def012345' \ + snapa_line="$(printf '%s\n' "$out" | has_match 'id=SNAP-A' || true)" + printf '%s' "$snapa_line" | has_match -q 'snapshot_sha=0123abc4567890def0123abc4567890def012345' \ || fail_msg "Q10: snapshot_sha must render on the ORIENTATION pointer line [$snapa_line]" - printf '%s' "$snapa_line" | grep -q 'snapshot_ts=1753850000' \ + printf '%s' "$snapa_line" | has_match -q 'snapshot_ts=1753850000' \ || fail_msg "Q10: snapshot_ts must render on the ORIENTATION pointer line (age = emit_ts - snapshot_ts, local arithmetic) [$snapa_line]" - printf '%s' "$out" | grep -q 'git show 0123abc4567890def0123abc4567890def012345:BOARD.md' \ + printf '%s' "$out" | has_match -q 'git show 0123abc4567890def0123abc4567890def012345:BOARD.md' \ || fail_msg "Q10: snapshot_sha+path must upgrade the actionable re-verify hint to a one-call git show" # The sibling without metadata must not grow empty snapshot_ fields. - snapb_line="$(printf '%s\n' "$out" | grep 'id=SNAP-B' || true)" - printf '%s' "$snapb_line" | grep -q 'snapshot_' \ + snapb_line="$(printf '%s\n' "$out" | has_match 'id=SNAP-B' || true)" + printf '%s' "$snapb_line" | has_match -q 'snapshot_' \ && fail_msg "Q10: an entry without metadata must render NO snapshot_ fields [$snapb_line]" true ) && ok @@ -434,28 +437,28 @@ echo "== Q11 (#944): REAL detector-shape actionable RENDERS as CLAIM@seq; addres [ "$rc" -eq 0 ] || fail_msg "Q11: render must exit 0, got rc=$rc" # (a) POSITIVE: the live entry RENDERS as an actionable claim (not merely # passes the predicate) with the one-call git-show re-verify hint. - printf '%s' "$out" | grep -q 'seq 68 — CLAIM@seq' || fail_msg "Q11a: the live seq-68 detector-shape entry must RENDER as CLAIM@seq (positive control)" - printf '%s' "$out" | grep -q 'git show 55d4909569d2b5fbccfec49ec9ca83db5049f3ce:docs/scratchpads/heartbeat-planning' \ + printf '%s' "$out" | has_match -q 'seq 68 — CLAIM@seq' || fail_msg "Q11a: the live seq-68 detector-shape entry must RENDER as CLAIM@seq (positive control)" + printf '%s' "$out" | has_match -q 'git show 55d4909569d2b5fbccfec49ec9ca83db5049f3ce:docs/scratchpads/heartbeat-planning' \ || fail_msg "Q11a: the rendered claim must carry the one-call re-verify hint git show :" # (b) POSITIVE: path arm alone suffices; hint degrades to one-call re-read. - printf '%s' "$out" | grep -q 'seq 69 — CLAIM@seq' || fail_msg "Q11b: a path-only detector-shape entry must RENDER as CLAIM@seq (path arm)" - printf '%s' "$out" | grep -q 're-read BOARD.md' || fail_msg "Q11b: the path-only claim must carry the one-call re-read hint" - n_claims="$(printf '%s\n' "$out" | grep -c 'CLAIM@seq' || true)" + printf '%s' "$out" | has_match -q 'seq 69 — CLAIM@seq' || fail_msg "Q11b: a path-only detector-shape entry must RENDER as CLAIM@seq (path arm)" + printf '%s' "$out" | has_match -q 're-read BOARD.md' || fail_msg "Q11b: the path-only claim must carry the one-call re-read hint" + n_claims="$(printf '%s\n' "$out" | count_lines 'CLAIM@seq' || true)" [ "$n_claims" = "2" ] || fail_msg "Q11: EXACTLY the two valid entries must render as CLAIM@seq (got $n_claims)" # (c)+(d) NEGATIVES retained: both quarantine, each with its OWN alarm. - printf '%s' "$out" | grep -q 'ADDR-FREE' && fail_msg "Q11c: the address-free entry must be EXCLUDED from the digest" - printf '%s' "$out" | grep -q 'SNAP-ONLY' && fail_msg "Q11d: no bare path-less snapshot_sha entry may appear in the digest (gate must not widen past board_file)" - grep -q 'ADDR-FREE' "$(dlq "$home")" 2>/dev/null || fail_msg "Q11c: the address-free entry must be DEAD-LETTERED" + printf '%s' "$out" | has_match -q 'ADDR-FREE' && fail_msg "Q11c: the address-free entry must be EXCLUDED from the digest" + printf '%s' "$out" | has_match -q 'SNAP-ONLY' && fail_msg "Q11d: no bare path-less snapshot_sha entry may appear in the digest (gate must not widen past board_file)" + has_match -q 'ADDR-FREE' "$(dlq "$home")" 2>/dev/null || fail_msg "Q11c: the address-free entry must be DEAD-LETTERED" for snap_id in SNAP-ONLY-40 SNAP-ONLY-7 SNAP-ONLY-64; do - grep -q "$snap_id" "$(dlq "$home")" 2>/dev/null || fail_msg "Q11d: the bare snapshot_sha entry ($snap_id) must be DEAD-LETTERED" + has_match -q "$snap_id" "$(dlq "$home")" 2>/dev/null || fail_msg "Q11d: the bare snapshot_sha entry ($snap_id) must be DEAD-LETTERED" done - grep -q 'heartbeat-planning' "$(dlq "$home")" 2>/dev/null && fail_msg "Q11a: the valid live entry must NOT be dead-lettered" - grep -q 'PILOT-LOCAL' "$(dlq "$home")" 2>/dev/null && fail_msg "Q11b: the valid path-only entry must NOT be dead-lettered" - n_alarms="$(grep -c . "$ALARM_OUT" 2>/dev/null || true)" + has_match -q 'heartbeat-planning' "$(dlq "$home")" 2>/dev/null && fail_msg "Q11a: the valid live entry must NOT be dead-lettered" + has_match -q 'PILOT-LOCAL' "$(dlq "$home")" 2>/dev/null && fail_msg "Q11b: the valid path-only entry must NOT be dead-lettered" + n_alarms="$(count_lines . "$ALARM_OUT" 2>/dev/null || true)" [ "$n_alarms" = "4" ] || fail_msg "Q11: exactly the four invalid entries must alarm (got $n_alarms) [$(cat "$ALARM_OUT" 2>/dev/null)]" - grep -q '"observed_seq":70' "$ALARM_OUT" 2>/dev/null || fail_msg "Q11c: seq 70's own alarm must be present" + has_match -q '"observed_seq":70' "$ALARM_OUT" 2>/dev/null || fail_msg "Q11c: seq 70's own alarm must be present" for snap_seq in 71 72 73; do - grep -q "\"observed_seq\":$snap_seq" "$ALARM_OUT" 2>/dev/null || fail_msg "Q11d: seq $snap_seq's own alarm must be present" + has_match -q "\"observed_seq\":$snap_seq" "$ALARM_OUT" 2>/dev/null || fail_msg "Q11d: seq $snap_seq's own alarm must be present" done true ) && ok @@ -477,15 +480,15 @@ echo "== Q12 (#946): quarantined entries are DISCLOSED (by seq, content withheld [ "$rc" -eq 0 ] || fail_msg "Q12: render must exit 0, got rc=$rc" # DISCLOSURE: a held entry must be VISIBLE in the digest it was held from — # a silent hold is how five successive digests each stepped past seq 68... - printf '%s' "$out" | grep -q 'QUARANTINED' || fail_msg "Q12: the digest must carry a QUARANTINED disclosure section (no silent hold)" - printf '%s' "$out" | grep -q 'seq 2 .*HELD' || fail_msg "Q12: the disclosure must name the held seq (2) as HELD" + printf '%s' "$out" | has_match -q 'QUARANTINED' || fail_msg "Q12: the digest must carry a QUARANTINED disclosure section (no silent hold)" + printf '%s' "$out" | has_match -q 'seq 2 .*HELD' || fail_msg "Q12: the disclosure must name the held seq (2) as HELD" # ...but WITHOUT re-injecting the refused content: disclosure is by seq only; # the Q1/Q4/Q5/Q6/Q9/Q11 exclusion property stands. - printf '%s' "$out" | grep -q 'ADDR-Q12' && fail_msg "Q12: the quarantined entry's content/locators must stay EXCLUDED from the digest" + printf '%s' "$out" | has_match -q 'ADDR-Q12' && fail_msg "Q12: the quarantined entry's content/locators must stay EXCLUDED from the digest" # CLAMP: the embedded ack stops BELOW the quarantined seq, and says so loudly. - printf '%s' "$out" | grep -Eq 'consumed --upto 1$' || fail_msg "Q12: the embedded ack must be CLAMPED to --upto 1 (below quarantined seq 2)" - printf '%s' "$out" | grep -Eq 'consumed --upto 2( |$)' && fail_msg "Q12: the raw observed cursor (2) must NOT be embedded while seq 2 is quarantined" - printf '%s' "$out" | grep -q 'ACK CLAMPED' || fail_msg "Q12: the clamp must be LOUDLY disclosed in the ACK section" + printf '%s' "$out" | has_match -Eq 'consumed --upto 1$' || fail_msg "Q12: the embedded ack must be CLAMPED to --upto 1 (below quarantined seq 2)" + printf '%s' "$out" | has_match -Eq 'consumed --upto 2( |$)' && fail_msg "Q12: the raw observed cursor (2) must NOT be embedded while seq 2 is quarantined" + printf '%s' "$out" | has_match -q 'ACK CLAMPED' || fail_msg "Q12: the clamp must be LOUDLY disclosed in the ACK section" # A foreign-data render must NOT rewrite the lane's quarantine truth. [ -e "$home/default/quarantined.set" ] && fail_msg "Q12: a --from-file render must NOT write the store's quarantined.set (lane truth is store-mode only)" true @@ -503,9 +506,9 @@ echo "== Q13 (#946): nothing quarantined -> ack UNCLAMPED at the observed cursor out="$("$DIGEST" render --from-file "$f" --agent default 2>/dev/null)" rc=$? [ "$rc" -eq 0 ] || fail_msg "Q13: render must exit 0, got rc=$rc" - printf '%s' "$out" | grep -Eq 'consumed --upto 1$' || fail_msg "Q13: with nothing quarantined the ack must embed the observed cursor (1) unchanged" - printf '%s' "$out" | grep -q 'QUARANTINED' && fail_msg "Q13: no disclosure section when nothing is quarantined" - printf '%s' "$out" | grep -q 'ACK CLAMPED' && fail_msg "Q13: no clamp note when nothing is quarantined" + printf '%s' "$out" | has_match -Eq 'consumed --upto 1$' || fail_msg "Q13: with nothing quarantined the ack must embed the observed cursor (1) unchanged" + printf '%s' "$out" | has_match -q 'QUARANTINED' && fail_msg "Q13: no disclosure section when nothing is quarantined" + printf '%s' "$out" | has_match -q 'ACK CLAMPED' && fail_msg "Q13: no clamp note when nothing is quarantined" true ) && ok @@ -520,8 +523,8 @@ echo "== Q14 (#946): store-mode render SYNCS quarantine truth into the store — out="$("$DIGEST" render --from-store --agent default 2>/dev/null)" rc=$? [ "$rc" -eq 0 ] || fail_msg "Q14: render must exit 0, got rc=$rc" - printf '%s' "$out" | grep -q 'QUARANTINED' || fail_msg "Q14: the store-mode digest must disclose the held entry" - printf '%s' "$out" | grep -Eq 'consumed --upto 1$' || fail_msg "Q14: the embedded ack must clamp to 1 (below quarantined seq 2)" + printf '%s' "$out" | has_match -q 'QUARANTINED' || fail_msg "Q14: the store-mode digest must disclose the held entry" + printf '%s' "$out" | has_match -Eq 'consumed --upto 1$' || fail_msg "Q14: the embedded ack must clamp to 1 (below quarantined seq 2)" qf="$home/default/quarantined.set" [ "$(cat "$qf" 2>/dev/null)" = "2" ] || fail_msg "Q14: a store-mode render must sync quarantined.set to exactly {2}, got [$(cat "$qf" 2>/dev/null)]" # END-TO-END: even a hand-typed upto past the held seq is refused at the @@ -545,8 +548,8 @@ echo "== Q15 (#946): gate-fix RECOVERY — a stale quarantined.set is REPLACED b out="$("$DIGEST" render --from-store --agent default 2>/dev/null)" rc=$? [ "$rc" -eq 0 ] || fail_msg "Q15: render must exit 0, got rc=$rc" - printf '%s' "$out" | grep -q 'QUARANTINED' && fail_msg "Q15: nothing quarantines under the fixed gate — no disclosure section" - printf '%s' "$out" | grep -Eq 'consumed --upto 2$' || fail_msg "Q15: the ack must embed the full observed cursor (2) once the gate is fixed" + printf '%s' "$out" | has_match -q 'QUARANTINED' && fail_msg "Q15: nothing quarantines under the fixed gate — no disclosure section" + printf '%s' "$out" | has_match -Eq 'consumed --upto 2$' || fail_msg "Q15: the ack must embed the full observed cursor (2) once the gate is fixed" [ -s "$home/default/quarantined.set" ] && fail_msg "Q15: the clean render must REPLACE (clear) the stale quarantined.set — a cumulative-forever set would block acks on entries that now render, got [$(cat "$home/default/quarantined.set")]" "$STORE" consume --upto 2 >/dev/null 2>&1 || fail_msg "Q15: the ordinary consume must succeed after the clamp self-heals" ) && ok @@ -557,7 +560,7 @@ echo "== Q16 (guard): Q2's ENUM-B fixture must STAY address-free — the reconci # Token concatenated so THIS guard's own source lines never contain the # literal fixture id and cannot self-match. enum_id='ENUM''-B' - fixture_line="$(grep -F "\"id\":\"$enum_id\"" "$self" | grep -F '"observed_seq":5' | head -n1)" + fixture_line="$(has_match -F "\"id\":\"$enum_id\"" "$self" | has_match -F '"observed_seq":5' | head -n1)" [ -n "$fixture_line" ] || fail_msg "Q16: could not locate Q2's $enum_id fixture line (renamed/renumbered? update this guard)" fixture_json="$(printf '%s' "$fixture_line" | sed "s/.*'\({.*}\)'.*/\1/")" # Positive controls FIRST (blind-instrument rule): the extraction must yield @@ -578,7 +581,7 @@ echo "== Q16 (guard): Q2's ENUM-B fixture must STAY address-free — the reconci echo if [ -s "$FAILFILE" ]; then - echo "wake digest-quarantine harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2 + echo "wake digest-quarantine harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 exit 1 fi echo "wake digest-quarantine harness: all invariants passed ($pass groups)" diff --git a/packages/mosaic/framework/tools/wake/test-wake-fn-oracle.sh b/packages/mosaic/framework/tools/wake/test-wake-fn-oracle.sh index e3766326..f97eb1a1 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-fn-oracle.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-fn-oracle.sh @@ -25,6 +25,9 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness. +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init ORACLE="$SCRIPT_DIR/fn-oracle.sh" DET="$SCRIPT_DIR/detector.sh" @@ -62,8 +65,8 @@ echo "== O1: healthy pipeline -> synthetic-canary FN-rate = 0 ==" out="$("$ORACLE" run --slo-seconds 120 --count 3 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail_msg "O1: a healthy pipeline must exit 0 (got $rc) [$out]" - echo "$out" | grep -q 'FN-RATE = 0/3' || fail_msg "O1: FN-rate must be 0/3 on a healthy pipeline [$out]" - echo "$out" | grep -q 'VERDICT = PASS' || fail_msg "O1: verdict must be PASS on a healthy pipeline [$out]" + echo "$out" | has_match -q 'FN-RATE = 0/3' || fail_msg "O1: FN-rate must be 0/3 on a healthy pipeline [$out]" + echo "$out" | has_match -q 'VERDICT = PASS' || fail_msg "O1: verdict must be PASS on a healthy pipeline [$out]" ) && ok echo "== O2: DISABLED detector (perfect no-op) -> FN-DETECTED (blindspot killer) ==" @@ -76,9 +79,9 @@ echo "== O2: DISABLED detector (perfect no-op) -> FN-DETECTED (blindspot killer) out="$("$ORACLE" run --slo-seconds 120 --count 3 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "O2: a detector that drops a KNOWN delta MUST fail the oracle (non-zero exit), even at a perfect no-op rate" - echo "$out" | grep -q 'FN-RATE = 3/3' || fail_msg "O2: every dropped canary must count as a false-negative (FN-RATE 3/3) [$out]" - echo "$out" | grep -q 'VERDICT = FN-DETECTED' || fail_msg "O2: verdict must be FN-DETECTED for a dropping detector [$out]" - echo "$out" | grep -qi 'never OBSERVED' || fail_msg "O2: the failure must name the dropped (never-observed) delta [$out]" + echo "$out" | has_match -q 'FN-RATE = 3/3' || fail_msg "O2: every dropped canary must count as a false-negative (FN-RATE 3/3) [$out]" + echo "$out" | has_match -q 'VERDICT = FN-DETECTED' || fail_msg "O2: verdict must be FN-DETECTED for a dropping detector [$out]" + echo "$out" | has_match -qi 'never OBSERVED' || fail_msg "O2: the failure must name the dropped (never-observed) delta [$out]" ) && ok echo "== O3: reached CONSUMED but too slow -> FN-DETECTED (within-SLO clause) ==" @@ -91,10 +94,10 @@ echo "== O3: reached CONSUMED but too slow -> FN-DETECTED (within-SLO clause) == out="$("$ORACLE" run --slo-seconds 1 --count 1 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "O3: a canary that reaches CONSUMED past its SLO must be a false-negative (non-zero exit)" - echo "$out" | grep -qi 'per-class SLO' || fail_msg "O3: the failure must attribute to the per-class SLO, not a drop [$out]" + echo "$out" | has_match -qi 'per-class SLO' || fail_msg "O3: the failure must attribute to the per-class SLO, not a drop [$out]" # It must NOT be the 'never observed' branch: the delta WAS observed/delivered, # just too slowly. This proves O3 exercises the SLO clause specifically. - if echo "$out" | grep -qi 'never OBSERVED'; then + if echo "$out" | has_match -qi 'never OBSERVED'; then fail_msg "O3: an SLO breach must NOT be misreported as a dropped delta [$out]" fi ) && ok @@ -110,7 +113,7 @@ echo "== O4: verdict is OFF-DOMAIN (from terminal consumed_seq, not detector sel out="$("$ORACLE" run --slo-seconds 120 --count 1 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "O4: a drive that exits 0 but delivers nothing must still be FN-DETECTED (verdict is off-domain)" - echo "$out" | grep -q 'VERDICT = FN-DETECTED' || fail_msg "O4: off-domain verdict must not be fooled by a clean exit code [$out]" + echo "$out" | has_match -q 'VERDICT = FN-DETECTED' || fail_msg "O4: off-domain verdict must not be fooled by a clean exit code [$out]" ) && ok echo "== O5: no invented SLO -> --slo-seconds is REQUIRED (fail-loud) ==" @@ -121,12 +124,12 @@ echo "== O5: no invented SLO -> --slo-seconds is REQUIRED (fail-loud) ==" err="$("$ORACLE" run --count 1 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "O5: run without an SLO must fail loud (no invented default)" - echo "$err" | grep -qi 'slo' || fail_msg "O5: the usage error must name the missing SLO [$err]" + echo "$err" | has_match -qi 'slo' || fail_msg "O5: the usage error must name the missing SLO [$err]" ) && ok echo if [ -s "$FAILFILE" ]; then - echo "wake fn-oracle harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2 + echo "wake fn-oracle harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 exit 1 fi echo "wake fn-oracle harness: all invariants passed ($pass groups)" diff --git a/packages/mosaic/framework/tools/wake/test-wake-install.sh b/packages/mosaic/framework/tools/wake/test-wake-install.sh index 794e0293..852c1378 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-install.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-install.sh @@ -34,6 +34,9 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness. +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init WI="$SCRIPT_DIR/wake-install.sh" BEACON="$SCRIPT_DIR/beacon.sh" FRAMEWORK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" @@ -94,7 +97,7 @@ EOF export WAKE_INSTALL_MANIFEST="$BAD_MANIFEST" out="$(bash "$WI" install 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "I2: install must FAIL when a wake candidate is not framework-owned (got rc=$rc)" - echo "$out" | grep -qi 'Gate A VIOLATION' || fail_msg "I2: refusal must name the Gate A violation [$out]" + echo "$out" | has_match -qi 'Gate A VIOLATION' || fail_msg "I2: refusal must name the Gate A violation [$out]" # No partial write: the target must have received NO wake tool file. [ ! -e "$TGT/tools/wake/beacon.sh" ] || fail_msg "I2: a refused install must not have written any wake file (partial write leaked)" # And the positive control: with the REAL manifest, the same candidates install. @@ -121,7 +124,7 @@ EOF || fail_msg "I3: blank-reset drop-in write failed" out="$(bash "$WI" verify-single "$UNIT" 2>&1)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "I3: blank-reset must resolve exactly one OnUnitActiveUSec (rc=$rc) [$out]" - echo "$out" | grep -qi 'EXACTLY ONE' || fail_msg "I3: verify must confirm exactly-one [$out]" + echo "$out" | has_match -qi 'EXACTLY ONE' || fail_msg "I3: verify must confirm exactly-one [$out]" # NEGATIVE CONTROL: a drop-in WITHOUT the reset line appends -> TWO values -> fail. cat >"$UD/$UNIT.d/interval.conf" <<'EOF' [Timer] @@ -141,7 +144,7 @@ echo "== I4: snapshot-guard — reap without a snapshot is REFUSED; allowed afte # (a) reap WITHOUT snapshot -> refuse, unit still present. out="$(bash "$WI" reap-unit "$UNIT" 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "I4: reap without a snapshot must be REFUSED (rc=$rc)" - echo "$out" | grep -qi 'snapshot-guard' || fail_msg "I4: refusal must name the snapshot-guard [$out]" + echo "$out" | has_match -qi 'snapshot-guard' || fail_msg "I4: refusal must name the snapshot-guard [$out]" [ -f "$UD/$UNIT" ] || fail_msg "I4: a refused reap must NOT delete the unit" # (b) snapshot, then reap -> allowed, unit gone, snapshot retained. bash "$WI" snapshot-unit "$UNIT" >/dev/null 2>&1 || fail_msg "I4: snapshot-unit failed" @@ -161,28 +164,28 @@ echo "== I5: fail-closed install-validate — unconfigured HMAC/alarm FAIL LOUD; export WAKE_HMAC_KEY_NAME="primary" out="$(bash "$WI" validate-hmac-key 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "I5: missing credential store must FAIL LOUD for the HMAC key (rc=$rc)" - echo "$out" | grep -qi 'UNSIGNED' || fail_msg "I5: HMAC failure must warn about unsigned wakes [$out]" + echo "$out" | has_match -qi 'UNSIGNED' || fail_msg "I5: HMAC failure must warn about unsigned wakes [$out]" # Now provide the key by-name -> pass, and the value must NEVER be echoed. jq -cn --arg k "$SECRET_KEY" '{wake:{hmac_keys:{"primary":$k}}}' >"$CRED" out2="$(bash "$WI" validate-hmac-key 2>&1)"; rc2=$? [ "$rc2" -eq 0 ] || fail_msg "I5: a by-name-resolvable HMAC key must pass (rc=$rc2) [$out2]" - echo "$out2" | grep -qF "$SECRET_KEY" && fail_msg "I5: validate must NEVER echo the HMAC key material" + echo "$out2" | has_match -qF "$SECRET_KEY" && fail_msg "I5: validate must NEVER echo the HMAC key material" # --- alarm target: unconfigured -> fail loud (silent no-alarm host). unset WAKE_ALARM_SINK_CMD out3="$(bash "$WI" validate-alarm-target 2>&1)"; rc3=$? [ "$rc3" -ne 0 ] || fail_msg "I5: unconfigured alarm target must FAIL LOUD (rc=$rc3)" - echo "$out3" | grep -qi 'silent no-alarm host' || fail_msg "I5: alarm failure must name the silent-no-alarm hazard [$out3]" + echo "$out3" | has_match -qi 'silent no-alarm host' || fail_msg "I5: alarm failure must name the silent-no-alarm hazard [$out3]" # unreachable -> fail loud. export WAKE_ALARM_SINK_CMD="false" out4="$(bash "$WI" validate-alarm-target 2>&1)"; rc4=$? [ "$rc4" -ne 0 ] || fail_msg "I5: unreachable alarm sink must FAIL LOUD (rc=$rc4)" - echo "$out4" | grep -qi 'UNREACHABLE' || fail_msg "I5: alarm failure must name the unreachable target [$out4]" + echo "$out4" | has_match -qi 'UNREACHABLE' || fail_msg "I5: alarm failure must name the unreachable target [$out4]" # reachable -> pass. export WAKE_ALARM_SINK_CMD="cat >/dev/null" bash "$WI" validate-alarm-target >/dev/null 2>&1 || fail_msg "I5: a configured+reachable alarm sink must pass" # The installer file must inline NO secret/endpoint. - grep -qF "$SECRET_ENDPOINT" "$WI" && fail_msg "I5: wake-install.sh must NOT inline any alarm endpoint" - grep -qE 'hmac_keys[^A-Za-z_].*=[^=].*[A-Za-z0-9]{8,}' "$WI" && fail_msg "I5: wake-install.sh must NOT inline key material" + has_match -qF "$SECRET_ENDPOINT" "$WI" && fail_msg "I5: wake-install.sh must NOT inline any alarm endpoint" + has_match -qE 'hmac_keys[^A-Za-z_].*=[^=].*[A-Za-z0-9]{8,}' "$WI" && fail_msg "I5: wake-install.sh must NOT inline key material" true ) && ok @@ -204,7 +207,7 @@ echo "== I6: reset->verify->retire — overlap keeps legacy running; only §4-ve [ "$rc" -eq 0 ] || fail_msg "I6: overlap reset-verify must succeed (rc=$rc) [$out]" [ -f "$UD/$TIMER" ] || fail_msg "I6: overlap phase must LEAVE the legacy timer running (retire withheld)" [ -f "$SNAP/$TIMER" ] || fail_msg "I6: the legacy timer must be snapshotted before any retire" - echo "$out" | grep -qi 'LEFT RUNNING' || fail_msg "I6: overlap must announce retire is withheld [$out]" + echo "$out" | has_match -qi 'LEFT RUNNING' || fail_msg "I6: overlap must announce retire is withheld [$out]" # (b) §4-vector pass: retire LAST, snapshot-guarded (fallback proven live above). out2="$(bash "$WI" reset-verify-retire c --interval 30min --vector-passed 2>&1)"; rc2=$? [ "$rc2" -eq 0 ] || fail_msg "I6: vector-passed retire must succeed (rc=$rc2) [$out2]" @@ -283,11 +286,11 @@ echo "== I9: dep-check — a host seed missing _lib/manifest.sh FAILS LOUD (name TGT="$(fresh i9-target)" out="$(WAKE_INSTALL_SOURCE="$FAKE" WAKE_INSTALL_TARGET="$TGT" bash "$WI_FAKE" install 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "I9: install MUST fail when _lib/manifest.sh is missing (rc=$rc)" - echo "$out" | grep -qi 'manifest.sh' || fail_msg "I9: the failure must NAME the missing library (manifest.sh) [$out]" - echo "$out" | grep -qiE 'REMEDY|mosaic update|re-seed|framework install' \ + echo "$out" | has_match -qi 'manifest.sh' || fail_msg "I9: the failure must NAME the missing library (manifest.sh) [$out]" + echo "$out" | has_match -qiE 'REMEDY|mosaic update|re-seed|framework install' \ || fail_msg "I9: the failure must give an actionable REMEDY, not just an error [$out]" # It must be a CLEAR fail-loud, NOT the obscure bare `source: No such file or directory`. - echo "$out" | grep -qi 'No such file or directory' \ + echo "$out" | has_match -qi 'No such file or directory' \ && fail_msg "I9: must not fail with a bare source error (obscure) [$out]" # Positive control: with the helper present (real framework root) install succeeds. TGT_OK="$(fresh i9-target-ok)" @@ -313,10 +316,10 @@ echo "== I10: systemd search-path — install links mosaic-wake.service into the rm -f "$UD/mosaic-wake.service" out2="$(bash "$WI" validate-systemd-path 2>&1)"; rc2=$? [ "$rc2" -ne 0 ] || fail_msg "I10: validate must FAIL when the unit is not in the search path (rc=$rc2) [$out2]" - echo "$out2" | grep -qi 'search path' || fail_msg "I10: validate failure must name the search-path miss [$out2]" + echo "$out2" | has_match -qi 'search path' || fail_msg "I10: validate failure must name the search-path miss [$out2]" # (c) idempotent re-install: exactly one search-path entry, still resolvable. bash "$WI" install >/dev/null 2>&1 || fail_msg "I10: re-run install must succeed" - n="$(find "$UD" -maxdepth 1 -name 'mosaic-wake.service' | grep -c .)" + n="$(find "$UD" -maxdepth 1 -name 'mosaic-wake.service' | count_lines .)" [ "$n" -eq 1 ] || fail_msg "I10: re-install must not duplicate the search-path entry (found $n)" bash "$WI" validate-systemd-path >/dev/null 2>&1 || fail_msg "I10: re-install must keep the unit resolvable" ) && ok @@ -334,12 +337,12 @@ echo "== I11: F7 — the legacy reap REFUSES unless the canon fallback wake is p # it must REFUSE (schedulable floor not met). out0="$(bash "$WI" fallback-proven-live 2>&1)"; rc0=$? [ "$rc0" -ne 0 ] || fail_msg "I11: fallback-proven-live must FAIL when the fallback wake is not installed (rc=$rc0)" - echo "$out0" | grep -qi 'F7' || fail_msg "I11: the refusal must name the F7 precondition [$out0]" + echo "$out0" | has_match -qi 'F7' || fail_msg "I11: the refusal must name the F7 precondition [$out0]" # (b) NEGATIVE — vector-passed reap with NO live fallback must be REFUSED and the # legacy timer LEFT RUNNING (never a coverage gap). out="$(bash "$WI" reset-verify-retire f --interval 30min --vector-passed 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "I11: reap must be REFUSED without a proven-live fallback (rc=$rc)" - echo "$out" | grep -qi 'FALLBACK WAKE is not proven live' || fail_msg "I11: refusal must name the un-proven fallback [$out]" + echo "$out" | has_match -qi 'FALLBACK WAKE is not proven live' || fail_msg "I11: refusal must name the un-proven fallback [$out]" [ -f "$UD/$TIMER" ] || fail_msg "I11: a refused reap must LEAVE the legacy timer running (F7 — no coverage gap)" # (c) POSITIVE — provision the fallback at the schedulable floor, then the reap # proceeds (F7 satisfied) and the legacy timer is retired. @@ -367,11 +370,11 @@ echo "== I12: fallback drain — a STALLED detector still gets delivery via the # even with no detector running — no starvation of the drain. out="$(bash "$DIGEST" render --from-store 2>/dev/null)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "I12: the canon fallback drain must exit 0 (rc=$rc)" - echo "$out" | grep -q 'STALLED-DRAIN-MARK' \ + echo "$out" | has_match -q 'STALLED-DRAIN-MARK' \ || fail_msg "I12: the fallback drain must DELIVER the pending obligation a stalled detector left (no delivery starvation) [$out]" # Bind the invariant to the SHIPPED unit: its ExecStart must be this canon drain. UNIT="$FRAMEWORK_ROOT/systemd/user/mosaic-wake-fallback.service" - grep -qE '^ExecStart=.*digest\.sh render --from-store' "$UNIT" \ + has_match -qE '^ExecStart=.*digest\.sh render --from-store' "$UNIT" \ || fail_msg "I12: mosaic-wake-fallback.service ExecStart must fire the canon drain (digest.sh render --from-store)" ) && ok @@ -391,11 +394,11 @@ echo "== I13: install links + validates the canon fallback timer+service into th rm -f "$UD/mosaic-wake-fallback.timer" out="$(bash "$WI" validate-fallback-units 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "I13: validate must FAIL when a fallback unit is not in the search path (rc=$rc) [$out]" - echo "$out" | grep -qi 'search path' || fail_msg "I13: validate failure must name the search-path miss [$out]" + echo "$out" | has_match -qi 'search path' || fail_msg "I13: validate failure must name the search-path miss [$out]" # (c) idempotent re-install: exactly one entry per unit, still resolvable. bash "$WI" install >/dev/null 2>&1 || fail_msg "I13: re-run install must succeed" - nt="$(find "$UD" -maxdepth 1 -name 'mosaic-wake-fallback.timer' | grep -c .)" - ns="$(find "$UD" -maxdepth 1 -name 'mosaic-wake-fallback.service' | grep -c .)" + nt="$(find "$UD" -maxdepth 1 -name 'mosaic-wake-fallback.timer' | count_lines .)" + ns="$(find "$UD" -maxdepth 1 -name 'mosaic-wake-fallback.service' | count_lines .)" [ "$nt" -eq 1 ] && [ "$ns" -eq 1 ] || fail_msg "I13: re-install must not duplicate the fallback entries (timer=$nt service=$ns)" bash "$WI" validate-fallback-units >/dev/null 2>&1 || fail_msg "I13: re-install must keep the fallback units resolvable" ) && ok @@ -407,17 +410,17 @@ echo "== I14: per-class fallback cadence — blank-reset yields EXACTLY ONE OnUn TIMER="mosaic-wake-fallback.timer" # The shipped timer carries the base OnUnitActiveSec placeholder (=1h). cp "$FRAMEWORK_ROOT/systemd/user/$TIMER" "$UD/$TIMER" - grep -q '^OnUnitActiveSec=1h' "$UD/$TIMER" || fail_msg "I14: shipped fallback timer must carry a base OnUnitActiveSec placeholder" + has_match -q '^OnUnitActiveSec=1h' "$UD/$TIMER" || fail_msg "I14: shipped fallback timer must carry a base OnUnitActiveSec placeholder" # write-fallback-cadence writes the per-class cadence in BLANK-RESET form. out="$(bash "$WI" write-fallback-cadence 30min 2>&1)"; rc=$? [ "$rc" -eq 0 ] || fail_msg "I14: write-fallback-cadence must succeed + verify single (rc=$rc) [$out]" - echo "$out" | grep -qi 'exactly one' || fail_msg "I14: the cadence write must confirm exactly-one OnUnitActiveUSec [$out]" + echo "$out" | has_match -qi 'exactly one' || fail_msg "I14: the cadence write must confirm exactly-one OnUnitActiveUSec [$out]" [ -f "$UD/$TIMER.d/cadence.conf" ] || fail_msg "I14: the per-class cadence drop-in must be written under .d/" # Assert the blank-reset FORM: an empty `OnUnitActiveSec=` reset line IMMEDIATELY # FOLLOWED by the new value. Portable across GNU and BusyBox grep (Alpine CI) — # `grep -z` (NUL-data) is a GNU-only extension BusyBox grep does NOT support, so # match the reset line and require the value on the very next line (-A1) instead. - grep -A1 '^OnUnitActiveSec=$' "$UD/$TIMER.d/cadence.conf" | grep -qx 'OnUnitActiveSec=30min' \ + has_match -A1 '^OnUnitActiveSec=$' "$UD/$TIMER.d/cadence.conf" | has_match -qx 'OnUnitActiveSec=30min' \ || fail_msg "I14: the drop-in must use the blank-reset form (empty reset line, then the value)" bash "$WI" verify-single "$TIMER" >/dev/null 2>&1 || fail_msg "I14: base(1h)+blank-reset(30min) must resolve exactly one OnUnitActiveUSec" # NEGATIVE CONTROL: a drop-in WITHOUT the reset line appends -> base 1h + 30min -> two -> fail. @@ -428,7 +431,7 @@ echo "== I14: per-class fallback cadence — blank-reset yields EXACTLY ONE OnUn echo if [ -s "$FAILFILE" ]; then - echo "wake install harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2 + echo "wake install harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 exit 1 fi echo "wake install harness: all invariants passed ($pass groups)" diff --git a/packages/mosaic/framework/tools/wake/test-wake-preimage.sh b/packages/mosaic/framework/tools/wake/test-wake-preimage.sh index f9058e60..3380dd5a 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-preimage.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-preimage.sh @@ -46,6 +46,9 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness. +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init PRE="$SCRIPT_DIR/preimage.sh" STORE="$SCRIPT_DIR/store.sh" DET="$SCRIPT_DIR/detector.sh" @@ -202,9 +205,9 @@ echo "== P4: credential-store PATH refused — bytes never captured (acceptance sd="$(state_dir)" row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")" [ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P4: row must record captured=false [$row]" - jq -r '.refused // ""' <<<"$row" | grep -qi 'path' || fail_msg "P4: refusal must name the path deny [$row]" + jq -r '.refused // ""' <<<"$row" | has_match -qi 'path' || fail_msg "P4: refusal must name the path deny [$row]" # THE invariant: the secret bytes exist NOWHERE under the wake state dir. - if grep -rq "$secret" "$sd" 2>/dev/null; then + if has_match -rq "$secret" "$sd" 2>/dev/null; then fail_msg "P4: credential bytes leaked into the wake state dir" fi ) && ok @@ -225,8 +228,8 @@ echo "== P5: secret-SHAPED content refused (refusal, not redaction) ==" sd="$(state_dir)" row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")" [ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P5: row must record captured=false [$row]" - jq -r '.refused // ""' <<<"$row" | grep -qi 'secret' || fail_msg "P5: refusal must name secret-shaped content [$row]" - if grep -rq "$tok" "$sd" 2>/dev/null; then + jq -r '.refused // ""' <<<"$row" | has_match -qi 'secret' || fail_msg "P5: refusal must name secret-shaped content [$row]" + if has_match -rq "$tok" "$sd" 2>/dev/null; then fail_msg "P5: secret-shaped bytes leaked into the wake state dir" fi ) && ok @@ -275,7 +278,7 @@ echo "== P8: unresolvable adapter -> FAIL LOUD, never 'no change' (D2 class) ==" unset WAKE_WATCH_LIST WAKE_PREIMAGE_EXTRA MOSAIC_HOME out="$("$PRE" check --enqueue 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "P8: an unobservable preimage definition must FAIL LOUD (rc=0)" - echo "$out" | grep -qi 'does not resolve' || fail_msg "P8: the failure must name the unresolvable adapter [$out]" + echo "$out" | has_match -qi 'does not resolve' || fail_msg "P8: the failure must name the unresolvable adapter [$out]" ) && ok echo "== P9: corrupt ledger -> REFUSE to compare/re-baseline ==" @@ -294,7 +297,7 @@ echo "== P9: corrupt ledger -> REFUSE to compare/re-baseline ==" printf 'v2 changed\n' >"$fx/f.env" out="$("$PRE" check --enqueue 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "P9: a corrupt ledger must FAIL LOUD (rc=0)" - echo "$out" | grep -qi 'unparseable' || fail_msg "P9: the failure must name the corrupt ledger [$out]" + echo "$out" | has_match -qi 'unparseable' || fail_msg "P9: the failure must name the corrupt ledger [$out]" [ "$(wc -l <"$sd/preimage/preimage-ledger.jsonl")" = "$r1" ] || fail_msg "P9: nothing may be appended over corrupt history" ) && ok @@ -314,7 +317,7 @@ echo "== P10: oversized file -> bytes refused, hash still recorded ==" sd="$(state_dir)" row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")" [ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P10: row must record captured=false [$row]" - jq -r '.refused // ""' <<<"$row" | grep -qi 'MAX_BYTES' || fail_msg "P10: refusal must name the size cap [$row]" + jq -r '.refused // ""' <<<"$row" | has_match -qi 'MAX_BYTES' || fail_msg "P10: refusal must name the size cap [$row]" sha="$(jq -r '.sha256' <<<"$row")" [ "$sha" = "$(sha256sum "$fx/big.bin" | awk '{print $1}')" ] || fail_msg "P10: hash must still be recorded [$row]" [ ! -f "$sd/preimage/objects/$sha" ] || fail_msg "P10: oversized bytes must NOT be stored" @@ -362,7 +365,7 @@ echo "== P12: detector — preimage infra failure is LOUD but does not starve ob printf 'NOT-JSON-GARBAGE{{{\n' >"$sd/preimage/preimage-ledger.jsonl" out="$("$DET" poll-once 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "P12: the pass must exit non-zero on preimage infra failure" - echo "$out" | grep -qi 'preimage' || fail_msg "P12: the failure must name the preimage check [$out]" + echo "$out" | has_match -qi 'preimage' || fail_msg "P12: the failure must name the preimage check [$out]" n="$(find "$sd/detector" -name 'watch-*.hash' 2>/dev/null | wc -l | tr -d '[:space:]')" [ "$n" -ge 1 ] || fail_msg "P12: source observation must still proceed (no watch hash baselined)" ) && ok @@ -397,10 +400,10 @@ echo "== P13: symlinked/renamed credential store refused on BOTH path forms (#96 n="$(jq -r 'select(.captured == false) | .path' "$sd/preimage/preimage-ledger.jsonl" | wc -l | tr -d '[:space:]')" [ "$n" = "2" ] || fail_msg "P13: both decoys must record captured=false (got $n)" # THE invariant (decoy method): the planted bytes exist NOWHERE in state. - if grep -rq "$s1" "$sd" 2>/dev/null; then + if has_match -rq "$s1" "$sd" 2>/dev/null; then fail_msg "P13: renamed-target store bytes leaked into the wake state dir" fi - if grep -rq "$s2" "$sd" 2>/dev/null; then + if has_match -rq "$s2" "$sd" 2>/dev/null; then fail_msg "P13: prefix-less key bytes leaked into the wake state dir" fi ) && ok @@ -423,7 +426,7 @@ echo "== P14: detector.env-class key — safe WITHOUT opt-in by polarity, refuse sd="$(state_dir)" row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")" [ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P14a: extra must be record-only without opt-in [$row]" - if grep -rq "$hm" "$sd" 2>/dev/null; then + if has_match -rq "$hm" "$sd" 2>/dev/null; then fail_msg "P14a: key bytes leaked without any opt-in" fi # (b) The operator opts the env file in (the exact error the old usage text @@ -434,8 +437,8 @@ echo "== P14: detector.env-class key — safe WITHOUT opt-in by polarity, refuse [ "$rc" -eq 0 ] || fail_msg "P14b: refusal is not an infra failure (rc=$rc) [$out]" row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")" [ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P14b: opted-in key material must still refuse [$row]" - jq -r '.refused // ""' <<<"$row" | grep -qi 'secret' || fail_msg "P14b: refusal must name secret-shaped content [$row]" - if grep -rq "$hm" "$sd" 2>/dev/null; then + jq -r '.refused // ""' <<<"$row" | has_match -qi 'secret' || fail_msg "P14b: refusal must name secret-shaped content [$row]" + if has_match -rq "$hm" "$sd" 2>/dev/null; then fail_msg "P14b: key bytes leaked despite refusal" fi ) && ok @@ -488,7 +491,7 @@ echo "== P16: deleted ledger over surviving objects/ — REFUSE to re-baseline ( printf '# v3\n' >>"$fx/adapter.sh" out="$("$PRE" check --enqueue 2>&1)"; rc=$? [ "$rc" -ne 0 ] || fail_msg "P16: absent ledger over prior objects must FAIL LOUD (rc=0) [$out]" - echo "$out" | grep -qi 'REFUSING to re-baseline' || fail_msg "P16: the failure must name the refusal [$out]" + echo "$out" | has_match -qi 'REFUSING to re-baseline' || fail_msg "P16: the failure must name the refusal [$out]" [ ! -e "$sd/preimage/preimage-ledger.jsonl" ] || fail_msg "P16: the ledger must NOT be silently recreated over deleted history" [ "$(ls "$sd/preimage/objects" | wc -l | tr -d '[:space:]')" = "$nobj" ] || fail_msg "P16: prior objects must remain untouched" [ "$(depth)" = "0" ] || fail_msg "P16: the v2->v3 change must NOT be absorbed or enqueued from an unverifiable baseline (depth=$(depth))" @@ -520,7 +523,7 @@ echo "== P17: allowlist symmetry — symlink to a DENIED target refuses; symlink sd="$(state_dir)" crow="$(jq -c --arg p "$(realpath "$fx/settings.json")" 'select(.path == $p)' "$sd/preimage/preimage-ledger.jsonl" | tail -n1)" [ "$(jq -r '.captured' <<<"$crow")" = "false" ] || fail_msg "P17: allowlisted symlink to a denied target must refuse [$crow]" - if grep -rq "$s" "$sd" 2>/dev/null; then + if has_match -rq "$s" "$sd" 2>/dev/null; then fail_msg "P17: credential bytes leaked via an allowlisted alias" fi brow="$(jq -c --arg p "$(realpath "$fx/alias.cfg")" 'select(.path == $p)' "$sd/preimage/preimage-ledger.jsonl" | tail -n1)" diff --git a/packages/mosaic/framework/tools/wake/test-wake-reconcile.sh b/packages/mosaic/framework/tools/wake/test-wake-reconcile.sh index e197c896..7705606f 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-reconcile.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-reconcile.sh @@ -29,6 +29,9 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness. +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init RECON="$SCRIPT_DIR/reconcile.sh" STORE="$SCRIPT_DIR/store.sh" DET="$SCRIPT_DIR/detector.sh" @@ -87,7 +90,7 @@ EOF out="$("$RECON" inventory 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail_msg "R1: a complete inventory must PASS (exit 0) [$out]" - echo "$out" | grep -qi 'COMPLETE' || fail_msg "R1: a complete inventory must report COMPLETE [$out]" + echo "$out" | has_match -qi 'COMPLETE' || fail_msg "R1: a complete inventory must report COMPLETE [$out]" ) && ok echo "== R2: OMITTED source -> FLAG (vacuous-pass prevented, not silently green) ==" @@ -106,8 +109,8 @@ EOF out="$("$RECON" inventory 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "R2: an omitted (declared-but-unwatched) source must FLAG (non-zero), not silently pass" - echo "$out" | grep -qi 'r2' || fail_msg "R2: the flag must name the omitted source r2 [$out]" - echo "$out" | grep -qi 'vacuous' || fail_msg "R2: the flag must state the vacuous-pass is prevented [$out]" + echo "$out" | has_match -qi 'r2' || fail_msg "R2: the flag must name the omitted source r2 [$out]" + echo "$out" | has_match -qi 'vacuous' || fail_msg "R2: the flag must state the vacuous-pass is prevented [$out]" ) && ok echo "== R3: required_sources omission -> FLAG ==" @@ -126,7 +129,7 @@ EOF out="$("$RECON" inventory 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "R3: a required_sources omission must FLAG (non-zero)" - echo "$out" | grep -qi "requires source 'r2'" || fail_msg "R3: the flag must name the required-but-omitted source r2 [$out]" + echo "$out" | has_match -qi "requires source 'r2'" || fail_msg "R3: the flag must name the required-but-omitted source r2 [$out]" ) && ok echo "== R4: dangling watch reference -> FLAG ==" @@ -144,7 +147,7 @@ EOF out="$("$RECON" inventory 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "R4: a dangling reference must FLAG (non-zero)" - echo "$out" | grep -qi 'dangling' || fail_msg "R4: the flag must state it is dangling [$out]" + echo "$out" | has_match -qi 'dangling' || fail_msg "R4: the flag must state it is dangling [$out]" ) && ok echo "== R5/R6: pre-existing state -> UNACCOUNTED flag + enumerated into store; re-run 0 ==" @@ -173,14 +176,14 @@ EOF out="$("$RECON" reconcile 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "R5: pre-existing unaccounted state must FLAG (non-zero) on first reconcile" - echo "$out" | grep -qi 'UNACCOUNTED=2' || fail_msg "R5: both pre-existing sources must be UNACCOUNTED [$out]" + echo "$out" | has_match -qi 'UNACCOUNTED=2' || fail_msg "R5: both pre-existing sources must be UNACCOUNTED [$out]" # R6: enumerated into the durable store — one entry per pre-existing source. [ "$(depth)" = "2" ] || fail_msg "R6: pre-existing state must be ENUMERATED into the store (depth 2), got $(depth)" # A follow-up reconcile now finds everything accounted -> 0 unaccounted. out2="$("$RECON" reconcile 2>&1)" rc2=$? [ "$rc2" -eq 0 ] || fail_msg "R6: a second reconcile must report 0 unaccounted (exit 0) [$out2]" - echo "$out2" | grep -qi 'UNACCOUNTED=0' || fail_msg "R6: second reconcile must be 0 unaccounted [$out2]" + echo "$out2" | has_match -qi 'UNACCOUNTED=0' || fail_msg "R6: second reconcile must be 0 unaccounted [$out2]" [ "$(depth)" = "2" ] || fail_msg "R6: a clean reconcile must NOT re-enumerate (depth still 2), got $(depth)" ) && ok @@ -211,7 +214,7 @@ EOF out="$("$RECON" reconcile 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail_msg "R7: state reflected in the inbox must be ACCOUNTED (exit 0) [$out]" - echo "$out" | grep -qi 'UNACCOUNTED=0' || fail_msg "R7: inbox-reflected state must be 0 unaccounted [$out]" + echo "$out" | has_match -qi 'UNACCOUNTED=0' || fail_msg "R7: inbox-reflected state must be 0 unaccounted [$out]" [ "$(depth)" = "1" ] || fail_msg "R7: reconciler must NOT double-enumerate inbox-reflected state (depth still 1), got $(depth)" ) && ok @@ -236,7 +239,7 @@ EOF out="$("$RECON" reconcile 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "R8: a source error must FAIL LOUD (non-zero exit)" - echo "$out" | grep -qi 'FAIL LOUD' || fail_msg "R8: the source error must be loud [$out]" + echo "$out" | has_match -qi 'FAIL LOUD' || fail_msg "R8: the source error must be loud [$out]" [ "$(depth)" = "0" ] || fail_msg "R8: a failed observation must NOT enumerate anything, got depth $(depth)" ) && ok @@ -278,7 +281,7 @@ EOF out="$("$RECON" reconcile 2>&1)" rc=$? [ "$rc" -ne 0 ] || fail_msg "R9: pre-existing beta is unaccounted on this pass -> must FLAG (non-zero) [$out]" - echo "$out" | grep -qi 'UNACCOUNTED=1' || fail_msg "R9: only beta should be unaccounted (alpha is inbox-accounted) [$out]" + echo "$out" | has_match -qi 'UNACCOUNTED=1' || fail_msg "R9: only beta should be unaccounted (alpha is inbox-accounted) [$out]" [ "$(depth)" = "2" ] || fail_msg "R9: reconciler must ENUMERATE beta into the co-fed store (depth 2), got $(depth)" # A further detector delta on beta must allocate 3 — the unified allocator @@ -292,8 +295,8 @@ EOF # already used, so this set would contain a duplicate (alias). seqs="$("$STORE" drain | jq -r '.observed_seq' | sort -n | tr '\n' ' ')" [ "$seqs" = "1 2 3 " ] || fail_msg "R9: co-fed observed_seqs must be distinct+gapless '1 2 3', got '$seqs' (a duplicate = the aliasing #908 dissolved)" - ndistinct="$("$STORE" drain | jq -r '.observed_seq' | sort -nu | grep -c .)" - ntotal="$("$STORE" drain | jq -r '.observed_seq' | grep -c .)" + ndistinct="$("$STORE" drain | jq -r '.observed_seq' | sort -nu | count_lines .)" + ntotal="$("$STORE" drain | jq -r '.observed_seq' | count_lines .)" [ "$ndistinct" = "$ntotal" ] || fail_msg "R9: aliasing detected — $ntotal entries but only $ndistinct distinct observed_seq" # The contiguous-prefix CONSUMED contract holds over the co-fed seqs. "$STORE" consume --upto 3 >/dev/null 2>&1 || fail_msg "R9: CONSUMED 3 must succeed over the gapless co-fed prefix" @@ -333,7 +336,7 @@ EOF out="$("$RECON" reconcile 2>&1)" rc=$? [ "$rc" -eq 0 ] || fail_msg "R10: a consumed state must be ACCOUNTED (exit 0, no spurious rc=1 CRITICAL) [$out]" - echo "$out" | grep -qi 'UNACCOUNTED=0' || fail_msg "R10: a consumed state must be 0 unaccounted (no re-enumeration) [$out]" + echo "$out" | has_match -qi 'UNACCOUNTED=0' || fail_msg "R10: a consumed state must be 0 unaccounted (no re-enumeration) [$out]" [ "$(depth)" = "0" ] || fail_msg "R10: reconciler must NOT re-enumerate a consumed state (depth still 0 = no duplicate wake), got $(depth)" ) && ok @@ -372,7 +375,7 @@ EOF [ "$rc" -ne 0 ] || fail_msg "R11: a genuine gap (r2) must still FLAG (non-zero) [$out]" # After #932: ONLY r2 is unaccounted (r1 suppressed by the store record). On # BASE both r1 and r2 re-enumerate (UNACCOUNTED=2) -> this assertion is red-first. - echo "$out" | grep -qi 'UNACCOUNTED=1' || fail_msg "R11: exactly ONE source (the gap r2) must be unaccounted; the consumed r1 must be suppressed [$out]" + echo "$out" | has_match -qi 'UNACCOUNTED=1' || fail_msg "R11: exactly ONE source (the gap r2) must be unaccounted; the consumed r1 must be suppressed [$out]" # Prove precisely WHICH state re-enumerated: the gap r2 IS enumerated, the # consumed r1 is NOT. (base re-enumerates r1 too -> the r1-absent assertion is # red-first; the r2-present assertion holds both before and after = no over-suppression.) @@ -383,7 +386,7 @@ EOF echo if [ -s "$FAILFILE" ]; then - echo "wake reconcile harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2 + echo "wake reconcile harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 exit 1 fi echo "wake reconcile harness: all invariants passed ($pass groups)" diff --git a/packages/mosaic/framework/tools/wake/test-wake-store-ack.sh b/packages/mosaic/framework/tools/wake/test-wake-store-ack.sh index f925fb2c..2be6a4c4 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-store-ack.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-store-ack.sh @@ -42,6 +42,9 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness. +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init STORE="$SCRIPT_DIR/store.sh" ACK="$SCRIPT_DIR/ack.sh" @@ -96,14 +99,14 @@ echo "== T1: three-cursor advancement ==" "$STORE" enqueue --seq 1 --class actionable --locators '{"repo":"r","issue":1}' >/dev/null "$STORE" enqueue --seq 2 --class actionable --locators '{"repo":"r","issue":2}' >/dev/null cur="$("$STORE" cursors)" - echo "$cur" | grep -q 'observed_seq=2' || fail_msg "T1: observed_seq should be 2 after enqueue 1,2 [$cur]" - echo "$cur" | grep -q 'consumed_seq=0' || fail_msg "T1: consumed_seq must NOT advance on enqueue (only on CONSUMED ack) [$cur]" - echo "$cur" | grep -q 'pending_depth=2' || fail_msg "T1: pending depth should be 2 [$cur]" + echo "$cur" | has_match -q 'observed_seq=2' || fail_msg "T1: observed_seq should be 2 after enqueue 1,2 [$cur]" + echo "$cur" | has_match -q 'consumed_seq=0' || fail_msg "T1: consumed_seq must NOT advance on enqueue (only on CONSUMED ack) [$cur]" + echo "$cur" | has_match -q 'pending_depth=2' || fail_msg "T1: pending depth should be 2 [$cur]" # consumed_seq advances only on CONSUMED. "$STORE" consume --upto 2 >/dev/null cur="$("$STORE" cursors)" - echo "$cur" | grep -q 'consumed_seq=2' || fail_msg "T1: consumed_seq should be 2 after CONSUMED 2 [$cur]" - echo "$cur" | grep -q 'pending_depth=0' || fail_msg "T1: pending drained after CONSUMED [$cur]" + echo "$cur" | has_match -q 'consumed_seq=2' || fail_msg "T1: consumed_seq should be 2 after CONSUMED 2 [$cur]" + echo "$cur" | has_match -q 'pending_depth=0' || fail_msg "T1: pending drained after CONSUMED [$cur]" ) && ok echo "== T2: digest coalesce-REPLACE vs actionable APPEND ==" @@ -116,16 +119,16 @@ echo "== T2: digest coalesce-REPLACE vs actionable APPEND ==" "$STORE" enqueue --seq 3 --class digest --locators '{"head":"bbb"}' >/dev/null "$STORE" enqueue --seq 4 --class human --locators '{"from":"peer"}' >/dev/null out="$("$STORE" drain)" - ndigest="$(printf '%s\n' "$out" | jq -c 'select(.class=="digest")' | grep -c . || true)" + ndigest="$(printf '%s\n' "$out" | jq -c 'select(.class=="digest")' | count_lines . || true)" [ "$ndigest" = "1" ] || fail_msg "T2: digest must COALESCE to a single pending entry, got $ndigest" head="$(printf '%s\n' "$out" | jq -r 'select(.class=="digest") | .locators.head')" [ "$head" = "bbb" ] || fail_msg "T2: newest digest must REPLACE prior (expected bbb, got $head)" - nactionable="$(printf '%s\n' "$out" | jq -c 'select(.class=="actionable")' | grep -c . || true)" - nhuman="$(printf '%s\n' "$out" | jq -c 'select(.class=="human")' | grep -c . || true)" + nactionable="$(printf '%s\n' "$out" | jq -c 'select(.class=="actionable")' | count_lines . || true)" + nhuman="$(printf '%s\n' "$out" | jq -c 'select(.class=="human")' | count_lines . || true)" [ "$nactionable" = "1" ] || fail_msg "T2: actionable must APPEND (never replaced), got $nactionable" [ "$nhuman" = "1" ] || fail_msg "T2: human must APPEND / stay durable, got $nhuman" # Durability never bypassed: all classes present in the durable store. - total="$(printf '%s\n' "$out" | grep -c . || true)" + total="$(printf '%s\n' "$out" | count_lines . || true)" [ "$total" = "3" ] || fail_msg "T2: durable store should hold digest+actionable+human = 3, got $total" ) && ok @@ -141,7 +144,7 @@ echo "== T3: contiguous-prefix CONSUMED (reject ack N while N-1 unconsumed) ==" fail_msg "T3: CONSUMED 3 must be REJECTED while seq 2 is a gap (unconsumed)" fi cur="$("$STORE" cursors)" - echo "$cur" | grep -q 'consumed_seq=0' || fail_msg "T3: rejected CONSUMED must NOT advance the cursor [$cur]" + echo "$cur" | has_match -q 'consumed_seq=0' || fail_msg "T3: rejected CONSUMED must NOT advance the cursor [$cur]" # Also reject via the ack wrapper. if "$ACK" consumed --upto 3 --no-sync >/dev/null 2>&1; then fail_msg "T3: ack.sh CONSUMED 3 must be REJECTED over a gap" @@ -150,11 +153,11 @@ echo "== T3: contiguous-prefix CONSUMED (reject ack N while N-1 unconsumed) ==" "$STORE" enqueue --seq 2 --class actionable --locators '{}' >/dev/null "$ACK" consumed --upto 3 --no-sync >/dev/null 2>&1 || fail_msg "T3: CONSUMED 3 should succeed once gap at 2 is filled" cur="$("$STORE" cursors)" - echo "$cur" | grep -q 'consumed_seq=3' || fail_msg "T3: consumed_seq should be 3 after contiguous prefix filled [$cur]" + echo "$cur" | has_match -q 'consumed_seq=3' || fail_msg "T3: consumed_seq should be 3 after contiguous prefix filled [$cur]" # Cumulative + can't regress: CONSUMED 2 after 3 is an idempotent no-op. "$ACK" consumed --upto 2 --no-sync >/dev/null 2>&1 || fail_msg "T3: cumulative CONSUMED 2 (<=3) should be an idempotent success" cur="$("$STORE" cursors)" - echo "$cur" | grep -q 'consumed_seq=3' || fail_msg "T3: cursor must not regress below 3 [$cur]" + echo "$cur" | has_match -q 'consumed_seq=3' || fail_msg "T3: cursor must not regress below 3 [$cur]" ) && ok echo "== T4: wake_id DELIVERY-dedup (dup delivery = re-RECEIVE, never re-action) ==" @@ -169,7 +172,7 @@ echo "== T4: wake_id DELIVERY-dedup (dup delivery = re-RECEIVE, never re-action) [ "$r2" = "DUP" ] || fail_msg "T4: duplicate delivery of same wake_id should be DUP (re-RECEIVE), got '$r2'" # RECEIVED (delivery) must NEVER advance consumed_seq (delivery != consumption). cur="$("$STORE" cursors)" - echo "$cur" | grep -q 'consumed_seq=0' || fail_msg "T4: RECEIVED must not advance consumed_seq [$cur]" + echo "$cur" | has_match -q 'consumed_seq=0' || fail_msg "T4: RECEIVED must not advance consumed_seq [$cur]" ) && ok echo "== T5: atomic write-tmp+rename survives crash mid-write ==" @@ -189,7 +192,7 @@ echo "== T5: atomic write-tmp+rename survives crash mid-write ==" drained="$("$STORE" drain)" printf '%s\n' "$drained" | jq -e . >/dev/null 2>&1 || fail_msg "T5: drain returned non-JSON — garbage temp corrupted the store" printf '%s\n' "$drained" | stream_any '.observed_seq==1' || fail_msg "T5: committed entry (seq 1) lost after simulated crash" - printf '%s\n' "$drained" | grep -q 999 && fail_msg "T5: uncommitted garbage (seq 999) leaked into the live store" + printf '%s\n' "$drained" | has_match -q 999 && fail_msg "T5: uncommitted garbage (seq 999) leaked into the live store" # A subsequent real mutation must succeed DESPITE the stale temp present. "$STORE" enqueue --seq 2 --class actionable --locators '{"issue":2}' >/dev/null || fail_msg "T5: enqueue after crash temp failed" # #927: the enqueue HOT PATH must NOT reap tmp files — an unconditional delete @@ -198,7 +201,7 @@ echo "== T5: atomic write-tmp+rename survives crash mid-write ==" # untouched by an enqueue; reaping it on the hot path is exactly the bug. [ -e "$STATE_DIR/.wake.tmp.crash12" ] || fail_msg "T5: the enqueue hot path must NOT delete tmp files off its own write (that hot-path reap was the #927 clobber)" d2="$("$STORE" drain)" - [ "$(printf '%s\n' "$d2" | grep -c .)" = "2" ] || fail_msg "T5: store not healthy after crash+recovery (expected 2 entries)" + [ "$(printf '%s\n' "$d2" | has_match -c .)" = "2" ] || fail_msg "T5: store not healthy after crash+recovery (expected 2 entries)" # Bounded accumulation is preserved via a MAINTENANCE reap (store.sh init / # detector tick), age-scoped so it only removes DEMONSTRABLY-orphaned tmps # (older than any plausible in-flight write) and never a live one. Age the @@ -209,7 +212,7 @@ echo "== T5: atomic write-tmp+rename survives crash mid-write ==" "$STORE" init >/dev/null 2>&1 || fail_msg "T5: store.sh init (maintenance) failed" [ -e "$STATE_DIR/.wake.tmp.crash12" ] && fail_msg "T5: maintenance reap (store.sh init) must remove a demonstrably-orphaned stale temp (bounded accumulation)" d3="$("$STORE" drain)" - [ "$(printf '%s\n' "$d3" | grep -c .)" = "2" ] || fail_msg "T5: store not healthy after maintenance reap (expected 2 entries)" + [ "$(printf '%s\n' "$d3" | has_match -c .)" = "2" ] || fail_msg "T5: store not healthy after maintenance reap (expected 2 entries)" printf '%s\n' "$d3" | jq -e . >/dev/null 2>&1 || fail_msg "T5: drain returned non-JSON after maintenance reap" ) && ok @@ -254,12 +257,12 @@ echo "== T6: durability survives restart (retain until CONSUMED) ==" # "Session 2": a brand-new process (this subshell invocation of store.sh) must # see the persisted entries — nothing was held in memory. d="$("$STORE" drain)" - [ "$(printf '%s\n' "$d" | grep -c .)" = "2" ] || fail_msg "T6: entries not retained across restart (expected 2)" + [ "$(printf '%s\n' "$d" | has_match -c .)" = "2" ] || fail_msg "T6: entries not retained across restart (expected 2)" printf '%s\n' "$d" | stream_any '.class=="human"' || fail_msg "T6: a human message was lost across restart (durability bypassed)" # Retained UNTIL consumed: after CONSUMED they are released. "$STORE" consume --upto 2 >/dev/null d2="$("$STORE" drain)" - n2="$(printf '%s\n' "$d2" | grep -c . || true)" + n2="$(printf '%s\n' "$d2" | count_lines . || true)" [ "$n2" = "0" ] || fail_msg "T6: entries should be released only AFTER CONSUMED (still $n2 pending)" ) && ok @@ -296,7 +299,7 @@ EOF end="$(date +%s.%N)" elapsed_ms="$(awk -v s="$start" -v e="$end" 'BEGIN { printf "%d", (e - s) * 1000 }')" [ "$elapsed_ms" -lt 1500 ] || fail_msg "T7: ack path blocked ${elapsed_ms}ms — must return without waiting on the background sync" - echo "$out" | grep -q 'CONSUMED 1' || fail_msg "T7: CONSUMED not reported [$out]" + echo "$out" | has_match -q 'CONSUMED 1' || fail_msg "T7: CONSUMED not reported [$out]" # Local write is durable IMMEDIATELY (no network needed to record the ack). STATE_DIR="$WAKE_STATE_HOME/default" jq_any "$STATE_DIR/ack-ledger.jsonl" '.type=="CONSUMED" and .upto==1' || @@ -321,8 +324,8 @@ echo "== T8: SOLE store-side allocator — enqueue (no --seq) allocates contiguo [ "$s1" = "1" ] || fail_msg "T8: first store-allocated observed_seq must be 1, got '$s1'" [ "$s2" = "2" ] || fail_msg "T8: second store-allocated observed_seq must be 2 (contiguous), got '$s2'" cur="$("$STORE" cursors)" - echo "$cur" | grep -q 'observed_seq=2' || fail_msg "T8: observed_seq cursor should be 2 [$cur]" - echo "$cur" | grep -q 'pending_depth=2' || fail_msg "T8: both allocations must be durably enqueued [$cur]" + echo "$cur" | has_match -q 'observed_seq=2' || fail_msg "T8: observed_seq cursor should be 2 [$cur]" + echo "$cur" | has_match -q 'pending_depth=2' || fail_msg "T8: both allocations must be durably enqueued [$cur]" # ANTI-SWALLOW: consume the prefix, then a LEGACY explicit --seq inside the # consumed prefix must FAIL LOUD (never the old silent seq<=consumed no-op). "$STORE" consume --upto 2 >/dev/null @@ -331,7 +334,7 @@ echo "== T8: SOLE store-side allocator — enqueue (no --seq) allocates contiguo fail_msg "T8: explicit --seq 1 <= consumed_seq 2 must FAIL LOUD (anti-swallow), not be a silent no-op" fi err="$("$STORE" enqueue --seq 1 --class actionable --locators '{}' 2>&1 || true)" - echo "$err" | grep -qi 'anti-swallow' || fail_msg "T8: the refusal must name the anti-swallow guarantee [$err]" + echo "$err" | has_match -qi 'anti-swallow' || fail_msg "T8: the refusal must name the anti-swallow guarantee [$err]" after_depth="$("$STORE" cursors | sed -n 's/pending_depth=//p')" [ "$before_depth" = "$after_depth" ] || fail_msg "T8: a refused enqueue must not mutate the store (depth $before_depth->$after_depth)" # And the NEXT real allocation is > consumed (2) — never inside the prefix. @@ -443,8 +446,8 @@ if command -v flock >/dev/null 2>&1; then [ -n "$a" ] && [ -n "$b" ] || fail_msg "T10: both concurrent enqueues must print an allocated seq (got '$a','$b')" [ "$a" != "$b" ] || fail_msg "T10: two concurrent enqueues must get DISTINCT seqs, both got '$a' (aliasing / lost write without the lock)" cur="$("$STORE" cursors)" - echo "$cur" | grep -q 'observed_seq=2' || fail_msg "T10: observed_seq must reach 2 after two enqueues [$cur]" - echo "$cur" | grep -q 'pending_depth=2' || fail_msg "T10: both entries must be durably stored (no lost write) [$cur]" + echo "$cur" | has_match -q 'observed_seq=2' || fail_msg "T10: observed_seq must reach 2 after two enqueues [$cur]" + echo "$cur" | has_match -q 'pending_depth=2' || fail_msg "T10: both entries must be durably stored (no lost write) [$cur]" # The two allocated seqs are exactly {1,2} (distinct, contiguous, gapless). lo="$a"; hi="$b"; [ "$a" -gt "$b" ] && { lo="$b"; hi="$a"; } { [ "$lo" = "1" ] && [ "$hi" = "2" ]; } || fail_msg "T10: concurrent seqs must be {1,2}, got {$lo,$hi}" @@ -492,7 +495,7 @@ echo "== T11: final observed_seq cursor write FAILURE — fail loud + observed.s # the _atomic_write failure and returns 0). [ "$rc" -ne 0 ] || fail_msg "T11: an enqueue whose FINAL cursor write fails must EXIT NON-ZERO (fail loud), got rc=$rc" # The loud diagnostic names the cursor write (not a generic error). - grep -qi 'cursor' "$errfile" || fail_msg "T11: the failure diagnostic must name the observed_seq cursor write [$(cat "$errfile")]" + has_match -qi 'cursor' "$errfile" || fail_msg "T11: the failure diagnostic must name the observed_seq cursor write [$(cat "$errfile")]" # (2) The cursor did NOT advance — the allocation is NOT committed. obs_after="$("$STORE" cursors | sed -n 's/^observed_seq=//p')" @@ -536,7 +539,7 @@ echo "== T12: #932 — consume records the last-consumed observed_hash per (kind r2h="$(jq -sr '[ .[] | select(.kind=="repo" and .id=="r2") ] | .[0].observed_hash' "$rec" 2>/dev/null)" [ "$r2h" = "HASH-C" ] || fail_msg "T12: repo/r2 must record HASH-C, got '$r2h'" # ADDITIVE: existing on-disk state files are unchanged/consistent post-consume. - "$STORE" cursors | grep -q 'consumed_seq=3' || fail_msg "T12: consumed_seq must be 3 after CONSUMED 3" + "$STORE" cursors | has_match -q 'consumed_seq=3' || fail_msg "T12: consumed_seq must be 3 after CONSUMED 3" ) && ok echo "== T13: #946 — ordinary consume REFUSES to advance past a quarantined seq (store + ack paths) ==" @@ -555,10 +558,10 @@ echo "== T13: #946 — ordinary consume REFUSES to advance past a quarantined se fail_msg "T13: ordinary consume --upto 3 must be REFUSED while seq 2 is quarantined" fi err="$("$STORE" consume --upto 3 2>&1 >/dev/null || true)" - echo "$err" | grep -q 'quarantined seq(s): 2' || fail_msg "T13: the refusal must NAME the quarantined seq [$err]" - echo "$err" | grep -q -- '--force-past-quarantine' || fail_msg "T13: the refusal must NAME the force flag [$err]" + echo "$err" | has_match -q 'quarantined seq(s): 2' || fail_msg "T13: the refusal must NAME the quarantined seq [$err]" + echo "$err" | has_match -q -- '--force-past-quarantine' || fail_msg "T13: the refusal must NAME the force flag [$err]" cur="$("$STORE" cursors)" - echo "$cur" | grep -q 'consumed_seq=0' || fail_msg "T13: a refused consume must NOT advance the cursor [$cur]" + echo "$cur" | has_match -q 'consumed_seq=0' || fail_msg "T13: a refused consume must NOT advance the cursor [$cur]" # BELOW the quarantined seq the ordinary path is unaffected. "$STORE" consume --upto 1 >/dev/null 2>&1 || fail_msg "T13: consume --upto 1 (below the quarantined seq) must succeed" # The ack wrapper propagates the refusal — no ordinary-path bypass exists. @@ -566,7 +569,7 @@ echo "== T13: #946 — ordinary consume REFUSES to advance past a quarantined se fail_msg "T13: ack.sh consumed --upto 3 must be REFUSED while seq 2 is quarantined (ordinary-path bypass)" fi cur="$("$STORE" cursors)" - echo "$cur" | grep -q 'consumed_seq=1' || fail_msg "T13: cursor must still be 1 after the refused ack [$cur]" + echo "$cur" | has_match -q 'consumed_seq=1' || fail_msg "T13: cursor must still be 1 after the refused ack [$cur]" ) && ok echo "== T14: #946 — FORCED step-over is LOUD, prunes the set, and NEVER fabricates a consumed-hash row for the quarantined entry ==" @@ -585,9 +588,9 @@ echo "== T14: #946 — FORCED step-over is LOUD, prunes the set, and NEVER fabri rc=$? [ "$rc" -eq 0 ] || fail_msg "T14: forced consume must succeed (rc=$rc) [$(cat "$errf")]" [ "$out" = "3" ] || fail_msg "T14: forced consume must print the new cursor 3, got '$out'" - grep -q 'FORCED PAST QUARANTINE' "$errf" || fail_msg "T14: the forced path must be LOUD on stderr [$(cat "$errf")]" - grep -q 'seq 2' "$errf" || fail_msg "T14: the forced-path diagnostic must name the stepped-over seq 2 [$(cat "$errf")]" - "$STORE" cursors | grep -q 'consumed_seq=3' || fail_msg "T14: forced consume must advance the cursor to 3" + has_match -q 'FORCED PAST QUARANTINE' "$errf" || fail_msg "T14: the forced path must be LOUD on stderr [$(cat "$errf")]" + has_match -q 'seq 2' "$errf" || fail_msg "T14: the forced-path diagnostic must name the stepped-over seq 2 [$(cat "$errf")]" + "$STORE" cursors | has_match -q 'consumed_seq=3' || fail_msg "T14: forced consume must advance the cursor to 3" # NO FALSE WITNESS: the quarantined entry (repo/b) was NEVER delivered, so no # consumed-hash row may exist for it — even on the forced path (the reconciler # re-enumerating it once is safe-but-noisy; a false witness silences it @@ -597,7 +600,7 @@ echo "== T14: #946 — FORCED step-over is LOUD, prunes the set, and NEVER fabri jq_any "$rec" '.kind=="repo" and .id=="b"' && fail_msg "T14: the quarantined entry repo/b must have NO consumed-hash row (a row would witness a delivery that never happened)" # The stepped-over seq is PRUNED from the set (it is consumed now; a stale # entry would re-refuse forever). - grep -qxF '2' "$qf" 2>/dev/null && fail_msg "T14: seq 2 must be PRUNED from quarantined.set after the forced step-over" + has_match -qxF '2' "$qf" 2>/dev/null && fail_msg "T14: seq 2 must be PRUNED from quarantined.set after the forced step-over" # The ack wrapper's force flag passes through, stays LOUD on stderr, and # still reports a CLEAN cursor line on stdout. "$STORE" enqueue --class actionable --locators '{"kind":"repo","id":"d","observed_hash":"HD"}' >/dev/null @@ -607,8 +610,8 @@ echo "== T14: #946 — FORCED step-over is LOUD, prunes the set, and NEVER fabri out2="$("$ACK" consumed --upto 5 --no-sync --force-past-quarantine 2>"$errf2")" rc2=$? [ "$rc2" -eq 0 ] || fail_msg "T14: forced ack must succeed (rc=$rc2) [$(cat "$errf2")]" - echo "$out2" | grep -q '^CONSUMED 5$' || fail_msg "T14: forced ack must report a CLEAN cursor line 'CONSUMED 5', got '$out2'" - grep -q 'FORCED PAST QUARANTINE' "$errf2" || fail_msg "T14: the forced-path loudness must survive the ack wrapper (stderr) [$(cat "$errf2")]" + echo "$out2" | has_match -q '^CONSUMED 5$' || fail_msg "T14: forced ack must report a CLEAN cursor line 'CONSUMED 5', got '$out2'" + has_match -q 'FORCED PAST QUARANTINE' "$errf2" || fail_msg "T14: the forced-path loudness must survive the ack wrapper (stderr) [$(cat "$errf2")]" jq_any "$rec" '.kind=="repo" and .id=="e"' && fail_msg "T14: the quarantined repo/e must have NO consumed-hash row via the forced ack path either" true ) && ok @@ -671,10 +674,10 @@ echo "== T16: #946 — quarantine-audit: a consumed-hash row matching a dead-let if "$STORE" quarantine-audit >"$rep" 2>&1; then fail_msg "T16: report-mode audit must exit NON-ZERO when false rows exist" fi - grep -q 'FALSE WITNESS' "$rep" || fail_msg "T16: the audit must name the false row loudly [$(cat "$rep")]" - grep -q '"id":"X"' "$rep" || fail_msg "T16: the audit must identify the false row (repo/X@1) [$(cat "$rep")]" - grep -q '"id":"Y"' "$rep" && fail_msg "T16: the clean row repo/Y must NOT be flagged" - grep -q '"id":"Z"' "$rep" && fail_msg "T16: the HEALED row repo/Z@4 must NOT be flagged (its dead-letter evidence is seq 3 with a different hash)" + has_match -q 'FALSE WITNESS' "$rep" || fail_msg "T16: the audit must name the false row loudly [$(cat "$rep")]" + has_match -q '"id":"X"' "$rep" || fail_msg "T16: the audit must identify the false row (repo/X@1) [$(cat "$rep")]" + has_match -q '"id":"Y"' "$rep" && fail_msg "T16: the clean row repo/Y must NOT be flagged" + has_match -q '"id":"Z"' "$rep" && fail_msg "T16: the HEALED row repo/Z@4 must NOT be flagged (its dead-letter evidence is seq 3 with a different hash)" # Report mode modifies nothing. jq_any "$rec" '.id=="X"' || fail_msg "T16: report mode must not modify the record" # REPAIR: exactly the false row is removed; the dead-letter LEDGER is history @@ -683,10 +686,10 @@ echo "== T16: #946 — quarantine-audit: a consumed-hash row matching a dead-let jq_any "$rec" '.id=="X"' && fail_msg "T16: --repair must REMOVE the provably-false X row" jq_any "$rec" '.id=="Y" and .observed_hash=="HY"' || fail_msg "T16: --repair must keep the clean Y row" jq_any "$rec" '.id=="Z" and .observed_hash=="HZ-NEW" and .observed_seq==4' || fail_msg "T16: --repair must keep the healed Z@4 row" - [ "$(grep -c . "$dl")" = "2" ] || fail_msg "T16: the dead-letter LEDGER must be untouched by --repair" + [ "$(has_match -c . "$dl")" = "2" ] || fail_msg "T16: the dead-letter LEDGER must be untouched by --repair" # Clean re-audit: OK, exit 0. "$STORE" quarantine-audit >"$TMP_ROOT/t16.ok" 2>&1 || fail_msg "T16: a clean audit must exit 0 [$(cat "$TMP_ROOT/t16.ok")]" - grep -qi 'OK' "$TMP_ROOT/t16.ok" || fail_msg "T16: a clean audit must say OK [$(cat "$TMP_ROOT/t16.ok")]" + has_match -qi 'OK' "$TMP_ROOT/t16.ok" || fail_msg "T16: a clean audit must say OK [$(cat "$TMP_ROOT/t16.ok")]" ) && ok echo "== T17: #952 — the clean-sweep message names BOTH unprovable residual classes; a surviving-but-empty-hash dead-letter row is correctly NOT convicted ==" @@ -722,20 +725,20 @@ echo "== T17: #952 — the clean-sweep message names BOTH unprovable residual cl # match can never fire: this is unprovable class 2, NOT a false witness. rep="$TMP_ROOT/t17.rep" "$STORE" quarantine-audit >"$rep" 2>&1 || fail_msg "T17: the audit must exit 0 — nothing here is provable [$(cat "$rep")]" - grep -q 'FALSE WITNESS' "$rep" && fail_msg "T17: the empty-hash evidence must NOT convict (the predicate is correct and must not change) [$(cat "$rep")]" + has_match -q 'FALSE WITNESS' "$rep" && fail_msg "T17: the empty-hash evidence must NOT convict (the predicate is correct and must not change) [$(cat "$rep")]" # The wording under test (#952): BOTH residual classes, named. - grep -qi 'pruned' "$rep" || fail_msg "T17: clean sweep must name residual class 1 — evidence pruned away [$(cat "$rep")]" - grep -qi 'empty observed_hash' "$rep" || fail_msg "T17: clean sweep must name residual class 2 — surviving evidence with an empty observed_hash [$(cat "$rep")]" + has_match -qi 'pruned' "$rep" || fail_msg "T17: clean sweep must name residual class 1 — evidence pruned away [$(cat "$rep")]" + has_match -qi 'empty observed_hash' "$rep" || fail_msg "T17: clean sweep must name residual class 2 — surviving evidence with an empty observed_hash [$(cat "$rep")]" # --repair on a clean sweep removes nothing: the unprovable row survives. "$STORE" quarantine-audit --repair >"$TMP_ROOT/t17.fix" 2>&1 || fail_msg "T17: --repair on a clean sweep must exit 0 [$(cat "$TMP_ROOT/t17.fix")]" jq_any "$rec" '.kind=="bench" and .observed_seq==13 and .observed_hash=="H-REAL-CONSUMED"' || fail_msg "T17: --repair must NOT remove the unprovable bench@13 row — the audit only removes what the ledger can convict" - [ "$(grep -c . "$dl")" = "1" ] || fail_msg "T17: the dead-letter LEDGER must be untouched" + [ "$(count_lines . "$dl")" = "1" ] || fail_msg "T17: the dead-letter LEDGER must be untouched" ) && ok echo if [ -s "$FAILFILE" ]; then - echo "wake store/ack harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2 + echo "wake store/ack harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 exit 1 fi echo "wake store/ack harness: all invariants passed ($pass groups)" diff --git a/packages/mosaic/framework/tools/wake/test-wake-store-enqueue-race.sh b/packages/mosaic/framework/tools/wake/test-wake-store-enqueue-race.sh index 9d738081..94b19785 100755 --- a/packages/mosaic/framework/tools/wake/test-wake-store-enqueue-race.sh +++ b/packages/mosaic/framework/tools/wake/test-wake-store-enqueue-race.sh @@ -32,6 +32,9 @@ set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness. +# shellcheck disable=SC1091 +. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init STORE="$SCRIPT_DIR/store.sh" command -v jq >/dev/null 2>&1 || { @@ -147,7 +150,7 @@ EOF wait "$pa" 2>/dev/null || true else # Confirm the window is genuinely open: A holds a live in-flight tmp now. - n_tmp="$(find "$STATE_DIR" -maxdepth 1 -name '.wake.tmp.*' -type f | grep -c . || true)" + n_tmp="$(find "$STATE_DIR" -maxdepth 1 -name '.wake.tmp.*' -type f | count_lines . || true)" [ "$n_tmp" -ge 1 ] || fail_msg "T-RACE: expected A's live in-flight tmp to exist while A holds the lock (got $n_tmp)" # B: its PRE-LOCK _wake_init_dir runs now (pre-fix: deletes A's live tmp), @@ -173,7 +176,7 @@ EOF if [ "$ra" -ne 0 ]; then fail_msg "T-RACE: enqueue A was SPURIOUSLY ABORTED (rc=$ra) — its live in-flight tmp was deleted by B's pre-lock cleanup [#927]. stderr: $(tr '\n' ' ' <"$a_err")" fi - if grep -q 'durable pending write FAILED' "$a_err" 2>/dev/null; then + if has_match -q 'durable pending write FAILED' "$a_err" 2>/dev/null; then fail_msg "T-RACE: enqueue A reported 'durable pending write FAILED' — the exact #927 spurious abort (its tmp was clobbered mid-write by a concurrent pre-lock cleanup)." fi [ "$rb" -eq 0 ] || fail_msg "T-RACE: enqueue B should also succeed (rc=$rb). stderr: $(tr '\n' ' ' <"$b_err")" @@ -189,20 +192,20 @@ EOF # #908 store invariants: both durably landed, cursor reached 2, no burned seq. cur="$("$STORE" cursors)" - echo "$cur" | grep -q 'observed_seq=2' || fail_msg "T-RACE: observed_seq must reach 2 (both allocations committed) [$cur]" - echo "$cur" | grep -q 'pending_depth=2' || fail_msg "T-RACE: both entries must be durably stored — no lost/aborted write [$cur]" - echo "$cur" | grep -q 'consumed_seq=0' || fail_msg "T-RACE: enqueue must never advance consumed_seq [$cur]" + echo "$cur" | has_match -q 'observed_seq=2' || fail_msg "T-RACE: observed_seq must reach 2 (both allocations committed) [$cur]" + echo "$cur" | has_match -q 'pending_depth=2' || fail_msg "T-RACE: both entries must be durably stored — no lost/aborted write [$cur]" + echo "$cur" | has_match -q 'consumed_seq=0' || fail_msg "T-RACE: enqueue must never advance consumed_seq [$cur]" # Gapless: the contiguous prefix 1..2 is consumable (no interior gap/burn). "$STORE" consume --upto 2 >/dev/null 2>&1 || fail_msg "T-RACE: CONSUMED 2 must succeed — seqs {1,2} are gapless (no burned seq)" # No stale tmp left leaking either. - n_leak="$(find "$STATE_DIR" -maxdepth 1 -name '.wake.tmp.*' -type f | grep -c . || true)" + n_leak="$(find "$STATE_DIR" -maxdepth 1 -name '.wake.tmp.*' -type f | count_lines . || true)" [ "$n_leak" = "0" ] || fail_msg "T-RACE: $n_leak stale tmp file(s) leaked after both enqueues committed" fi ) && ok echo if [ -s "$FAILFILE" ]; then - echo "wake store enqueue-race harness: FAILED ($(grep -c . "$FAILFILE") assertion(s)) — #927 TOCTOU reproduced (RED)" >&2 + echo "wake store enqueue-race harness: FAILED ($(count_lines . "$FAILFILE") assertion(s)) — #927 TOCTOU reproduced (RED)" >&2 exit 1 fi echo "wake store enqueue-race harness: all invariants passed ($pass group) — #927 race closed (GREEN)" diff --git a/packages/mosaic/framework/tools/wake/validate-973/convert-973.py b/packages/mosaic/framework/tools/wake/validate-973/convert-973.py new file mode 100644 index 00000000..e696e8f6 --- /dev/null +++ b/packages/mosaic/framework/tools/wake/validate-973/convert-973.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""convert-973.py — mechanical #973 site conversion, driven by the frozen +denominator artifact (denominator-089615f.json), never by ad-hoc grepping. + +For every row in the artifact it FIRST verifies the worktree line still says +what the artifact froze (exact match after whitespace strip, artifact-side +truncation as prefix match, or the normalized suite-summary template), and +aborts before touching anything on the first verification failure — a +conversion applied to a line the denominator did not measure would be the +umbrella defect wearing the converter's clothes. + +Transforms (behaviour-preserving; verdict semantics unchanged on grep rc 0/1): + E-count-capture `grep -c ARGS` -> `count_lines ARGS` (helper adds -c) + all other forms `grep ARGS` -> `has_match ARGS` (drop-in) + env prefixes (`LC_ALL=C grep`) are kept — the prefix reaches the grep child + through the function (microtest C8). + +The two multi-grep pipeline sites (digest-quarantine:560, install:420) convert +BOTH greps: each is measurement-bearing, and under pipefail an rc=2 in the +left element is masked by an rc=1 in the right — the same defect one pipe +deeper. They stay ONE denominator site each (one coordinate); the ledger +records helper calls, so those coordinates appear twice per execution and the +equality check compares SETS of coordinates. + +Finally each suite gains three header lines directly after its SCRIPT_DIR +assignment (source + wake_assert_init + comment), shifting every site by +3 +lines exactly; the validation harness maps base coordinates accordingly. +""" + +import json +import re +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +WAKE = HERE.parent +ART = HERE / "denominator-089615f.json" + +RX_GREP_TOKEN = re.compile(r"(^|[^A-Za-z0-9_.-])grep([^A-Za-z0-9_.-]|$)") + +HEADER = [ + "# #973: three-valued grep assertion helpers (has_match/count_lines); init saves real stderr for abort loudness.\n", + "# shellcheck disable=SC1091\n", + '. "$SCRIPT_DIR/_wake-common.sh" && wake_assert_init\n', +] + +MULTI_GREP_CONVERT_BOTH = { + ("test-wake-digest-quarantine.sh", 560), + ("test-wake-install.sh", 420), +} + +SUMMARY_TEMPLATE_MARK = '$(grep -c . "$FAILFILE")' + + +def verify(row, actual): + a = actual.strip() + t = row["text"].strip() + if a == t: + return True + if t and a.startswith(t): # artifact-side truncation + return True + # normalized suite-summary template rows + if t.startswith('echo "wake ') and SUMMARY_TEMPLATE_MARK in actual and a.startswith('echo "wake '): + return True + return False + + +def convert_line(row, line): + key = (row["file"], row["line"]) + n_grep = len(RX_GREP_TOKEN.findall(line)) + if key in MULTI_GREP_CONVERT_BOTH: + assert n_grep == 2, f"{key}: expected 2 grep tokens, found {n_grep}" + else: + assert n_grep == 1, f"{key}: expected 1 grep token, found {n_grep}: {line!r}" + + if row["form"].startswith("E"): + assert line.count("grep -c ") == 1, f"{key}: E row without single 'grep -c ': {line!r}" + return line.replace("grep -c ", "count_lines ", 1) + + def repl(m): + return m.group(1) + "has_match" + m.group(2) + + count = 2 if key in MULTI_GREP_CONVERT_BOTH else 1 + return RX_GREP_TOKEN.sub(repl, line, count=count) + + +def main(): + art = json.loads(ART.read_text()) + rows = art["rows"] + by_file = {} + for r in rows: + by_file.setdefault(r["file"], []).append(r) + + # pass 1: verify every row before touching any file + bad = 0 + texts = {} + for f, frs in by_file.items(): + lines = (WAKE / f).read_text().split("\n") + texts[f] = lines + for r in frs: + if not verify(r, lines[r["line"] - 1]): + bad += 1 + print(f"VERIFY-FAIL {f}:{r['line']}\n artifact: {r['text']!r}\n worktree: {lines[r['line'] - 1]!r}") + if bad: + sys.exit(f"ABORT: {bad} row(s) failed verification; nothing was modified.") + + # pass 2: convert + insert header + total = {"has_match": 0, "count_lines": 0} + for f, frs in sorted(by_file.items()): + lines = texts[f] + for r in frs: + i = r["line"] - 1 + new = convert_line(r, lines[i]) + assert new != lines[i], f"{f}:{r['line']}: no-op conversion" + lines[i] = new + total["count_lines" if r["form"].startswith("E") else "has_match"] += 1 + # header insertion after the SCRIPT_DIR= line + sd = [i for i, ln in enumerate(lines) if ln.startswith('SCRIPT_DIR="$(')] + assert len(sd) == 1, f"{f}: expected exactly one SCRIPT_DIR line, found {len(sd)}" + first_site = min(r["line"] for r in frs) - 1 + assert sd[0] < first_site, f"{f}: SCRIPT_DIR line {sd[0] + 1} not before first site {first_site + 1}" + lines[sd[0] + 1 : sd[0] + 1] = [h.rstrip("\n") for h in HEADER] + (WAKE / f).write_text("\n".join(lines)) + print(f"{f}: {len(frs)} sites converted, header at line {sd[0] + 2}") + + print(f"TOTAL: {total['has_match']} has_match + {total['count_lines']} count_lines = {sum(total.values())} sites in {len(by_file)} files") + assert sum(total.values()) == art["total"] == 261, "site count mismatch vs artifact" + + +if __name__ == "__main__": + main()