Files
stack/packages/mosaic/framework/tools/wake/test-wake-install.sh
mosaic-coder 231ba2ef32 feat(wake): W7 A10 idempotent component installer + mosaic-wake.service
Framework install machinery for the wake component (EPIC #892 W7, the last
build slice). ADDITIVE per #869: adds an `install.sh --component wake` early
dispatch that never enters the full-framework sync, never alters
framework-manifest ownership behavior, and touches nothing the #869
install-ordering-guard covers (no runtime-asset linking, no lease-enforcement
hook wiring).

(i)   Idempotent component-manifest install + Gate A (wake-install.sh install):
      the wake manifest.txt is VERSION METADATA ONLY; the component file set is
      INTERSECTED-AND-VALIDATED against the single SSOT framework-manifest.txt.
      A candidate the SSOT does not own is REFUSED fail-closed with no partial
      write. Re-running writes zero files (no diff).
(ii)  systemd/user/mosaic-wake.service — the long-lived detector daemon
      (detector.sh run). Per-class SLO lives inside the daemon, not a systemd
      interval; it is a SERVICE not a timer, so blank-reset does not apply.
(iii) blank-reset idiom on the legacy mosaic-heartbeat@<agent>.timer cadence
      drop-in during the §5 overlap->retire lifecycle (empty OnUnitActiveSec=
      reset before the new value => exactly one OnUnitActiveUSec), with a
      reset->verify->retire acceptance path (retire LAST, only on §4-vector pass).
(iv)  snapshot-guard — a reap/clean-checkout of a deployed unit is REFUSED
      without a prior snapshot (the deployed-from-uncommitted failure class).
(v)   fail-closed alarm-target + HMAC-key install-validation (G1/G2a): the
      operator W6 alarm sink must be configured + reachable and the W3/W7 HMAC
      key must resolve BY NAME; missing/unreachable => FAIL LOUD. The installer
      ships/writes NO endpoint value and NO secret, and echoes neither.

Red-first harness test-wake-install.sh (6 groups) wired into test:framework-shell;
Gate-A parity extended to prove bash+TS both resolve the wake component paths
framework-owned. wake component manifest bumped 0.5.0 -> 0.6.0.

Part of #892

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
2026-07-25 22:00:48 -05:00

207 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# test-wake-install.sh — RED-FIRST invariant harness for W7 (EPIC #892, A10): the
# idempotent, fail-closed wake COMPONENT INSTALLER (wake-install.sh) + the W6
# monitor-integration hardening in beacon.sh (folded-in observation set).
#
# Each group asserts ONE enforcement-path invariant and is designed to go RED if
# that invariant regresses (a targeted mutation to wake-install.sh / beacon.sh
# makes exactly its group fail):
# I1 idempotency — a second component install produces NO diff / no rewrite (i)
# I2 Gate A — a candidate the framework-manifest SSOT does not own is REFUSED,
# fail-closed, with NO partial write (the wake manifest authorizes nothing) (i)
# I3 blank-reset — the reset-line idiom collapses the cadence to EXACTLY ONE
# OnUnitActiveUSec; without it, two values leak (negative control) (iii)
# I4 snapshot-guard — a reap with NO prior snapshot is REFUSED (fail-closed);
# after a snapshot it is allowed (iv)
# I5 fail-closed install-validate — unconfigured HMAC key OR unconfigured/
# unreachable alarm target FAILS LOUD (non-zero); configured passes; and
# wake-install.sh inlines NO secret/endpoint (v)
# I6 reset->verify->retire lifecycle — overlap leaves the legacy timer running;
# only a §4-vector pass retires it, and only snapshot-guarded (iii)
#
# The W6 monitor-integration hardening folded into beacon.sh (ingested_ts
# staleness + beacon HMAC-verify at record) is exercised by test-wake-beacon.sh
# and appended here as I7/I8 in the same change that lands that beacon hardening.
#
# Isolated per-group XDG/systemd/target dirs; no live network, no operator queue,
# no real systemd manager touched (the blank-reset verify uses the deterministic
# merge simulation, which mirrors `systemctl show -p OnUnitActiveUSec`).
#
# SC2030/SC2031 disabled: each group runs in its own ( ) subshell and re-exports
# its env, so environments are isolated by design (same idiom as the W2-W6 harnesses).
# shellcheck disable=SC2030,SC2031
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WI="$SCRIPT_DIR/wake-install.sh"
FRAMEWORK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
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() { local d="$TMP_ROOT/$1"; rm -rf "$d"; mkdir -p "$d"; printf '%s' "$d"; }
echo "== I1: idempotency — a second component install produces NO diff =="
(
TGT="$(fresh i1-target)"
export WAKE_INSTALL_SOURCE="$FRAMEWORK_ROOT"
export WAKE_INSTALL_TARGET="$TGT"
out1="$(bash "$WI" install 2>/dev/null)"
w1="$(echo "$out1" | sed -n 's/.*written=\([0-9]*\).*/\1/p')"
[ "${w1:-0}" -gt 0 ] || fail_msg "I1: first install must write the wake component (written=$w1)"
# Snapshot the installed tree, run again, and require byte-identical output.
cp1="$(fresh i1-copy)"; cp -a "$TGT/." "$cp1/"
out2="$(bash "$WI" install 2>/dev/null)"
w2="$(echo "$out2" | sed -n 's/.*written=\([0-9]*\).*/\1/p')"
[ "${w2:-1}" -eq 0 ] || fail_msg "I1: a re-run must write ZERO files (idempotent); got written=$w2"
if ! diff -r "$cp1" "$TGT" >/dev/null 2>&1; then
fail_msg "I1: second install changed the target tree (not idempotent)"
fi
) && ok
echo "== I2: Gate A — a non-framework-owned candidate is REFUSED (fail-closed, no partial write) =="
(
TGT="$(fresh i2-target)"
# A manifest that DE-AUTHORIZES the wake tools: tools/wake/** is operator-owned.
# The install MUST refuse the whole thing and write nothing (deny-wins).
BAD_MANIFEST="$TMP_ROOT/i2-manifest.txt"
cat >"$BAD_MANIFEST" <<'EOF'
[framework]
tools/**
systemd/**
defaults/**
[operator]
tools/wake/**
EOF
export WAKE_INSTALL_SOURCE="$FRAMEWORK_ROOT"
export WAKE_INSTALL_TARGET="$TGT"
export WAKE_INSTALL_MANIFEST="$BAD_MANIFEST"
out="$(bash "$WI" install 2>&1)"; rc=$?
[ "$rc" -ne 0 ] || fail_msg "I2: install must FAIL when a wake candidate is not framework-owned (got rc=$rc)"
echo "$out" | grep -qi 'Gate A VIOLATION' || fail_msg "I2: refusal must name the Gate A violation [$out]"
# 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.
unset WAKE_INSTALL_MANIFEST
bash "$WI" install >/dev/null 2>&1 || fail_msg "I2: install must SUCCEED under the real framework-manifest"
[ -f "$TGT/tools/wake/beacon.sh" ] || fail_msg "I2: real-manifest install must write the wake tools"
) && ok
echo "== I3: blank-reset — reset idiom yields EXACTLY ONE OnUnitActiveUSec (negative control leaks two) =="
(
UD="$(fresh i3-units)"
export WAKE_SYSTEMD_USER_DIR="$UD"
UNIT="mosaic-heartbeat@a.timer"
# Base unit already carries a cadence (the legacy value being superseded).
mkdir -p "$UD"
cat >"$UD/$UNIT" <<'EOF'
[Timer]
OnUnitActiveSec=15min
[Install]
WantedBy=timers.target
EOF
# Blank-reset drop-in: empty reset then the new value.
bash "$WI" blank-reset "$UD/$UNIT.d/interval.conf" "30min" >/dev/null 2>&1 \
|| fail_msg "I3: blank-reset drop-in write failed"
out="$(bash "$WI" verify-single "$UNIT" 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "I3: blank-reset must resolve exactly one OnUnitActiveUSec (rc=$rc) [$out]"
echo "$out" | grep -qi 'EXACTLY ONE' || fail_msg "I3: verify must confirm exactly-one [$out]"
# NEGATIVE CONTROL: a drop-in WITHOUT the reset line appends -> TWO values -> fail.
cat >"$UD/$UNIT.d/interval.conf" <<'EOF'
[Timer]
OnUnitActiveSec=30min
EOF
out2="$(bash "$WI" verify-single "$UNIT" 2>&1)"; rc2=$?
[ "$rc2" -ne 0 ] || fail_msg "I3: without the reset line two cadences must leak (verify should FAIL) [$out2]"
) && ok
echo "== I4: snapshot-guard — reap without a snapshot is REFUSED; allowed after snapshot =="
(
UD="$(fresh i4-units)"; SNAP="$(fresh i4-snap)"
export WAKE_SYSTEMD_USER_DIR="$UD"
export WAKE_SNAPSHOT_DIR="$SNAP"
UNIT="mosaic-heartbeat@b.timer"
printf '[Timer]\nOnUnitActiveSec=10min\n' >"$UD/$UNIT"
# (a) reap WITHOUT snapshot -> refuse, unit still present.
out="$(bash "$WI" reap-unit "$UNIT" 2>&1)"; rc=$?
[ "$rc" -ne 0 ] || fail_msg "I4: reap without a snapshot must be REFUSED (rc=$rc)"
echo "$out" | grep -qi 'snapshot-guard' || fail_msg "I4: refusal must name the snapshot-guard [$out]"
[ -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"
bash "$WI" reap-unit "$UNIT" >/dev/null 2>&1 || fail_msg "I4: reap after snapshot must succeed"
[ ! -f "$UD/$UNIT" ] || fail_msg "I4: reap after snapshot must remove the unit"
[ -f "$SNAP/$UNIT" ] || fail_msg "I4: the snapshot must be retained after reap"
) && ok
echo "== I5: fail-closed install-validate — unconfigured HMAC/alarm FAIL LOUD; no secret inlined =="
(
H="$(fresh i5)"
CRED="$H/credentials.json"
export MOSAIC_CREDENTIALS_FILE="$CRED"
SECRET_KEY="s3cr3t-HMAC-DO-NOT-ECHO-xyz"
SECRET_ENDPOINT="https://monitor.invalid/alarm/DO-NOT-INLINE-abc123"
# --- HMAC key: absent store -> fail loud.
export WAKE_HMAC_KEY_NAME="primary"
out="$(bash "$WI" validate-hmac-key 2>&1)"; rc=$?
[ "$rc" -ne 0 ] || fail_msg "I5: missing credential store must FAIL LOUD for the HMAC key (rc=$rc)"
echo "$out" | grep -qi 'UNSIGNED' || fail_msg "I5: HMAC failure must warn about unsigned wakes [$out]"
# Now provide the key by-name -> pass, and the value must NEVER be echoed.
jq -cn --arg k "$SECRET_KEY" '{wake:{hmac_keys:{"primary":$k}}}' >"$CRED"
out2="$(bash "$WI" validate-hmac-key 2>&1)"; rc2=$?
[ "$rc2" -eq 0 ] || fail_msg "I5: a by-name-resolvable HMAC key must pass (rc=$rc2) [$out2]"
echo "$out2" | grep -qF "$SECRET_KEY" && fail_msg "I5: validate must NEVER echo the HMAC key material"
# --- alarm target: unconfigured -> fail loud (silent no-alarm host).
unset WAKE_ALARM_SINK_CMD
out3="$(bash "$WI" validate-alarm-target 2>&1)"; rc3=$?
[ "$rc3" -ne 0 ] || fail_msg "I5: unconfigured alarm target must FAIL LOUD (rc=$rc3)"
echo "$out3" | grep -qi 'silent no-alarm host' || fail_msg "I5: alarm failure must name the silent-no-alarm hazard [$out3]"
# unreachable -> fail loud.
export WAKE_ALARM_SINK_CMD="false"
out4="$(bash "$WI" validate-alarm-target 2>&1)"; rc4=$?
[ "$rc4" -ne 0 ] || fail_msg "I5: unreachable alarm sink must FAIL LOUD (rc=$rc4)"
echo "$out4" | grep -qi 'UNREACHABLE' || fail_msg "I5: alarm failure must name the unreachable target [$out4]"
# reachable -> pass.
export WAKE_ALARM_SINK_CMD="cat >/dev/null"
bash "$WI" validate-alarm-target >/dev/null 2>&1 || fail_msg "I5: a configured+reachable alarm sink must pass"
# The installer file must inline NO secret/endpoint.
grep -qF "$SECRET_ENDPOINT" "$WI" && fail_msg "I5: wake-install.sh must NOT inline any alarm endpoint"
grep -qE 'hmac_keys[^A-Za-z_].*=[^=].*[A-Za-z0-9]{8,}' "$WI" && fail_msg "I5: wake-install.sh must NOT inline key material"
true
) && ok
echo "== I6: reset->verify->retire — overlap keeps legacy running; only §4-vector pass retires =="
(
UD="$(fresh i6-units)"; SNAP="$(fresh i6-snap)"
export WAKE_SYSTEMD_USER_DIR="$UD"
export WAKE_SNAPSHOT_DIR="$SNAP"
TIMER="mosaic-heartbeat@c.timer"
printf '[Timer]\nOnUnitActiveSec=15min\n' >"$UD/$TIMER"
# (a) overlap phase (NO --vector-passed): reset+verify, legacy LEFT RUNNING.
out="$(bash "$WI" reset-verify-retire c --interval 30min 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "I6: overlap reset-verify must succeed (rc=$rc) [$out]"
[ -f "$UD/$TIMER" ] || fail_msg "I6: overlap phase must LEAVE the legacy timer running (retire withheld)"
[ -f "$SNAP/$TIMER" ] || fail_msg "I6: the legacy timer must be snapshotted before any retire"
echo "$out" | grep -qi 'LEFT RUNNING' || fail_msg "I6: overlap must announce retire is withheld [$out]"
# (b) §4-vector pass: retire LAST, snapshot-guarded.
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]"
[ ! -f "$UD/$TIMER" ] || fail_msg "I6: on §4-vector pass the legacy timer must be RETIRED"
[ -f "$SNAP/$TIMER" ] || fail_msg "I6: the snapshot must survive the retire"
) && ok
echo
if [ -s "$FAILFILE" ]; then
echo "wake install harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
exit 1
fi
echo "wake install harness: all invariants passed ($pass groups)"