#!/usr/bin/env bash
# test-wake-preimage.sh — RED-FIRST invariant harness for A11 (#958): durable
# provenance for the OPERATOR-SIDE preimage definition (preimage.sh).
#
# Each test asserts ONE #958 invariant and is designed to go RED if it
# regresses:
#   P1  first-seen -> SILENT baseline (ledger rows, NO enqueue)   (detector idiom)
#   P2  change -> attributable from durable state alone: new ledger row with
#       prev, PRIOR BYTES retrievable content-addressed, first-class actionable
#       enqueued with a §2.1 hard locator (path)                 (acceptance a+c)
#   P3  detector ordering: the preimage cause line is enqueued at a LOWER
#       observed_seq than the per-source delta it explains        (acceptance c)
#   P4  credential-store PATH is refused — bytes NEVER captured, even when the
#       operator lists it by error (hash/mtime row still written) (acceptance b)
#   P5  secret-SHAPED content is refused — refusal, not redaction (acceptance b)
#   P6  deletion of a preimage file is a tracked CHANGE (ABSENT), not an error
#   P7  no-change is idempotent (no row growth, no enqueue)
#   P8  unresolvable adapter command -> FAIL LOUD, never "no change" (D2/#955 class)
#   P9  corrupt ledger -> REFUSE to compare/re-baseline (loud, no append)
#   P10 oversized file -> bytes refused, hash still recorded (no data hoovering)
#   P11 reconcile pre-step: a preimage change surfaces as a first-class line
#       BEFORE the UNACCOUNTED enumerations it explains           (acceptance c)
#   P12 detector: preimage infra failure is LOUD (pass exits non-zero) but does
#       NOT starve source observation
#
# Regression needles from the #964 NOT-CLEAR review (decoy method: plant the
# bytes, grep the WHOLE state dir — the decoy does not know what the code
# believes):
#   P13 case C: symlinked/renamed credential store (mosaic-home itself behind
#       a symlink; a renamed target outside every path rule) — refused on BOTH
#       path forms / by content, bytes grep-absent          (acceptance b)
#   P14 case D: prefix-less high-entropy key (detector.env class) — safe
#       WITHOUT opt-in by POLARITY alone (must hold with the shape list
#       deleted), and refused WITH opt-in by the named-assignment shape
#   P15 polarity: extras are RECORD-ONLY by default (change row + enqueue, no
#       bytes); byte capture requires explicit WAKE_PREIMAGE_CAPTURE opt-in
#   P16 case B11: ledger deleted while objects/ survives — LOUD refusal to
#       re-baseline; the v2->v3 change is NOT absorbed as first-seen
#   P17 allowlist symmetry: an allowlisted symlink whose TARGET is denied
#       refuses (deny runs first, on both forms — precedence is not path
#       agreement); a symlink to an ALLOWED target still captures (the fix
#       must not close case C by breaking every symlinked path)
#
# 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)"
# #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
PRE="$SCRIPT_DIR/preimage.sh"
STORE="$SCRIPT_DIR/store.sh"
DET="$SCRIPT_DIR/detector.sh"
RECON="$SCRIPT_DIR/reconcile.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'; }
state_dir() { printf '%s/default' "$WAKE_STATE_HOME"; }
ledger_rows() {
  local f
  f="$(state_dir)/preimage/preimage-ledger.jsonl"
  [ -f "$f" ] && wc -l <"$f" | tr -d '[:space:]' || echo 0
}

# make_stub DIR — a source adapter reading $DIR/<kind>_<id> (same shape as the
# reconcile harness stub).
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"
}

make_wl() { # make_wl FILE — one repo source r1
  cat >"$1" <<'EOF'
{ "schema_version": 1,
  "repos": [ { "id": "r1" } ],
  "watches": [ { "lane": "L", "sources": [ { "kind": "repo", "id": "r1" } ] } ] }
EOF
}

echo "== P1: first-seen -> SILENT baseline (rows, no enqueue) =="
(
  WAKE_STATE_HOME="$(fresh_state p1)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p1fx"; mkdir -p "$fx"
  make_stub "$fx"; make_wl "$fx/wl.json"
  printf 'extra v1\n' >"$fx/extra.env"
  export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh" WAKE_WATCH_LIST="$fx/wl.json" WAKE_PREIMAGE_EXTRA="$fx/extra.env"
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -eq 0 ] || fail_msg "P1: baseline check must exit 0 (rc=$rc) [$out]"
  [ "$(ledger_rows)" -eq 3 ] || fail_msg "P1: expected 3 baseline rows (adapter, watch-list, extra), got $(ledger_rows)"
  [ "$(depth)" = "0" ] || fail_msg "P1: first-seen must NOT enqueue (depth=$(depth))"
) && ok

echo "== P2: adapter change -> prior bytes + first-class hard-locator enqueue =="
(
  WAKE_STATE_HOME="$(fresh_state p2)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p2fx"; mkdir -p "$fx"
  make_stub "$fx"; make_wl "$fx/wl.json"
  # Acceptance (a) names the OPERATOR ADAPTER — a core member. Core members
  # capture by default (they ARE the preimage definition); extras do not (P15).
  export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh" WAKE_WATCH_LIST="$fx/wl.json"
  unset WAKE_PREIMAGE_EXTRA WAKE_PREIMAGE_CAPTURE MOSAIC_HOME
  "$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P2: baseline failed"
  cp "$fx/adapter.sh" "$fx/adapter.v1"
  old_sha="$(sha256sum "$fx/adapter.sh" | awk '{print $1}')"
  printf '# adapter v2 CHANGED\n' >>"$fx/adapter.sh"
  new_sha="$(sha256sum "$fx/adapter.sh" | awk '{print $1}')"
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -eq 0 ] || fail_msg "P2: change check must exit 0 (rc=$rc) [$out]"
  sd="$(state_dir)"
  row="$(jq -c --arg p "$(realpath "$fx/adapter.sh")" 'select(.path == $p)' "$sd/preimage/preimage-ledger.jsonl" | tail -n1)"
  [ "$(jq -r '.prev // ""' <<<"$row")" = "$old_sha" ] || fail_msg "P2: new row must carry prev=<old sha> [$row]"
  [ "$(jq -r '.sha256' <<<"$row")" = "$new_sha" ] || fail_msg "P2: new row sha mismatch [$row]"
  # Acceptance (a): the PRIOR BYTES are retrievable from durable state alone.
  cmp -s "$sd/preimage/objects/$old_sha" "$fx/adapter.v1" || fail_msg "P2: prior bytes missing or not byte-exact"
  cmp -s "$sd/preimage/objects/$new_sha" "$fx/adapter.sh" || fail_msg "P2: current bytes object missing or mismatched"
  # Acceptance (c): ONE first-class actionable with a §2.1 hard locator (path).
  [ "$(depth)" = "1" ] || fail_msg "P2: exactly one enqueue expected (depth=$(depth))"
  ent="$(tail -n1 "$sd/pending.jsonl")"
  [ "$(jq -r '.class' <<<"$ent")" = "actionable" ] || fail_msg "P2: entry class must be actionable [$ent]"
  [ "$(jq -r '.locators.kind' <<<"$ent")" = "preimage" ] || fail_msg "P2: locator kind must be preimage [$ent]"
  [ -n "$(jq -r '.locators.path // ""' <<<"$ent")" ] || fail_msg "P2: locator must carry path (hard locator) [$ent]"
  [ "$(jq -r '.locators.observed_hash' <<<"$ent")" = "$new_sha" ] || fail_msg "P2: locator observed_hash mismatch [$ent]"
  [ "$(jq -r '.locators.prev_hash' <<<"$ent")" = "$old_sha" ] || fail_msg "P2: locator prev_hash mismatch [$ent]"
) && ok

echo "== P3: detector orders the cause line BEFORE the delta it explains =="
(
  WAKE_STATE_HOME="$(fresh_state p3)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p3fx"; mkdir -p "$fx"
  make_stub "$fx"; make_wl "$fx/wl.json"
  printf 'r1 state v1\n' >"$fx/repo_r1"
  export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh" WAKE_WATCH_LIST="$fx/wl.json"
  unset WAKE_PREIMAGE_EXTRA
  "$DET" poll-once >/dev/null 2>&1 || fail_msg "P3: baseline poll failed"
  [ "$(depth)" = "0" ] || fail_msg "P3: baseline poll must enqueue nothing (depth=$(depth))"
  # Change the ADAPTER (preimage) and the source state in the same window.
  printf '# comment: adapter changed\n' >>"$fx/adapter.sh"
  printf 'r1 state v2\n' >"$fx/repo_r1"
  "$DET" poll-once >/dev/null 2>&1 || fail_msg "P3: second poll failed"
  sd="$(state_dir)"
  pre_seq="$(jq -r 'select(.locators.kind == "preimage") | .observed_seq' "$sd/pending.jsonl" | head -n1)"
  src_seq="$(jq -r 'select(.locators.kind == "repo") | .observed_seq' "$sd/pending.jsonl" | head -n1)"
  [ -n "$pre_seq" ] || fail_msg "P3: no preimage cause entry enqueued"
  [ -n "$src_seq" ] || fail_msg "P3: no source delta entry enqueued"
  if [ -n "$pre_seq" ] && [ -n "$src_seq" ]; then
    [ "$pre_seq" -lt "$src_seq" ] || fail_msg "P3: cause line must precede the delta (preimage seq=$pre_seq, source seq=$src_seq)"
  fi
) && ok

echo "== P4: credential-store PATH refused — bytes never captured (acceptance b) =="
(
  WAKE_STATE_HOME="$(fresh_state p4)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p4fx"; mkdir -p "$fx"
  export MOSAIC_HOME="$fx/mosaic-home"
  mkdir -p "$MOSAIC_HOME"
  secret='cred-value-P4-do-not-capture'
  printf '{"svc":{"token":"%s"}}\n' "$secret" >"$MOSAIC_HOME/credentials.json"
  # Operator error: the credential store listed as a preimage extra — AND
  # explicitly opted in to capture. Deny must win over the opt-in (#964: a
  # path both denied and allowlisted must refuse).
  export WAKE_PREIMAGE_EXTRA="$MOSAIC_HOME/credentials.json"
  export WAKE_PREIMAGE_CAPTURE="$MOSAIC_HOME/credentials.json"
  unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -eq 0 ] || fail_msg "P4: refusal is not an infra failure (rc=$rc) [$out]"
  sd="$(state_dir)"
  row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
  [ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P4: row must record captured=false [$row]"
  jq -r '.refused // ""' <<<"$row" | has_match -qi 'path' || fail_msg "P4: refusal must name the path deny [$row]"
  # THE invariant: the secret bytes exist NOWHERE under the wake state dir.
  if has_match -rq "$secret" "$sd" 2>/dev/null; then
    fail_msg "P4: credential bytes leaked into the wake state dir"
  fi
) && ok

echo "== P5: secret-SHAPED content refused (refusal, not redaction) =="
(
  WAKE_STATE_HOME="$(fresh_state p5)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p5fx"; mkdir -p "$fx"
  tok='ghp_abcdefghijklmnop0123456789ABCDEF'
  printf 'export MY_TOKEN=%s\n' "$tok" >"$fx/leaky.env"
  # Opted in, so the CONTENT gate (not record-only polarity) is what refuses.
  export WAKE_PREIMAGE_EXTRA="$fx/leaky.env" WAKE_PREIMAGE_CAPTURE="$fx/leaky.env"
  unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -eq 0 ] || fail_msg "P5: refusal is not an infra failure (rc=$rc) [$out]"
  sd="$(state_dir)"
  row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
  [ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P5: row must record captured=false [$row]"
  jq -r '.refused // ""' <<<"$row" | has_match -qi 'secret' || fail_msg "P5: refusal must name secret-shaped content [$row]"
  if has_match -rq "$tok" "$sd" 2>/dev/null; then
    fail_msg "P5: secret-shaped bytes leaked into the wake state dir"
  fi
) && ok

echo "== P6: deletion is a tracked CHANGE (ABSENT), not an error =="
(
  WAKE_STATE_HOME="$(fresh_state p6)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p6fx"; mkdir -p "$fx"
  printf 'to be deleted\n' >"$fx/gone.env"
  export WAKE_PREIMAGE_EXTRA="$fx/gone.env"
  unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME
  "$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P6: baseline failed"
  rm -f "$fx/gone.env"
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -eq 0 ] || fail_msg "P6: deletion check must exit 0 (rc=$rc) [$out]"
  sd="$(state_dir)"
  row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
  [ "$(jq -r '.sha256' <<<"$row")" = "ABSENT" ] || fail_msg "P6: deletion must record sha256=ABSENT [$row]"
  [ "$(depth)" = "1" ] || fail_msg "P6: deletion is a change and must enqueue (depth=$(depth))"
) && ok

echo "== P7: no-change is idempotent =="
(
  WAKE_STATE_HOME="$(fresh_state p7)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p7fx"; mkdir -p "$fx"
  printf 'stable\n' >"$fx/stable.env"
  export WAKE_PREIMAGE_EXTRA="$fx/stable.env"
  unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME
  "$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P7: baseline failed"
  r1="$(ledger_rows)"
  "$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P7: second check failed"
  [ "$(ledger_rows)" = "$r1" ] || fail_msg "P7: no-change must not append rows ($r1 -> $(ledger_rows))"
  [ "$(depth)" = "0" ] || fail_msg "P7: no-change must not enqueue (depth=$(depth))"
) && ok

echo "== P8: unresolvable adapter -> FAIL LOUD, never 'no change' (D2 class) =="
(
  WAKE_STATE_HOME="$(fresh_state p8)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  export WAKE_DETECTOR_SOURCE_CMD="$TMP_ROOT/does-not-exist-adapter"
  unset WAKE_WATCH_LIST WAKE_PREIMAGE_EXTRA MOSAIC_HOME
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -ne 0 ] || fail_msg "P8: an unobservable preimage definition must FAIL LOUD (rc=0)"
  echo "$out" | has_match -qi 'does not resolve' || fail_msg "P8: the failure must name the unresolvable adapter [$out]"
) && ok

echo "== P9: corrupt ledger -> REFUSE to compare/re-baseline =="
(
  WAKE_STATE_HOME="$(fresh_state p9)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p9fx"; mkdir -p "$fx"
  printf 'v1\n' >"$fx/f.env"
  export WAKE_PREIMAGE_EXTRA="$fx/f.env"
  unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME
  "$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P9: baseline failed"
  sd="$(state_dir)"
  printf 'NOT-JSON-GARBAGE{{{\n' >>"$sd/preimage/preimage-ledger.jsonl"
  r1="$(wc -l <"$sd/preimage/preimage-ledger.jsonl")"
  printf 'v2 changed\n' >"$fx/f.env"
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -ne 0 ] || fail_msg "P9: a corrupt ledger must FAIL LOUD (rc=0)"
  echo "$out" | has_match -qi 'unparseable' || fail_msg "P9: the failure must name the corrupt ledger [$out]"
  [ "$(wc -l <"$sd/preimage/preimage-ledger.jsonl")" = "$r1" ] || fail_msg "P9: nothing may be appended over corrupt history"
) && ok

echo "== P10: oversized file -> bytes refused, hash still recorded =="
(
  WAKE_STATE_HOME="$(fresh_state p10)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p10fx"; mkdir -p "$fx"
  head -c 200 /dev/zero | tr '\0' 'A' >"$fx/big.bin"
  # Opted in, so the SIZE gate (not record-only polarity) is what refuses.
  export WAKE_PREIMAGE_EXTRA="$fx/big.bin" WAKE_PREIMAGE_MAX_BYTES=64
  export WAKE_PREIMAGE_CAPTURE="$fx/big.bin"
  unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -eq 0 ] || fail_msg "P10: size refusal is not an infra failure (rc=$rc) [$out]"
  sd="$(state_dir)"
  row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
  [ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P10: row must record captured=false [$row]"
  jq -r '.refused // ""' <<<"$row" | has_match -qi 'MAX_BYTES' || fail_msg "P10: refusal must name the size cap [$row]"
  sha="$(jq -r '.sha256' <<<"$row")"
  [ "$sha" = "$(sha256sum "$fx/big.bin" | awk '{print $1}')" ] || fail_msg "P10: hash must still be recorded [$row]"
  [ ! -f "$sd/preimage/objects/$sha" ] || fail_msg "P10: oversized bytes must NOT be stored"
) && ok

echo "== P11: reconcile surfaces the cause line before its enumerations =="
(
  WAKE_STATE_HOME="$(fresh_state p11)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p11fx"; mkdir -p "$fx"
  make_stub "$fx"; make_wl "$fx/wl.json"
  printf 'r1 state\n' >"$fx/repo_r1"
  export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh" WAKE_WATCH_LIST="$fx/wl.json"
  unset WAKE_PREIMAGE_EXTRA MOSAIC_HOME
  # Baseline the preimage set, then change the adapter while "the detector is
  # down" — the reconcile path must still produce the first-class cause line.
  "$PRE" check >/dev/null 2>&1 || fail_msg "P11: preimage baseline failed"
  printf '# adapter changed while detector down\n' >>"$fx/adapter.sh"
  "$RECON" reconcile >/dev/null 2>&1 # rc 1 expected (unaccounted enumerated)
  sd="$(state_dir)"
  pre_seq="$(jq -r 'select(.locators.kind == "preimage") | .observed_seq' "$sd/pending.jsonl" | head -n1)"
  enum_seq="$(jq -r 'select(.locators.reconciled == true) | .observed_seq' "$sd/pending.jsonl" | head -n1)"
  [ -n "$pre_seq" ] || fail_msg "P11: reconcile must enqueue the preimage cause line"
  [ -n "$enum_seq" ] || fail_msg "P11: reconcile must still enumerate the unaccounted source"
  if [ -n "$pre_seq" ] && [ -n "$enum_seq" ]; then
    [ "$pre_seq" -lt "$enum_seq" ] || fail_msg "P11: cause line must precede the enumeration (preimage seq=$pre_seq, enum seq=$enum_seq)"
  fi
) && ok

echo "== P12: detector — preimage infra failure is LOUD but does not starve observation =="
(
  WAKE_STATE_HOME="$(fresh_state p12)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p12fx"; mkdir -p "$fx"
  make_stub "$fx"; make_wl "$fx/wl.json"
  printf 'r1 state\n' >"$fx/repo_r1"
  export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh" WAKE_WATCH_LIST="$fx/wl.json"
  unset WAKE_PREIMAGE_EXTRA MOSAIC_HOME
  # Corrupt the ledger BEFORE the first poll: the preimage check fails loud,
  # but the poll must still observe + baseline the source.
  sd="$(state_dir)"
  mkdir -p "$sd/preimage"
  printf 'NOT-JSON-GARBAGE{{{\n' >"$sd/preimage/preimage-ledger.jsonl"
  out="$("$DET" poll-once 2>&1)"; rc=$?
  [ "$rc" -ne 0 ] || fail_msg "P12: the pass must exit non-zero on preimage infra failure"
  echo "$out" | has_match -qi 'preimage' || fail_msg "P12: the failure must name the preimage check [$out]"
  n="$(find "$sd/detector" -name 'watch-*.hash' 2>/dev/null | wc -l | tr -d '[:space:]')"
  [ "$n" -ge 1 ] || fail_msg "P12: source observation must still proceed (no watch hash baselined)"
) && ok

echo "== P13: symlinked/renamed credential store refused on BOTH path forms (#964 C) =="
(
  WAKE_STATE_HOME="$(fresh_state p13)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p13fx"; mkdir -p "$fx"
  # The mosaic home is ITSELF behind a symlink: a deny list written in
  # unresolved paths cannot match a candidate that was realpath'd before the
  # deny saw it — unless the anchors are resolved too.
  mkdir -p "$fx/real-store/credentials"
  ln -s "$fx/real-store" "$fx/mh"
  export MOSAIC_HOME="$fx/mh"
  s1='decoy-P13a-renamed-target-under-store'
  printf 'x_token=%s\n' "$s1" >"$fx/mh/credentials/brain-creds.json"
  # And a renamed target OUTSIDE every known store location, reached via a
  # benign-looking symlink: no path rule can name it — the content gate must
  # hold on the resolved file.
  s2='0123456789abcdef0123456789abcdefP13b'
  printf 'service_hmac_key=%s\n' "$s2" >"$fx/renamed-anywhere.cfg"
  ln -s "$fx/renamed-anywhere.cfg" "$fx/link-b.env"
  export WAKE_PREIMAGE_EXTRA="$fx/mh/credentials/brain-creds.json:$fx/link-b.env"
  # Explicit opt-in for BOTH: deny must win over the allowlist.
  export WAKE_PREIMAGE_CAPTURE="$fx/mh/credentials/brain-creds.json:$fx/link-b.env"
  unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -eq 0 ] || fail_msg "P13: refusal is not an infra failure (rc=$rc) [$out]"
  sd="$(state_dir)"
  n="$(jq -r 'select(.captured == false) | .path' "$sd/preimage/preimage-ledger.jsonl" | wc -l | tr -d '[:space:]')"
  [ "$n" = "2" ] || fail_msg "P13: both decoys must record captured=false (got $n)"
  # THE invariant (decoy method): the planted bytes exist NOWHERE in state.
  if has_match -rq "$s1" "$sd" 2>/dev/null; then
    fail_msg "P13: renamed-target store bytes leaked into the wake state dir"
  fi
  if has_match -rq "$s2" "$sd" 2>/dev/null; then
    fail_msg "P13: prefix-less key bytes leaked into the wake state dir"
  fi
) && ok

echo "== P14: detector.env-class key — safe WITHOUT opt-in by polarity, refused WITH opt-in by shape (#964 D) =="
(
  WAKE_STATE_HOME="$(fresh_state p14)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p14fx"; mkdir -p "$fx"
  hm='9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c'
  printf 'WAKE_HMAC_KEY=%s\n' "$hm" >"$fx/detector.env"
  export WAKE_PREIMAGE_EXTRA="$fx/detector.env"
  unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME WAKE_PREIMAGE_CAPTURE
  # (a) NO opt-in: polarity alone keeps the bytes out. This leg must hold even
  # with the content shape list deleted — the shape is defense-in-depth for
  # the opt-in path, never the thing that makes case D read safe.
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -eq 0 ] || fail_msg "P14a: record-only tracking must succeed (rc=$rc) [$out]"
  sd="$(state_dir)"
  row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
  [ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P14a: extra must be record-only without opt-in [$row]"
  if has_match -rq "$hm" "$sd" 2>/dev/null; then
    fail_msg "P14a: key bytes leaked without any opt-in"
  fi
  # (b) The operator opts the env file in (the exact error the old usage text
  # invited): the named-assignment shape must still refuse the bytes.
  export WAKE_PREIMAGE_CAPTURE="$fx/detector.env"
  printf 'WAKE_HMAC_KEY=%s\nrotated=1\n' "$hm" >"$fx/detector.env"
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -eq 0 ] || fail_msg "P14b: refusal is not an infra failure (rc=$rc) [$out]"
  row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
  [ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P14b: opted-in key material must still refuse [$row]"
  jq -r '.refused // ""' <<<"$row" | has_match -qi 'secret' || fail_msg "P14b: refusal must name secret-shaped content [$row]"
  if has_match -rq "$hm" "$sd" 2>/dev/null; then
    fail_msg "P14b: key bytes leaked despite refusal"
  fi
) && ok

echo "== P15: extras are RECORD-ONLY by default; capture requires explicit opt-in =="
(
  WAKE_STATE_HOME="$(fresh_state p15)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p15fx"; mkdir -p "$fx"
  printf 'plain v1\n' >"$fx/notes.cfg"
  export WAKE_PREIMAGE_EXTRA="$fx/notes.cfg"
  unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST MOSAIC_HOME WAKE_PREIMAGE_CAPTURE
  "$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P15: baseline failed"
  printf 'plain v2\n' >"$fx/notes.cfg"
  sha2="$(sha256sum "$fx/notes.cfg" | awk '{print $1}')"
  "$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P15: change check failed"
  sd="$(state_dir)"
  row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
  [ "$(jq -r '.captured' <<<"$row")" = "false" ] || fail_msg "P15: default must be record-only [$row]"
  [ ! -f "$sd/preimage/objects/$sha2" ] || fail_msg "P15: bytes captured without opt-in"
  [ "$(depth)" = "1" ] || fail_msg "P15: record-only must still track the change (depth=$(depth))"
  # Opt in -> the next change captures.
  export WAKE_PREIMAGE_CAPTURE="$fx/notes.cfg"
  printf 'plain v3\n' >"$fx/notes.cfg"
  sha3="$(sha256sum "$fx/notes.cfg" | awk '{print $1}')"
  "$PRE" check --enqueue >/dev/null 2>&1 || fail_msg "P15: opted-in change check failed"
  row="$(tail -n1 "$sd/preimage/preimage-ledger.jsonl")"
  [ "$(jq -r '.captured' <<<"$row")" = "true" ] || fail_msg "P15: opt-in must capture [$row]"
  cmp -s "$sd/preimage/objects/$sha3" "$fx/notes.cfg" || fail_msg "P15: opted-in bytes missing or mismatched"
) && ok

echo "== P16: deleted ledger over surviving objects/ — REFUSE to re-baseline (#964 B11) =="
(
  WAKE_STATE_HOME="$(fresh_state p16)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p16fx"; mkdir -p "$fx"
  make_stub "$fx"
  export WAKE_DETECTOR_SOURCE_CMD="$fx/adapter.sh"
  unset WAKE_WATCH_LIST WAKE_PREIMAGE_EXTRA WAKE_PREIMAGE_CAPTURE MOSAIC_HOME
  "$PRE" check >/dev/null 2>&1 || fail_msg "P16: v1 baseline failed"
  printf '# v2\n' >>"$fx/adapter.sh"
  "$PRE" check >/dev/null 2>&1 || fail_msg "P16: v2 change failed"
  sd="$(state_dir)"
  nobj="$(ls "$sd/preimage/objects" | wc -l | tr -d '[:space:]')"
  # Delete the ledger; objects/ survives. ABSENT is not CORRUPT — and it is
  # not a first install either: that asymmetry is locally detectable.
  rm -f "$sd/preimage/preimage-ledger.jsonl"
  printf '# v3\n' >>"$fx/adapter.sh"
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -ne 0 ] || fail_msg "P16: absent ledger over prior objects must FAIL LOUD (rc=0) [$out]"
  echo "$out" | has_match -qi 'REFUSING to re-baseline' || fail_msg "P16: the failure must name the refusal [$out]"
  [ ! -e "$sd/preimage/preimage-ledger.jsonl" ] || fail_msg "P16: the ledger must NOT be silently recreated over deleted history"
  [ "$(ls "$sd/preimage/objects" | wc -l | tr -d '[:space:]')" = "$nobj" ] || fail_msg "P16: prior objects must remain untouched"
  [ "$(depth)" = "0" ] || fail_msg "P16: the v2->v3 change must NOT be absorbed or enqueued from an unverifiable baseline (depth=$(depth))"
) && ok

echo "== P17: allowlist symmetry — symlink to a DENIED target refuses; symlink to an allowed target captures =="
(
  WAKE_STATE_HOME="$(fresh_state p17)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT
  fx="$TMP_ROOT/p17fx"; mkdir -p "$fx"
  export MOSAIC_HOME="$fx/mh"
  mkdir -p "$MOSAIC_HOME"
  s='decoy-P17-cred-behind-benign-alias'
  printf '{"svc":{"token":"%s"}}\n' "$s" >"$MOSAIC_HOME/credentials.json"
  # A benign-looking alias whose TARGET is denied: the allowlist matches the
  # alias string, but deny runs FIRST and on BOTH forms — "deny wins over
  # opt-in" is a precedence rule, path agreement is what makes it hold.
  ln -s "$MOSAIC_HOME/credentials.json" "$fx/settings.json"
  # And a symlink to an ALLOWED target: the fix must not close case C by
  # breaking every symlinked path outright.
  printf 'benign payload v1\n' >"$fx/target.cfg"
  ln -s "$fx/target.cfg" "$fx/alias.cfg"
  export WAKE_PREIMAGE_EXTRA="$fx/settings.json:$fx/alias.cfg"
  export WAKE_PREIMAGE_CAPTURE="$fx/settings.json:$fx/alias.cfg"
  unset WAKE_DETECTOR_SOURCE_CMD WAKE_WATCH_LIST
  out="$("$PRE" check --enqueue 2>&1)"; rc=$?
  [ "$rc" -eq 0 ] || fail_msg "P17: check must exit 0 (rc=$rc) [$out]"
  sd="$(state_dir)"
  crow="$(jq -c --arg p "$(realpath "$fx/settings.json")" 'select(.path == $p)' "$sd/preimage/preimage-ledger.jsonl" | tail -n1)"
  [ "$(jq -r '.captured' <<<"$crow")" = "false" ] || fail_msg "P17: allowlisted symlink to a denied target must refuse [$crow]"
  if has_match -rq "$s" "$sd" 2>/dev/null; then
    fail_msg "P17: credential bytes leaked via an allowlisted alias"
  fi
  brow="$(jq -c --arg p "$(realpath "$fx/alias.cfg")" 'select(.path == $p)' "$sd/preimage/preimage-ledger.jsonl" | tail -n1)"
  [ "$(jq -r '.captured' <<<"$brow")" = "true" ] || fail_msg "P17: symlink to an allowed target must still capture [$brow]"
) && ok

echo
total=$((pass + $(wc -l <"$FAILFILE")))
echo "== test-wake-preimage: $pass/$total passed =="
[ -s "$FAILFILE" ] && exit 1
exit 0
