All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Adds A6 (fn-oracle.sh) and A7 (reconcile.sh) of the wake/heartbeat canon (EPIC #892, W5), building on merged W2/W3/W4 via their public APIs only (store.sh/ack.sh/detector.sh untouched). A6 fn-oracle.sh — synthetic-canary FN-oracle (§4 vector: FN-rate=0; §7 res5). Injects a KNOWN delta at the SOURCE boundary, drives the pipeline through the detector's public poll-once (a black box), and renders its verdict SOLELY from the terminal store cursor (did a CONSUMED ack cover the canary's observed_seq within the per-class SLO?). Off-domain / detector-independent: it never reads a detector hash-file or self-report, so a detector that silently DROPS changes — including a fully-disabled one at a perfect 0-wake rate — FAILS the oracle (the §4/A8 false-negative-blindspot killer). Runs in its own isolated XDG namespace, never the operator's live queue. No invented SLO (design law): --slo-seconds is required. A7 reconcile.sh — source-parity reconciler (§4/G3). (i) source-coverage PARITY INVENTORY first: a declared source not covered by any watch, a dangling reference, an empty lane, or a required_sources omission FLAGS — an omitted source cannot make the §4 vector pass vacuously (not silently green). (ii) periodic full reconcile: enumerate each covered source's current state vs observed_seq/inbox => 0 unaccounted; any gap FLAGS. Handles the W4-ratified division (detector first-seen-baseline does NOT wake) by ENUMERATING pre-existing/startup state into the durable store; inbox-reflected state is accounted (no double-enumeration). Fail-loud (G2a) on adapter error. RED-FIRST tests (wired into test:framework-shell), shellcheck clean: test-wake-fn-oracle.sh O1 healthy FN-rate=0; O2 disabled detector -> FN-DETECTED at perfect no-op; O3 CONSUMED-but-late -> FN; O4 off-domain verdict; O5 SLO required. test-wake-reconcile.sh R1 complete inventory; R2 omitted-source vacuous-pass prevented; R3 required_sources omission; R4 dangling; R5/R6 pre-existing -> flagged + enumerated -> re-run 0; R7 inbox-accounted no-double; R8 G2a fail-loud. manifest.txt version 0.3.0 -> 0.4.0 + inventory (schema range untouched). Operator-agnostic (XDG/env only; no operator paths/names/hosts/secrets). Part of #892 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
238 lines
9.1 KiB
Bash
Executable File
238 lines
9.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# test-wake-reconcile.sh — RED-FIRST invariant harness for W5 (EPIC #892):
|
|
# the source-parity reconciler (reconcile.sh, A7).
|
|
#
|
|
# Each test asserts ONE CONVERGED-DESIGN invariant and is designed to go RED if
|
|
# that invariant regresses:
|
|
# R1 complete inventory -> PASS (exit 0) (§4/G3-i)
|
|
# R2 an OMITTED source (declared but unwatched) -> FLAG; the §4 vector
|
|
# cannot pass VACUOUSLY, and it is NOT silently green (§4/G3-i)
|
|
# R3 a required_sources omission -> FLAG (explicit lane-dependency form) (§4/G3-i)
|
|
# R4 a dangling watch reference -> FLAG (§4/G3-i)
|
|
# R5 an UNACCOUNTED source-state -> FLAGGED (found + non-zero exit) (§4/G3-ii)
|
|
# R6 pre-existing state at startup -> ENUMERATED into the durable store;
|
|
# a follow-up reconcile then reports 0 unaccounted (W4-division)
|
|
# R7 a source already reflected in the inbox -> ACCOUNTED (no double-
|
|
# enumeration of detector-delivered state) (§4/G3-ii)
|
|
# R8 a source adapter error -> FAIL LOUD (G2a parity; never 'no state') (§4/G2a)
|
|
#
|
|
# Uses FAKE/STUB sources only (no live network). Isolated per test.
|
|
# shellcheck disable=SC2030,SC2031
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
RECON="$SCRIPT_DIR/reconcile.sh"
|
|
STORE="$SCRIPT_DIR/store.sh"
|
|
DET="$SCRIPT_DIR/detector.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)); }
|
|
|
|
fresh_state() {
|
|
local d="$TMP_ROOT/$1"
|
|
rm -rf "$d"
|
|
mkdir -p "$d"
|
|
printf '%s' "$d"
|
|
}
|
|
depth() { "$STORE" cursors | sed -n 's/pending_depth=//p'; }
|
|
|
|
# make_stub DIR — a source adapter reading $DIR/<kind>_<id> with optional .rc.
|
|
make_stub() {
|
|
local dir="$1"
|
|
cat >"$dir/adapter.sh" <<EOF
|
|
#!/usr/bin/env bash
|
|
set -u
|
|
kind="\$1"; id="\$2"; base="$dir/\${kind}_\${id}"; rc=0
|
|
[ -f "\$base.rc" ] && rc="\$(cat "\$base.rc")"
|
|
[ -f "\$base" ] && cat "\$base"
|
|
exit "\$rc"
|
|
EOF
|
|
chmod +x "$dir/adapter.sh"
|
|
}
|
|
|
|
echo "== R1: complete inventory -> PASS =="
|
|
(
|
|
WAKE_STATE_HOME="$(fresh_state r1)"
|
|
export WAKE_STATE_HOME
|
|
unset WAKE_AGENT
|
|
wl="$TMP_ROOT/r1.json"
|
|
cat >"$wl" <<'EOF'
|
|
{ "schema_version": 1,
|
|
"repos": [ { "id": "r1" }, { "id": "r2" } ],
|
|
"watches": [ { "lane": "L", "sources": [ { "kind": "repo", "id": "r1" }, { "kind": "repo", "id": "r2" } ] } ] }
|
|
EOF
|
|
export WAKE_WATCH_LIST="$wl"
|
|
out="$("$RECON" inventory 2>&1)"
|
|
rc=$?
|
|
[ "$rc" -eq 0 ] || fail_msg "R1: a complete inventory must PASS (exit 0) [$out]"
|
|
echo "$out" | grep -qi 'COMPLETE' || fail_msg "R1: a complete inventory must report COMPLETE [$out]"
|
|
) && ok
|
|
|
|
echo "== R2: OMITTED source -> FLAG (vacuous-pass prevented, not silently green) =="
|
|
(
|
|
WAKE_STATE_HOME="$(fresh_state r2)"
|
|
export WAKE_STATE_HOME
|
|
unset WAKE_AGENT
|
|
# r2 is DECLARED (operationally depended-on) but no watch covers it.
|
|
wl="$TMP_ROOT/r2.json"
|
|
cat >"$wl" <<'EOF'
|
|
{ "schema_version": 1,
|
|
"repos": [ { "id": "r1" }, { "id": "r2" } ],
|
|
"watches": [ { "lane": "L", "sources": [ { "kind": "repo", "id": "r1" } ] } ] }
|
|
EOF
|
|
export WAKE_WATCH_LIST="$wl"
|
|
out="$("$RECON" inventory 2>&1)"
|
|
rc=$?
|
|
[ "$rc" -ne 0 ] || fail_msg "R2: an omitted (declared-but-unwatched) source must FLAG (non-zero), not silently pass"
|
|
echo "$out" | grep -qi 'r2' || fail_msg "R2: the flag must name the omitted source r2 [$out]"
|
|
echo "$out" | grep -qi 'vacuous' || fail_msg "R2: the flag must state the vacuous-pass is prevented [$out]"
|
|
) && ok
|
|
|
|
echo "== R3: required_sources omission -> FLAG =="
|
|
(
|
|
WAKE_STATE_HOME="$(fresh_state r3)"
|
|
export WAKE_STATE_HOME
|
|
unset WAKE_AGENT
|
|
# The lane explicitly declares it depends on r1 AND r2, but only watches r1.
|
|
wl="$TMP_ROOT/r3.json"
|
|
cat >"$wl" <<'EOF'
|
|
{ "schema_version": 1,
|
|
"repos": [ { "id": "r1" }, { "id": "r2" } ],
|
|
"watches": [ { "lane": "L", "required_sources": [ "r1", "r2" ], "sources": [ { "kind": "repo", "id": "r1" } ] } ] }
|
|
EOF
|
|
export WAKE_WATCH_LIST="$wl"
|
|
out="$("$RECON" inventory 2>&1)"
|
|
rc=$?
|
|
[ "$rc" -ne 0 ] || fail_msg "R3: a required_sources omission must FLAG (non-zero)"
|
|
echo "$out" | grep -qi "requires source 'r2'" || fail_msg "R3: the flag must name the required-but-omitted source r2 [$out]"
|
|
) && ok
|
|
|
|
echo "== R4: dangling watch reference -> FLAG =="
|
|
(
|
|
WAKE_STATE_HOME="$(fresh_state r4)"
|
|
export WAKE_STATE_HOME
|
|
unset WAKE_AGENT
|
|
wl="$TMP_ROOT/r4.json"
|
|
cat >"$wl" <<'EOF'
|
|
{ "schema_version": 1,
|
|
"repos": [ { "id": "r1" } ],
|
|
"watches": [ { "lane": "L", "sources": [ { "kind": "repo", "id": "r1" }, { "kind": "repo", "id": "r9" } ] } ] }
|
|
EOF
|
|
export WAKE_WATCH_LIST="$wl"
|
|
out="$("$RECON" inventory 2>&1)"
|
|
rc=$?
|
|
[ "$rc" -ne 0 ] || fail_msg "R4: a dangling reference must FLAG (non-zero)"
|
|
echo "$out" | grep -qi 'dangling' || fail_msg "R4: the flag must state it is dangling [$out]"
|
|
) && ok
|
|
|
|
echo "== R5/R6: pre-existing state -> UNACCOUNTED flag + enumerated into store; re-run 0 =="
|
|
(
|
|
WAKE_STATE_HOME="$(fresh_state r56)"
|
|
export WAKE_STATE_HOME
|
|
unset WAKE_AGENT
|
|
stub="$TMP_ROOT/r56stub"
|
|
mkdir -p "$stub"
|
|
make_stub "$stub"
|
|
export WAKE_DETECTOR_SOURCE_CMD="$stub/adapter.sh"
|
|
# TWO pre-existing sources with content already present at startup.
|
|
printf 'PRE-EXISTING-1\n' >"$stub/repo_r1"
|
|
printf 'PRE-EXISTING-2\n' >"$stub/repo_r2"
|
|
wl="$TMP_ROOT/r56.json"
|
|
cat >"$wl" <<'EOF'
|
|
{ "schema_version": 1,
|
|
"repos": [ { "id": "r1" }, { "id": "r2" } ],
|
|
"watches": [ { "lane": "L", "sources": [ { "kind": "repo", "id": "r1" }, { "kind": "repo", "id": "r2" } ] } ] }
|
|
EOF
|
|
export WAKE_WATCH_LIST="$wl"
|
|
[ "$(depth)" = "0" ] || fail_msg "R5: store must start empty"
|
|
out="$("$RECON" reconcile 2>&1)"
|
|
rc=$?
|
|
[ "$rc" -ne 0 ] || fail_msg "R5: pre-existing unaccounted state must FLAG (non-zero) on first reconcile"
|
|
echo "$out" | grep -qi 'UNACCOUNTED=2' || fail_msg "R5: both pre-existing sources must be UNACCOUNTED [$out]"
|
|
# R6: enumerated into the durable store — one entry per pre-existing source.
|
|
[ "$(depth)" = "2" ] || fail_msg "R6: pre-existing state must be ENUMERATED into the store (depth 2), got $(depth)"
|
|
# A follow-up reconcile now finds everything accounted -> 0 unaccounted.
|
|
out2="$("$RECON" reconcile 2>&1)"
|
|
rc2=$?
|
|
[ "$rc2" -eq 0 ] || fail_msg "R6: a second reconcile must report 0 unaccounted (exit 0) [$out2]"
|
|
echo "$out2" | grep -qi 'UNACCOUNTED=0' || fail_msg "R6: second reconcile must be 0 unaccounted [$out2]"
|
|
[ "$(depth)" = "2" ] || fail_msg "R6: a clean reconcile must NOT re-enumerate (depth still 2), got $(depth)"
|
|
) && ok
|
|
|
|
echo "== R7: source already in the inbox -> ACCOUNTED (no double-enumeration) =="
|
|
(
|
|
WAKE_STATE_HOME="$(fresh_state r7)"
|
|
export WAKE_STATE_HOME
|
|
unset WAKE_AGENT
|
|
stub="$TMP_ROOT/r7stub"
|
|
mkdir -p "$stub"
|
|
make_stub "$stub"
|
|
export WAKE_DETECTOR_SOURCE_CMD="$stub/adapter.sh"
|
|
printf 'V0\n' >"$stub/repo_r1"
|
|
wl="$TMP_ROOT/r7.json"
|
|
cat >"$wl" <<'EOF'
|
|
{ "schema_version": 1,
|
|
"repos": [ { "id": "r1" } ],
|
|
"watches": [ { "lane": "L", "sources": [ { "kind": "repo", "id": "r1" } ] } ] }
|
|
EOF
|
|
export WAKE_WATCH_LIST="$wl"
|
|
# Detector baselines (silent) then observes a real delta -> pending inbox entry.
|
|
"$DET" poll-once >/dev/null 2>&1 || fail_msg "R7: detector baseline failed"
|
|
printf 'V1\n' >"$stub/repo_r1"
|
|
"$DET" poll-once >/dev/null 2>&1 || fail_msg "R7: detector delta failed"
|
|
[ "$(depth)" = "1" ] || fail_msg "R7: detector delta should leave one pending entry, got $(depth)"
|
|
# The source's CURRENT state IS the pending inbox entry -> ACCOUNTED, and the
|
|
# reconciler must NOT enumerate a duplicate.
|
|
out="$("$RECON" reconcile 2>&1)"
|
|
rc=$?
|
|
[ "$rc" -eq 0 ] || fail_msg "R7: state reflected in the inbox must be ACCOUNTED (exit 0) [$out]"
|
|
echo "$out" | grep -qi 'UNACCOUNTED=0' || fail_msg "R7: inbox-reflected state must be 0 unaccounted [$out]"
|
|
[ "$(depth)" = "1" ] || fail_msg "R7: reconciler must NOT double-enumerate inbox-reflected state (depth still 1), got $(depth)"
|
|
) && ok
|
|
|
|
echo "== R8: source adapter error -> FAIL LOUD (G2a; never 'no state') =="
|
|
(
|
|
WAKE_STATE_HOME="$(fresh_state r8)"
|
|
export WAKE_STATE_HOME
|
|
unset WAKE_AGENT
|
|
stub="$TMP_ROOT/r8stub"
|
|
mkdir -p "$stub"
|
|
make_stub "$stub"
|
|
export WAKE_DETECTOR_SOURCE_CMD="$stub/adapter.sh"
|
|
printf 'FORBIDDEN\n' >"$stub/repo_r1"
|
|
printf '3\n' >"$stub/repo_r1.rc" # adapter exits non-zero (403/partial class)
|
|
wl="$TMP_ROOT/r8.json"
|
|
cat >"$wl" <<'EOF'
|
|
{ "schema_version": 1,
|
|
"repos": [ { "id": "r1" } ],
|
|
"watches": [ { "lane": "L", "sources": [ { "kind": "repo", "id": "r1" } ] } ] }
|
|
EOF
|
|
export WAKE_WATCH_LIST="$wl"
|
|
out="$("$RECON" reconcile 2>&1)"
|
|
rc=$?
|
|
[ "$rc" -ne 0 ] || fail_msg "R8: a source error must FAIL LOUD (non-zero exit)"
|
|
echo "$out" | grep -qi 'FAIL LOUD' || fail_msg "R8: the source error must be loud [$out]"
|
|
[ "$(depth)" = "0" ] || fail_msg "R8: a failed observation must NOT enumerate anything, got depth $(depth)"
|
|
) && ok
|
|
|
|
echo
|
|
if [ -s "$FAILFILE" ]; then
|
|
echo "wake reconcile harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
|
exit 1
|
|
fi
|
|
echo "wake reconcile harness: all invariants passed ($pass groups)"
|