#!/usr/bin/env bash
# test-wake-digest-hmac.sh — RED-FIRST invariant harness for W3 (EPIC #892):
# the cumulative-state digest renderer (digest.sh, A3) + the non-circular HMAC
# signer (sign.sh, A5).
#
# Each test asserts ONE CONVERGED-DESIGN invariant and is designed to go RED if
# that invariant regresses:
#   D1 CUMULATIVE-STATE: two still-pending changes both render (not just the
#      latest delta) — a delta would silently drop the older change.      (§2.1)
#   D2 HARD-LOCATOR enforcement: an actionable claim with no precise locator is
#      QUARANTINED — dead-lettered + alarmed + excluded — never delivered as
#      valid (fail-loud PER-ENTRY, #920; the rest of the drain still renders). (§2.1)
#   D3 TWO-TIER trust: orientation decides the no-op case with ZERO tool calls;
#      an actionable fact is a CLAIM-TO-VERIFY, never auto-actioned.       (§2.1)
#   D4 SCRUB: a secret-canary + ANSI/bidi/zero-width in SOURCE free-text is
#      stripped/neutralized in the rendered digest.                       (§2.1)
#   H1 NON-CIRCULAR HMAC: wake_id is NOT a member of the signed field-tuple and
#      is independently generated; wake_mac is not its own input; tampering ANY
#      signed field (or wake_id) breaks the MAC.                          (§2.5)
#   H2 KEY BY-NAME, NEVER INLINE: the key is resolved by NAME from the credential
#      store; no flag accepts key material; the key never leaks into output; the
#      same-uid threat boundary is documented.                           (§2.5)
#   D5 (#914a) EMBEDDED ACK NAMESPACE: the copy-run ack line rendered at
#      WAKE_AGENT=X render time targets X EXPLICITLY, so an env-less copy-run
#      still resolves to the correct per-agent namespace (never silently
#      falls back to `default`).                                         (§2.1/§2.2)
#   D6 (#914b) DIGEST-CLASS LOCATOR THREADING: an ORIENTATION pointer for a
#      digest-class entry (the shape detector.sh actually enqueues:
#      kind/id/observed_hash/remote/path, not repo/issue/sha/file) carries a
#      non-empty, usable locator — and the ACTIONABLE-tier hard-locator
#      FAIL-LOUD is PRESERVED (now per-entry quarantine, #920) for a malformed
#      actionable claim.                                                  (§2.1)
#
# Isolated: every test runs against a fresh temp state / credential file.
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
STORE="$SCRIPT_DIR/store.sh"
DIGEST="$SCRIPT_DIR/digest.sh"
SIGN="$SCRIPT_DIR/sign.sh"

command -v jq >/dev/null 2>&1 || {
  echo "SKIP: jq not available" >&2
  exit 0
}
# openssl gates the HMAC legs (H1/H2). #912: the wake trust layer MUST be
# exercised in real CI, so when running under CI (Woodpecker sets CI=woodpecker)
# openssl is HARD-REQUIRED — a missing openssl fails the suite LOUD rather than
# silently skipping the signer (the §4 G6 evidence must come from an
# actually-run HMAC leg, not a skipped one). In an openssl-less LOCAL DEV env
# the whole suite still skips so `pnpm test` stays runnable without openssl.
if ! command -v openssl >/dev/null 2>&1; then
  if [ -n "${CI:-}" ]; then
    echo "FATAL (#912): openssl is REQUIRED in CI to exercise the wake digest/HMAC trust layer, but is not on PATH. The CI image must provide openssl (see .woodpecker/ci-image.yml)." >&2
    exit 1
  fi
  echo "SKIP: openssl not available (local dev; CI hard-requires it)" >&2
  exit 0
fi

TMP_ROOT="$(mktemp -d)"
trap 'rm -rf "$TMP_ROOT"' EXIT

# Failures recorded to a marker FILE (not a shell var): each test runs in a
# subshell for env isolation and a subshell cannot mutate a parent variable, so
# a var-based counter would silently swallow failures.
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"
}

# A 40-hex sha, for hard locators.
SHA40="abcdef0123456789abcdef0123456789abcdef01"

# fresh_cred NAME KEYNAME KEYVALUE — write a temp credential store with a wake
# HMAC key at .wake.hmac_keys.<KEYNAME> and echo its path.
fresh_cred() {
  local f="$TMP_ROOT/$1.cred.json"
  jq -cn --arg k "$2" --arg v "$3" '{wake:{hmac_keys:{($k):$v}}}' >"$f"
  printf '%s' "$f"
}

# ===========================================================================
echo "== D1: CUMULATIVE-STATE — two pending changes BOTH render (not just latest) =="
(
  WAKE_STATE_HOME="$(fresh_state d1)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT WAKE_LANE
  # Two distinct obligations observed since the last CONSUMED. A delta-style
  # renderer would show only the newest; cumulative-state must show BOTH.
  "$STORE" enqueue --seq 1 --class actionable --locators '{"repo":"r","issue":11}' >/dev/null
  "$STORE" enqueue --seq 2 --class actionable --locators '{"repo":"r","issue":22}' >/dev/null
  out="$("$DIGEST" render)" || fail_msg "D1: render exited non-zero"
  echo "$out" | has_match -q 'issue=#11' || fail_msg "D1: OLDER change (issue #11) dropped — digest is a delta, not cumulative state"
  echo "$out" | has_match -q 'issue=#22' || fail_msg "D1: newer change (issue #22) missing"
  echo "$out" | has_match -q 'seq 1' || fail_msg "D1: seq 1 not listed in cumulative set"
  echo "$out" | has_match -q 'seq 2' || fail_msg "D1: seq 2 not listed in cumulative set"
  # A coalescing digest-class entry is STATE (full), not a delta: a later digest
  # subsumes the earlier, but the cumulative unacked set (both actionables) stays.
  echo "$out" | has_match -q 'pending=2' || fail_msg "D1: cumulative pending count wrong (expected 2)"
) && ok

echo "== D2: HARD-LOCATOR enforcement — a malformed actionable claim is QUARANTINED (fail-loud PER-ENTRY, #920); never delivered as valid =="
(
  h="$(fresh_state d2)"
  WAKE_STATE_HOME="$h"
  export WAKE_STATE_HOME
  unset WAKE_AGENT WAKE_LANE
  # An actionable claim carrying NO hard locator (no repo+issue / sha / file).
  "$STORE" enqueue --seq 1 --class actionable --locators '{"claim":"mergeable=true"}' >/dev/null
  err="$TMP_ROOT/d2.err"
  rc=0
  out="$("$DIGEST" render 2>"$err")" || rc=$?
  # #920: NO longer a whole-digest exit-4 — the malformed entry is QUARANTINED
  # (dead-lettered + alarmed + excluded) and the digest STILL renders (exit 0),
  # but the unlocated claim is NEVER delivered as a valid CLAIM (fail-loud is
  # preserved, now per-entry). The old exit-4 wedged the entire drain (#920).
  [ "$rc" -eq 0 ] || fail_msg "D2: a malformed actionable must be quarantined (render exit 0, #920), got $rc"
  printf '%s' "$out" | has_match -q 'mergeable=true' && fail_msg "D2: an unlocated actionable claim must NOT be delivered as a valid CLAIM"
  has_match -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2: the malformed claim must be DEAD-LETTERED (fail-loud preserved, per-entry)"
  has_match -qi 'QUARANTINE' "$err" || fail_msg "D2: a LOUD per-entry alarm must fire for the malformed claim"

  # A present-but-imprecise sha (not 40 hex) does NOT satisfy the hard locator ->
  # quarantined, not delivered.
  h="$(fresh_state d2b)"
  WAKE_STATE_HOME="$h"
  export WAKE_STATE_HOME
  "$STORE" enqueue --seq 1 --class actionable --locators '{"claim":"ci=green","sha":"abc123"}' >/dev/null
  rc=0
  out="$("$DIGEST" render 2>/dev/null)" || rc=$?
  [ "$rc" -eq 0 ] || fail_msg "D2: imprecise-sha entry must be quarantined (render exit 0), got $rc"
  printf '%s' "$out" | has_match -q 'ci=green' && fail_msg "D2: imprecise sha (not 40-hex) must not satisfy the hard-locator gate (claim must not deliver)"
  has_match -q 'ci=green' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2: imprecise-sha claim must be dead-lettered"

  # The SAME claim WITH a precise 40-hex sha renders fine (delivered, not quarantined).
  h="$(fresh_state d2c)"
  WAKE_STATE_HOME="$h"
  export WAKE_STATE_HOME
  "$STORE" enqueue --seq 1 --class actionable --locators "$(jq -cn --arg s "$SHA40" '{claim:"ci=green",sha:$s}')" >/dev/null
  out="$("$DIGEST" render 2>/dev/null)" || fail_msg "D2: a well-located actionable claim must render"
  printf '%s' "$out" | has_match -q "$SHA40" || fail_msg "D2: a well-located actionable claim must be DELIVERED"
  [ -s "$h/default/dead-letter.jsonl" ] && fail_msg "D2: a well-located claim must NOT be quarantined"
  # --- #905 bypass-prevention (STILL enforced, now via quarantine): a NON-
  # CANONICAL entry with a TOP-LEVEL `.claim` (store.sh never emits this shape;
  # only .locators.claim is canonical) and EMPTY .locators, fed via --stdin /
  # --from-file, must ALSO be gated. The classifier honors `.claim //
  # .locators.claim`, so a hand-crafted caller cannot bypass the hard-locator gate
  # — the entry is QUARANTINED (not delivered), not silently rendered.
  h="$(fresh_state d2d)"
  WAKE_STATE_HOME="$h"
  export WAKE_STATE_HOME
  toplevel_claim_entry='{"observed_seq":1,"class":"reaction","claim":"mergeable=true","locators":{}}'
  rc=0
  out="$(printf '%s\n' "$toplevel_claim_entry" | "$DIGEST" render --stdin 2>/dev/null)" || rc=$?
  [ "$rc" -eq 0 ] || fail_msg "D2(#905): top-level .claim must be quarantined via --stdin (exit 0), got $rc"
  printf '%s' "$out" | has_match -q 'mergeable=true' && fail_msg "D2(#905): --stdin top-level-.claim must NOT be delivered (gate not bypassed)"
  has_match -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2(#905): --stdin top-level .claim must be dead-lettered (gate enforced)"
  ff="$TMP_ROOT/d2d-entry.jsonl"
  printf '%s\n' "$toplevel_claim_entry" >"$ff"
  h="$(fresh_state d2e)"
  WAKE_STATE_HOME="$h"
  export WAKE_STATE_HOME
  rc=0
  out2="$("$DIGEST" render --from-file "$ff" 2>/dev/null)" || rc=$?
  [ "$rc" -eq 0 ] || fail_msg "D2(#905): top-level .claim must be quarantined via --from-file (exit 0), got $rc"
  printf '%s' "$out2" | has_match -q 'mergeable=true' && fail_msg "D2(#905): --from-file top-level-.claim must NOT be delivered (gate not bypassed)"
  has_match -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D2(#905): --from-file top-level .claim must be dead-lettered (gate enforced)"
) && ok

echo "== D3: TWO-TIER — orientation no-op with ZERO tool calls; actionable = claim-to-verify =="
(
  WAKE_STATE_HOME="$(fresh_state d3)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT WAKE_LANE
  # Tool-call tripwire: shim git/gh/tea/curl on PATH; any invocation touches a
  # marker. The orientation no-op path must decide with ZERO of them.
  bin="$TMP_ROOT/d3bin"
  mkdir -p "$bin"
  for t in git gh tea curl wget; do
    {
      printf '#!/usr/bin/env bash\n'
      printf 'touch "%s/TOOL_CALLED"\n' "$WAKE_STATE_HOME"
      printf 'exit 0\n'
    } >"$bin/$t"
    chmod +x "$bin/$t"
  done
  # No pending obligations => no-op common case.
  out="$(PATH="$bin:$PATH" "$DIGEST" render --lane build)" || fail_msg "D3: no-op render failed"
  echo "$out" | has_match -q 'NO-OP' || fail_msg "D3: empty inbox must render an explicit NO-OP orientation"
  [ -e "$WAKE_STATE_HOME/TOOL_CALLED" ] && fail_msg "D3: orientation no-op made a live tool call (must be ZERO)"
  # Now an actionable, consequential fact. It must be a CLAIM-TO-VERIFY, never
  # rendered as a trusted assertion or an auto-action.
  "$STORE" enqueue --seq 1 --class actionable \
    --locators "$(jq -cn --arg s "$SHA40" '{repo:"r",issue:7,claim:"ci=success",file:"pkg/x.ts",sha:$s}')" >/dev/null
  out2="$(PATH="$bin:$PATH" "$DIGEST" render)" || fail_msg "D3: actionable render failed"
  # Rendering itself still makes ZERO live calls (it only lays out claims).
  [ -e "$WAKE_STATE_HOME/TOOL_CALLED" ] && fail_msg "D3: rendering an actionable claim made a live call (must defer to the consumer's gate)"
  echo "$out2" | has_match -q 'CLAIM@seq' || fail_msg "D3: consequential fact not framed as CLAIM@seq"
  echo "$out2" | has_match -qi 'VERIFY LIVE' || fail_msg "D3: claim not marked for live verification"
  echo "$out2" | has_match -qi 'do NOT act on this line' || fail_msg "D3: claim missing do-not-auto-action framing"
  # The consequential fact must NOT appear as a bare trusted directive.
  echo "$out2" | has_match -qiE 'merge now|go ahead and (merge|deploy)|safe to merge' &&
    fail_msg "D3: digest auto-actioned a consequential fact (imperative present)"
  # And its hard locator + one-call re-verify hint are present.
  echo "$out2" | has_match -q "re-verify (ONE call)" || fail_msg "D3: actionable claim missing one-call re-verify locator"
  true
) && ok

echo "== D4: SCRUB — secret-canary + ANSI/bidi/zero-width in source free-text neutralized =="
(
  WAKE_STATE_HOME="$(fresh_state d4)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT WAKE_LANE
  # Build hostile source free-text: ANSI CSI, bidi override (U+202E), zero-width
  # space (U+200B), ZWNBSP (U+FEFF), a C0 control (BEL), and two secret canaries.
  nasty="$(printf 'good\x1b[31mRED\xe2\x80\xaeEVIL\xe2\x80\x8bZW\xef\xbb\xbfBOM\x07BEL ghp_0123456789abcdefghijABCDEFGHIJ012345 AKIAIOSFODNN7EXAMPLE')"
  loc="$(jq -cn --arg s "$nasty" --arg sha "$SHA40" '{repo:"r",issue:3,claim:"changed",sha:$sha,summary:$s}')"
  "$STORE" enqueue --seq 1 --class actionable --locators "$loc" >/dev/null
  out="$("$DIGEST" render)" || fail_msg "D4: render failed"
  # ANSI escape / CSI must be gone.
  printf '%s' "$out" | LC_ALL=C has_match -q "$(printf '\x1b')" && fail_msg "D4: ANSI ESC survived the scrub"
  # bidi/zero-width/BOM UTF-8 sequences must be gone.
  # #912: patterns are LITERAL bytes + `grep -E`, NOT PCRE `grep -P`. BusyBox
  # grep (Alpine/musl CI) has no `-P` — a `grep -qP` there errors
  # ("unrecognized option: P"), returns non-zero, and the `&&` silently skips
  # the assertion, so the scrub was NEVER checked in CI. Literal-byte ranges
  # under `grep -E` + LC_ALL=C match identically on BusyBox and GNU grep.
  # Two DISJOINT byte ranges: U+200B..U+200F (E2 80 8B..8F, zero-width) and
  # U+202A..U+202E (E2 80 AA..AE, bidi). NOT a single 8B..AE range — that would
  # wrongly flag legitimate E2 80 xx punctuation in between, e.g. U+2014 EM DASH
  # (E2 80 94) which the digest body uses.
  _b280="$(printf '%b' '\xe2\x80')"
  _b8b="$(printf '%b' '\x8b')"; _b8f="$(printf '%b' '\x8f')"
  _baa="$(printf '%b' '\xaa')"; _bae="$(printf '%b' '\xae')"
  _bbom="$(printf '%b' '\xef\xbb\xbf')"
  printf '%s' "$out" | LC_ALL=C has_match -qE "${_b280}[${_b8b}-${_b8f}${_baa}-${_bae}]|${_bbom}" &&
    fail_msg "D4: bidi/zero-width/BOM survived the scrub"
  # C0 control bytes (except tab/newline) must be gone.
  _c00="$(printf '%b' '\x01')"; _c08="$(printf '%b' '\x08')"
  _c0e="$(printf '%b' '\x0e')"; _c1f="$(printf '%b' '\x1f')"; _c7f="$(printf '%b' '\x7f')"
  printf '%s' "$out" | LC_ALL=C has_match -qE "[${_c00}-${_c08}${_c0e}-${_c1f}${_c7f}]" &&
    fail_msg "D4: a C0 control byte survived the scrub"
  # Secret canaries must be redacted, never inlined.
  printf '%s' "$out" | has_match -q 'ghp_0123456789' && fail_msg "D4: GitHub-token canary LEAKED into the digest"
  printf '%s' "$out" | has_match -q 'AKIAIOSFODNN7EXAMPLE' && fail_msg "D4: AWS-key canary LEAKED into the digest"
  printf '%s' "$out" | has_match -q 'REDACTED-SECRET' || fail_msg "D4: secret redaction marker absent — canary may not have been scrubbed"
  # Free-text is quoted inside a DELIMITED untrusted block, framed as NOT instructions.
  printf '%s' "$out" | has_match -q 'BEGIN UNTRUSTED DATA' || fail_msg "D4: source free-text not placed in a delimited untrusted block"
  # The 40-hex git SHA locator must NOT be mangled by the secret scrubber.
  printf '%s' "$out" | has_match -q "$SHA40" || fail_msg "D4: legitimate 40-hex SHA locator was wrongly scrubbed"
  true
) && ok

echo "== H1: NON-CIRCULAR HMAC — wake_id independent + not a signed field; tamper breaks MAC =="
(
  MOSAIC_CREDENTIALS_FILE="$(fresh_cred h1 default 'key-material-h1')"
  export MOSAIC_CREDENTIALS_FILE
  WAKE_AGENT=agentA
  WAKE_MISSION_GENERATION=3
  export WAKE_AGENT WAKE_MISSION_GENERATION
  unset WAKE_HMAC_KEY_NAME
  env1="$("$SIGN" sign --observed-seq 5 --content-hash "$SHA40")" || fail_msg "H1: sign failed"
  # wake_id is NOT a member of the signed field-tuple (it is prepended to the MAC
  # input, not one of the signed fields whose authenticity the MAC establishes).
  echo "$env1" | jq -e '.signed | has("wake_id") | not' >/dev/null || fail_msg "H1: wake_id must NOT appear inside the signed field-tuple"
  # wake_mac is not its own input: it is not a member of `signed` either.
  echo "$env1" | jq -e '.signed | has("wake_mac") | not' >/dev/null || fail_msg "H1: wake_mac must not be part of its own signed inputs (self-reference)"
  echo "$env1" | jq -e 'has("wake_id") and has("wake_mac")' >/dev/null || fail_msg "H1: envelope must carry independent wake_id + wake_mac"
  # Independently generated: two emits over IDENTICAL fields yield DIFFERENT
  # wake_ids (id not derived from the tuple) AND different MACs (id is bound in).
  env2="$("$SIGN" sign --observed-seq 5 --content-hash "$SHA40")"
  w1="$(echo "$env1" | jq -r .wake_id)"
  w2="$(echo "$env2" | jq -r .wake_id)"
  [ -n "$w1" ] && [ "$w1" != "null" ] || fail_msg "H1: wake_id missing"
  [ "$w1" != "$w2" ] || fail_msg "H1: wake_id not independently generated (identical fields produced identical id — derived, not fresh)"
  m1="$(echo "$env1" | jq -r .wake_mac)"
  m2="$(echo "$env2" | jq -r .wake_mac)"
  [ "$m1" != "$m2" ] || fail_msg "H1: MAC did not bind the independent wake_id (same MAC despite different wake_id)"
  # Untampered verifies.
  echo "$env1" | "$SIGN" verify >/dev/null 2>&1 || fail_msg "H1: untampered envelope failed to verify"
  # Tampering ANY signed field breaks the MAC.
  for mut in \
    '.signed.agent_identity="attacker"' \
    '.signed.mission_generation="9"' \
    '.signed.observed_seq="6"' \
    '.signed.emit_ts="0"' \
    '.signed.content_hash="deadbeef"'; do
    if echo "$env1" | jq "$mut" | "$SIGN" verify >/dev/null 2>&1; then
      fail_msg "H1: tampering [$mut] did NOT break the MAC"
    fi
  done
  # Tampering the independent wake_id also breaks the MAC (it is bound in).
  if echo "$env1" | jq '.wake_id="wake_forged"' | "$SIGN" verify >/dev/null 2>&1; then
    fail_msg "H1: forging wake_id did not break the MAC (id not bound)"
  fi
  # A wrong key fails verification (the MAC genuinely depends on the key).
  wrong="$(fresh_cred h1b default 'DIFFERENT-key')"
  if echo "$env1" | MOSAIC_CREDENTIALS_FILE="$wrong" "$SIGN" verify >/dev/null 2>&1; then
    fail_msg "H1: verify passed under a DIFFERENT key (MAC not key-dependent)"
  fi
) && ok

echo "== H2: KEY BY-NAME, never inline; no key leak; same-uid boundary documented =="
(
  MOSAIC_CREDENTIALS_FILE="$(fresh_cred h2 signing-key 'SUPERSECRET-KEYVALUE-XYZ')"
  export MOSAIC_CREDENTIALS_FILE
  WAKE_AGENT=agentB
  WAKE_MISSION_GENERATION=1
  export WAKE_AGENT WAKE_MISSION_GENERATION
  # Resolve BY NAME (the credential-store key name), never the key material.
  env1="$(WAKE_HMAC_KEY_NAME=signing-key "$SIGN" sign --observed-seq 1 --content-hash "$SHA40")" ||
    fail_msg "H2: by-name key resolution failed"
  echo "$env1" | jq -e .wake_mac >/dev/null || fail_msg "H2: no MAC produced from by-name key"
  # The key VALUE must never appear in the signed output.
  echo "$env1" | has_match -q 'SUPERSECRET-KEYVALUE-XYZ' && fail_msg "H2: key material LEAKED into the signed envelope"
  # A signed store-entry (hmac placeholder filled) must not leak the key either.
  entry="$(printf '{"observed_seq":1,"locators":{"repo":"r","issue":1},"class":"actionable","emit_ts":1700000000,"hmac":""}' |
    WAKE_HMAC_KEY_NAME=signing-key "$SIGN" sign-entry)"
  echo "$entry" | has_match -q 'SUPERSECRET-KEYVALUE-XYZ' && fail_msg "H2: key material LEAKED into the signed entry"
  echo "$entry" | jq -e '.hmac != "" and .hmac != null' >/dev/null || fail_msg "H2: sign-entry did not fill the hmac placeholder"
  # No flag may accept key MATERIAL inline — only a key NAME. An attempt to pass
  # a literal key must be rejected as an unknown option (never silently honored).
  for badflag in --key --secret --hmac-key --key-value; do
    if WAKE_HMAC_KEY_NAME=signing-key "$SIGN" sign --observed-seq 1 --content-hash "$SHA40" "$badflag" 'INLINE-KEY' >/dev/null 2>&1; then
      fail_msg "H2: sign.sh accepted an inline-key flag [$badflag] (key must be by-name only)"
    fi
  done
  # An unknown key NAME fails loud (never signs unsigned / with an empty key).
  if WAKE_HMAC_KEY_NAME=does-not-exist "$SIGN" sign --observed-seq 1 --content-hash "$SHA40" >/dev/null 2>&1; then
    fail_msg "H2: signing with an unresolvable key name must FAIL LOUD"
  fi
  # The same-uid threat boundary + off-uid follow-up must be DOCUMENTED in the tool.
  has_match -qi 'same-uid' "$SIGN" || fail_msg "H2: same-uid threat boundary not documented in sign.sh"
  has_match -qi 'off-uid' "$SIGN" || fail_msg "H2: off-uid future signer not named in sign.sh"
) && ok

echo "== D5 (#914a): EMBEDDED ACK NAMESPACE — env-less copy-run targets the RENDER-TIME agent, not 'default' =="
(
  WAKE_STATE_HOME="$(fresh_state d5)"
  export WAKE_STATE_HOME
  # A pending obligation observed under the 'someagent' namespace (render-time
  # WAKE_AGENT is known; the bug is that the EMBEDDED line forgets it).
  WAKE_AGENT=someagent "$STORE" enqueue --seq 1 --class actionable --locators '{"repo":"r","issue":41}' >/dev/null
  out="$(WAKE_AGENT=someagent "$DIGEST" render)" || fail_msg "D5: render (WAKE_AGENT=someagent) failed"
  ack_line="$(printf '%s\n' "$out" | awk '/^-- ACK/{f=1;next} f && NF {print; exit}')"
  [ -n "$ack_line" ] || fail_msg "D5: no embedded ack line extracted from the digest"
  printf '%s' "$ack_line" | has_match -q 'WAKE_AGENT=someagent' ||
    fail_msg "D5: embedded ack line has no explicit WAKE_AGENT=someagent prefix — an env-less copy-run silently resolves to 'default' [$ack_line]"
  # Actually RUN it with NO WAKE_AGENT in the environment (the real failure
  # mode: a consumer copy-pastes the line into a fresh shell).
  ( unset WAKE_AGENT; env -u WAKE_AGENT sh -c "$ack_line" >/dev/null 2>&1 )
  rc=$?
  [ "$rc" -eq 0 ] || fail_msg "D5: env-less copy-run of the embedded ack line exited non-zero (rc=$rc) [$ack_line]"
  someagent_consumed="$(cat "$WAKE_STATE_HOME/someagent/consumed_seq" 2>/dev/null || echo '?')"
  [ "$someagent_consumed" = "1" ] ||
    fail_msg "D5: env-less copy-run did NOT advance the someagent namespace's consumed_seq (got '$someagent_consumed') — resolved to the wrong namespace"
  # The 'default' namespace must stay untouched — no silent fallback.
  if [ -f "$WAKE_STATE_HOME/default/consumed_seq" ]; then
    default_consumed="$(cat "$WAKE_STATE_HOME/default/consumed_seq" 2>/dev/null || echo 0)"
    [ "$default_consumed" = "0" ] || fail_msg "D5: env-less copy-run advanced the WRONG ('default') namespace instead of someagent"
  fi

  # --- injection-safety: a hostile agent value must not become a shell
  # injection vector when the embedded line is later copy-run, and must be
  # scrubbed (control/ANSI bytes stripped) like any other inlined value.
  wd="$TMP_ROOT/d5-wd"
  rm -rf "$wd"
  mkdir -p "$wd"
  WAKE_STATE_HOME2="$(fresh_state d5b)"
  # shellcheck disable=SC2016 # deliberately literal: this is the injection
  # payload under test, not an expression we want the test script to expand.
  nasty='$(touch INJECTED)nasty'
  WAKE_AGENT="$nasty" WAKE_STATE_HOME="$WAKE_STATE_HOME2" "$STORE" enqueue --seq 1 --class actionable --locators '{"repo":"r","issue":1}' >/dev/null
  out2="$(WAKE_AGENT="$nasty" WAKE_STATE_HOME="$WAKE_STATE_HOME2" "$DIGEST" render)" || fail_msg "D5: render with a hostile agent value failed"
  ack_line2="$(printf '%s\n' "$out2" | awk '/^-- ACK/{f=1;next} f && NF {print; exit}')"
  ( cd "$wd" && unset WAKE_AGENT && env -u WAKE_AGENT WAKE_STATE_HOME="$WAKE_STATE_HOME2" sh -c "$ack_line2" >/dev/null 2>&1 )
  [ -e "$wd/INJECTED" ] && fail_msg "D5: a hostile WAKE_AGENT value achieved shell injection via the embedded ack line [$ack_line2]"
  true
) && ok

echo "== D6 (#914b): DIGEST-CLASS LOCATOR THREADING — ORIENTATION pointer carries its (soft) locator; ACTIONABLE hard-locator FAIL-LOUD unchanged =="
(
  WAKE_STATE_HOME="$(fresh_state d6)"
  export WAKE_STATE_HOME
  unset WAKE_AGENT WAKE_LANE
  # The REAL shape store.sh receives for a digest-class entry, per detector.sh's
  # own enqueue-building jq filter (kind/id/observed_hash + whichever of
  # repo/path/anchor/remote/branches the source def declares) — NOT
  # repo/issue/sha/file, which is what _locator_line originally recognized.
  loc="$(jq -cn '{kind:"repo", id:"r1", observed_hash:"deadbeefcafe0123456789abcdef0123456789abcdef0123456789abcdef01", remote:"example/repo"}')"
  "$STORE" enqueue --seq 1 --class digest --locators "$loc" >/dev/null
  out="$("$DIGEST" render)" || fail_msg "D6: render failed"
  orientline="$(printf '%s\n' "$out" | has_match -E '^\s*\* seq 1 \[digest\]')"
  [ -n "$orientline" ] || fail_msg "D6: no ORIENTATION line found for seq 1"
  printf '%s' "$orientline" | has_match -qE 'locator: *$' &&
    fail_msg "D6: digest-class ORIENTATION pointer rendered an EMPTY locator despite a populated .locators field [$orientline]"
  # Must surface something a consumer can act on to re-verify.
  printf '%s' "$orientline" | has_match -qE 'remote=example/repo|id=r1|kind=repo' ||
    fail_msg "D6: digest-class ORIENTATION pointer does not carry a usable locator [$orientline]"

  # --- ACTIONABLE-tier hard-locator FAIL-LOUD must be PRESERVED (now PER-ENTRY
  # quarantine, #920): a digest-class ORIENTATION pointer (above) renders soft,
  # but a genuine ACTIONABLE claim with no hard locator is still fail-loud — it is
  # QUARANTINED (dead-lettered + alarmed + excluded), never delivered as valid.
  h="$(fresh_state d6b)"
  WAKE_STATE_HOME="$h"
  export WAKE_STATE_HOME
  "$STORE" enqueue --seq 1 --class actionable --locators '{"claim":"mergeable=true"}' >/dev/null
  err="$TMP_ROOT/d6b.err"
  rc=0
  out="$("$DIGEST" render 2>"$err")" || rc=$?
  [ "$rc" -eq 0 ] || fail_msg "D6: a malformed actionable is now per-entry quarantined (render exit 0, #920), got $rc"
  printf '%s' "$out" | has_match -q 'mergeable=true' && fail_msg "D6: a malformed actionable claim must NOT be delivered as valid (fail-loud preserved)"
  has_match -q 'mergeable=true' "$h/default/dead-letter.jsonl" 2>/dev/null || fail_msg "D6: a malformed actionable must be DEAD-LETTERED (fail-loud preserved, per-entry)"
  has_match -qi 'QUARANTINE' "$err" || fail_msg "D6: a malformed actionable must raise a loud per-entry alarm"
) && ok

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