#!/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/_ with optional .rc. make_stub() { local dir="$1" cat >"$dir/adapter.sh" < 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)"