Some checks failed
ci/woodpecker/pr/ci Pipeline was canceled
Live pilot finding #6 (BLOCKING): a reconciler enumeration enqueued class=actionable with soft locators {kind,id,path,observed_hash} failed §2.1's ACTIONABLE hard-locator taxonomy, so digest render exit-4'd the ENTIRE cumulative-state drain — 4 consecutive timer failures, NOTHING delivered, including clean unrelated entries (head-of-line blocking). FIX 1 (quarantine, don't wedge): digest.sh cmd_render's fail-loud pass is now PER-ENTRY. A render-refused ACTIONABLE entry (no hard locator) is DEAD-LETTERED to $STATE_DIR/dead-letter.jsonl + a loud per-entry alarm and EXCLUDED, while the REST of the cumulative set still renders (exit 0). The bad entry is accounted-for, never silently dropped; one malformed entry can no longer wedge the whole drain. FIX 2 (AMENDED — render-tier, NOT class=digest): reconcile.sh's store class is UNCHANGED (non-coalescing) — the naive class=digest was rejected because store §2.3/T2 coalescing would silently collapse distinct enumerations (proven: reconcile R6 goes RED). Instead digest.sh's actionable-classifier now treats locators.reconciled==true (set only by reconcile.sh; not source-forgeable) as ORIENTATION-tier: gate-exempt, rendered as an orientation pointer via _locator_line's digest-class vocabulary. No exit-4, no wedge, no coalescing loss; §2.3/T2/G3-R6 intact. RED-FIRST: new test-wake-digest-quarantine.sh (openssl-independent) proves (a) malformed {kind,id,path,observed_hash} quarantines while a clean sibling still delivers, (b) a reconciled enumeration renders orientation-tier (no exit-4), (c) TWO distinct enumerations both survive as separate orientation renders (anti-collapse — the class=digest silent-drop we rejected), and the preserved per-entry actionable fail-loud. D2/D6 in test-wake-digest-hmac.sh updated from whole-digest exit-4 to per-entry quarantine. Wake manifest bumped 0.6.2 -> 0.6.3 + inventory. Closes #920 Part of #892 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
174 lines
9.1 KiB
Bash
Executable File
174 lines
9.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# test-wake-digest-quarantine.sh — RED-FIRST invariant harness for #920 (EPIC
|
|
# #892): per-entry QUARANTINE of a render-refused digest entry (no head-of-line
|
|
# blocking) + reconciler ENUMERATIONS render ORIENTATION-tier.
|
|
#
|
|
# Each test asserts ONE invariant and goes RED against the pre-#920 digest.sh:
|
|
# Q1 (a) QUARANTINE + REST-DELIVERS: a malformed `actionable` carrying the live
|
|
# pilot's EXACT locator shape {kind,id,path,observed_hash} (no hard
|
|
# locator, no reconciled marker) is DEAD-LETTERED + alarmed AND EXCLUDED,
|
|
# while a clean valid sibling in the SAME drain still renders (exit 0).
|
|
# RED baseline: the old whole-digest exit-4 delivered NOTHING (the live
|
|
# head-of-line-blocking wedge). (#920 FIX1)
|
|
# Q2 (b) ENUM-AS-ORIENTATION: a reconciler enumeration (locators.reconciled==
|
|
# true) renders as an ORIENTATION-tier pointer and does NOT exit-4 / is
|
|
# NOT quarantined. RED baseline: reconciled `actionable` + soft locators
|
|
# {kind,id,path,observed_hash} -> exit-4. (#920 FIX2)
|
|
# Q3 (c) TWO DISTINCT ENUMERATIONS BOTH SURVIVE: two distinct enumerations both
|
|
# render as SEPARATE orientation pointers (neither coalesced away) — the
|
|
# anti-collapse proof pinning the amended ruling (the REJECTED class=digest
|
|
# would have collapsed them to one; store class stays non-coalescing). (#920 FIX2)
|
|
# Q4 PRESERVED ACTIONABLE FAIL-LOUD: a genuine malformed ACTIONABLE claim
|
|
# (no reconciled marker, no hard locator) is STILL loudly surfaced
|
|
# (dead-letter + alarm) and NEVER delivered as if valid — fail-loud is
|
|
# preserved, now per-entry. (§2.1)
|
|
# Q5 CLAIM-PRECEDENCE GATED: a non-actionable-class entry that carries a
|
|
# `claim` (a consequential fact) with no hard locator is STILL gated
|
|
# (quarantined), and the reconciled exemption does not leak to it. (§2.1)
|
|
#
|
|
# Hermetic: feeds controlled JSONL via `digest.sh render --from-file` — NO store,
|
|
# NO network, NO openssl (so it runs identically under the CI openssl-mask).
|
|
#
|
|
# Each test runs in its own (..) subshell for env isolation; the per-subshell
|
|
# WAKE_STATE_HOME export is intentional (mirrors test-wake-reconcile.sh).
|
|
# shellcheck disable=SC2030,SC2031
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
DIGEST="$SCRIPT_DIR/digest.sh"
|
|
|
|
command -v jq >/dev/null 2>&1 || {
|
|
echo "SKIP: jq not available" >&2
|
|
exit 0
|
|
}
|
|
|
|
TMP_ROOT="$(mktemp -d)"
|
|
trap 'rm -rf "$TMP_ROOT"' EXIT
|
|
|
|
FAILFILE="$TMP_ROOT/failures"
|
|
: >"$FAILFILE"
|
|
pass=0
|
|
fail_msg() {
|
|
echo " FAIL: $*" >&2
|
|
echo "x" >>"$FAILFILE"
|
|
}
|
|
ok() { pass=$((pass + 1)); }
|
|
|
|
# A 40-hex sha = a valid §2.1 hard locator.
|
|
SHA40="abcdef0123456789abcdef0123456789abcdef01"
|
|
|
|
# fresh_home NAME — a fresh WAKE_STATE_HOME dir, echoed.
|
|
fresh_home() {
|
|
local d="$TMP_ROOT/$1"
|
|
rm -rf "$d"
|
|
mkdir -p "$d"
|
|
printf '%s' "$d"
|
|
}
|
|
# dlq HOME — the dead-letter path for the default agent under HOME.
|
|
dlq() { printf '%s/default/dead-letter.jsonl' "$1"; }
|
|
|
|
echo "== Q1 (a): malformed {kind,id,path,observed_hash} QUARANTINES; clean sibling STILL delivers =="
|
|
(
|
|
home="$(fresh_home q1)"
|
|
export WAKE_STATE_HOME="$home"
|
|
unset WAKE_AGENT
|
|
f="$TMP_ROOT/q1.jsonl"
|
|
# The live pilot's EXACT malformed shape (no reconciled marker) + a clean sibling.
|
|
{
|
|
printf '%s\n' '{"observed_seq":1,"class":"actionable","locators":{"kind":"repo","id":"MALFORMED-Q","path":"docs/x.md","observed_hash":"deadbeef"},"emit_ts":1}'
|
|
printf '{"observed_seq":2,"class":"actionable","locators":{"sha":"%s","file":"src/a.ts"},"emit_ts":1}\n' "$SHA40"
|
|
} >"$f"
|
|
err="$TMP_ROOT/q1.err"
|
|
out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")"
|
|
rc=$?
|
|
[ "$rc" -eq 0 ] || fail_msg "Q1: render must EXIT 0 (per-entry quarantine, not whole-digest exit-4), got rc=$rc"
|
|
printf '%s' "$out" | grep -q "$SHA40" || fail_msg "Q1: the clean sibling (sha $SHA40) must STILL be delivered in the same drain [head-of-line block]"
|
|
printf '%s' "$out" | grep -q 'MALFORMED-Q' && fail_msg "Q1: the quarantined entry must be EXCLUDED from the rendered digest"
|
|
[ -f "$(dlq "$home")" ] || fail_msg "Q1: a durable dead-letter file must be written"
|
|
grep -q 'MALFORMED-Q' "$(dlq "$home")" 2>/dev/null || fail_msg "Q1: the malformed entry must be DEAD-LETTERED (accounted-for, not silently dropped)"
|
|
grep -qi 'QUARANTINE' "$err" || fail_msg "Q1: a LOUD per-entry alarm must fire on stderr"
|
|
grep -q 'observed_seq=1' "$err" || fail_msg "Q1: the alarm must identify the offending entry (observed_seq=1)"
|
|
) && ok
|
|
|
|
echo "== Q2 (b): reconciler enumeration (reconciled:true) renders ORIENTATION-tier, NO exit-4 =="
|
|
(
|
|
home="$(fresh_home q2)"
|
|
export WAKE_STATE_HOME="$home"
|
|
unset WAKE_AGENT
|
|
f="$TMP_ROOT/q2.jsonl"
|
|
# A reconciler enumeration: store class actionable (unchanged) + reconciled marker.
|
|
printf '%s\n' '{"observed_seq":5,"class":"actionable","locators":{"kind":"repo","id":"ENUM-B","path":"BOARD.md","observed_hash":"cafe1234","reconciled":true},"emit_ts":1}' >"$f"
|
|
err="$TMP_ROOT/q2.err"
|
|
out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")"
|
|
rc=$?
|
|
[ "$rc" -eq 0 ] || fail_msg "Q2: a reconciler enumeration must NOT exit-4 (it is ORIENTATION-tier), got rc=$rc"
|
|
printf '%s' "$out" | grep -q 'id=ENUM-B' || fail_msg "Q2: the enumeration must render as an ORIENTATION pointer (id=ENUM-B via _locator_line)"
|
|
printf '%s' "$out" | grep -q 'CLAIM@seq' && fail_msg "Q2: an enumeration must NOT render as an ACTIONABLE CLAIM@seq"
|
|
[ -s "$(dlq "$home")" ] && fail_msg "Q2: an ORIENTATION-tier enumeration must NOT be quarantined/dead-lettered"
|
|
true
|
|
) && ok
|
|
|
|
echo "== Q3 (c): TWO DISTINCT enumerations BOTH survive as SEPARATE orientation pointers (anti-collapse) =="
|
|
(
|
|
home="$(fresh_home q3)"
|
|
export WAKE_STATE_HOME="$home"
|
|
unset WAKE_AGENT
|
|
f="$TMP_ROOT/q3.jsonl"
|
|
{
|
|
printf '%s\n' '{"observed_seq":6,"class":"actionable","locators":{"kind":"repo","id":"ENUM-C1","path":"a.md","observed_hash":"1111","reconciled":true},"emit_ts":1}'
|
|
printf '%s\n' '{"observed_seq":7,"class":"actionable","locators":{"kind":"repo","id":"ENUM-C2","path":"b.md","observed_hash":"2222","reconciled":true},"emit_ts":1}'
|
|
} >"$f"
|
|
err="$TMP_ROOT/q3.err"
|
|
out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")"
|
|
rc=$?
|
|
[ "$rc" -eq 0 ] || fail_msg "Q3: two enumerations must render (exit 0), got rc=$rc"
|
|
n="$(printf '%s\n' "$out" | grep -c 'id=ENUM-C[12]' || true)"
|
|
[ "$n" = "2" ] || fail_msg "Q3: BOTH distinct enumerations must survive as SEPARATE orientation pointers (expected 2, got $n) — neither coalesced away"
|
|
printf '%s' "$out" | grep -q 'id=ENUM-C1' || fail_msg "Q3: enumeration ENUM-C1 must be present"
|
|
printf '%s' "$out" | grep -q 'id=ENUM-C2' || fail_msg "Q3: enumeration ENUM-C2 must be present"
|
|
[ -s "$(dlq "$home")" ] && fail_msg "Q3: enumerations must NOT be quarantined"
|
|
true
|
|
) && ok
|
|
|
|
echo "== Q4: PRESERVED — a genuine malformed ACTIONABLE claim is STILL loud (dead-letter+alarm), never delivered as valid =="
|
|
(
|
|
home="$(fresh_home q4)"
|
|
export WAKE_STATE_HOME="$home"
|
|
unset WAKE_AGENT
|
|
f="$TMP_ROOT/q4.jsonl"
|
|
printf '%s\n' '{"observed_seq":9,"class":"actionable","locators":{"kind":"board_file","id":"CLAIM-KEEP","observed_hash":"beef"},"emit_ts":1}' >"$f"
|
|
err="$TMP_ROOT/q4.err"
|
|
out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")"
|
|
rc=$?
|
|
[ "$rc" -eq 0 ] || fail_msg "Q4: render must exit 0 (per-entry quarantine), got rc=$rc"
|
|
printf '%s' "$out" | grep -q 'CLAIM-KEEP' && fail_msg "Q4: a malformed actionable must NOT be delivered as if valid"
|
|
printf '%s' "$out" | grep -q '(none) — no consequential claims pending' || fail_msg "Q4: with the only claim quarantined, the ACTIONABLE section must show (none)"
|
|
grep -q 'CLAIM-KEEP' "$(dlq "$home")" 2>/dev/null || fail_msg "Q4: the malformed actionable must be DEAD-LETTERED (loudly surfaced, not silent)"
|
|
grep -qi 'QUARANTINE' "$err" || fail_msg "Q4: the malformed actionable must raise a LOUD alarm"
|
|
) && ok
|
|
|
|
echo "== Q5: claim-precedence gated — a non-actionable-class entry carrying a claim (no hard locator) is STILL quarantined =="
|
|
(
|
|
home="$(fresh_home q5)"
|
|
export WAKE_STATE_HOME="$home"
|
|
unset WAKE_AGENT
|
|
f="$TMP_ROOT/q5.jsonl"
|
|
# class=digest but locators carry a consequential `claim` -> actionable-tier by
|
|
# precedence; no hard locator + no reconciled marker -> must be quarantined.
|
|
printf '%s\n' '{"observed_seq":11,"class":"digest","locators":{"claim":"CI is green","kind":"repo","id":"CLAIMY"},"emit_ts":1}' >"$f"
|
|
err="$TMP_ROOT/q5.err"
|
|
out="$("$DIGEST" render --from-file "$f" --agent default 2>"$err")"
|
|
rc=$?
|
|
[ "$rc" -eq 0 ] || fail_msg "Q5: render must exit 0, got rc=$rc"
|
|
grep -q 'CLAIMY' "$(dlq "$home")" 2>/dev/null || fail_msg "Q5: a claim-carrying entry with no hard locator must be quarantined (claim precedence, §2.1)"
|
|
printf '%s' "$out" | grep -q 'CI is green' && fail_msg "Q5: the un-verifiable claim must NOT be delivered"
|
|
true
|
|
) && ok
|
|
|
|
echo
|
|
if [ -s "$FAILFILE" ]; then
|
|
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)"
|