Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1806b3a57a | ||
|
|
fc80138326 | ||
|
|
8fdc8738ed | ||
|
|
8310075d33 | ||
|
|
b0fb208b89 | ||
|
|
62e3bf9e28 | ||
|
|
b7d391f9f4 | ||
|
|
1036449b57 | ||
|
|
63c5d2056f | ||
|
|
1288ce3721 | ||
|
|
92272a6400 |
@@ -109,55 +109,6 @@ else
|
||||
detect_platform >/dev/null
|
||||
fi
|
||||
|
||||
# Render the provider's own explanation for a failed request, for appending to
|
||||
# an error message (#1004). Every HTTP arm in this file already has the response
|
||||
# body on disk; without this it was discarded unread at exactly the moment the
|
||||
# caller needed it, which pushes an operator toward re-issuing the request by
|
||||
# hand to find out what the server said. Gitea returns {"message": "..."} on a
|
||||
# refusal; anything unparseable falls back to a truncated raw first line so a
|
||||
# proxy's HTML error page still says something. Prints "" when there is nothing
|
||||
# to add, so callers can interpolate unconditionally.
|
||||
#
|
||||
# Args: $1 = path to the response body file.
|
||||
gitea_error_detail() {
|
||||
local body_file="$1"
|
||||
[[ -s "$body_file" ]] || return 0
|
||||
python3 - "$body_file" <<'PY' 2>/dev/null || true
|
||||
import json
|
||||
import sys
|
||||
|
||||
LIMIT = 300
|
||||
try:
|
||||
with open(sys.argv[1], encoding="utf-8", errors="replace") as response:
|
||||
raw = response.read().strip()
|
||||
except OSError:
|
||||
raise SystemExit(0)
|
||||
if not raw:
|
||||
raise SystemExit(0)
|
||||
detail = ""
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
if isinstance(parsed, dict):
|
||||
for key in ("message", "error", "errors"):
|
||||
value = parsed.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
detail = value.strip()
|
||||
break
|
||||
if isinstance(value, list) and value:
|
||||
detail = "; ".join(str(item) for item in value).strip()
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
if not detail:
|
||||
detail = raw.splitlines()[0].strip()
|
||||
if not detail:
|
||||
raise SystemExit(0)
|
||||
if len(detail) > LIMIT:
|
||||
detail = detail[:LIMIT] + "..."
|
||||
print(f" — provider said: {detail}")
|
||||
PY
|
||||
}
|
||||
|
||||
# Post a comment to a Gitea PR (PR comments ARE issue comments) via the
|
||||
# supported REST API and verify it against a PROVIDER-RETURNED created id. The
|
||||
# write is a direct POST that returns the created comment object, so we learn
|
||||
@@ -199,7 +150,7 @@ print(json.dumps({"body": os.environ["COMMENT_BODY"]}))
|
||||
return 1
|
||||
fi
|
||||
if [[ "$write_status" != "201" ]]; then
|
||||
echo "Error: Gitea comment write failed with HTTP $write_status$(gitea_error_detail "$write_file")" >&2
|
||||
echo "Error: Gitea comment write failed with HTTP $write_status" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -228,7 +179,7 @@ PY
|
||||
return 1
|
||||
fi
|
||||
if [[ "$readback_status" != "200" ]]; then
|
||||
echo "Error: Gitea comment read-back failed with HTTP $readback_status$(gitea_error_detail "$readback_file")" >&2
|
||||
echo "Error: Gitea comment read-back failed with HTTP $readback_status" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -419,7 +370,7 @@ gitea_authenticated_login() {
|
||||
return 1
|
||||
fi
|
||||
if [[ "$status" != "200" ]]; then
|
||||
echo "Error: Gitea authenticated-identity read failed with HTTP $status$(gitea_error_detail "$response_file")" >&2
|
||||
echo "Error: Gitea authenticated-identity read failed with HTTP $status" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -456,7 +407,7 @@ gitea_read_pr_head_into() {
|
||||
return 1
|
||||
fi
|
||||
if [[ "$status" != "200" ]]; then
|
||||
echo "Error: Gitea PR head read failed with HTTP $status$(gitea_error_detail "$pr_file")" >&2
|
||||
echo "Error: Gitea PR head read failed with HTTP $status" >&2
|
||||
return 1
|
||||
fi
|
||||
python3 - "$pr_file" <<'PY'
|
||||
@@ -546,7 +497,7 @@ print(json.dumps({
|
||||
fi
|
||||
# Gitea returns 200 (occasionally 201) with the created review object.
|
||||
if [[ "$write_status" != "200" && "$write_status" != "201" ]]; then
|
||||
echo "Error: Gitea review submit failed with HTTP $write_status$(gitea_error_detail "$write_file")" >&2
|
||||
echo "Error: Gitea review submit failed with HTTP $write_status (#865: no durable review created)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -575,7 +526,7 @@ PY
|
||||
return 1
|
||||
fi
|
||||
if [[ "$readback_status" != "200" ]]; then
|
||||
echo "Error: Gitea review read-back failed with HTTP $readback_status$(gitea_error_detail "$readback_file")" >&2
|
||||
echo "Error: Gitea review read-back failed with HTTP $readback_status" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -7,40 +7,14 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/gitea-login-resolution}"
|
||||
REPO_DIR="$WORK_DIR/repo"
|
||||
BIN_DIR="$WORK_DIR/bin"
|
||||
HOME_DIR="$WORK_DIR/home"
|
||||
LOG_FILE="$WORK_DIR/calls.log"
|
||||
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$REPO_DIR" "$BIN_DIR" "$HOME_DIR"
|
||||
mkdir -p "$REPO_DIR" "$BIN_DIR"
|
||||
|
||||
git -C "$REPO_DIR" init -q
|
||||
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
|
||||
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
|
||||
#
|
||||
# OPERATIVE: the empty repo-local `mosaic.gitIdentity` below. get_gitea_token()
|
||||
# step 0 resolves a per-agent identity from `git config --get mosaic.gitIdentity`,
|
||||
# which on a provisioned agent seat is set GLOBALLY and so leaks into this fresh
|
||||
# repo. It then reads a REAL per-slot token from $HOME and returns it WITHOUT ever
|
||||
# consulting MOSAIC_CREDENTIALS_FILE, so the fixture credentials below are silently
|
||||
# ignored. This suite is the one where the consequence is not subtle: it FAILS
|
||||
# outright on a provisioned seat (rc=1 bare, rc=0 with $HOME sandboxed, one
|
||||
# variable changed) and passes everywhere else, including CI, which has no
|
||||
# per-agent token to leak.
|
||||
#
|
||||
# CONTAINMENT: the sandboxed HOME in the four run helpers below. It only has to
|
||||
# bound a failure that the pin should already have prevented.
|
||||
#
|
||||
# NOTE FOR ANYONE AUDITING THIS SUITE: the sandboxed HOME is containment, NOT an
|
||||
# assay. Running a suite under a decoy HOME to test for this defect REMOVES the
|
||||
# trigger — ~/.gitconfig is where the global identity lives, so step 0 is skipped
|
||||
# by construction and every suite reads clean however vulnerable it is. To measure,
|
||||
# REPLICATE a seat (a decoy HOME whose .gitconfig sets mosaic.gitIdentity, with no
|
||||
# per-slot token) so step 0 reaches its fail-loud branch.
|
||||
#
|
||||
# Note the env-var route does NOT work: detect-platform.sh reads
|
||||
# "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset.
|
||||
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
||||
|
||||
cat > "$CREDENTIALS_FILE" <<'JSON'
|
||||
{
|
||||
@@ -112,7 +86,6 @@ run_in_repo() {
|
||||
(
|
||||
cd "$REPO_DIR"
|
||||
PATH="$BIN_DIR:$PATH" \
|
||||
HOME="$HOME_DIR" \
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||
MOSAIC_TEST_LOG="$LOG_FILE" \
|
||||
"$@"
|
||||
@@ -310,7 +283,6 @@ run_in_repo2() {
|
||||
(
|
||||
cd "$REPO_DIR"
|
||||
PATH="$BIN_DIR2:$PATH" \
|
||||
HOME="$HOME_DIR" \
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||
MOSAIC_TEST_LOG="$LOG_FILE" \
|
||||
"$@"
|
||||
@@ -371,7 +343,7 @@ write_fixture() { printf '%s' "$1" > "$FIXTURE_XDG/tea/config.yml"; }
|
||||
token_fallback() {
|
||||
(
|
||||
cd "$REPO_DIR"
|
||||
HOME="$HOME_DIR" XDG_CONFIG_HOME="$FIXTURE_XDG" PYTHONPATH="$NOYAML_DIR" bash -c '
|
||||
XDG_CONFIG_HOME="$FIXTURE_XDG" PYTHONPATH="$NOYAML_DIR" bash -c '
|
||||
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
||||
get_gitea_token_for_login "$1" "$2"
|
||||
' _ "$1" "$2"
|
||||
@@ -382,7 +354,7 @@ token_fallback() {
|
||||
token_pyyaml() {
|
||||
(
|
||||
cd "$REPO_DIR"
|
||||
HOME="$HOME_DIR" XDG_CONFIG_HOME="$FIXTURE_XDG" bash -c '
|
||||
XDG_CONFIG_HOME="$FIXTURE_XDG" bash -c '
|
||||
source "'"$SCRIPT_DIR"'/detect-platform.sh"
|
||||
get_gitea_token_for_login "$1" "$2"
|
||||
' _ "$1" "$2"
|
||||
|
||||
@@ -61,54 +61,15 @@ STATE_FILE="$WORK_DIR/comments.json"
|
||||
# A dedicated scratch dir the wrapper is pointed at via TMPDIR, so the leak
|
||||
# check can assert every POST/GET body + metadata temp file is cleaned up.
|
||||
TMP_SCRATCH="$WORK_DIR/scratch"
|
||||
HOME_DIR="$WORK_DIR/home"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$REPO_DIR" "$BIN_DIR" "$XDG_DIR" "$TMP_SCRATCH" "$HOME_DIR"
|
||||
mkdir -p "$REPO_DIR" "$BIN_DIR" "$XDG_DIR" "$TMP_SCRATCH"
|
||||
git -C "$REPO_DIR" init -q
|
||||
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
|
||||
#
|
||||
# OPERATIVE: the empty repo-local `mosaic.gitIdentity` below. get_gitea_token()
|
||||
# step 0 resolves a per-agent identity from `git config --get mosaic.gitIdentity`,
|
||||
# which on a provisioned agent seat is set GLOBALLY and so leaks into this fresh
|
||||
# repo. It then reads a REAL per-slot token from $HOME and returns it WITHOUT ever
|
||||
# consulting MOSAIC_CREDENTIALS_FILE, so the fixture credential below is silently
|
||||
# ignored. The stub curl then rejects the unrecognised bearer, and this suite
|
||||
# fails at its FIRST case with `Gitea authenticated-identity read failed with
|
||||
# HTTP 401`. An empty repo-local value shadows the global one and reads back
|
||||
# empty at rc=0. Measured: without this pin the suite is RED on every seat.
|
||||
#
|
||||
# CONTAINMENT: the sandboxed HOME in run_comment(). It only has to bound a
|
||||
# failure that the pin should already have prevented.
|
||||
#
|
||||
# THIS SUITE WAS THE HARDEST OF THE FIVE TO SEE, and the reason is worth stating
|
||||
# because it generalises: run_comment() sends the wrapper's stdout AND stderr to
|
||||
# $OUTPUT_FILE, and the EXIT trap above deletes $WORK_DIR. So the 401 — the only
|
||||
# thing that says what went wrong — exists only inside a directory that is gone
|
||||
# by the time anyone looks. The suite exits 1 with ZERO bytes on stdout and
|
||||
# stderr. A suite that discards or deletes its own evidence turns any post-hoc
|
||||
# assay into a non-measurement: "nothing found" there means "no surviving
|
||||
# trace", never "clean". It was found by intercepting the identity read at its
|
||||
# SOURCE (a PATH shim over `git` logging every `mosaic.gitIdentity` read to a
|
||||
# file outside $WORK_DIR), which is deletion-proof by construction, rather than
|
||||
# by grepping for the symptom.
|
||||
#
|
||||
# NOTE FOR ANYONE AUDITING THIS SUITE: the sandboxed HOME is containment, NOT an
|
||||
# assay. Running a suite under a decoy HOME to test for this defect REMOVES the
|
||||
# trigger — ~/.gitconfig is where the global identity lives, so step 0 is skipped
|
||||
# by construction and every suite reads clean however vulnerable it is. To
|
||||
# measure, REPLICATE a seat (a decoy HOME whose .gitconfig sets
|
||||
# mosaic.gitIdentity, with no per-slot token) so step 0 reaches its fail-loud
|
||||
# branch — or intercept the read as described above.
|
||||
#
|
||||
# Note the env-var route does NOT work: detect-platform.sh reads
|
||||
# "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset.
|
||||
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
||||
|
||||
ISSUE_NUMBER=7
|
||||
REPO_SLUG="mosaicstack/stack"
|
||||
@@ -405,7 +366,6 @@ run_comment() {
|
||||
cd "$REPO_DIR"
|
||||
PATH="$BIN_DIR:$PATH" \
|
||||
TMPDIR="$TMP_SCRATCH" \
|
||||
HOME="$HOME_DIR" \
|
||||
XDG_CONFIG_HOME="$XDG_DIR" \
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||
ISSUE_COMMENT_TEA_LOG="$TEA_LOG" \
|
||||
|
||||
@@ -7,38 +7,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/issue-create-interactive-auth}"
|
||||
REPO_DIR="$WORK_DIR/repo"
|
||||
BIN_DIR="$WORK_DIR/bin"
|
||||
HOME_DIR="$WORK_DIR/home"
|
||||
LOG_FILE="$WORK_DIR/calls.log"
|
||||
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$REPO_DIR" "$BIN_DIR" "$HOME_DIR"
|
||||
mkdir -p "$REPO_DIR" "$BIN_DIR"
|
||||
git -C "$REPO_DIR" init -q
|
||||
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
|
||||
#
|
||||
# OPERATIVE: the empty repo-local `mosaic.gitIdentity` below. get_gitea_token()
|
||||
# step 0 resolves a per-agent identity from `git config --get mosaic.gitIdentity`,
|
||||
# which on a provisioned agent seat is set GLOBALLY and so leaks into this fresh
|
||||
# repo. It then reads a REAL per-slot token from $HOME and returns it WITHOUT ever
|
||||
# consulting MOSAIC_CREDENTIALS_FILE, so the fixture credential below is silently
|
||||
# ignored and the suite runs against a production credential. An empty repo-local
|
||||
# value shadows the global one and reads back empty at rc=0. Measured: this suite
|
||||
# resolves a per-slot token without it.
|
||||
#
|
||||
# CONTAINMENT: the sandboxed HOME in run_wrapper(). It only has to bound a failure
|
||||
# that the pin should already have prevented.
|
||||
#
|
||||
# NOTE FOR ANYONE AUDITING THIS SUITE: the sandboxed HOME is containment, NOT an
|
||||
# assay. Running a suite under a decoy HOME to test for this defect REMOVES the
|
||||
# trigger — ~/.gitconfig is where the global identity lives, so step 0 is skipped
|
||||
# by construction and every suite reads clean however vulnerable it is. To measure,
|
||||
# REPLICATE a seat (a decoy HOME whose .gitconfig sets mosaic.gitIdentity, with no
|
||||
# per-slot token) so step 0 reaches its fail-loud branch.
|
||||
#
|
||||
# Note the env-var route does NOT work: detect-platform.sh reads
|
||||
# "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset.
|
||||
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
||||
|
||||
cat > "$CREDENTIALS_FILE" <<'JSON'
|
||||
{"gitea":{"mosaicstack":{"url":"https://git.mosaicstack.dev","token":"test-token"}}}
|
||||
@@ -75,7 +50,6 @@ run_wrapper() {
|
||||
(
|
||||
cd "$REPO_DIR"
|
||||
PATH="$BIN_DIR:$PATH" \
|
||||
HOME="$HOME_DIR" \
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||
MOSAIC_TEST_LOG="$LOG_FILE" \
|
||||
"$@"
|
||||
|
||||
@@ -8,7 +8,6 @@ WORK_ROOT="${AGENT_WORK_ROOT:-${HOME:-/tmp}/mosaic/agent-work}"
|
||||
SANDBOX="$WORK_ROOT/pr-merge-empty-uid-test-$$"
|
||||
MOCK_BIN="$SANDBOX/bin"
|
||||
REPO_DIR="$SANDBOX/repo"
|
||||
HOME_DIR="$SANDBOX/home"
|
||||
LOG_FILE="$SANDBOX/mock.log"
|
||||
|
||||
cleanup() {
|
||||
@@ -16,7 +15,7 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$MOCK_BIN" "$REPO_DIR" "$HOME_DIR"
|
||||
mkdir -p "$MOCK_BIN" "$REPO_DIR"
|
||||
: > "$LOG_FILE"
|
||||
|
||||
cat > "$MOCK_BIN/tea" <<'EOF'
|
||||
@@ -100,48 +99,7 @@ chmod +x "$MOCK_BIN/curl"
|
||||
cd "$REPO_DIR"
|
||||
git init -q
|
||||
git remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
|
||||
#
|
||||
# OPERATIVE: the empty repo-local `mosaic.gitIdentity` below. get_gitea_token()
|
||||
# step 0 resolves a per-agent identity from `git config --get mosaic.gitIdentity`,
|
||||
# which on a provisioned agent seat is set GLOBALLY and so leaks into this fresh
|
||||
# repo. Step 0 runs BEFORE the credential loader AND before the GITEA_TOKEN env
|
||||
# check, so the `GITEA_TOKEN=redacted-test-token` exported below is silently
|
||||
# overridden and a REAL per-slot token from $HOME is what flows through the
|
||||
# wrapper. Measured on a provisioned seat before this pin: all 5 mock-curl calls
|
||||
# carried the real per-slot token in argv and the fixture token was never used at
|
||||
# ALL. Three consequences specific to this suite:
|
||||
# 1. pr-merge.sh passes the token as `-H "Authorization: token $token"` and the
|
||||
# mock curl logs full argv, so the real credential is written to $LOG_FILE
|
||||
# on disk — transiently: the suite truncates that file between phases and
|
||||
# the EXIT trap removes $SANDBOX, so it leaves NO post-hoc trace. That is
|
||||
# why this suite was the hardest of the three to detect; observing it needs
|
||||
# an instrument that captures argv while the run is live.
|
||||
# 2. Every failure path dumps $OUTPUT/$LOG_FILE to stderr through
|
||||
# `sed 's/redacted-test-token/***REDACTED***/g'` — a redaction pattern that
|
||||
# is the literal fixture string and therefore CANNOT match the token
|
||||
# actually in use.
|
||||
# 3. The leak assertion at "Token leaked to pr-merge.sh output" greps for that
|
||||
# same fixture string, so on a provisioned seat it passes vacuously: it is
|
||||
# searching for a value the run never used.
|
||||
# An empty repo-local value shadows the global one and reads back empty at rc=0.
|
||||
#
|
||||
# CONTAINMENT: the sandboxed HOME exported below. It only has to bound a failure
|
||||
# that the pin should already have prevented.
|
||||
#
|
||||
# NOTE FOR ANYONE AUDITING THIS SUITE: the sandboxed HOME is containment, NOT an
|
||||
# assay. Running a suite under a decoy HOME to test for this defect REMOVES the
|
||||
# trigger — ~/.gitconfig is where the global identity lives, so step 0 is skipped
|
||||
# by construction and every suite reads clean however vulnerable it is. To measure,
|
||||
# REPLICATE a seat (a decoy HOME whose .gitconfig sets mosaic.gitIdentity, with no
|
||||
# per-slot token) so step 0 reaches its fail-loud branch.
|
||||
#
|
||||
# Note the env-var route does NOT work: detect-platform.sh reads
|
||||
# "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset.
|
||||
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
||||
|
||||
# $SANDBOX/$HOME_DIR were derived from the real $HOME above, before this export.
|
||||
export HOME="$HOME_DIR"
|
||||
export PATH="$MOCK_BIN:$PATH"
|
||||
export PR_MERGE_TEST_LOG="$LOG_FILE"
|
||||
export GITEA_LOGIN="git.mosaicstack.dev"
|
||||
|
||||
@@ -8,68 +8,12 @@ WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-metadata-gitea}"
|
||||
REPO_DIR="$WORK_DIR/repo"
|
||||
FIXTURE_DIR="$WORK_DIR/fixtures"
|
||||
STUB_DIR="$WORK_DIR/stubs"
|
||||
HOME_DIR="$WORK_DIR/home"
|
||||
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$REPO_DIR" "$FIXTURE_DIR" "$STUB_DIR" "$HOME_DIR"
|
||||
mkdir -p "$REPO_DIR" "$FIXTURE_DIR" "$STUB_DIR"
|
||||
|
||||
git -C "$REPO_DIR" init -q
|
||||
git -C "$REPO_DIR" remote add origin https://git.uscllc.com/USC/uconnect.git
|
||||
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
|
||||
#
|
||||
# OPERATIVE: the empty repo-local `mosaic.gitIdentity` below. get_gitea_token()
|
||||
# step 0 resolves a per-agent identity from `git config --get mosaic.gitIdentity`,
|
||||
# which on a provisioned agent seat is set GLOBALLY and so leaks into this fresh
|
||||
# repo. Step 0 runs BEFORE the credential loader AND before the GITEA_TOKEN env
|
||||
# check, so the `GITEA_TOKEN="stub-token"` set in the run helpers below is
|
||||
# silently overridden and a REAL per-slot token from $HOME is what reaches curl.
|
||||
# Measured on a provisioned seat before this pin: both stub-curl calls carried
|
||||
# the real token in argv. An empty repo-local value shadows the global one and
|
||||
# reads back empty at rc=0.
|
||||
#
|
||||
# CONTAINMENT: the sandboxed HOME in the three run helpers below. It only has to
|
||||
# bound a failure that the pin should already have prevented.
|
||||
#
|
||||
# NOTE FOR ANYONE AUDITING THIS SUITE: the sandboxed HOME is containment, NOT an
|
||||
# assay. Running a suite under a decoy HOME to test for this defect REMOVES the
|
||||
# trigger — ~/.gitconfig is where the global identity lives, so step 0 is skipped
|
||||
# by construction and every suite reads clean however vulnerable it is. To measure,
|
||||
# REPLICATE a seat (a decoy HOME whose .gitconfig sets mosaic.gitIdentity, with no
|
||||
# per-slot token) so step 0 reaches its fail-loud branch. See
|
||||
# test-gitea-token-identity.sh for the stronger `env -i HOME=…` form used where a
|
||||
# suite's whole subject IS identity resolution.
|
||||
#
|
||||
# Note the env-var route does NOT work: detect-platform.sh reads
|
||||
# "${MOSAIC_GIT_IDENTITY:-}", and `:-` treats set-but-empty identically to unset.
|
||||
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
||||
|
||||
# The pin above removes step 0, but this suite has a SECOND, independent
|
||||
# dependency on operator state, and closing only the first would leave the suite
|
||||
# red on any hermetic environment. The `GITEA_TOKEN="stub-token"` /
|
||||
# `GITEA_URL="https://git.example.test"` pair the run helpers set is INERT: step 2
|
||||
# of get_gitea_token accepts GITEA_TOKEN only when GITEA_URL matches the remote
|
||||
# host, and this repo's origin is git.uscllc.com, so that pair can never satisfy
|
||||
# it. Before this fixture the only credential that could reach the authenticated
|
||||
# curl branch was a REAL one — from step 0 on an agent seat, or from step 1
|
||||
# reading the operator's own ~/.config/mosaic/credentials.json. That is why the
|
||||
# "curl success path" case passed: not because the stub credential worked, but
|
||||
# because a production credential was available.
|
||||
#
|
||||
# A fixture is used rather than relying on the sandboxed HOME making step 1 find
|
||||
# nothing: a test that passes because production configuration is ABSENT fails
|
||||
# the moment it is present. Step 1 now resolves deterministically to a value that
|
||||
# is a fixture on every machine.
|
||||
cat > "$CREDENTIALS_FILE" <<'JSON'
|
||||
{
|
||||
"gitea": {
|
||||
"usc": {
|
||||
"url": "https://git.uscllc.com",
|
||||
"token": "stub-token"
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > "$FIXTURE_DIR/gitea-standard.json" <<'JSON'
|
||||
{
|
||||
@@ -187,8 +131,6 @@ run_curl_success_case() {
|
||||
set +e
|
||||
output=$(cd "$REPO_DIR" && \
|
||||
PATH="$STUB_DIR:$PATH" \
|
||||
HOME="$HOME_DIR" \
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||
TMPDIR="$tmpdir" \
|
||||
GITEA_TOKEN="stub-token" \
|
||||
GITEA_URL="https://git.example.test" \
|
||||
@@ -228,8 +170,6 @@ run_curl_early_exit_cleanup_case() {
|
||||
set +e
|
||||
output=$(cd "$REPO_DIR" && \
|
||||
PATH="$STUB_DIR:$PATH" \
|
||||
HOME="$HOME_DIR" \
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||
TMPDIR="$tmpdir" \
|
||||
GITEA_TOKEN="stub-token" \
|
||||
GITEA_URL="https://git.example.test" \
|
||||
@@ -264,8 +204,7 @@ run_curl_early_exit_cleanup_case() {
|
||||
run_case() {
|
||||
local fixture="$1" expected_number="$2" expected_head="$3"
|
||||
local output
|
||||
output=$(cd "$REPO_DIR" && HOME="$HOME_DIR" MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||
MOSAIC_GITEA_PR_METADATA_RAW_FILE="$fixture" "$SCRIPT_DIR/pr-metadata.sh" -n "$expected_number")
|
||||
output=$(cd "$REPO_DIR" && MOSAIC_GITEA_PR_METADATA_RAW_FILE="$fixture" "$SCRIPT_DIR/pr-metadata.sh" -n "$expected_number")
|
||||
PR_METADATA_OUTPUT="$output" python3 - "$expected_number" "$expected_head" <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -58,9 +58,6 @@ CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
||||
# A dedicated scratch dir the wrapper is pointed at via TMPDIR, so the leak
|
||||
# check can assert every POST/GET body + metadata temp file is cleaned up.
|
||||
TMP_SCRATCH="$WORK_DIR/scratch"
|
||||
# Sandboxed HOME so nothing under the real $HOME (notably the per-slot Gitea token
|
||||
# store at ~/.config/mosaic/secrets/gitea-tokens/) is reachable from the wrapper.
|
||||
HOME_DIR="$WORK_DIR/home"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
@@ -81,18 +78,9 @@ OVERRIDE_TOKEN="override-token-placeholder"
|
||||
CROSS_HOST_LOGIN="foreign-host-reviewer"
|
||||
CROSS_HOST_TOKEN="cross-host-token-placeholder"
|
||||
|
||||
mkdir -p "$REPO_DIR" "$BIN_DIR" "$XDG_DIR" "$STATE_DIR" "$TMP_SCRATCH" "$HOME_DIR"
|
||||
mkdir -p "$REPO_DIR" "$BIN_DIR" "$XDG_DIR" "$STATE_DIR" "$TMP_SCRATCH"
|
||||
git -C "$REPO_DIR" init -q
|
||||
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||
# HERMETICITY: get_gitea_token() step 0 resolves a per-agent identity from
|
||||
# `git config --get mosaic.gitIdentity`, which on a provisioned agent seat is set
|
||||
# GLOBALLY and therefore leaks into this fresh repo. It then reads a REAL per-slot
|
||||
# token from $HOME and returns it WITHOUT ever consulting MOSAIC_CREDENTIALS_FILE,
|
||||
# so the fixture credentials below are silently ignored and the suite runs against
|
||||
# production credentials. An empty repo-local value shadows the global one and reads
|
||||
# back as empty at rc=0, restoring the shared-credential path this suite intends to
|
||||
# exercise. Paired with the sandboxed HOME in run_review().
|
||||
git -C "$REPO_DIR" config mosaic.gitIdentity ""
|
||||
|
||||
# tea config: the override login carries its own token here. The default login
|
||||
# name ("mosaicstack") is deliberately absent, so the no-override default path
|
||||
@@ -278,24 +266,6 @@ submitted = json.loads(os.environ["PR_REVIEW_PAYLOAD"])
|
||||
with open(state_path, encoding="utf-8") as handle:
|
||||
reviews = json.load(handle)
|
||||
|
||||
# review-refused-422 (#1004): the server REFUSES the submit outright with a
|
||||
# definite, correct, machine-readable reason in the body — the shape Gitea
|
||||
# returns when the acting credential authored the PR. Nothing is created. The
|
||||
# wrapper must surface what the server said and must NOT relabel this as the
|
||||
# #865 silent-no-op defect class, which is precisely what it is not.
|
||||
if mode == "review-refused-422":
|
||||
print("422")
|
||||
print(json.dumps({"message": "Cannot approve your own pull request"}))
|
||||
raise SystemExit(0)
|
||||
|
||||
# review-refused-html (#1004): a non-JSON error body, as a fronting proxy or
|
||||
# gateway emits. The detail extraction must degrade to the first raw line rather
|
||||
# than silently dropping the only explanation available.
|
||||
if mode == "review-refused-html":
|
||||
print("502")
|
||||
print("<html><head><title>502 Bad Gateway</title></head>\n<body>nginx</body></html>")
|
||||
raise SystemExit(0)
|
||||
|
||||
# no-op-concurrent-review: the wrapper's own submit is SUPPRESSED (200, no
|
||||
# created object) even though a concurrent same-identity, same-state review at
|
||||
# the same head already exists. Nothing is persisted; no created id to verify.
|
||||
@@ -547,8 +517,6 @@ run_review() {
|
||||
cd "$REPO_DIR"
|
||||
PATH="$BIN_DIR:$PATH" \
|
||||
TMPDIR="$TMP_SCRATCH" \
|
||||
HOME="$HOME_DIR" \
|
||||
MOSAIC_GIT_IDENTITY="" \
|
||||
XDG_CONFIG_HOME="$XDG_DIR" \
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||
PR_REVIEW_TEA_LOG="$TEA_LOG" \
|
||||
@@ -972,44 +940,4 @@ if grep -q 'Approved and verified' "$OUTPUT_FILE"; then
|
||||
fi
|
||||
assert_no_temp_leak "review-body-null"
|
||||
|
||||
# Case 19 (#1004): an outright server REFUSAL must report the provider's own
|
||||
# reason and must NOT be relabelled as the #865 silent-no-op defect class. The
|
||||
# old arm hardcoded "(#865: no durable review created)" for EVERY non-2xx, so a
|
||||
# 422/403/404 — all of them definite, correct refusals the server explained in
|
||||
# the discarded body — arrived at the caller wearing the name of the one defect
|
||||
# they are not. That misdirection is what makes an operator re-issue the request
|
||||
# by hand against the live object to find out what actually happened.
|
||||
if run_review review-refused-422 approve; then
|
||||
echo "FAIL: approve reported success when the server refused the submit" >&2
|
||||
cat "$OUTPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'HTTP 422' "$OUTPUT_FILE"
|
||||
if ! grep -q 'Cannot approve your own pull request' "$OUTPUT_FILE"; then
|
||||
echo "FAIL: the provider's stated reason was discarded (#1004)" >&2
|
||||
cat "$OUTPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q '#865: no durable review created' "$OUTPUT_FILE"; then
|
||||
echo "FAIL: a server refusal was misattributed to the #865 defect class (#1004)" >&2
|
||||
cat "$OUTPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_no_temp_leak "review-refused-422"
|
||||
|
||||
# Case 20 (#1004): a non-JSON error body (a fronting proxy's HTML page) must
|
||||
# still yield something the caller can act on, rather than a bare status code.
|
||||
if run_review review-refused-html approve; then
|
||||
echo "FAIL: approve reported success on a 502" >&2
|
||||
cat "$OUTPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q 'HTTP 502' "$OUTPUT_FILE"
|
||||
if ! grep -q '502 Bad Gateway' "$OUTPUT_FILE"; then
|
||||
echo "FAIL: a non-JSON error body was dropped instead of degrading to its first line (#1004)" >&2
|
||||
cat "$OUTPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_no_temp_leak "review-refused-html"
|
||||
|
||||
echo "pr-review.sh REST review + comment create/read-back regression passed"
|
||||
|
||||
@@ -191,177 +191,3 @@ _wake_init_dir() {
|
||||
[ -f "$dir/pending.jsonl" ] || printf '' | _atomic_write "$dir/pending.jsonl"
|
||||
[ -f "$dir/ack-ledger.jsonl" ] || printf '' | _atomic_write "$dir/ack-ledger.jsonl"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# #973 — three-valued grep assertion helpers for the wake test suites.
|
||||
#
|
||||
# grep's exit contract is three-valued: 0 = match, 1 = no match, >1 = ERROR
|
||||
# (bad file, bad pattern, resource failure). Every wake-suite assertion used
|
||||
# to read all non-zero as "absent", so a grep that COULD NOT LOOK wore the
|
||||
# colour of a verdict: OR-polarity sites (`|| fail`) went falsely red,
|
||||
# AND-polarity sites (`&& fail` — including the credential canaries) went
|
||||
# falsely green. The repair is to refuse to answer: rc 0 -> match, rc 1 -> no
|
||||
# match, anything else -> loud abort naming the call site, the raw exit code,
|
||||
# and the arguments. An error NEVER becomes a verdict.
|
||||
#
|
||||
# Production tools (store.sh, ack.sh) source this file but call none of the
|
||||
# helpers below; they are inert outside the suites.
|
||||
#
|
||||
# Suite integration contract:
|
||||
# - Call `wake_assert_init` ONCE at suite top level, right after sourcing.
|
||||
# It dups the suite's real stderr to a saved fd BEFORE any call-site
|
||||
# redirect exists, so an abort stays loud even at sites that append
|
||||
# `2>/dev/null` (the preimage credential canaries pre-swallow stderr —
|
||||
# exactly where a silent abort would recreate the defect being fixed).
|
||||
# - Assertion sites live inside `( ... ) && ok` subshell blocks, pipelines,
|
||||
# and `$(...)` substitutions, where a plain `exit` dies one layer deep and
|
||||
# the suite would carry on to emit a verdict. The abort therefore signals
|
||||
# the suite's MAIN shell ($$ is the main PID in every subshell) and then
|
||||
# exits the current context: the suite dies by signal, non-zero, with NO
|
||||
# verdict line emitted.
|
||||
#
|
||||
# Validation instrumentation (#973 evidence, not part of the assertion fix):
|
||||
# - WAKE_ASSERT_LEDGER=<file>: every helper call appends
|
||||
# "<helper> <caller-file>:<caller-line>" to <file>. That is the ONLY
|
||||
# divergence from production behaviour — the suite otherwise runs its
|
||||
# normal arms, so a validate run exercises exactly the shipped paths.
|
||||
# - WAKE_ASSERT_FORCE_GREP_ERROR_AT=<caller-file>:<caller-line>: at exactly
|
||||
# that call site, the invocation is routed through a REAL grep driven onto
|
||||
# its real error path (unknown option -> rc 2) — a genuinely executed
|
||||
# failing process, not a stubbed return — to prove per-site that the abort
|
||||
# fires. Unset in production; matching no site is a no-op.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# wake_assert_init — dup the suite's real stderr once, for abort loudness.
|
||||
# MUST be called at suite TOP LEVEL, immediately after sourcing and before any
|
||||
# test block: a lazy (first-call) dup could capture an already-redirected
|
||||
# stderr if the first executed helper call sat under a call-site 2>/dev/null,
|
||||
# silencing every abort thereafter. The fd is allocated dynamically (>= 10),
|
||||
# so it cannot collide with the wake lock fds (8) or the detector run-loop
|
||||
# lock (9).
|
||||
#
|
||||
# Init also PINS the BASH_LINENO convention the site coordinates depend on:
|
||||
# a helper call written across a backslash continuation must report at its
|
||||
# FIRST physical line (the denominator artifact's convention). That was
|
||||
# measured on a developer bash (5.3.x); CI runs whatever bash its base image
|
||||
# baked in, and that version floats silently between image rebuilds. A bash
|
||||
# that disagrees would shift every continuation-site coordinate by one line
|
||||
# UNDER the validation instead of in front of it — so the convention is
|
||||
# asserted at runtime, in the same bash binary that runs the suite, and a
|
||||
# disagreeing bash aborts the suite loudly instead of skewing coordinates.
|
||||
_wake_assert_lineno_pin() {
|
||||
local _wa_pin_tmp _wa_pin_got
|
||||
_wa_pin_tmp="$(mktemp)" || {
|
||||
_wake_assert_err_note "WAKE-ASSERT INIT ABORT: mktemp failed; cannot pin the BASH_LINENO convention — a pin that silently does not run is not a pin (#973)"
|
||||
exit 97
|
||||
}
|
||||
cat >"$_wa_pin_tmp" <<'WAKE_ASSERT_PIN'
|
||||
_wap() { printf '%s\n' "${BASH_LINENO[0]}"; }
|
||||
(
|
||||
_wap simple
|
||||
_wap \
|
||||
continuation
|
||||
)
|
||||
WAKE_ASSERT_PIN
|
||||
# WAKE_ASSERT_PIN_BASH: test-only interpreter override so the pin's abort
|
||||
# arm can be PROVEN to fire (microtest C10) — bash resets $BASH at startup,
|
||||
# so the real probe interpreter cannot be spoofed from the environment.
|
||||
_wa_pin_got="$("${WAKE_ASSERT_PIN_BASH:-${BASH:-bash}}" "$_wa_pin_tmp" 2>/dev/null)"
|
||||
rm -f "$_wa_pin_tmp"
|
||||
if [ "$_wa_pin_got" != "$(printf '3\n4')" ]; then
|
||||
_wake_assert_err_note "WAKE-ASSERT INIT ABORT: BASH_LINENO convention violated on bash ${BASH_VERSION}: probe reported [${_wa_pin_got:-<no output>}], expected [3 4] (simple call at own line, continuation call at FIRST physical line) — site coordinates are untrustworthy on this bash (#973)"
|
||||
exit 97
|
||||
fi
|
||||
}
|
||||
|
||||
wake_assert_init() {
|
||||
if [ -z "${_wake_assert_err_fd:-}" ]; then
|
||||
exec {_wake_assert_err_fd}>&2
|
||||
_wake_assert_lineno_pin
|
||||
fi
|
||||
}
|
||||
|
||||
# _wake_assert_err_note MSG — write MSG to the saved real-stderr fd, falling
|
||||
# back to the current stderr if init was never called.
|
||||
_wake_assert_err_note() {
|
||||
if [ -n "${_wake_assert_err_fd:-}" ]; then
|
||||
printf '%s\n' "$1" >&"$_wake_assert_err_fd" 2>/dev/null ||
|
||||
printf '%s\n' "$1" >&2
|
||||
else
|
||||
printf '%s\n' "$1" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# _wake_assert_abort HELPER SITE RC ARGS... — refuse to answer, loudly.
|
||||
# Writes the named reason to the saved real-stderr fd (falling back to the
|
||||
# current stderr), signals the suite's main shell, and exits this context.
|
||||
_wake_assert_abort() {
|
||||
local _wa_helper="$1" _wa_where="$2" _wa_code="$3"
|
||||
shift 3
|
||||
_wake_assert_err_note "WAKE-ASSERT ABORT: ${_wa_helper} at ${_wa_where}: grep exit ${_wa_code} is an error, not a verdict (args: $*) — refusing to answer (#973)"
|
||||
if [ -n "${BASHPID:-}" ] && [ "$BASHPID" != "$$" ]; then
|
||||
kill -TERM "$$" 2>/dev/null || true
|
||||
fi
|
||||
exit 97
|
||||
}
|
||||
|
||||
# _wake_assert_armed SITE — true iff the forced-error arm targets SITE; on a
|
||||
# match it emits a positive confirmation FIRST, so "site did not abort" can
|
||||
# never conflate SITE NOT CONVERTED with ARM NEVER REACHED IT: an armed run
|
||||
# with no ARMED line means the arm matched nothing (typo/renumber/drift), and
|
||||
# an ARMED line with no abort means the site's error path is broken. The two
|
||||
# defects are separable on stderr alone.
|
||||
_wake_assert_armed() {
|
||||
[ "${WAKE_ASSERT_FORCE_GREP_ERROR_AT:-}" = "$1" ] || return 1
|
||||
_wake_assert_err_note "WAKE-ASSERT ARMED: forcing real grep error at $1 (#973)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# has_match GREP_ARGS... — three-valued grep verdict.
|
||||
# Drop-in for verdict-bearing `grep` calls (flags, files, stdin all pass
|
||||
# through; stdout is not captured, so extract-form call sites may use it
|
||||
# inside a substitution). Returns 0 on match, 1 on no-match; any other grep
|
||||
# exit aborts the suite via _wake_assert_abort.
|
||||
has_match() {
|
||||
local _wa_site="${BASH_SOURCE[1]##*/}:${BASH_LINENO[0]}" _wa_rc=0
|
||||
if [ -n "${WAKE_ASSERT_LEDGER:-}" ]; then
|
||||
printf 'has_match %s\n' "$_wa_site" >>"$WAKE_ASSERT_LEDGER"
|
||||
fi
|
||||
if _wake_assert_armed "$_wa_site"; then
|
||||
command grep --wake-assert-forced-error -- /dev/null
|
||||
_wa_rc=$?
|
||||
else
|
||||
command grep "$@"
|
||||
_wa_rc=$?
|
||||
fi
|
||||
case "$_wa_rc" in
|
||||
0) return 0 ;;
|
||||
1) return 1 ;;
|
||||
*) _wake_assert_abort has_match "$_wa_site" "$_wa_rc" "$@" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# count_lines GREP_ARGS... — `grep -c` with the same three-way discipline.
|
||||
# Call sites drop their `-c` (the helper supplies it) and keep every other
|
||||
# argument. Prints the count on rc 0 AND rc 1 (rc 1 is grep's "count is 0" —
|
||||
# a valid measurement, not an error); any other exit aborts. The abort still
|
||||
# kills the suite from inside a `$(...)` capture: the substitution subshell
|
||||
# cannot exit the suite, but the signal to the main shell can — a count from
|
||||
# a failed measurement is never printed.
|
||||
count_lines() {
|
||||
local _wa_site="${BASH_SOURCE[1]##*/}:${BASH_LINENO[0]}" _wa_rc=0 _wa_out=""
|
||||
if [ -n "${WAKE_ASSERT_LEDGER:-}" ]; then
|
||||
printf 'count_lines %s\n' "$_wa_site" >>"$WAKE_ASSERT_LEDGER"
|
||||
fi
|
||||
if _wake_assert_armed "$_wa_site"; then
|
||||
_wa_out="$(command grep --wake-assert-forced-error -c -- /dev/null)"
|
||||
_wa_rc=$?
|
||||
else
|
||||
_wa_out="$(command grep -c "$@")"
|
||||
_wa_rc=$?
|
||||
fi
|
||||
case "$_wa_rc" in
|
||||
0 | 1) printf '%s\n' "$_wa_out" ;;
|
||||
*) _wake_assert_abort count_lines "$_wa_site" "$_wa_rc" "$@" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -465,26 +465,8 @@
|
||||
# never a hand-built flat dead-letter row, which would make the
|
||||
# audit's correct non-conviction look exactly like the defect
|
||||
# under hunt (the #951 review's false-defect near-miss).
|
||||
# 0.7.2 #973 three-valued grep verdicts across ALL TEN wake test suites.
|
||||
# grep's exit contract is three-valued (0 match / 1 no-match /
|
||||
# >=2 ERROR); every suite assertion read non-zero as "absent",
|
||||
# so a grep that COULD NOT LOOK wore the colour of a verdict —
|
||||
# OR-polarity sites failed falsely RED, AND-polarity sites
|
||||
# (including all 19 credential canaries) failed falsely GREEN
|
||||
# under load. _wake-common.sh gains has_match/count_lines
|
||||
# (rc 0/1 pass through; anything else LOUDLY ABORTS the whole
|
||||
# suite naming file:line + raw rc — an error is never a
|
||||
# verdict), wake_assert_init (saved-fd abort loudness that
|
||||
# survives call-site 2>/dev/null + a runtime pin of the
|
||||
# BASH_LINENO coordinate convention against CI bash drift),
|
||||
# and 261 call sites converted mechanically from a frozen
|
||||
# denominator artifact. Production tools source but never call
|
||||
# the helpers; suite verdict semantics on rc 0/1 are UNCHANGED.
|
||||
# Evidence chain in validate-973/ (microtest C1-C11, expected/
|
||||
# static/trace set arithmetic, 21 forced-error arms, residual
|
||||
# sweep with per-form plants).
|
||||
component=wake
|
||||
version=0.7.2
|
||||
version=0.7.1
|
||||
|
||||
# Watch-list schema this component consumes, and the INCLUSIVE range of
|
||||
# schema_version values it supports. A wake-watch-list.json whose schema_version
|
||||
|
||||
@@ -31,13 +31,6 @@
|
||||
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
|
||||
if ! . "$SCRIPT_DIR/_wake-common.sh"; then
|
||||
echo "WAKE-ASSERT INIT ABORT: cannot source _wake-common.sh — suite ran ZERO wake assertions (#984)" >&2
|
||||
exit 97
|
||||
fi
|
||||
wake_assert_init
|
||||
BEACON="$SCRIPT_DIR/beacon.sh"
|
||||
|
||||
command -v jq >/dev/null 2>&1 || {
|
||||
@@ -136,7 +129,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" | has_match -qi 'ALARM FIRED' || fail_msg "B2: absence must announce the alarm fired [$out]"
|
||||
echo "$out" | grep -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).
|
||||
@@ -157,13 +150,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" | has_match -qi 'silent no-alarm host' || fail_msg "B3a: the failure must name the silent-no-alarm-host hazard [$out]"
|
||||
echo "$out" | grep -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" | has_match -qi 'UNREACHABLE' || fail_msg "B3b: the failure must name the unreachable target [$out2]"
|
||||
echo "$out2" | grep -qi 'UNREACHABLE' || fail_msg "B3b: the failure must name the unreachable target [$out2]"
|
||||
) && ok
|
||||
|
||||
echo "== B4: isolated host -> DEGRADED different-supervision-root beacon FLAGGED =="
|
||||
@@ -176,8 +169,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" | 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]"
|
||||
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]"
|
||||
) && ok
|
||||
|
||||
echo "== B5: same-host-sibling -> REJECTED as non-independent, seq NOT advanced =="
|
||||
@@ -189,7 +182,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" | has_match -qi 'not an independent' || fail_msg "B5: the rejection must explain it is NOT an independent leg [$out]"
|
||||
echo "$out" | grep -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)"
|
||||
@@ -208,10 +201,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 | has_match -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 | grep -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.
|
||||
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"
|
||||
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"
|
||||
true
|
||||
) && ok
|
||||
|
||||
@@ -225,7 +218,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" | has_match -qi 'silent no-alarm host' || fail_msg "B7a: the failure must name the silent-no-alarm-host hazard [$out]"
|
||||
echo "$out" | grep -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)"
|
||||
@@ -234,7 +227,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" | has_match -qi 'UNREACHABLE' || fail_msg "B7b: the failure must name the unreachable sink [$out2]"
|
||||
echo "$out2" | grep -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) =="
|
||||
@@ -245,7 +238,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" | has_match -qi 'slo' || fail_msg "B8: the usage error must name the missing SLO [$out]"
|
||||
echo "$out" | grep -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) =="
|
||||
@@ -274,7 +267,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" | has_match -qi 'ALIVE' || fail_msg "B10: a fresh beacon must report ALIVE [$out]"
|
||||
echo "$out" | grep -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
|
||||
|
||||
@@ -298,7 +291,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" | has_match -qi 'ALARM FIRED' || fail_msg "B11: the receive-time-stale beacon must fire the absence alarm [$out]"
|
||||
echo "$out" | grep -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
|
||||
|
||||
@@ -336,7 +329,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" | has_match -qi 'spoofed beacon' || fail_msg "B12: the rejection must name the spoof [$out]"
|
||||
echo "$out" | grep -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"
|
||||
@@ -344,14 +337,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.
|
||||
has_match -qF "test-beacon-hmac-key-do-not-echo" "$BEACON" && fail_msg "B12: beacon.sh must NOT inline the HMAC key"
|
||||
grep -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 ($(count_lines . "$FAILFILE") assertion(s))" >&2
|
||||
echo "wake beacon harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "wake beacon harness: all invariants passed ($pass groups)"
|
||||
|
||||
@@ -29,13 +29,6 @@
|
||||
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
|
||||
if ! . "$SCRIPT_DIR/_wake-common.sh"; then
|
||||
echo "WAKE-ASSERT INIT ABORT: cannot source _wake-common.sh — suite ran ZERO wake assertions (#984)" >&2
|
||||
exit 97
|
||||
fi
|
||||
wake_assert_init
|
||||
DET="$SCRIPT_DIR/detector.sh"
|
||||
STORE="$SCRIPT_DIR/store.sh"
|
||||
|
||||
@@ -260,8 +253,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" | 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]"
|
||||
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]"
|
||||
# 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)"
|
||||
@@ -294,7 +287,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" | has_match -qi 'FAIL LOUD' || fail_msg "D6: the source error must be loud [$err]"
|
||||
echo "$err" | grep -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)"
|
||||
|
||||
@@ -305,7 +298,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" | has_match -qi 'AMBIGUOUS-EMPTY' || fail_msg "D6: ambiguous-empty must be named in the loud failure [$err]"
|
||||
echo "$err" | grep -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)"
|
||||
|
||||
@@ -449,7 +442,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" | has_match -qi 'range' || fail_msg "D9: the out-of-range failure must still state it is out of the supported range [$err]"
|
||||
echo "$err" | grep -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 =="
|
||||
@@ -520,7 +513,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" | has_match -qi 'snapshot' || fail_msg "D11: dropping malformed metadata must be LOUD on stderr [$err]"
|
||||
echo "$err" | grep -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]"
|
||||
@@ -529,7 +522,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" | has_match -qi 'snapshot' || fail_msg "D11: rejecting a bad snapshot_sha must be LOUD on stderr [$err]"
|
||||
echo "$err" | grep -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]"
|
||||
@@ -567,7 +560,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" | has_match -qi 'without a valid snapshot_sha' || fail_msg "D12: dropping ts-without-sha must be LOUD on stderr [$err]"
|
||||
echo "$err" | grep -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]"
|
||||
@@ -578,7 +571,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" | has_match -qi 'future-skew' || fail_msg "D12: dropping a future ts must be LOUD on stderr [$err]"
|
||||
echo "$err" | grep -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]"
|
||||
@@ -588,7 +581,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" | has_match -qi 'sane positive epoch' || fail_msg "D12: rejecting an absurd ts must be LOUD on stderr [$err]"
|
||||
echo "$err" | grep -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]"
|
||||
@@ -628,7 +621,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" | has_match -qi 'falling back to 300' || fail_msg "D13: malformed slack must be LOUD on stderr [$err]"
|
||||
echo "$err" | grep -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]"
|
||||
@@ -637,7 +630,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" | has_match -qi 'falling back to 300' || fail_msg "D13: non-numeric slack must be LOUD on stderr [$err]"
|
||||
echo "$err" | grep -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]"
|
||||
@@ -647,7 +640,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" | has_match -qi 'falling back to 300' || fail_msg "D13: negative slack must be LOUD on stderr [$err]"
|
||||
echo "$err" | grep -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]"
|
||||
@@ -659,7 +652,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" | has_match -qi 'falling back to 300' || fail_msg "D13: multi-line slack must be LOUD on stderr [$err]"
|
||||
echo "$err" | grep -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]"
|
||||
@@ -667,7 +660,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" | has_match -qi 'falling back to 300' || fail_msg "D13: multi-line non-numeric slack must be LOUD on stderr [$err]"
|
||||
echo "$err" | grep -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]"
|
||||
@@ -694,7 +687,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" | has_match -qi 'future-skew' || fail_msg "D13: a valid tightened slack must still reject a future ts [$err]"
|
||||
echo "$err" | grep -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]"
|
||||
@@ -703,7 +696,7 @@ EOF
|
||||
|
||||
echo
|
||||
if [ -s "$FAILFILE" ]; then
|
||||
echo "wake detector harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2
|
||||
echo "wake detector harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "wake detector harness: all invariants passed ($pass groups)"
|
||||
|
||||
@@ -35,13 +35,6 @@
|
||||
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
|
||||
if ! . "$SCRIPT_DIR/_wake-common.sh"; then
|
||||
echo "WAKE-ASSERT INIT ABORT: cannot source _wake-common.sh — suite ran ZERO wake assertions (#984)" >&2
|
||||
exit 97
|
||||
fi
|
||||
wake_assert_init
|
||||
STORE="$SCRIPT_DIR/store.sh"
|
||||
DIGEST="$SCRIPT_DIR/digest.sh"
|
||||
SIGN="$SCRIPT_DIR/sign.sh"
|
||||
@@ -109,13 +102,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" | 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"
|
||||
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"
|
||||
# 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" | has_match -q 'pending=2' || fail_msg "D1: cumulative pending count wrong (expected 2)"
|
||||
echo "$out" | grep -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 =="
|
||||
@@ -134,9 +127,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" | 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"
|
||||
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"
|
||||
|
||||
# A present-but-imprecise sha (not 40 hex) does NOT satisfy the hard locator ->
|
||||
# quarantined, not delivered.
|
||||
@@ -147,8 +140,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" | 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"
|
||||
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"
|
||||
|
||||
# The SAME claim WITH a precise 40-hex sha renders fine (delivered, not quarantined).
|
||||
h="$(fresh_state d2c)"
|
||||
@@ -156,7 +149,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" | has_match -q "$SHA40" || fail_msg "D2: a well-located actionable claim must be DELIVERED"
|
||||
printf '%s' "$out" | grep -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;
|
||||
@@ -171,8 +164,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" | 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)"
|
||||
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)"
|
||||
ff="$TMP_ROOT/d2d-entry.jsonl"
|
||||
printf '%s\n' "$toplevel_claim_entry" >"$ff"
|
||||
h="$(fresh_state d2e)"
|
||||
@@ -181,8 +174,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" | 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)"
|
||||
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)"
|
||||
) && ok
|
||||
|
||||
echo "== D3: TWO-TIER — orientation no-op with ZERO tool calls; actionable = claim-to-verify =="
|
||||
@@ -204,7 +197,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" | has_match -q 'NO-OP' || fail_msg "D3: empty inbox must render an explicit NO-OP orientation"
|
||||
echo "$out" | grep -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.
|
||||
@@ -213,14 +206,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" | 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"
|
||||
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"
|
||||
# The consequential fact must NOT appear as a bare trusted directive.
|
||||
echo "$out2" | has_match -qiE 'merge now|go ahead and (merge|deploy)|safe to merge' &&
|
||||
echo "$out2" | grep -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" | has_match -q "re-verify (ONE call)" || fail_msg "D3: actionable claim missing one-call re-verify locator"
|
||||
echo "$out2" | grep -q "re-verify (ONE call)" || fail_msg "D3: actionable claim missing one-call re-verify locator"
|
||||
true
|
||||
) && ok
|
||||
|
||||
@@ -236,7 +229,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 has_match -q "$(printf '\x1b')" && fail_msg "D4: ANSI ESC survived the scrub"
|
||||
printf '%s' "$out" | LC_ALL=C grep -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
|
||||
@@ -251,21 +244,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 has_match -qE "${_b280}[${_b8b}-${_b8f}${_baa}-${_bae}]|${_bbom}" &&
|
||||
printf '%s' "$out" | LC_ALL=C grep -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 has_match -qE "[${_c00}-${_c08}${_c0e}-${_c1f}${_c7f}]" &&
|
||||
printf '%s' "$out" | LC_ALL=C grep -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" | 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"
|
||||
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"
|
||||
# Free-text is quoted inside a DELIMITED untrusted block, framed as NOT instructions.
|
||||
printf '%s' "$out" | has_match -q 'BEGIN UNTRUSTED DATA' || fail_msg "D4: source free-text not placed in a delimited untrusted block"
|
||||
printf '%s' "$out" | grep -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" | has_match -q "$SHA40" || fail_msg "D4: legitimate 40-hex SHA locator was wrongly scrubbed"
|
||||
printf '%s' "$out" | grep -q "$SHA40" || fail_msg "D4: legitimate 40-hex SHA locator was wrongly scrubbed"
|
||||
true
|
||||
) && ok
|
||||
|
||||
@@ -330,11 +323,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" | has_match -q 'SUPERSECRET-KEYVALUE-XYZ' && fail_msg "H2: key material LEAKED into the signed envelope"
|
||||
echo "$env1" | grep -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" | has_match -q 'SUPERSECRET-KEYVALUE-XYZ' && fail_msg "H2: key material LEAKED into the signed entry"
|
||||
echo "$entry" | grep -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).
|
||||
@@ -348,8 +341,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.
|
||||
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"
|
||||
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"
|
||||
) && ok
|
||||
|
||||
echo "== D5 (#914a): EMBEDDED ACK NAMESPACE — env-less copy-run targets the RENDER-TIME agent, not 'default' =="
|
||||
@@ -362,7 +355,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" | has_match -q 'WAKE_AGENT=someagent' ||
|
||||
printf '%s' "$ack_line" | grep -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).
|
||||
@@ -408,12 +401,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" | has_match -E '^\s*\* seq 1 \[digest\]')"
|
||||
orientline="$(printf '%s\n' "$out" | grep -E '^\s*\* seq 1 \[digest\]')"
|
||||
[ -n "$orientline" ] || fail_msg "D6: no ORIENTATION line found for seq 1"
|
||||
printf '%s' "$orientline" | has_match -qE 'locator: *$' &&
|
||||
printf '%s' "$orientline" | grep -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" | has_match -qE 'remote=example/repo|id=r1|kind=repo' ||
|
||||
printf '%s' "$orientline" | grep -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
|
||||
@@ -428,14 +421,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" | 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"
|
||||
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"
|
||||
) && ok
|
||||
|
||||
echo
|
||||
if [ -s "$FAILFILE" ]; then
|
||||
echo "wake digest/hmac harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2
|
||||
echo "wake digest/hmac harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "wake digest/hmac harness: all invariants passed ($pass groups)"
|
||||
|
||||
@@ -117,13 +117,6 @@
|
||||
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
|
||||
if ! . "$SCRIPT_DIR/_wake-common.sh"; then
|
||||
echo "WAKE-ASSERT INIT ABORT: cannot source _wake-common.sh — suite ran ZERO wake assertions (#984)" >&2
|
||||
exit 97
|
||||
fi
|
||||
wake_assert_init
|
||||
DIGEST="$SCRIPT_DIR/digest.sh"
|
||||
|
||||
command -v jq >/dev/null 2>&1 || {
|
||||
@@ -183,12 +176,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" | 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"
|
||||
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"
|
||||
[ -f "$(dlq "$home")" ] || fail_msg "Q1: a durable dead-letter file must be written"
|
||||
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)"
|
||||
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)"
|
||||
) && ok
|
||||
|
||||
echo "== Q2 (b): reconciler enumeration (reconciled:true) renders ORIENTATION-tier, NO exit-4 =="
|
||||
@@ -207,8 +200,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" | 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"
|
||||
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"
|
||||
[ -s "$(dlq "$home")" ] && fail_msg "Q2: an ORIENTATION-tier enumeration must NOT be quarantined/dead-lettered"
|
||||
true
|
||||
) && ok
|
||||
@@ -227,10 +220,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" | count_lines 'id=ENUM-C[12]' || true)"
|
||||
n="$(printf '%s\n' "$out" | grep -c '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" | 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"
|
||||
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"
|
||||
[ -s "$(dlq "$home")" ] && fail_msg "Q3: enumerations must NOT be quarantined"
|
||||
true
|
||||
) && ok
|
||||
@@ -246,10 +239,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" | 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"
|
||||
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"
|
||||
) && ok
|
||||
|
||||
echo "== Q5: claim-precedence gated — a non-actionable-class entry carrying a claim (no hard locator) is STILL quarantined =="
|
||||
@@ -265,8 +258,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"
|
||||
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"
|
||||
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"
|
||||
true
|
||||
) && ok
|
||||
|
||||
@@ -285,11 +278,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="$(count_lines . "$ALARM_OUT" 2>/dev/null || true)"
|
||||
n="$(grep -c . "$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)]"
|
||||
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"
|
||||
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"
|
||||
true
|
||||
) && ok
|
||||
|
||||
@@ -307,9 +300,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="$(count_lines . "$ALARM_OUT" 2>/dev/null || true)"
|
||||
n="$(grep -c . "$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)]"
|
||||
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)"
|
||||
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)"
|
||||
) && ok
|
||||
|
||||
echo "== Q8 (c): a NEW distinct dead-lettered entry routes its OWN one alarm (dedup is per-entry, not a global latch) =="
|
||||
@@ -328,10 +321,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="$(count_lines . "$ALARM_OUT" 2>/dev/null || true)"
|
||||
n="$(grep -c . "$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)]"
|
||||
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"
|
||||
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"
|
||||
) && ok
|
||||
|
||||
echo "== Q9 (d): WAKE_ALARM_SINK_CMD unconfigured OR unreachable -> FAIL LOUD (never silent no-alarm); per-entry, render still exits 0 =="
|
||||
@@ -347,9 +340,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"
|
||||
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"
|
||||
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"
|
||||
true
|
||||
) && ok
|
||||
(
|
||||
@@ -362,9 +355,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"
|
||||
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"
|
||||
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"
|
||||
true
|
||||
) && ok
|
||||
|
||||
@@ -390,16 +383,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" | has_match 'id=SNAP-A' || true)"
|
||||
printf '%s' "$snapa_line" | has_match -q 'snapshot_sha=0123abc4567890def0123abc4567890def012345' \
|
||||
snapa_line="$(printf '%s\n' "$out" | grep 'id=SNAP-A' || true)"
|
||||
printf '%s' "$snapa_line" | grep -q 'snapshot_sha=0123abc4567890def0123abc4567890def012345' \
|
||||
|| fail_msg "Q10: snapshot_sha must render on the ORIENTATION pointer line [$snapa_line]"
|
||||
printf '%s' "$snapa_line" | has_match -q 'snapshot_ts=1753850000' \
|
||||
printf '%s' "$snapa_line" | grep -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" | has_match -q 'git show 0123abc4567890def0123abc4567890def012345:BOARD.md' \
|
||||
printf '%s' "$out" | grep -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" | has_match 'id=SNAP-B' || true)"
|
||||
printf '%s' "$snapb_line" | has_match -q 'snapshot_' \
|
||||
snapb_line="$(printf '%s\n' "$out" | grep 'id=SNAP-B' || true)"
|
||||
printf '%s' "$snapb_line" | grep -q 'snapshot_' \
|
||||
&& fail_msg "Q10: an entry without metadata must render NO snapshot_ fields [$snapb_line]"
|
||||
true
|
||||
) && ok
|
||||
@@ -441,28 +434,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" | 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' \
|
||||
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' \
|
||||
|| fail_msg "Q11a: the rendered claim must carry the one-call re-verify hint git show <snapshot_sha>:<path>"
|
||||
# (b) POSITIVE: path arm alone suffices; hint degrades to one-call re-read.
|
||||
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 <path> hint"
|
||||
n_claims="$(printf '%s\n' "$out" | count_lines 'CLAIM@seq' || true)"
|
||||
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 <path> hint"
|
||||
n_claims="$(printf '%s\n' "$out" | grep -c '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" | 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"
|
||||
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"
|
||||
for snap_id in SNAP-ONLY-40 SNAP-ONLY-7 SNAP-ONLY-64; do
|
||||
has_match -q "$snap_id" "$(dlq "$home")" 2>/dev/null || fail_msg "Q11d: the bare snapshot_sha entry ($snap_id) must be DEAD-LETTERED"
|
||||
grep -q "$snap_id" "$(dlq "$home")" 2>/dev/null || fail_msg "Q11d: the bare snapshot_sha entry ($snap_id) must be DEAD-LETTERED"
|
||||
done
|
||||
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)"
|
||||
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)"
|
||||
[ "$n_alarms" = "4" ] || fail_msg "Q11: exactly the four invalid entries must alarm (got $n_alarms) [$(cat "$ALARM_OUT" 2>/dev/null)]"
|
||||
has_match -q '"observed_seq":70' "$ALARM_OUT" 2>/dev/null || fail_msg "Q11c: seq 70's own alarm must be present"
|
||||
grep -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
|
||||
has_match -q "\"observed_seq\":$snap_seq" "$ALARM_OUT" 2>/dev/null || fail_msg "Q11d: seq $snap_seq's own alarm must be present"
|
||||
grep -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
|
||||
@@ -484,15 +477,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" | 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"
|
||||
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"
|
||||
# ...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" | has_match -q 'ADDR-Q12' && fail_msg "Q12: the quarantined entry's content/locators must stay EXCLUDED from the digest"
|
||||
printf '%s' "$out" | grep -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" | 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"
|
||||
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"
|
||||
# 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
|
||||
@@ -510,9 +503,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" | 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"
|
||||
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"
|
||||
true
|
||||
) && ok
|
||||
|
||||
@@ -527,8 +520,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" | 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)"
|
||||
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)"
|
||||
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
|
||||
@@ -552,8 +545,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" | 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"
|
||||
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"
|
||||
[ -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
|
||||
@@ -564,7 +557,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="$(has_match -F "\"id\":\"$enum_id\"" "$self" | has_match -F '"observed_seq":5' | head -n1)"
|
||||
fixture_line="$(grep -F "\"id\":\"$enum_id\"" "$self" | grep -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
|
||||
@@ -585,7 +578,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 ($(count_lines . "$FAILFILE") assertion(s))" >&2
|
||||
echo "wake digest-quarantine harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "wake digest-quarantine harness: all invariants passed ($pass groups)"
|
||||
|
||||
@@ -25,13 +25,6 @@
|
||||
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
|
||||
if ! . "$SCRIPT_DIR/_wake-common.sh"; then
|
||||
echo "WAKE-ASSERT INIT ABORT: cannot source _wake-common.sh — suite ran ZERO wake assertions (#984)" >&2
|
||||
exit 97
|
||||
fi
|
||||
wake_assert_init
|
||||
ORACLE="$SCRIPT_DIR/fn-oracle.sh"
|
||||
DET="$SCRIPT_DIR/detector.sh"
|
||||
|
||||
@@ -69,8 +62,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" | 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]"
|
||||
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]"
|
||||
) && ok
|
||||
|
||||
echo "== O2: DISABLED detector (perfect no-op) -> FN-DETECTED (blindspot killer) =="
|
||||
@@ -83,9 +76,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" | 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]"
|
||||
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]"
|
||||
) && ok
|
||||
|
||||
echo "== O3: reached CONSUMED but too slow -> FN-DETECTED (within-SLO clause) =="
|
||||
@@ -98,10 +91,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" | has_match -qi 'per-class SLO' || fail_msg "O3: the failure must attribute to the per-class SLO, not a drop [$out]"
|
||||
echo "$out" | grep -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" | has_match -qi 'never OBSERVED'; then
|
||||
if echo "$out" | grep -qi 'never OBSERVED'; then
|
||||
fail_msg "O3: an SLO breach must NOT be misreported as a dropped delta [$out]"
|
||||
fi
|
||||
) && ok
|
||||
@@ -117,7 +110,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" | has_match -q 'VERDICT = FN-DETECTED' || fail_msg "O4: off-domain verdict must not be fooled by a clean exit code [$out]"
|
||||
echo "$out" | grep -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) =="
|
||||
@@ -128,12 +121,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" | has_match -qi 'slo' || fail_msg "O5: the usage error must name the missing SLO [$err]"
|
||||
echo "$err" | grep -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 ($(count_lines . "$FAILFILE") assertion(s))" >&2
|
||||
echo "wake fn-oracle harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "wake fn-oracle harness: all invariants passed ($pass groups)"
|
||||
|
||||
@@ -34,13 +34,6 @@
|
||||
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
|
||||
if ! . "$SCRIPT_DIR/_wake-common.sh"; then
|
||||
echo "WAKE-ASSERT INIT ABORT: cannot source _wake-common.sh — suite ran ZERO wake assertions (#984)" >&2
|
||||
exit 97
|
||||
fi
|
||||
wake_assert_init
|
||||
WI="$SCRIPT_DIR/wake-install.sh"
|
||||
BEACON="$SCRIPT_DIR/beacon.sh"
|
||||
FRAMEWORK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
@@ -101,7 +94,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" | has_match -qi 'Gate A VIOLATION' || fail_msg "I2: refusal must name the Gate A violation [$out]"
|
||||
echo "$out" | grep -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.
|
||||
@@ -128,7 +121,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" | has_match -qi 'EXACTLY ONE' || fail_msg "I3: verify must confirm exactly-one [$out]"
|
||||
echo "$out" | grep -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]
|
||||
@@ -148,7 +141,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" | has_match -qi 'snapshot-guard' || fail_msg "I4: refusal must name the snapshot-guard [$out]"
|
||||
echo "$out" | grep -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"
|
||||
@@ -168,28 +161,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" | has_match -qi 'UNSIGNED' || fail_msg "I5: HMAC failure must warn about unsigned wakes [$out]"
|
||||
echo "$out" | grep -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" | has_match -qF "$SECRET_KEY" && fail_msg "I5: validate must NEVER echo the HMAC key material"
|
||||
echo "$out2" | grep -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" | has_match -qi 'silent no-alarm host' || fail_msg "I5: alarm failure must name the silent-no-alarm hazard [$out3]"
|
||||
echo "$out3" | grep -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" | has_match -qi 'UNREACHABLE' || fail_msg "I5: alarm failure must name the unreachable target [$out4]"
|
||||
echo "$out4" | grep -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.
|
||||
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"
|
||||
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"
|
||||
true
|
||||
) && ok
|
||||
|
||||
@@ -211,7 +204,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" | has_match -qi 'LEFT RUNNING' || fail_msg "I6: overlap must announce retire is withheld [$out]"
|
||||
echo "$out" | grep -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]"
|
||||
@@ -290,11 +283,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" | 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' \
|
||||
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' \
|
||||
|| 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" | has_match -qi 'No such file or directory' \
|
||||
echo "$out" | grep -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)"
|
||||
@@ -320,10 +313,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" | has_match -qi 'search path' || fail_msg "I10: validate failure must name the search-path miss [$out2]"
|
||||
echo "$out2" | grep -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' | count_lines .)"
|
||||
n="$(find "$UD" -maxdepth 1 -name 'mosaic-wake.service' | grep -c .)"
|
||||
[ "$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
|
||||
@@ -341,12 +334,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" | has_match -qi 'F7' || fail_msg "I11: the refusal must name the F7 precondition [$out0]"
|
||||
echo "$out0" | grep -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" | has_match -qi 'FALLBACK WAKE is not proven live' || fail_msg "I11: refusal must name the un-proven fallback [$out]"
|
||||
echo "$out" | grep -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.
|
||||
@@ -374,11 +367,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" | has_match -q 'STALLED-DRAIN-MARK' \
|
||||
echo "$out" | grep -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"
|
||||
has_match -qE '^ExecStart=.*digest\.sh render --from-store' "$UNIT" \
|
||||
grep -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
|
||||
|
||||
@@ -398,11 +391,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" | has_match -qi 'search path' || fail_msg "I13: validate failure must name the search-path miss [$out]"
|
||||
echo "$out" | grep -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' | count_lines .)"
|
||||
ns="$(find "$UD" -maxdepth 1 -name 'mosaic-wake-fallback.service' | count_lines .)"
|
||||
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" -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
|
||||
@@ -414,17 +407,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"
|
||||
has_match -q '^OnUnitActiveSec=1h' "$UD/$TIMER" || fail_msg "I14: shipped fallback timer must carry a base OnUnitActiveSec placeholder"
|
||||
grep -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" | has_match -qi 'exactly one' || fail_msg "I14: the cadence write must confirm exactly-one OnUnitActiveUSec [$out]"
|
||||
echo "$out" | grep -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 <timer>.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.
|
||||
has_match -A1 '^OnUnitActiveSec=$' "$UD/$TIMER.d/cadence.conf" | has_match -qx 'OnUnitActiveSec=30min' \
|
||||
grep -A1 '^OnUnitActiveSec=$' "$UD/$TIMER.d/cadence.conf" | grep -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.
|
||||
@@ -435,7 +428,7 @@ echo "== I14: per-class fallback cadence — blank-reset yields EXACTLY ONE OnUn
|
||||
|
||||
echo
|
||||
if [ -s "$FAILFILE" ]; then
|
||||
echo "wake install harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2
|
||||
echo "wake install harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "wake install harness: all invariants passed ($pass groups)"
|
||||
|
||||
@@ -46,13 +46,6 @@
|
||||
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
|
||||
if ! . "$SCRIPT_DIR/_wake-common.sh"; then
|
||||
echo "WAKE-ASSERT INIT ABORT: cannot source _wake-common.sh — suite ran ZERO wake assertions (#984)" >&2
|
||||
exit 97
|
||||
fi
|
||||
wake_assert_init
|
||||
PRE="$SCRIPT_DIR/preimage.sh"
|
||||
STORE="$SCRIPT_DIR/store.sh"
|
||||
DET="$SCRIPT_DIR/detector.sh"
|
||||
@@ -209,9 +202,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" | has_match -qi 'path' || fail_msg "P4: refusal must name the path deny [$row]"
|
||||
jq -r '.refused // ""' <<<"$row" | grep -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 has_match -rq "$secret" "$sd" 2>/dev/null; then
|
||||
if grep -rq "$secret" "$sd" 2>/dev/null; then
|
||||
fail_msg "P4: credential bytes leaked into the wake state dir"
|
||||
fi
|
||||
) && ok
|
||||
@@ -232,8 +225,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" | has_match -qi 'secret' || fail_msg "P5: refusal must name secret-shaped content [$row]"
|
||||
if has_match -rq "$tok" "$sd" 2>/dev/null; then
|
||||
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
|
||||
fail_msg "P5: secret-shaped bytes leaked into the wake state dir"
|
||||
fi
|
||||
) && ok
|
||||
@@ -282,7 +275,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" | has_match -qi 'does not resolve' || fail_msg "P8: the failure must name the unresolvable adapter [$out]"
|
||||
echo "$out" | grep -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 =="
|
||||
@@ -301,7 +294,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" | has_match -qi 'unparseable' || fail_msg "P9: the failure must name the corrupt ledger [$out]"
|
||||
echo "$out" | grep -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
|
||||
|
||||
@@ -321,7 +314,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" | has_match -qi 'MAX_BYTES' || fail_msg "P10: refusal must name the size cap [$row]"
|
||||
jq -r '.refused // ""' <<<"$row" | grep -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"
|
||||
@@ -369,7 +362,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" | has_match -qi 'preimage' || fail_msg "P12: the failure must name the preimage check [$out]"
|
||||
echo "$out" | grep -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
|
||||
@@ -404,10 +397,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 has_match -rq "$s1" "$sd" 2>/dev/null; then
|
||||
if grep -rq "$s1" "$sd" 2>/dev/null; then
|
||||
fail_msg "P13: renamed-target store bytes leaked into the wake state dir"
|
||||
fi
|
||||
if has_match -rq "$s2" "$sd" 2>/dev/null; then
|
||||
if grep -rq "$s2" "$sd" 2>/dev/null; then
|
||||
fail_msg "P13: prefix-less key bytes leaked into the wake state dir"
|
||||
fi
|
||||
) && ok
|
||||
@@ -430,7 +423,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 has_match -rq "$hm" "$sd" 2>/dev/null; then
|
||||
if grep -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
|
||||
@@ -441,8 +434,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" | has_match -qi 'secret' || fail_msg "P14b: refusal must name secret-shaped content [$row]"
|
||||
if has_match -rq "$hm" "$sd" 2>/dev/null; then
|
||||
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
|
||||
fail_msg "P14b: key bytes leaked despite refusal"
|
||||
fi
|
||||
) && ok
|
||||
@@ -495,7 +488,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" | has_match -qi 'REFUSING to re-baseline' || fail_msg "P16: the failure must name the refusal [$out]"
|
||||
echo "$out" | grep -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))"
|
||||
@@ -527,7 +520,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 has_match -rq "$s" "$sd" 2>/dev/null; then
|
||||
if grep -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)"
|
||||
|
||||
@@ -29,13 +29,6 @@
|
||||
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
|
||||
if ! . "$SCRIPT_DIR/_wake-common.sh"; then
|
||||
echo "WAKE-ASSERT INIT ABORT: cannot source _wake-common.sh — suite ran ZERO wake assertions (#984)" >&2
|
||||
exit 97
|
||||
fi
|
||||
wake_assert_init
|
||||
RECON="$SCRIPT_DIR/reconcile.sh"
|
||||
STORE="$SCRIPT_DIR/store.sh"
|
||||
DET="$SCRIPT_DIR/detector.sh"
|
||||
@@ -94,7 +87,7 @@ EOF
|
||||
out="$("$RECON" inventory 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] || fail_msg "R1: a complete inventory must PASS (exit 0) [$out]"
|
||||
echo "$out" | has_match -qi 'COMPLETE' || fail_msg "R1: a complete inventory must report COMPLETE [$out]"
|
||||
echo "$out" | grep -qi 'COMPLETE' || fail_msg "R1: a complete inventory must report COMPLETE [$out]"
|
||||
) && ok
|
||||
|
||||
echo "== R2: OMITTED source -> FLAG (vacuous-pass prevented, not silently green) =="
|
||||
@@ -113,8 +106,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" | 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]"
|
||||
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]"
|
||||
) && ok
|
||||
|
||||
echo "== R3: required_sources omission -> FLAG =="
|
||||
@@ -133,7 +126,7 @@ EOF
|
||||
out="$("$RECON" inventory 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -ne 0 ] || fail_msg "R3: a required_sources omission must FLAG (non-zero)"
|
||||
echo "$out" | has_match -qi "requires source 'r2'" || fail_msg "R3: the flag must name the required-but-omitted source r2 [$out]"
|
||||
echo "$out" | grep -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 =="
|
||||
@@ -151,7 +144,7 @@ EOF
|
||||
out="$("$RECON" inventory 2>&1)"
|
||||
rc=$?
|
||||
[ "$rc" -ne 0 ] || fail_msg "R4: a dangling reference must FLAG (non-zero)"
|
||||
echo "$out" | has_match -qi 'dangling' || fail_msg "R4: the flag must state it is dangling [$out]"
|
||||
echo "$out" | grep -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 =="
|
||||
@@ -180,14 +173,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" | has_match -qi 'UNACCOUNTED=2' || fail_msg "R5: both pre-existing sources must be UNACCOUNTED [$out]"
|
||||
echo "$out" | grep -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" | has_match -qi 'UNACCOUNTED=0' || fail_msg "R6: second reconcile must be 0 unaccounted [$out2]"
|
||||
echo "$out2" | grep -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
|
||||
|
||||
@@ -218,7 +211,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" | has_match -qi 'UNACCOUNTED=0' || fail_msg "R7: inbox-reflected state must be 0 unaccounted [$out]"
|
||||
echo "$out" | grep -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
|
||||
|
||||
@@ -243,7 +236,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" | has_match -qi 'FAIL LOUD' || fail_msg "R8: the source error must be loud [$out]"
|
||||
echo "$out" | grep -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
|
||||
|
||||
@@ -285,7 +278,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" | has_match -qi 'UNACCOUNTED=1' || fail_msg "R9: only beta should be unaccounted (alpha is inbox-accounted) [$out]"
|
||||
echo "$out" | grep -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
|
||||
@@ -299,8 +292,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 | count_lines .)"
|
||||
ntotal="$("$STORE" drain | jq -r '.observed_seq' | count_lines .)"
|
||||
ndistinct="$("$STORE" drain | jq -r '.observed_seq' | sort -nu | grep -c .)"
|
||||
ntotal="$("$STORE" drain | jq -r '.observed_seq' | grep -c .)"
|
||||
[ "$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"
|
||||
@@ -340,7 +333,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" | has_match -qi 'UNACCOUNTED=0' || fail_msg "R10: a consumed state must be 0 unaccounted (no re-enumeration) [$out]"
|
||||
echo "$out" | grep -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
|
||||
|
||||
@@ -379,7 +372,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" | has_match -qi 'UNACCOUNTED=1' || fail_msg "R11: exactly ONE source (the gap r2) must be unaccounted; the consumed r1 must be suppressed [$out]"
|
||||
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]"
|
||||
# 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.)
|
||||
@@ -390,7 +383,7 @@ EOF
|
||||
|
||||
echo
|
||||
if [ -s "$FAILFILE" ]; then
|
||||
echo "wake reconcile harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2
|
||||
echo "wake reconcile harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "wake reconcile harness: all invariants passed ($pass groups)"
|
||||
|
||||
@@ -42,13 +42,6 @@
|
||||
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
|
||||
if ! . "$SCRIPT_DIR/_wake-common.sh"; then
|
||||
echo "WAKE-ASSERT INIT ABORT: cannot source _wake-common.sh — suite ran ZERO wake assertions (#984)" >&2
|
||||
exit 97
|
||||
fi
|
||||
wake_assert_init
|
||||
STORE="$SCRIPT_DIR/store.sh"
|
||||
ACK="$SCRIPT_DIR/ack.sh"
|
||||
|
||||
@@ -103,14 +96,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" | 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]"
|
||||
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]"
|
||||
# consumed_seq advances only on CONSUMED.
|
||||
"$STORE" consume --upto 2 >/dev/null
|
||||
cur="$("$STORE" cursors)"
|
||||
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]"
|
||||
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]"
|
||||
) && ok
|
||||
|
||||
echo "== T2: digest coalesce-REPLACE vs actionable APPEND =="
|
||||
@@ -123,16 +116,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")' | count_lines . || true)"
|
||||
ndigest="$(printf '%s\n' "$out" | jq -c 'select(.class=="digest")' | grep -c . || 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")' | count_lines . || true)"
|
||||
nhuman="$(printf '%s\n' "$out" | jq -c 'select(.class=="human")' | count_lines . || true)"
|
||||
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" = "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" | count_lines . || true)"
|
||||
total="$(printf '%s\n' "$out" | grep -c . || true)"
|
||||
[ "$total" = "3" ] || fail_msg "T2: durable store should hold digest+actionable+human = 3, got $total"
|
||||
) && ok
|
||||
|
||||
@@ -148,7 +141,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" | has_match -q 'consumed_seq=0' || fail_msg "T3: rejected CONSUMED must NOT advance the cursor [$cur]"
|
||||
echo "$cur" | grep -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"
|
||||
@@ -157,11 +150,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" | has_match -q 'consumed_seq=3' || fail_msg "T3: consumed_seq should be 3 after contiguous prefix filled [$cur]"
|
||||
echo "$cur" | grep -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" | has_match -q 'consumed_seq=3' || fail_msg "T3: cursor must not regress below 3 [$cur]"
|
||||
echo "$cur" | grep -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) =="
|
||||
@@ -176,7 +169,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" | has_match -q 'consumed_seq=0' || fail_msg "T4: RECEIVED must not advance consumed_seq [$cur]"
|
||||
echo "$cur" | grep -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 =="
|
||||
@@ -196,7 +189,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" | has_match -q 999 && fail_msg "T5: uncommitted garbage (seq 999) leaked into the live store"
|
||||
printf '%s\n' "$drained" | grep -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
|
||||
@@ -205,7 +198,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" | has_match -c .)" = "2" ] || fail_msg "T5: store not healthy after crash+recovery (expected 2 entries)"
|
||||
[ "$(printf '%s\n' "$d2" | grep -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
|
||||
@@ -216,7 +209,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" | has_match -c .)" = "2" ] || fail_msg "T5: store not healthy after maintenance reap (expected 2 entries)"
|
||||
[ "$(printf '%s\n' "$d3" | grep -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
|
||||
|
||||
@@ -261,12 +254,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" | has_match -c .)" = "2" ] || fail_msg "T6: entries not retained across restart (expected 2)"
|
||||
[ "$(printf '%s\n' "$d" | grep -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" | count_lines . || true)"
|
||||
n2="$(printf '%s\n' "$d2" | grep -c . || true)"
|
||||
[ "$n2" = "0" ] || fail_msg "T6: entries should be released only AFTER CONSUMED (still $n2 pending)"
|
||||
) && ok
|
||||
|
||||
@@ -303,7 +296,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" | has_match -q 'CONSUMED 1' || fail_msg "T7: CONSUMED not reported [$out]"
|
||||
echo "$out" | grep -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' ||
|
||||
@@ -328,8 +321,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" | 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]"
|
||||
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]"
|
||||
# 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
|
||||
@@ -338,7 +331,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" | has_match -qi 'anti-swallow' || fail_msg "T8: the refusal must name the anti-swallow guarantee [$err]"
|
||||
echo "$err" | grep -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.
|
||||
@@ -450,8 +443,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" | 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]"
|
||||
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]"
|
||||
# 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}"
|
||||
@@ -499,7 +492,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).
|
||||
has_match -qi 'cursor' "$errfile" || fail_msg "T11: the failure diagnostic must name the observed_seq cursor write [$(cat "$errfile")]"
|
||||
grep -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')"
|
||||
@@ -543,7 +536,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 | has_match -q 'consumed_seq=3' || fail_msg "T12: consumed_seq must be 3 after CONSUMED 3"
|
||||
"$STORE" cursors | grep -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) =="
|
||||
@@ -562,10 +555,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" | 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]"
|
||||
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]"
|
||||
cur="$("$STORE" cursors)"
|
||||
echo "$cur" | has_match -q 'consumed_seq=0' || fail_msg "T13: a refused consume must NOT advance the cursor [$cur]"
|
||||
echo "$cur" | grep -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.
|
||||
@@ -573,7 +566,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" | has_match -q 'consumed_seq=1' || fail_msg "T13: cursor must still be 1 after the refused ack [$cur]"
|
||||
echo "$cur" | grep -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 =="
|
||||
@@ -592,9 +585,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'"
|
||||
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"
|
||||
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"
|
||||
# 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
|
||||
@@ -604,7 +597,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).
|
||||
has_match -qxF '2' "$qf" 2>/dev/null && fail_msg "T14: seq 2 must be PRUNED from quarantined.set after the forced step-over"
|
||||
grep -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
|
||||
@@ -614,8 +607,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" | 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")]"
|
||||
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")]"
|
||||
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
|
||||
@@ -678,10 +671,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
|
||||
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)"
|
||||
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)"
|
||||
# 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
|
||||
@@ -690,10 +683,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"
|
||||
[ "$(has_match -c . "$dl")" = "2" ] || fail_msg "T16: the dead-letter LEDGER must be untouched by --repair"
|
||||
[ "$(grep -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")]"
|
||||
has_match -qi 'OK' "$TMP_ROOT/t16.ok" || fail_msg "T16: a clean audit must say OK [$(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")]"
|
||||
) && 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 =="
|
||||
@@ -729,20 +722,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")]"
|
||||
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")]"
|
||||
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")]"
|
||||
# The wording under test (#952): BOTH residual classes, named.
|
||||
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")]"
|
||||
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")]"
|
||||
# --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"
|
||||
[ "$(count_lines . "$dl")" = "1" ] || fail_msg "T17: the dead-letter LEDGER must be untouched"
|
||||
[ "$(grep -c . "$dl")" = "1" ] || fail_msg "T17: the dead-letter LEDGER must be untouched"
|
||||
) && ok
|
||||
|
||||
echo
|
||||
if [ -s "$FAILFILE" ]; then
|
||||
echo "wake store/ack harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2
|
||||
echo "wake store/ack harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "wake store/ack harness: all invariants passed ($pass groups)"
|
||||
|
||||
@@ -32,13 +32,6 @@
|
||||
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
|
||||
if ! . "$SCRIPT_DIR/_wake-common.sh"; then
|
||||
echo "WAKE-ASSERT INIT ABORT: cannot source _wake-common.sh — suite ran ZERO wake assertions (#984)" >&2
|
||||
exit 97
|
||||
fi
|
||||
wake_assert_init
|
||||
STORE="$SCRIPT_DIR/store.sh"
|
||||
|
||||
command -v jq >/dev/null 2>&1 || {
|
||||
@@ -154,7 +147,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 | count_lines . || true)"
|
||||
n_tmp="$(find "$STATE_DIR" -maxdepth 1 -name '.wake.tmp.*' -type f | grep -c . || 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),
|
||||
@@ -180,7 +173,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 has_match -q 'durable pending write FAILED' "$a_err" 2>/dev/null; then
|
||||
if grep -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")"
|
||||
@@ -196,20 +189,20 @@ EOF
|
||||
|
||||
# #908 store invariants: both durably landed, cursor reached 2, no burned seq.
|
||||
cur="$("$STORE" cursors)"
|
||||
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]"
|
||||
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]"
|
||||
# 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 | count_lines . || true)"
|
||||
n_leak="$(find "$STATE_DIR" -maxdepth 1 -name '.wake.tmp.*' -type f | grep -c . || 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 ($(count_lines . "$FAILFILE") assertion(s)) — #927 TOCTOU reproduced (RED)" >&2
|
||||
echo "wake store enqueue-race harness: FAILED ($(grep -c . "$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)"
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""check-973.py — computation backend for the #973 validation harness.
|
||||
|
||||
Everything here derives from exactly two inputs: the frozen denominator
|
||||
artifact (denominator-089615f.json) and the SOURCE TEXT of the ten converted
|
||||
suites at the current tree. It never reads the ledger — set arithmetic against
|
||||
the runtime trace belongs to validate-973.sh, so the two legs of the
|
||||
comparison come from independent code paths.
|
||||
|
||||
Subcommands (all print sorted, stable output; non-zero exit on any failure):
|
||||
|
||||
expected The expected coordinate set from the ARTIFACT: one
|
||||
"<helper> <file>:<line+7>" row per denominator row (+7 = 3
|
||||
converter header lines + 4 lines from the #984 source guard,
|
||||
uniform across all ten suites).
|
||||
Multi-grep lines stay ONE coordinate.
|
||||
|
||||
static The converted-site inventory from the SOURCE TEXT at the current
|
||||
tree: every non-comment line bearing a has_match/count_lines
|
||||
token, as "<helper> <file>:<line>". Independent of the artifact
|
||||
row list, so `expected == static` is a real check on the
|
||||
conversion, not a tautology. (Amendment ONE, leg 1: the ledger is
|
||||
an execution trace, not an inventory — the inventory must come
|
||||
from the text.)
|
||||
|
||||
arms The forced-error arm list: the 19 denominator canaries plus one
|
||||
E-form arm (store-ack:733→740, a $(count_lines) capture compared
|
||||
afterward — the A6 shape) plus one F-form arm (quarantine:560→567,
|
||||
the multi-grep pipeline capture), as "<helper> <file>:<line+7>
|
||||
<form>". Both extras are asserted to exist in the artifact with
|
||||
the expected form — a renumber that moved them fails here, not
|
||||
silently downstream.
|
||||
|
||||
sweep Residual sweep: the denominator's own classifier (ported from the
|
||||
frozen derivation) over the ten suites at the current tree must
|
||||
find ZERO unconverted verdict-form grep sites; and, IN THE SAME
|
||||
RUN, eight specimens (six per-form + two absorb-branch probes, #985)
|
||||
planted into a temp copy of a real
|
||||
suite must ALL be found with their correct forms — an instrument
|
||||
that reports zero must first be seen finding what it claims to
|
||||
find (A5).
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
WAKE = HERE.parent
|
||||
ART = HERE / "denominator-089615f.json"
|
||||
|
||||
HEADER_SHIFT = 7 # 3 converter header lines after SCRIPT_DIR + 4 lines from the
|
||||
# #984 source guard (1-line `. _wake-common.sh && wake_assert_init` became a 5-line
|
||||
# guarded block) — both uniform across all ten suites, both above every site.
|
||||
|
||||
# The two hand-picked extra arms (base coordinates; forms asserted at load).
|
||||
EXTRA_ARMS = [
|
||||
("test-wake-store-ack.sh", 733, "E-count-capture"),
|
||||
("test-wake-digest-quarantine.sh", 560, "F-extract-capture"),
|
||||
]
|
||||
|
||||
RX_HELPER = re.compile(r"(^|[^A-Za-z0-9_.-])(has_match|count_lines)([^A-Za-z0-9_.-]|$)")
|
||||
|
||||
# ---- classifier, ported verbatim in logic from the frozen denominator
|
||||
# ---- derivation (docs/journal/fleet/drift-derive-089615f__pepper.py)
|
||||
RX_FAIL_SAME = re.compile(r"(\|\||&&)\s*fail")
|
||||
RX_COUNT_SUB = re.compile(r"\$\(.*grep\s+[^)]*-c|\$\(\s*grep\s+-c")
|
||||
RX_ASSIGN_SUB = re.compile(r'=\s*"?\$\(.*grep')
|
||||
RX_IF = re.compile(r"^\s*(el)?if\s+.*grep")
|
||||
RX_GREP = re.compile(r"(^|[^A-Za-z0-9_.-])grep([^A-Za-z0-9_.-]|$)")
|
||||
|
||||
# grep in COMMAND position: at line start or after a command separator / subshell
|
||||
# opener / shell keyword / `!`. Quote-unaware by design — a quoted "grep" after a
|
||||
# separator reads as a command and lands the line in residual, which fails LOUD;
|
||||
# the absorb direction (note) is the one that must never fire on a real verdict.
|
||||
RX_GREP_CMD = re.compile(
|
||||
r"(?:^|[;|&(`]|\$\(|\bif\b|\belif\b|\bthen\b|\belse\b|\bdo\b|\bwhile\b|\buntil\b|!)"
|
||||
r"\s*grep(?:\s|$)"
|
||||
)
|
||||
|
||||
|
||||
def classify(lines):
|
||||
"""Return (sites, dispo). Every line containing the word grep gets a row."""
|
||||
sites, dispo = [], []
|
||||
n = len(lines)
|
||||
for i, raw in enumerate(lines):
|
||||
line = raw
|
||||
ln = i + 1
|
||||
if not RX_GREP.search(line):
|
||||
continue
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("#"):
|
||||
dispo.append((ln, "comment", stripped))
|
||||
continue
|
||||
nxt = ""
|
||||
for j in range(i + 1, min(i + 3, n)):
|
||||
if lines[j].strip():
|
||||
nxt = lines[j].strip()
|
||||
break
|
||||
if "$(" in line and RX_COUNT_SUB.search(line):
|
||||
sites.append((ln, "E-count-capture", stripped))
|
||||
continue
|
||||
if RX_ASSIGN_SUB.search(line) and "grep -c" not in line:
|
||||
sites.append((ln, "F-extract-capture", stripped))
|
||||
continue
|
||||
if RX_FAIL_SAME.search(line):
|
||||
sites.append((ln, "A-same-line", stripped))
|
||||
continue
|
||||
if stripped.endswith("\\"):
|
||||
k = i + 1
|
||||
joined = stripped[:-1]
|
||||
while k < n:
|
||||
cont = lines[k].strip()
|
||||
joined += " " + (cont[:-1] if cont.endswith("\\") else cont)
|
||||
if not cont.endswith("\\"):
|
||||
break
|
||||
k += 1
|
||||
if re.search(r"(\|\||&&)\s*fail", joined) or (
|
||||
joined.rstrip().endswith(("||", "&&"))
|
||||
and k + 1 < n
|
||||
and lines[k + 1].strip().startswith("fail")
|
||||
):
|
||||
sites.append((ln, "C-cont-backslash", stripped))
|
||||
continue
|
||||
dispo.append((ln, "backslash-no-fail-continuation", stripped))
|
||||
continue
|
||||
if stripped.endswith(("||", "&&")) and nxt.startswith("fail"):
|
||||
sites.append((ln, "B-cont-operator", stripped))
|
||||
continue
|
||||
if RX_IF.search(line):
|
||||
window = " ".join(lines[j] for j in range(i, min(i + 5, n)))
|
||||
if "fail" in window:
|
||||
sites.append((ln, "D-if-form", stripped))
|
||||
continue
|
||||
dispo.append((ln, "if-grep-no-fail-window", stripped))
|
||||
continue
|
||||
win = " ".join(lines[j] for j in range(max(0, i - 2), min(i + 3, n)))
|
||||
if re.search(r"fail", win, re.I):
|
||||
dispo.append((ln, "BACKSTOP-HAND-REVIEW", stripped))
|
||||
else:
|
||||
dispo.append((ln, "no-verdict-context", stripped))
|
||||
return sites, dispo
|
||||
|
||||
|
||||
def residual_sites(lines):
|
||||
"""classify() plus the absorb decision — the ONE path both sweep legs share.
|
||||
|
||||
A classified site is absorbed as a note only when its line carries a wake
|
||||
helper token AND the line shows no grep in command position: a converted
|
||||
line whose PATTERN argument merely contains the word grep. A helper line
|
||||
that also runs a real grep verdict (has_match ... && grep -q SECRET ... &&
|
||||
fail) stays residual (#985). Multi-line forms anchor the site at the line
|
||||
containing grep, so a command-position grep on a continuation line never
|
||||
shares its line with the helper token and stays residual by construction.
|
||||
"""
|
||||
sites, dispo = classify(lines)
|
||||
residual, notes = [], []
|
||||
for ln, form, text in sites:
|
||||
line = lines[ln - 1]
|
||||
if RX_HELPER.search(line) and not RX_GREP_CMD.search(line):
|
||||
notes.append((ln, form, text))
|
||||
else:
|
||||
residual.append((ln, form, text))
|
||||
return residual, notes, dispo
|
||||
|
||||
|
||||
def load_art():
|
||||
art = json.loads(ART.read_text())
|
||||
assert art["total"] == 261 == len(art["rows"]), "artifact self-consistency"
|
||||
return art
|
||||
|
||||
|
||||
def helper_for(row):
|
||||
return "count_lines" if row["form"].startswith("E") else "has_match"
|
||||
|
||||
|
||||
def suite_files(art):
|
||||
return sorted({r["file"] for r in art["rows"]})
|
||||
|
||||
|
||||
def cmd_expected():
|
||||
art = load_art()
|
||||
out = sorted(
|
||||
f"{helper_for(r)} {r['file']}:{r['line'] + HEADER_SHIFT}" for r in art["rows"]
|
||||
)
|
||||
assert len(out) == len(set(out)) == 261, "expected set must be 261 distinct rows"
|
||||
print("\n".join(out))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_static():
|
||||
art = load_art()
|
||||
rows = []
|
||||
for f in suite_files(art):
|
||||
for i, line in enumerate((WAKE / f).read_text().split("\n"), start=1):
|
||||
if line.strip().startswith("#"):
|
||||
continue
|
||||
m = RX_HELPER.search(line)
|
||||
if not m:
|
||||
continue
|
||||
helper = (
|
||||
"count_lines"
|
||||
if RX_HELPER.search(line).group(2) == "count_lines"
|
||||
else "has_match"
|
||||
)
|
||||
rows.append(f"{helper} {f}:{i}")
|
||||
print("\n".join(sorted(rows)))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_arms():
|
||||
art = load_art()
|
||||
by_key = {(r["file"], r["line"]): r for r in art["rows"]}
|
||||
rows = []
|
||||
canaries = [r for r in art["rows"] if r.get("canary")]
|
||||
assert len(canaries) == 19, f"expected 19 canaries, artifact has {len(canaries)}"
|
||||
for r in canaries:
|
||||
rows.append(f"{helper_for(r)} {r['file']}:{r['line'] + HEADER_SHIFT} {r['form']}")
|
||||
for f, ln, want_form in EXTRA_ARMS:
|
||||
r = by_key.get((f, ln))
|
||||
assert r is not None, f"extra arm {f}:{ln} not in artifact — renumbered?"
|
||||
assert r["form"] == want_form, f"extra arm {f}:{ln} form {r['form']} != {want_form}"
|
||||
rows.append(f"{helper_for(r)} {f}:{ln + HEADER_SHIFT} {r['form']}")
|
||||
assert len(rows) == 21
|
||||
print("\n".join(rows))
|
||||
return 0
|
||||
|
||||
|
||||
# (expected classify form, expected disposition through residual_sites, snippet)
|
||||
PLANTS = [
|
||||
("A-same-line", "residual", ['grep -q needle haystack || fail "plant-A"']),
|
||||
("B-cont-operator", "residual", ["grep -q needle haystack ||", ' fail "plant-B"']),
|
||||
("C-cont-backslash", "residual", ["grep -q needle \\", ' haystack || fail "plant-C"']),
|
||||
("D-if-form", "residual", ["if ! grep -q needle haystack; then", ' fail "plant-D"', "fi"]),
|
||||
("E-count-capture", "residual", ['[ "$(grep -c needle haystack)" = "1" ] || fail "plant-E"']),
|
||||
("F-extract-capture", "residual", ['val="$(grep needle haystack)"']),
|
||||
# G: a converted line that ALSO runs a raw grep verdict — the helper token
|
||||
# must not absorb it (#985)
|
||||
("A-same-line", "residual", ['has_match -q needle "$F" && grep -q SECRET "$F" && fail "plant-G"']),
|
||||
# H: negative control — helper whose PATTERN argument is the word grep;
|
||||
# must be absorbed as a note, never residual
|
||||
("A-same-line", "note", ['has_match -q "grep" haystack || fail "plant-H"']),
|
||||
]
|
||||
|
||||
|
||||
def cmd_sweep():
|
||||
art = load_art()
|
||||
bad = 0
|
||||
|
||||
# leg 1: real suites at the current tree must be residual-free
|
||||
for f in suite_files(art):
|
||||
residual, notes, _dispo = residual_sites((WAKE / f).read_text().split("\n"))
|
||||
for ln, form, text in notes:
|
||||
# converted line whose PATTERN argument contains the word grep:
|
||||
# not an unconverted site, but never silently absorbed either
|
||||
print(f"SWEEP-NOTE {f}:{ln} converted line matches grep-token ({form}): {text[:80]}")
|
||||
for ln, form, text in residual:
|
||||
print(f"SWEEP-RESIDUAL {f}:{ln} {form}: {text[:100]}")
|
||||
bad += 1
|
||||
print(f"SWEEP {f}: {len(residual)} residual verdict site(s)")
|
||||
|
||||
# leg 2, SAME RUN, SAME PATH as leg 1: the instrument must find every plant
|
||||
# with the right form AND the right absorb disposition — plants G/H exercise
|
||||
# the absorb branch itself, so this leg must go through residual_sites(),
|
||||
# not raw classify()
|
||||
donor = suite_files(art)[0]
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
planted = Path(td) / donor
|
||||
shutil.copy(WAKE / donor, planted)
|
||||
base_lines = planted.read_text().split("\n")
|
||||
offset = len(base_lines)
|
||||
expect = {}
|
||||
for form, dispo, snippet in PLANTS:
|
||||
expect[offset + 1] = (form, dispo) # first physical line of each plant
|
||||
base_lines.extend(snippet)
|
||||
offset = len(base_lines)
|
||||
planted.write_text("\n".join(base_lines))
|
||||
residual, notes, _ = residual_sites(planted.read_text().split("\n"))
|
||||
found = {ln: (form, "residual") for ln, form, _t in residual}
|
||||
found.update({ln: (form, "note") for ln, form, _t in notes})
|
||||
unexpected = [(ln, form) for ln, form, _t in residual if ln not in expect]
|
||||
hits = sum(1 for ln, want in expect.items() if found.get(ln) == want)
|
||||
n_plants = len(PLANTS)
|
||||
print(f"SWEEP-PLANTS found={hits}/{n_plants} in planted copy of {donor}")
|
||||
if hits != n_plants:
|
||||
for ln, want in sorted(expect.items()):
|
||||
got = found.get(ln, ("<missed>", "<missed>"))
|
||||
if got != want:
|
||||
print(f"SWEEP-PLANT-MISS line {ln}: expected {want}, got {got}")
|
||||
bad += 1
|
||||
if unexpected:
|
||||
# the donor is a converted suite: any non-plant RESIDUAL site in the
|
||||
# copy contradicts the zero leg 1 just reported on the original
|
||||
# (non-plant notes mirror leg 1's treatment: printed there, not bad)
|
||||
for ln, form in unexpected:
|
||||
print(f"SWEEP-PLANT-UNEXPECTED {donor}(copy):{ln} {form}")
|
||||
bad += 1
|
||||
|
||||
return 1 if bad else 0
|
||||
|
||||
|
||||
def main():
|
||||
cmds = {
|
||||
"expected": cmd_expected,
|
||||
"static": cmd_static,
|
||||
"arms": cmd_arms,
|
||||
"sweep": cmd_sweep,
|
||||
}
|
||||
if len(sys.argv) != 2 or sys.argv[1] not in cmds:
|
||||
sys.exit(f"usage: check-973.py {{{'|'.join(cmds)}}}")
|
||||
sys.exit(cmds[sys.argv[1]]())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,131 +0,0 @@
|
||||
#!/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()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,299 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# microtest-wake-assert.sh — #973 instrument self-test. Run BEFORE trusting any
|
||||
# validate-run evidence: it proves the counted ledger and the abort mechanics on
|
||||
# two generated mini-suites, so a defect in the instrument cannot silently wear
|
||||
# the colour of a clean validation.
|
||||
#
|
||||
# What it proves (each check named C1..C8 below):
|
||||
# C1 green run: ledger set EQUALS a text-derived expected set spanning TWO
|
||||
# files (file-field discrimination), row count > 1, both sentinels emitted,
|
||||
# exit 0. Also pins the BASH_LINENO convention for backslash-continuation
|
||||
# call sites against the first-physical-line convention the denominator
|
||||
# artifact uses.
|
||||
# C2 early-exit truncation: a suite that exits before its later site yields a
|
||||
# SHORT ledger, and the expected-set comparison catches it — a counted
|
||||
# ledger must report its own truncation, never a smaller total.
|
||||
# C3 abort from inside a `( ... )` test subshell kills the WHOLE suite: no
|
||||
# sentinel, non-zero exit, loud named reason (file:line + raw rc).
|
||||
# C4 abort stays loud at a call site that appends 2>/dev/null (the preimage
|
||||
# canary shape) — the saved-fd path.
|
||||
# C5 abort escapes a `$( count_lines ... )` substitution (A6 shape): the
|
||||
# count from a failed measurement is never compared and the suite dies.
|
||||
# C6 abort escapes a pipeline tail (`printf | has_match`).
|
||||
# C7 count_lines prints 0 on grep rc 1 (zero matches is a measurement, not an
|
||||
# error) — implicit in C1's green run via the delta-count site.
|
||||
# C8 an env-prefix on the helper (`LC_ALL=C has_match ...`) reaches the grep
|
||||
# child — pins the conversion shape for the digest-hmac LC_ALL site.
|
||||
# C9 an arm that matches NO site is loud about it by omission: green run,
|
||||
# sentinel present, and NO "WAKE-ASSERT ARMED" line — so "did not abort"
|
||||
# is separable into arm-never-matched (no ARMED line) vs error-path-
|
||||
# broken (ARMED line, no abort). C3..C6 require the ARMED line AND the
|
||||
# aborting site's ledger row (append lands BEFORE the grep runs, so an
|
||||
# abort can never shorten the count it is part of).
|
||||
# C10 the BASH_LINENO pin's abort arm fires: under a probe interpreter that
|
||||
# misreports the continuation line, wake_assert_init aborts loudly and
|
||||
# nothing past init executes — a pin whose failure arm was never seen
|
||||
# firing is an undertaking, not a control.
|
||||
# C11 the FAILED-summary template executes on the red path: a mini-suite
|
||||
# driven deterministically red emits the exact converted summary shape
|
||||
# (`FAILED ($(count_lines . "$FAILFILE") assertion(s))`) with the right
|
||||
# count, exits 1, and the summary site's ledger row lands. The nine
|
||||
# real-suite summary sites are structurally unreachable in a green run
|
||||
# (guarded by [ -s "$FAILFILE" ]); their dispositions cite THIS check as
|
||||
# the measured execution of the same template, so "unexecuted in the
|
||||
# green run" never silently means "never executed anywhere".
|
||||
set -uo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
export WAKE_COMMON="$HERE/../_wake-common.sh"
|
||||
[ -f "$WAKE_COMMON" ] || {
|
||||
echo "microtest: _wake-common.sh not found at $WAKE_COMMON" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
fails=0
|
||||
check() { # check NAME COND-DESCRIPTION (pass/fail already decided by caller: $1=name $2=0|1 $3=detail)
|
||||
if [ "$2" -eq 0 ]; then
|
||||
echo " PASS $1"
|
||||
else
|
||||
echo " FAIL $1 — $3"
|
||||
fails=$((fails + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# --- fixture data ----------------------------------------------------------
|
||||
printf 'alpha\nbeta\nbeta\ngamma-unused\n' >"$TMP/data.txt"
|
||||
|
||||
# --- mini-suite A: six helper sites across every converted form ------------
|
||||
cat >"$TMP/mini-a.sh" <<'MINI_A'
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
. "$WAKE_COMMON"
|
||||
wake_assert_init
|
||||
TMP="$1"
|
||||
FAILFILE="$TMP/failures-a"
|
||||
: >"$FAILFILE"
|
||||
fail_msg() { echo " FAIL: $*" >&2; echo x >>"$FAILFILE"; }
|
||||
ok() { :; }
|
||||
(
|
||||
has_match -q alpha "$TMP/data.txt" || fail_msg "alpha missing" # SITE:or-subshell
|
||||
) && ok
|
||||
(
|
||||
has_match -q FORBIDDEN "$TMP/data.txt" 2>/dev/null && fail_msg "forbidden present" # SITE:and-swallow
|
||||
) && ok
|
||||
(
|
||||
[ "$(count_lines beta "$TMP/data.txt")" = "2" ] || fail_msg "beta count" # SITE:count-capture
|
||||
) && ok
|
||||
(
|
||||
printf 'gamma\n' | has_match -q gamma || fail_msg "gamma pipeline" # SITE:pipeline
|
||||
) && ok
|
||||
(
|
||||
has_match -q \
|
||||
alpha "$TMP/data.txt" || fail_msg "continuation" # SITE:continuation
|
||||
) && ok
|
||||
(
|
||||
[ "$(count_lines delta "$TMP/data.txt")" = "0" ] || fail_msg "delta zero" # SITE:count-zero
|
||||
) && ok
|
||||
if [ -s "$FAILFILE" ]; then
|
||||
echo "mini-a: FAILED" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "mini-a: OK" >&2
|
||||
MINI_A
|
||||
|
||||
# --- mini-suite B: second file, one site behind an early exit --------------
|
||||
cat >"$TMP/mini-b.sh" <<'MINI_B'
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
. "$WAKE_COMMON"
|
||||
wake_assert_init
|
||||
TMP="$1"
|
||||
(
|
||||
has_match -q alpha "$TMP/data.txt" || echo "b1 missing" >&2 # SITE:b-first
|
||||
)
|
||||
if [ "${MINI_B_EARLY_EXIT:-}" = "1" ]; then
|
||||
exit 0
|
||||
fi
|
||||
(
|
||||
has_match -q beta "$TMP/data.txt" || echo "b2 missing" >&2 # SITE:b-second
|
||||
)
|
||||
echo "mini-b: OK" >&2
|
||||
MINI_B
|
||||
chmod +x "$TMP/mini-a.sh" "$TMP/mini-b.sh"
|
||||
|
||||
# Text-derived expected set: helper-name + basename:line for every SITE-marked
|
||||
# call, taken from the generated files' TEXT (independent of BASH_LINENO), with
|
||||
# the continuation site expected at its FIRST physical line — the denominator
|
||||
# artifact's convention.
|
||||
expected_set() { # expected_set FILE
|
||||
local f="$1" base
|
||||
base="$(basename "$f")"
|
||||
awk '
|
||||
/# SITE:/ {
|
||||
line = NR
|
||||
if ($0 !~ /has_match|count_lines/) line = NR - 1 # marker on the continuation tail
|
||||
print line
|
||||
}
|
||||
' "$f" | while read -r ln; do
|
||||
txt="$(sed -n "${ln}p" "$f")"
|
||||
case "$txt" in
|
||||
*count_lines*) printf 'count_lines %s:%s\n' "$base" "$ln" ;;
|
||||
*) printf 'has_match %s:%s\n' "$base" "$ln" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
site_line() { # site_line FILE MARKER -> first physical line of that call
|
||||
local f="$1" marker="$2" ln
|
||||
ln="$(grep -n "# SITE:${marker}\$" "$f" | cut -d: -f1)"
|
||||
# continuation marker sits on the tail line; the call starts one line up
|
||||
if ! sed -n "${ln}p" "$f" | grep -Eq 'has_match|count_lines'; then
|
||||
ln=$((ln - 1))
|
||||
fi
|
||||
printf '%s' "$ln"
|
||||
}
|
||||
|
||||
# --- C1: green run, two files, set equality --------------------------------
|
||||
LEDGER="$TMP/ledger-c1"
|
||||
: >"$LEDGER"
|
||||
outA="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$TMP/mini-a.sh" "$TMP" 2>&1)"
|
||||
rcA=$?
|
||||
outB="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$TMP/mini-b.sh" "$TMP" 2>&1)"
|
||||
rcB=$?
|
||||
{ expected_set "$TMP/mini-a.sh"; expected_set "$TMP/mini-b.sh"; } | sort >"$TMP/expected-c1"
|
||||
sort "$LEDGER" >"$TMP/got-c1"
|
||||
n_expected="$(grep -c . "$TMP/expected-c1")"
|
||||
if [ "$rcA" -eq 0 ] && [ "$rcB" -eq 0 ] &&
|
||||
printf '%s' "$outA" | grep -q 'mini-a: OK' &&
|
||||
printf '%s' "$outB" | grep -q 'mini-b: OK' &&
|
||||
[ "$n_expected" -gt 1 ] &&
|
||||
cmp -s "$TMP/expected-c1" "$TMP/got-c1"; then
|
||||
check C1 0 ""
|
||||
else
|
||||
check C1 1 "rcA=$rcA rcB=$rcB expected($n_expected)/got diff: $(diff "$TMP/expected-c1" "$TMP/got-c1" 2>&1 | head -n 10 | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
# --- C2: early exit -> short ledger, comparison catches it -----------------
|
||||
LEDGER="$TMP/ledger-c2"
|
||||
: >"$LEDGER"
|
||||
WAKE_ASSERT_LEDGER="$LEDGER" MINI_B_EARLY_EXIT=1 bash "$TMP/mini-b.sh" "$TMP" >/dev/null 2>&1
|
||||
expected_set "$TMP/mini-b.sh" | sort >"$TMP/expected-c2"
|
||||
sort "$LEDGER" >"$TMP/got-c2"
|
||||
if ! cmp -s "$TMP/expected-c2" "$TMP/got-c2" &&
|
||||
grep -q "has_match mini-b.sh:$(site_line "$TMP/mini-b.sh" b-first)" "$TMP/got-c2" &&
|
||||
! grep -q "mini-b.sh:$(site_line "$TMP/mini-b.sh" b-second)" "$TMP/got-c2"; then
|
||||
check C2 0 ""
|
||||
else
|
||||
check C2 1 "truncated ledger was not detected as short"
|
||||
fi
|
||||
|
||||
# --- C3..C6: per-shape abort proofs ----------------------------------------
|
||||
abort_case() { # abort_case NAME MARKER HELPER
|
||||
local name="$1" marker="$2" helper="$3" ln site out rc ledger
|
||||
ln="$(site_line "$TMP/mini-a.sh" "$marker")"
|
||||
site="mini-a.sh:${ln}"
|
||||
ledger="$TMP/ledger-${name}"
|
||||
: >"$ledger"
|
||||
out="$(WAKE_ASSERT_LEDGER="$ledger" WAKE_ASSERT_FORCE_GREP_ERROR_AT="$site" \
|
||||
bash "$TMP/mini-a.sh" "$TMP" 2>&1)"
|
||||
rc=$?
|
||||
if [ "$rc" -ne 0 ] &&
|
||||
! printf '%s' "$out" | grep -q 'mini-a: OK' &&
|
||||
! printf '%s' "$out" | grep -q 'mini-a: FAILED' &&
|
||||
printf '%s' "$out" | grep -q "WAKE-ASSERT ARMED: forcing real grep error at $site" &&
|
||||
printf '%s' "$out" | grep -q "WAKE-ASSERT ABORT" &&
|
||||
printf '%s' "$out" | grep -q "$site" &&
|
||||
printf '%s' "$out" | grep -q "grep exit 2" &&
|
||||
grep -q "^${helper} ${site}\$" "$ledger"; then
|
||||
check "$name" 0 ""
|
||||
else
|
||||
check "$name" 1 "rc=$rc site=$site ledger=$(grep -c . "$ledger") out=$(printf '%s' "$out" | tail -n 3 | tr '\n' ' ')"
|
||||
fi
|
||||
}
|
||||
abort_case C3 or-subshell has_match
|
||||
abort_case C4 and-swallow has_match
|
||||
abort_case C5 count-capture count_lines
|
||||
abort_case C6 pipeline has_match
|
||||
|
||||
# --- C7: covered by C1 (delta-count site prints 0 on grep rc 1) ------------
|
||||
check C7 0 ""
|
||||
|
||||
# --- C8: env-prefix on a function reaches the grep child -------------------
|
||||
envprobe() { command env | command grep -c '^LC_ALL=xx_wake_test$'; }
|
||||
got="$(LC_ALL=xx_wake_test envprobe 2>/dev/null)" # bash's setlocale warning about the fake locale is itself proof the prefix landed
|
||||
if [ "$got" = "1" ]; then check C8 0 ""; else check C8 1 "env-prefix did not reach child (got=$got)"; fi
|
||||
|
||||
# --- C9: arm matching NO site -> green run, no ARMED line ------------------
|
||||
out="$(WAKE_ASSERT_FORCE_GREP_ERROR_AT="mini-a.sh:9999" bash "$TMP/mini-a.sh" "$TMP" 2>&1)"
|
||||
rc=$?
|
||||
if [ "$rc" -eq 0 ] &&
|
||||
printf '%s' "$out" | grep -q 'mini-a: OK' &&
|
||||
! printf '%s' "$out" | grep -q 'WAKE-ASSERT ARMED'; then
|
||||
check C9 0 ""
|
||||
else
|
||||
check C9 1 "rc=$rc out=$(printf '%s' "$out" | tail -n 3 | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
# --- C10: lineno pin aborts under an interpreter that breaks the convention -
|
||||
cat >"$TMP/fake-bash" <<'FAKE'
|
||||
#!/usr/bin/env bash
|
||||
# stand-in for a bash whose BASH_LINENO convention differs: misreports the
|
||||
# continuation call one line low (the exact skew the pin exists to catch)
|
||||
printf '3\n5\n'
|
||||
FAKE
|
||||
chmod +x "$TMP/fake-bash"
|
||||
out="$(WAKE_ASSERT_PIN_BASH="$TMP/fake-bash" bash -c '. "$WAKE_COMMON" && wake_assert_init && echo REACHED-PAST-INIT' 2>&1)"
|
||||
rc=$?
|
||||
if [ "$rc" -ne 0 ] &&
|
||||
! printf '%s' "$out" | grep -q 'REACHED-PAST-INIT' &&
|
||||
printf '%s' "$out" | grep -q 'WAKE-ASSERT INIT ABORT: BASH_LINENO convention violated'; then
|
||||
check C10 0 ""
|
||||
else
|
||||
check C10 1 "rc=$rc out=$(printf '%s' "$out" | tail -n 2 | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
# --- C11: red path executes the converted FAILED-summary template -----------
|
||||
cat >"$TMP/mini-c.sh" <<'MINI_C'
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
. "$WAKE_COMMON"
|
||||
wake_assert_init
|
||||
TMP="$1"
|
||||
FAILFILE="$TMP/failures-c"
|
||||
: >"$FAILFILE"
|
||||
fail_msg() { echo " FAIL: $*" >&2; echo x >>"$FAILFILE"; }
|
||||
ok() { :; }
|
||||
(
|
||||
has_match -q alpha "$TMP/data.txt" && fail_msg "alpha present" # SITE:c-inverted (deterministically red: alpha IS in the fixture)
|
||||
) && ok
|
||||
echo
|
||||
if [ -s "$FAILFILE" ]; then
|
||||
echo "wake mini-c harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 # SITE:c-summary
|
||||
exit 1
|
||||
fi
|
||||
echo "wake mini-c harness: all invariants passed (1 group)"
|
||||
MINI_C
|
||||
chmod +x "$TMP/mini-c.sh"
|
||||
LEDGER="$TMP/ledger-c11"
|
||||
: >"$LEDGER"
|
||||
out="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$TMP/mini-c.sh" "$TMP" 2>&1)"
|
||||
rc=$?
|
||||
summary_ln="$(site_line "$TMP/mini-c.sh" c-summary)"
|
||||
if [ "$rc" -eq 1 ] &&
|
||||
printf '%s' "$out" | grep -q 'wake mini-c harness: FAILED (1 assertion(s))' &&
|
||||
! printf '%s' "$out" | grep -q 'all invariants passed' &&
|
||||
grep -q "^count_lines mini-c.sh:${summary_ln}\$" "$LEDGER"; then
|
||||
check C11 0 ""
|
||||
else
|
||||
check C11 1 "rc=$rc summary_ln=$summary_ln ledger=$(tr '\n' ' ' <"$LEDGER") out=$(printf '%s' "$out" | tail -n 2 | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fails" -gt 0 ]; then
|
||||
echo "microtest-wake-assert: FAILED ($fails check(s))" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "microtest-wake-assert: OK (all checks passed)"
|
||||
@@ -1,37 +0,0 @@
|
||||
# unexecuted-sites-dispositions.txt — #973 validation, amendment ONE leg 3.
|
||||
#
|
||||
# The ledger is an execution trace, not an inventory: a green instrumented run
|
||||
# cannot execute a site that only lives on a suite's red path. Every converted
|
||||
# site that did NOT appear in the green-run trace is enumerated here with an
|
||||
# individual disposition; validate-973.sh fails if any unexecuted site lacks a
|
||||
# row here, and ALSO fails if a row here names a site that DID execute (stale
|
||||
# disposition). Key = first two whitespace-separated fields; text after "—" is
|
||||
# the adjudication.
|
||||
#
|
||||
# All nine sites below are the same structural shape, adjudicated one by one
|
||||
# from source text: the suite's FAILED-branch summary line,
|
||||
# echo "wake <name> harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2
|
||||
# guarded by `if [ -s "$FAILFILE" ]` — structurally unreachable while every
|
||||
# assertion passes, which is precisely the state a green validation run is
|
||||
# required to be in. (The tenth suite, test-wake-preimage.sh, uses its own
|
||||
# X/Y summary format with no grep in the red branch, so it has no row here.)
|
||||
#
|
||||
# The disposition is NOT "it would work": the exact template is EXECUTED red
|
||||
# in microtest C11 (deterministically failed mini-suite, same
|
||||
# count_lines-in-substitution summary shape → right count, exit 1, ledger row
|
||||
# at the summary coordinate), and the E-in-substitution abort path is proven
|
||||
# by microtest C5 plus the forced-error arm at test-wake-store-ack.sh:736.
|
||||
# Each site's conversion text is independently verified by the static
|
||||
# inventory (check-973.py static == expected, all 261 rows).
|
||||
#
|
||||
# Verified guard per site (line numbers at branch tip, +3 header shift):
|
||||
|
||||
count_lines test-wake-beacon.sh:354 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 353; template execution measured by microtest C11; text verified by static inventory
|
||||
count_lines test-wake-detector.sh:706 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 705; template execution measured by microtest C11; text verified by static inventory
|
||||
count_lines test-wake-digest-hmac.sh:438 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 437; template execution measured by microtest C11; text verified by static inventory
|
||||
count_lines test-wake-digest-quarantine.sh:588 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 587; template execution measured by microtest C11; text verified by static inventory
|
||||
count_lines test-wake-fn-oracle.sh:136 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 135; template execution measured by microtest C11; text verified by static inventory
|
||||
count_lines test-wake-install.sh:438 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 437; template execution measured by microtest C11; text verified by static inventory
|
||||
count_lines test-wake-reconcile.sh:393 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 392; template execution measured by microtest C11; text verified by static inventory
|
||||
count_lines test-wake-store-ack.sh:745 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 744; template execution measured by microtest C11; text verified by static inventory
|
||||
count_lines test-wake-store-enqueue-race.sh:212 — red-path summary (with "#927 TOCTOU reproduced (RED)" tail); guard `[ -s "$FAILFILE" ]` at line 211; template execution measured by microtest C11; text verified by static inventory
|
||||
@@ -1,218 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# validate-973.sh — #973 validation driver. One run produces the complete
|
||||
# evidence chain for the 261-site conversion:
|
||||
#
|
||||
# 0. instrument self-test (microtest) — no validate evidence is trusted
|
||||
# before the instrument itself has been proven, including its abort arms.
|
||||
# 1. expected set: 261 coordinates from the FROZEN artifact (+7 header
|
||||
# shift: 3 converter lines + 4 #984 guard lines), count asserted against the number declared below BEFORE any
|
||||
# suite runs.
|
||||
# 2. static inventory: converted call sites re-derived from SOURCE TEXT,
|
||||
# must equal the expected set exactly (amendment ONE, leg 1 — the
|
||||
# inventory comes from the text, never from the ledger).
|
||||
# 3. green instrumented run: all ten suites with WAKE_ASSERT_LEDGER; each
|
||||
# must exit 0 AND emit its own sentinel (per-suite formats differ and are
|
||||
# pinned here — a suite that died early must never pass on another
|
||||
# suite's output).
|
||||
# 4. trace arithmetic on coordinate SETS (loops re-execute sites and the
|
||||
# multi-grep lines append twice per pass, so counts are meaningless;
|
||||
# sets are not):
|
||||
# trace − expected MUST be empty (a helper ran at a coordinate the
|
||||
# denominator never measured);
|
||||
# expected − trace = converted-but-never-executed: enumerated, and
|
||||
# every entry must carry a disposition in the
|
||||
# committed unexecuted-sites-dispositions.txt, with
|
||||
# no stale dispositions the other way (amendment
|
||||
# ONE, legs 2+3 — the ledger is an execution trace,
|
||||
# not an inventory; the difference is enumerated and
|
||||
# individually dispositioned, never silently absent).
|
||||
# 5. forced-error arms: the 19 denominator canaries plus one E-form and one
|
||||
# F-form site, each run with WAKE_ASSERT_FORCE_GREP_ERROR_AT: the suite
|
||||
# must emit the ARMED line (the arm proved it fired), the ABORT line
|
||||
# naming the site, exit non-zero, emit NO sentinel, and the aborting
|
||||
# site's ledger row must already be present (the append lands before the
|
||||
# grep).
|
||||
# 6. residual sweep: the denominator's own classifier finds zero unconverted
|
||||
# verdict greps in the suites — and eight plants (six per-form + two
|
||||
# absorb-branch probes, #985) in the same run.
|
||||
#
|
||||
# Output discipline (A10): every line that reports on a suite names the file
|
||||
# under test; exit codes are reported before failure counts.
|
||||
set -uo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WAKE="$(cd "$HERE/.." && pwd)"
|
||||
CHECK="$HERE/check-973.py"
|
||||
DISPO="$HERE/unexecuted-sites-dispositions.txt"
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# Declared BEFORE any suite runs (A2): the run must produce THESE numbers,
|
||||
# not be described by whatever numbers it produced.
|
||||
EXPECTED_SUITES=10
|
||||
EXPECTED_SITES=261
|
||||
EXPECTED_ARMS=21
|
||||
|
||||
fails=0
|
||||
flag() {
|
||||
printf 'FAIL %s\n' "$*"
|
||||
fails=$((fails + 1))
|
||||
}
|
||||
|
||||
SUITES=(
|
||||
test-wake-beacon.sh
|
||||
test-wake-detector.sh
|
||||
test-wake-digest-hmac.sh
|
||||
test-wake-digest-quarantine.sh
|
||||
test-wake-fn-oracle.sh
|
||||
test-wake-install.sh
|
||||
test-wake-preimage.sh
|
||||
test-wake-reconcile.sh
|
||||
test-wake-store-ack.sh
|
||||
test-wake-store-enqueue-race.sh
|
||||
)
|
||||
[ "${#SUITES[@]}" -eq "$EXPECTED_SUITES" ] ||
|
||||
flag "suite list has ${#SUITES[@]} entries, declared $EXPECTED_SUITES"
|
||||
|
||||
# Per-suite sentinel patterns, pinned: nine suites share the harness template
|
||||
# (enqueue-race appends a tail after it); preimage uses its own format.
|
||||
sentinel_for() {
|
||||
case "$1" in
|
||||
test-wake-preimage.sh) printf '%s' '^== test-wake-preimage: 17/17 passed ==$' ;;
|
||||
*) printf '%s' 'harness: all invariants passed' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- 0: instrument self-test ------------------------------------------------
|
||||
if bash "$HERE/microtest-wake-assert.sh" >"$TMP/microtest.out" 2>&1; then
|
||||
echo "MICROTEST microtest-wake-assert.sh exit=0 (instrument proven)"
|
||||
else
|
||||
rc=$?
|
||||
echo "MICROTEST microtest-wake-assert.sh exit=$rc"
|
||||
sed 's/^/ /' "$TMP/microtest.out" | tail -n 15
|
||||
flag "instrument self-test failed — no validate evidence below is trustworthy"
|
||||
fi
|
||||
|
||||
# --- 1+2: expected set (artifact) vs static inventory (source text) ---------
|
||||
python3 "$CHECK" expected | sort >"$TMP/expected.txt" ||
|
||||
flag "check-973.py expected failed"
|
||||
n_expected="$(grep -c . "$TMP/expected.txt")"
|
||||
echo "EXPECTED-SET $n_expected coordinates (declared: $EXPECTED_SITES)"
|
||||
[ "$n_expected" -eq "$EXPECTED_SITES" ] ||
|
||||
flag "expected set has $n_expected coordinates, declared $EXPECTED_SITES"
|
||||
|
||||
python3 "$CHECK" static | sort >"$TMP/static.txt" ||
|
||||
flag "check-973.py static failed"
|
||||
if cmp -s "$TMP/expected.txt" "$TMP/static.txt"; then
|
||||
echo "STATIC-INVENTORY equals expected set ($(grep -c . "$TMP/static.txt") rows from source text)"
|
||||
else
|
||||
flag "static inventory (source text) differs from expected set (artifact):"
|
||||
diff "$TMP/expected.txt" "$TMP/static.txt" | head -n 20 | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
# --- 3: green instrumented run ----------------------------------------------
|
||||
LEDGER="$TMP/ledger"
|
||||
: >"$LEDGER"
|
||||
for s in "${SUITES[@]}"; do
|
||||
out="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$WAKE/$s" 2>&1)"
|
||||
rc=$?
|
||||
if printf '%s\n' "$out" | grep -Eq "$(sentinel_for "$s")"; then
|
||||
sent="present"
|
||||
else
|
||||
sent="ABSENT"
|
||||
fi
|
||||
echo "SUITE $s exit=$rc sentinel=$sent"
|
||||
[ "$rc" -eq 0 ] || flag "$s exited $rc in the green instrumented run"
|
||||
[ "$sent" = "present" ] || flag "$s did not emit its sentinel"
|
||||
done
|
||||
|
||||
# --- 4: trace arithmetic on coordinate sets ---------------------------------
|
||||
sort -u "$LEDGER" >"$TMP/trace.txt"
|
||||
echo "TRACE $(grep -c . "$TMP/trace.txt") distinct coordinates from $(grep -c . "$LEDGER") ledger rows"
|
||||
|
||||
comm -13 "$TMP/expected.txt" "$TMP/trace.txt" >"$TMP/rogue.txt"
|
||||
if [ -s "$TMP/rogue.txt" ]; then
|
||||
flag "trace contains coordinates OUTSIDE the frozen denominator:"
|
||||
sed 's/^/ ROGUE /' "$TMP/rogue.txt"
|
||||
else
|
||||
echo "TRACE-MINUS-EXPECTED empty (no helper ran at an unmeasured coordinate)"
|
||||
fi
|
||||
|
||||
comm -23 "$TMP/expected.txt" "$TMP/trace.txt" >"$TMP/unexec.txt"
|
||||
n_unexec="$(grep -c . "$TMP/unexec.txt" || true)"
|
||||
echo "UNEXECUTED $n_unexec of $EXPECTED_SITES converted sites did not execute in the green run"
|
||||
if [ ! -f "$DISPO" ]; then
|
||||
flag "disposition file missing: $DISPO — every unexecuted site must be individually dispositioned"
|
||||
sed 's/^/ UNDISPOSITIONED /' "$TMP/unexec.txt"
|
||||
else
|
||||
awk '!/^#/ && NF >= 2 {print $1, $2}' "$DISPO" | sort -u >"$TMP/dispo-keys.txt"
|
||||
comm -23 "$TMP/unexec.txt" "$TMP/dispo-keys.txt" >"$TMP/undispo.txt"
|
||||
comm -13 "$TMP/unexec.txt" "$TMP/dispo-keys.txt" >"$TMP/stale-dispo.txt"
|
||||
if [ -s "$TMP/undispo.txt" ]; then
|
||||
flag "unexecuted sites WITHOUT a disposition:"
|
||||
sed 's/^/ UNDISPOSITIONED /' "$TMP/undispo.txt"
|
||||
fi
|
||||
if [ -s "$TMP/stale-dispo.txt" ]; then
|
||||
flag "dispositions for sites that DID execute (stale — the file no longer matches the run):"
|
||||
sed 's/^/ STALE-DISPO /' "$TMP/stale-dispo.txt"
|
||||
fi
|
||||
if [ ! -s "$TMP/undispo.txt" ] && [ ! -s "$TMP/stale-dispo.txt" ]; then
|
||||
echo "DISPOSITIONS all $n_unexec unexecuted sites individually dispositioned, none stale"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 5: forced-error arms ---------------------------------------------------
|
||||
python3 "$CHECK" arms >"$TMP/arms.txt" || flag "check-973.py arms failed"
|
||||
n_arms="$(grep -c . "$TMP/arms.txt")"
|
||||
echo "ARMS $n_arms forced-error arms (declared: $EXPECTED_ARMS)"
|
||||
[ "$n_arms" -eq "$EXPECTED_ARMS" ] ||
|
||||
flag "arm list has $n_arms entries, declared $EXPECTED_ARMS"
|
||||
|
||||
while read -r helper site form; do
|
||||
f="${site%%:*}"
|
||||
aled="$TMP/ledger-arm"
|
||||
: >"$aled"
|
||||
out="$(WAKE_ASSERT_LEDGER="$aled" WAKE_ASSERT_FORCE_GREP_ERROR_AT="$site" \
|
||||
bash "$WAKE/$f" 2>&1)"
|
||||
rc=$?
|
||||
bad=""
|
||||
[ "$rc" -ne 0 ] || bad="$bad exit=0"
|
||||
printf '%s\n' "$out" | grep -q "WAKE-ASSERT ARMED: forcing real grep error at $site" ||
|
||||
bad="$bad no-ARMED-line"
|
||||
printf '%s\n' "$out" | grep -q "WAKE-ASSERT ABORT: ${helper} at ${site}: grep exit" ||
|
||||
bad="$bad no-ABORT-line"
|
||||
# AND-polarity check (a match is the defect): a grep error (rc>=2) must be
|
||||
# its own loud arm — it cannot fall through as "no sentinel = pass".
|
||||
rc_sent=0
|
||||
printf '%s\n' "$out" | grep -Eq "$(sentinel_for "$f")" || rc_sent=$?
|
||||
case "$rc_sent" in
|
||||
0) bad="$bad sentinel-emitted" ;;
|
||||
1) : ;;
|
||||
*) bad="$bad sentinel-grep-error-rc=$rc_sent" ;;
|
||||
esac
|
||||
grep -q "^${helper} ${site}\$" "$aled" ||
|
||||
bad="$bad no-ledger-row"
|
||||
if [ -z "$bad" ]; then
|
||||
echo "ARM $site ($form) exit=$rc armed+abort+no-sentinel+ledger-row"
|
||||
else
|
||||
echo "ARM $site ($form) exit=$rc DEFECTS:$bad"
|
||||
flag "arm $site ($form) failed:$bad"
|
||||
fi
|
||||
done <"$TMP/arms.txt"
|
||||
|
||||
# --- 6: residual sweep ------------------------------------------------------
|
||||
if python3 "$CHECK" sweep >"$TMP/sweep.out" 2>&1; then
|
||||
echo "SWEEP exit=0"
|
||||
else
|
||||
echo "SWEEP exit=$?"
|
||||
flag "residual sweep failed"
|
||||
fi
|
||||
sed 's/^/ /' "$TMP/sweep.out"
|
||||
|
||||
# --- summary (exit codes above, failure count last — A10) --------------------
|
||||
echo
|
||||
if [ "$fails" -gt 0 ]; then
|
||||
echo "validate-973: FAILED ($fails failure(s))" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "validate-973: OK — $EXPECTED_SUITES suites, $EXPECTED_SITES sites, $EXPECTED_ARMS arms, sweep clean"
|
||||
Reference in New Issue
Block a user