#!/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)
#   I7  ingested_ts staleness — staleness is computed from the monitor's
#       receive-time, so a host shipping a FAR-FUTURE emit_ts STILL goes stale  (vi-a)
#   I8  beacon HMAC-verify at record — a spoofed (bad-sig) beacon is REJECTED   (vi-b)
# (I7/I8 exercise the W6 monitor-integration hardening folded into beacon.sh; the
# same invariants are also asserted in depth by test-wake-beacon.sh B11/B12.)
#
# 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)"
# #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)"

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

# Isolation default: no group may ever touch the operator's REAL user systemd dir
# (~/.config/systemd/user). The (b/#913) search-path link step defaults there, so
# pin an isolated per-run dir here; groups that need their own re-export it.
export WAKE_SYSTEMD_USER_DIR="$TMP_ROOT/systemd-user-default"
mkdir -p "$WAKE_SYSTEMD_USER_DIR"

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" | has_match -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" | has_match -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" | has_match -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" | has_match -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"
  # --- 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]"
  # 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]"
  # 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"
  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"
  # F7 (#925): the vector-passed reap now REFUSES unless the canon FALLBACK WAKE is
  # proven live (replacement-before-retirement). Provision it at the schedulable
  # floor so the lifecycle path under test still reaches the reap. (I11 is the
  # dedicated F7 refuse/allow control.)
  cp "$FRAMEWORK_ROOT/systemd/user/mosaic-wake-fallback.timer"   "$UD/mosaic-wake-fallback.timer"
  cp "$FRAMEWORK_ROOT/systemd/user/mosaic-wake-fallback.service" "$UD/mosaic-wake-fallback.service"
  # (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" | has_match -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]"
  [ ! -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

# ── W6 monitor-integration hardening folded into beacon.sh (vi) ────────────────

echo "== I7: ingested_ts staleness — a far-future emit_ts still goes stale (receive-time governs) =="
(
  H="$(fresh i7)"
  export WAKE_BEACON_RECEIVED="$H/received.json"
  export WAKE_ALARM_SINK_CMD="cat >$H/alarm.json"
  now="$(date +%s)"
  # Host lies with a far-future emit_ts; record stamps the monitor's ingested_ts.
  jq -cn --argjson ts "$((now + 100000))" \
    '{kind:"wake-beacon", beacon_seq:3, emit_ts:$ts, host_id:"h", independence:"off-host", degraded:false}' \
    | bash "$BEACON" record
  # Simulate the beacon having been received 100s ago (backdate ingested_ts only).
  jq -c --argjson ing "$((now - 100))" '.ingested_ts = $ing' "$WAKE_BEACON_RECEIVED" >"$H/t" && mv "$H/t" "$WAKE_BEACON_RECEIVED"
  out="$(bash "$BEACON" check --slo-seconds 5 2>&1)"; rc=$?
  [ "$rc" -eq 1 ] || fail_msg "I7: far-future emit_ts must NOT defer staleness — receive-time governs (expected exit 1); got $rc [$out]"
) && ok

echo "== I8: beacon HMAC-verify at record — a spoofed (bad-sig) beacon is REJECTED =="
# #912: hard-require openssl in CI (Woodpecker sets CI=woodpecker) so the install
# beacon-sign leg is actually exercised; keep the skip for openssl-less local dev.
if ! command -v openssl >/dev/null 2>&1; then
  if [ -n "${CI:-}" ]; then
    echo "  FAIL: I8 requires openssl in CI (#912) but it is not on PATH — the CI image must provide it" >&2
    echo "x" >>"$FAILFILE"
  else
    echo "SKIP: openssl not available (local dev; CI hard-requires it)" >&2
  fi
else
(
  H="$(fresh i8)"
  export WAKE_STATE_HOME="$H"
  export MOSAIC_CREDENTIALS_FILE="$H/credentials.json"
  export WAKE_BEACON_HMAC_KEY_NAME="beacon"
  jq -cn --arg k "i8-hmac-key" '{wake:{hmac_keys:{"beacon":$k}}}' >"$MOSAIC_CREDENTIALS_FILE"
  export WAKE_BEACON_INDEPENDENCE="off-host"
  export WAKE_BEACON_SINK_CMD="cat >$H/shipped.json"
  export WAKE_BEACON_RECEIVED="$H/received.json"
  bash "$BEACON" emit >/dev/null 2>&1 || fail_msg "I8: signed emit must succeed"
  bash "$BEACON" record <"$H/shipped.json" >/dev/null 2>&1 || fail_msg "I8: an authentic signed beacon must be accepted"
  rm -f "$WAKE_BEACON_RECEIVED"
  jq -c '.beacon_seq = 88888 | .host_id = "attacker"' "$H/shipped.json" >"$H/spoof.json"
  out="$(bash "$BEACON" record <"$H/spoof.json" 2>&1)"; rc=$?
  [ "$rc" -ne 0 ] || fail_msg "I8: a spoofed beacon must be REJECTED at record (rc=$rc) [$out]"
  [ ! -s "$WAKE_BEACON_RECEIVED" ] || fail_msg "I8: a rejected spoof must NOT advance the dead-man clock"
) && ok
fi

# ── (a #913) missing _lib/manifest.sh dependency — FAIL LOUD, not a bare source error ──

echo "== I9: dep-check — a host seed missing _lib/manifest.sh FAILS LOUD (names dep + remedy), not a bare source error =="
(
  # Simulate an OLDER host seed that predates the shared manifest reader: copy the
  # framework tree the installer needs, but OMIT tools/_lib/manifest.sh. Running the
  # copied wake-install.sh reproduces the adoption gap on a not-yet-updated host.
  FAKE="$(fresh i9-fw)"
  mkdir -p "$FAKE/tools/wake" "$FAKE/tools/_lib" "$FAKE/systemd/user"
  cp "$FRAMEWORK_ROOT/tools/wake/"* "$FAKE/tools/wake/" 2>/dev/null || true
  # Copy _lib EXCEPT manifest.sh — the exact file older seeds lack.
  for f in "$FRAMEWORK_ROOT/tools/_lib/"*; do
    [ -e "$f" ] || continue
    case "$(basename "$f")" in manifest.sh) continue ;; esac
    cp "$f" "$FAKE/tools/_lib/" 2>/dev/null || true
  done
  cp "$FRAMEWORK_ROOT/systemd/user/mosaic-wake.service" "$FAKE/systemd/user/" 2>/dev/null || true
  cp "$FRAMEWORK_ROOT/framework-manifest.txt" "$FAKE/" 2>/dev/null || true
  [ ! -e "$FAKE/tools/_lib/manifest.sh" ] || fail_msg "I9: fixture setup — manifest.sh must be absent to simulate an older seed"
  WI_FAKE="$FAKE/tools/wake/wake-install.sh"
  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' \
    || 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' \
    && 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)"
  WAKE_INSTALL_SOURCE="$FRAMEWORK_ROOT" WAKE_INSTALL_TARGET="$TGT_OK" \
    bash "$WI" install >/dev/null 2>&1 || fail_msg "I9: with _lib/manifest.sh present the install must succeed"
) && ok

# ── (b #913) mosaic-wake.service lands IN the user systemd search path + validates ──

echo "== I10: systemd search-path — install links mosaic-wake.service into the search path; validate catches a miss; idempotent =="
(
  TGT="$(fresh i10-target)"
  UD="$(fresh i10-systemd)"     # isolated stand-in for ~/.config/systemd/user
  export WAKE_INSTALL_SOURCE="$FRAMEWORK_ROOT"
  export WAKE_INSTALL_TARGET="$TGT"
  export WAKE_SYSTEMD_USER_DIR="$UD"
  bash "$WI" install >/dev/null 2>&1 || fail_msg "I10: install must succeed"
  # (a) the unit is resolvable in the user systemd search path.
  [ -e "$UD/mosaic-wake.service" ] || fail_msg "I10: mosaic-wake.service must be linked into the user systemd search path ($UD)"
  out="$(bash "$WI" validate-systemd-path 2>&1)"; rc=$?
  [ "$rc" -eq 0 ] || fail_msg "I10: post-install validate must confirm the unit resolves (rc=$rc) [$out]"
  # (b) NEGATIVE CONTROL: not-in-search-path is CAUGHT (fail loud, names the miss).
  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]"
  # (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" -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

# ── (#925) canon FALLBACK WAKE (F7 replacement-before-retirement) ──────────────

echo "== I11: F7 — the legacy reap REFUSES unless the canon fallback wake is proven live =="
(
  UD="$(fresh i11-units)"; SNAP="$(fresh i11-snap)"
  export WAKE_SYSTEMD_USER_DIR="$UD"
  export WAKE_SNAPSHOT_DIR="$SNAP"
  TIMER="mosaic-heartbeat@f.timer"
  printf '[Timer]\nOnUnitActiveSec=15min\n' >"$UD/$TIMER"
  # (a) fallback-proven-live is the F7 gate primitive: with NO fallback installed,
  #     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]"
  # (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]"
  [ -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.
  cp "$FRAMEWORK_ROOT/systemd/user/mosaic-wake-fallback.timer"   "$UD/mosaic-wake-fallback.timer"
  cp "$FRAMEWORK_ROOT/systemd/user/mosaic-wake-fallback.service" "$UD/mosaic-wake-fallback.service"
  bash "$WI" fallback-proven-live >/dev/null 2>&1 || fail_msg "I11: fallback-proven-live must PASS once the fallback units are schedulable"
  out2="$(bash "$WI" reset-verify-retire f --interval 30min --vector-passed 2>&1)"; rc2=$?
  [ "$rc2" -eq 0 ] || fail_msg "I11: with a proven-live fallback the reap must succeed (rc=$rc2) [$out2]"
  [ ! -f "$UD/$TIMER" ] || fail_msg "I11: with F7 satisfied the legacy timer must be RETIRED"
) && ok

echo "== I12: fallback drain — a STALLED detector still gets delivery via the canon fallback drain =="
(
  H="$(fresh i12-state)"
  export WAKE_STATE_HOME="$H"
  unset WAKE_AGENT
  STORE="$SCRIPT_DIR/store.sh"; DIGEST="$SCRIPT_DIR/digest.sh"
  # A pending obligation lands in the durable inbox — but the detector daemon is
  # STALLED/absent (we never run detector.sh). Enqueue directly to the store.
  seq="$(bash "$STORE" enqueue --class digest \
    --locators '{"kind":"repo","id":"STALLED-DRAIN-MARK","path":"BOARD.md","observed_hash":"deadbeef"}' 2>/dev/null)"
  [ -n "$seq" ] || fail_msg "I12: store enqueue must allocate a seq"
  # The canon fallback drain is EXACTLY the shipped unit's ExecStart: digest render
  # --from-store. Firing it (as the timer would) must surface the pending obligation
  # 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' \
    || 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" \
    || fail_msg "I12: mosaic-wake-fallback.service ExecStart must fire the canon drain (digest.sh render --from-store)"
) && ok

echo "== I13: install links + validates the canon fallback timer+service into the search path (idempotent) =="
(
  TGT="$(fresh i13-target)"
  UD="$(fresh i13-systemd)"
  export WAKE_INSTALL_SOURCE="$FRAMEWORK_ROOT"
  export WAKE_INSTALL_TARGET="$TGT"
  export WAKE_SYSTEMD_USER_DIR="$UD"
  bash "$WI" install >/dev/null 2>&1 || fail_msg "I13: install must succeed"
  # (a) both fallback units resolve in the user systemd search path.
  [ -e "$UD/mosaic-wake-fallback.timer" ]   || fail_msg "I13: fallback timer must be linked into the search path"
  [ -e "$UD/mosaic-wake-fallback.service" ] || fail_msg "I13: fallback service must be linked into the search path"
  bash "$WI" validate-fallback-units >/dev/null 2>&1 || fail_msg "I13: validate-fallback-units must pass after install"
  # (b) NEGATIVE CONTROL: a missing fallback unit is CAUGHT (fail loud, names the miss).
  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]"
  # (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" -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

echo "== I14: per-class fallback cadence — blank-reset yields EXACTLY ONE OnUnitActiveUSec (negative control leaks two) =="
(
  UD="$(fresh i14-units)"
  export WAKE_SYSTEMD_USER_DIR="$UD"
  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"
  # 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]"
  [ -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' \
    || 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.
  printf '[Timer]\nOnUnitActiveSec=30min\n' >"$UD/$TIMER.d/cadence.conf"
  out2="$(bash "$WI" verify-single "$TIMER" 2>&1)"; rc2=$?
  [ "$rc2" -ne 0 ] || fail_msg "I14: without the reset line two cadences must leak (verify should FAIL) [$out2]"
) && ok

echo
if [ -s "$FAILFILE" ]; then
  echo "wake install harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2
  exit 1
fi
echo "wake install harness: all invariants passed ($pass groups)"
