feat(wake): #940 snapshot-datable digests — adapter fd-3 snapshot-metadata channel #941
@@ -225,15 +225,31 @@ _poll_source() {
|
|||||||
# The source definition is handed to the adapter on STDIN via a temp FILE
|
# The source definition is handed to the adapter on STDIN via a temp FILE
|
||||||
# (not a pipe): an adapter that ignores stdin must not take SIGPIPE and read
|
# (not a pipe): an adapter that ignores stdin must not take SIGPIPE and read
|
||||||
# back as a spurious "source error" under `pipefail`.
|
# back as a spurious "source error" under `pipefail`.
|
||||||
local raw rc deftmp
|
local raw rc deftmp metatmp
|
||||||
mkdir -p "$DET_DIR"
|
mkdir -p "$DET_DIR"
|
||||||
# _wake_tmp_prefix is provided by _wake-common.sh (sourced above).
|
# _wake_tmp_prefix is provided by _wake-common.sh (sourced above).
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
deftmp="$(mktemp "$DET_DIR/${_wake_tmp_prefix}defXXXXXX")" || return 1
|
deftmp="$(mktemp "$DET_DIR/${_wake_tmp_prefix}defXXXXXX")" || return 1
|
||||||
printf '%s' "$def" >"$deftmp"
|
printf '%s' "$def" >"$deftmp"
|
||||||
raw="$("$WAKE_DETECTOR_SOURCE_CMD" "$kind" "$id" <"$deftmp" 2>/dev/null)"
|
# fd 3 is the OUT-OF-BAND snapshot-metadata channel (#940): the adapter MAY
|
||||||
|
# write one JSON object {"snapshot_sha": "<git commit sha>", "snapshot_ts":
|
||||||
|
# <epoch>} there. It must stay out of stdout because EVERYTHING on stdout is
|
||||||
|
# hashed by the delta gate — an in-band tip-commit SHA would advance
|
||||||
|
# observed_hash on every unrelated push. Adapters that never write fd 3 leave
|
||||||
|
# the file empty: byte-identical legacy behavior.
|
||||||
|
metatmp="$(mktemp "$DET_DIR/${_wake_tmp_prefix}metaXXXXXX")" || {
|
||||||
|
rm -f "$deftmp"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
raw="$("$WAKE_DETECTOR_SOURCE_CMD" "$kind" "$id" <"$deftmp" 3>"$metatmp" 2>/dev/null)"
|
||||||
rc=$?
|
rc=$?
|
||||||
rm -f "$deftmp"
|
rm -f "$deftmp"
|
||||||
|
# Slurp + remove the metadata file NOW so every return path below is clean;
|
||||||
|
# it is parsed only after the fail-loud gates (metadata from a FAILED
|
||||||
|
# observation is meaningless and must not be trusted or diagnosed).
|
||||||
|
local rawmeta=""
|
||||||
|
[ -s "$metatmp" ] && rawmeta="$(cat "$metatmp")"
|
||||||
|
rm -f "$metatmp"
|
||||||
if [ "$rc" -ne 0 ]; then
|
if [ "$rc" -ne 0 ]; then
|
||||||
echo "detector.sh: FAIL LOUD (G2a) — source '$kind/$id' errored (adapter exit $rc: network/401/403/privacy-404/partial). observed_seq NOT advanced; NOT treated as 'no change'." >&2
|
echo "detector.sh: FAIL LOUD (G2a) — source '$kind/$id' errored (adapter exit $rc: network/401/403/privacy-404/partial). observed_seq NOT advanced; NOT treated as 'no change'." >&2
|
||||||
return 1
|
return 1
|
||||||
@@ -243,6 +259,50 @@ _poll_source() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- snapshot metadata (fd 3, #940) — ADVISORY, validated, never gating ----
|
||||||
|
# Malformed metadata is dropped with a LOUD diagnostic but cannot fail the
|
||||||
|
# poll or suppress the wake: the obligation never depends on optional dating.
|
||||||
|
local snap_sha="" snap_ts="" snap_json=""
|
||||||
|
if [ -n "$rawmeta" ]; then
|
||||||
|
if snap_json="$(jq -ce '.' <<<"$rawmeta" 2>/dev/null)"; then
|
||||||
|
snap_sha="$(jq -r 'if (.snapshot_sha|type) == "string" then .snapshot_sha else "" end' <<<"$snap_json")"
|
||||||
|
snap_ts="$(jq -r 'if (.snapshot_ts|type) == "number" then (.snapshot_ts|floor|tostring) else "" end' <<<"$snap_json")"
|
||||||
|
if [ -n "$snap_sha" ] && ! printf '%s' "$snap_sha" | grep -Eq '^[0-9a-f]{7,64}$'; then
|
||||||
|
echo "detector.sh: source '$kind/$id' snapshot_sha rejected (not a 7-64 char lowercase-hex git sha) — snapshot metadata DROPPED, poll continues (#940)." >&2
|
||||||
|
snap_sha=""
|
||||||
|
snap_ts=""
|
||||||
|
fi
|
||||||
|
# A ts must be a sane positive epoch BEFORE any arithmetic touches it: a
|
||||||
|
# negative or absurdly large value would make the shell integer comparison
|
||||||
|
# below error out and silently KEEP the bad ts — validate first, compare after.
|
||||||
|
if [ -n "$snap_ts" ] && ! printf '%s' "$snap_ts" | grep -Eq '^[0-9]{1,12}$'; then
|
||||||
|
echo "detector.sh: source '$kind/$id' snapshot_ts rejected (not a sane positive epoch) — snapshot_ts DROPPED, poll continues (#940)." >&2
|
||||||
|
snap_ts=""
|
||||||
|
fi
|
||||||
|
# ts is only meaningful anchored to a revision the consumer can re-verify —
|
||||||
|
# a bare number with no sha behind it is the weakest possible attestation,
|
||||||
|
# so ts requires a valid sha (#940 review §2).
|
||||||
|
if [ -n "$snap_ts" ] && [ -z "$snap_sha" ]; then
|
||||||
|
echo "detector.sh: source '$kind/$id' snapshot_ts without a valid snapshot_sha — snapshot_ts DROPPED (unverifiable dating), poll continues (#940)." >&2
|
||||||
|
snap_ts=""
|
||||||
|
fi
|
||||||
|
# A FUTURE ts renders a stale snapshot fresher-than-fresh (negative age) —
|
||||||
|
# wrong in the reassuring direction, the exact failure class #940 fixes.
|
||||||
|
# Cross-host NTP skew of a few seconds is the NORMAL case, so allow a small
|
||||||
|
# slack; beyond it, drop the ts (the sha stays: independently verifiable).
|
||||||
|
if [ -n "$snap_ts" ]; then
|
||||||
|
local now_s
|
||||||
|
now_s="$(date +%s)"
|
||||||
|
if [ "$snap_ts" -gt $((now_s + ${WAKE_SNAPSHOT_TS_FUTURE_SLACK:-300})) ]; then
|
||||||
|
echo "detector.sh: source '$kind/$id' snapshot_ts is beyond the ${WAKE_SNAPSHOT_TS_FUTURE_SLACK:-300}s future-skew allowance (ts=$snap_ts now=$now_s) — snapshot_ts DROPPED, poll continues (#940)." >&2
|
||||||
|
snap_ts=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "detector.sh: source '$kind/$id' wrote UNPARSEABLE snapshot metadata on fd 3 — DROPPED, poll continues (#940)." >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# --- anchor-scope (feature (a)) then hash ---------------------------------
|
# --- anchor-scope (feature (a)) then hash ---------------------------------
|
||||||
local scoped
|
local scoped
|
||||||
if [ -n "$anchor" ]; then
|
if [ -n "$anchor" ]; then
|
||||||
@@ -281,12 +341,17 @@ _poll_source() {
|
|||||||
local locators emit_ts
|
local locators emit_ts
|
||||||
emit_ts="$(date +%s)"
|
emit_ts="$(date +%s)"
|
||||||
# NB: `def` is a reserved word in jq — the source-definition arg is `$sdef`.
|
# NB: `def` is a reserved word in jq — the source-definition arg is `$sdef`.
|
||||||
|
# snapshot_sha/snapshot_ts (#940) join only when the adapter attested them.
|
||||||
locators="$(jq -cn \
|
locators="$(jq -cn \
|
||||||
--arg kind "$kind" \
|
--arg kind "$kind" \
|
||||||
--arg id "$id" \
|
--arg id "$id" \
|
||||||
--argjson sdef "$def" \
|
--argjson sdef "$def" \
|
||||||
--arg hash "$h" \
|
--arg hash "$h" \
|
||||||
|
--arg ssha "$snap_sha" \
|
||||||
|
--arg sts "$snap_ts" \
|
||||||
'{kind:$kind, id:$id, observed_hash:$hash}
|
'{kind:$kind, id:$id, observed_hash:$hash}
|
||||||
|
+ (if $ssha != "" then {snapshot_sha: $ssha} else {} end)
|
||||||
|
+ (if $sts != "" then {snapshot_ts: ($sts|tonumber)} else {} end)
|
||||||
+ ( $sdef | {repo, path, anchor, remote, branches} | with_entries(select(.value != null)) )')"
|
+ ( $sdef | {repo, path, anchor, remote, branches} | with_entries(select(.value != null)) )')"
|
||||||
|
|
||||||
local args=(enqueue --locators "$locators" --emit-ts "$emit_ts")
|
local args=(enqueue --locators "$locators" --emit-ts "$emit_ts")
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ _has_hard_locator() {
|
|||||||
# ACTIONABLE-tier hard-locator FAIL-LOUD gate is untouched.
|
# ACTIONABLE-tier hard-locator FAIL-LOUD gate is untouched.
|
||||||
_locator_line() {
|
_locator_line() {
|
||||||
local loc="$1" repo issue sha file anchor head parts='' reverify=''
|
local loc="$1" repo issue sha file anchor head parts='' reverify=''
|
||||||
local remote path kind id ohash branches
|
local remote path kind id ohash branches snap_sha snap_ts
|
||||||
repo="$(jq -r '.repo // ""' <<<"$loc")"
|
repo="$(jq -r '.repo // ""' <<<"$loc")"
|
||||||
issue="$(jq -r '(.issue // "") | tostring' <<<"$loc")"
|
issue="$(jq -r '(.issue // "") | tostring' <<<"$loc")"
|
||||||
sha="$(jq -r '.sha // ""' <<<"$loc")"
|
sha="$(jq -r '.sha // ""' <<<"$loc")"
|
||||||
@@ -247,6 +247,8 @@ _locator_line() {
|
|||||||
kind="$(jq -r '.kind // ""' <<<"$loc")"
|
kind="$(jq -r '.kind // ""' <<<"$loc")"
|
||||||
id="$(jq -r '(.id // "") | tostring' <<<"$loc")"
|
id="$(jq -r '(.id // "") | tostring' <<<"$loc")"
|
||||||
ohash="$(jq -r '.observed_hash // ""' <<<"$loc")"
|
ohash="$(jq -r '.observed_hash // ""' <<<"$loc")"
|
||||||
|
snap_sha="$(jq -r '.snapshot_sha // ""' <<<"$loc")"
|
||||||
|
snap_ts="$(jq -r '(.snapshot_ts // "") | tostring' <<<"$loc")"
|
||||||
branches="$(jq -r '(.branches // []) | if length > 0 then join(",") else "" end' <<<"$loc" 2>/dev/null || true)"
|
branches="$(jq -r '(.branches // []) | if length > 0 then join(",") else "" end' <<<"$loc" 2>/dev/null || true)"
|
||||||
[ -n "$kind" ] && parts="$parts kind=$(_scrub_inline "$kind")"
|
[ -n "$kind" ] && parts="$parts kind=$(_scrub_inline "$kind")"
|
||||||
[ "$id" != "" ] && parts="$parts id=$(_scrub_inline "$id")"
|
[ "$id" != "" ] && parts="$parts id=$(_scrub_inline "$id")"
|
||||||
@@ -273,9 +275,18 @@ _locator_line() {
|
|||||||
# git commit SHA — kept distinct from `sha` so it never impersonates one or
|
# git commit SHA — kept distinct from `sha` so it never impersonates one or
|
||||||
# feeds the `git show <sha>` re-verify hint below.
|
# feeds the `git show <sha>` re-verify hint below.
|
||||||
[ -n "$ohash" ] && parts="$parts observed_hash=$(_scrub_inline "$ohash")"
|
[ -n "$ohash" ] && parts="$parts observed_hash=$(_scrub_inline "$ohash")"
|
||||||
|
# snapshot_sha/snapshot_ts (#940): the SNAPSHOT'S git commit sha + commit
|
||||||
|
# epoch, attested by the source adapter at OBSERVE time (out-of-band fd 3,
|
||||||
|
# detector-validated). Unlike observed_hash this IS a commit sha, so it may
|
||||||
|
# feed the `git show` re-verify hint; with emit_ts already in the header,
|
||||||
|
# snapshot age becomes local arithmetic for the consumer.
|
||||||
|
[ -n "$snap_sha" ] && parts="$parts snapshot_sha=$(_scrub_inline "$snap_sha")"
|
||||||
|
[ "$snap_ts" != "" ] && parts="$parts snapshot_ts=$(_scrub_inline "$snap_ts")"
|
||||||
# One-targeted-call re-verify hint (best available, most-specific first).
|
# One-targeted-call re-verify hint (best available, most-specific first).
|
||||||
if [ -n "$sha" ] && [ -n "$file" ]; then
|
if [ -n "$sha" ] && [ -n "$file" ]; then
|
||||||
reverify="git show $(_scrub_inline "$sha"):$(_scrub_inline "$file")"
|
reverify="git show $(_scrub_inline "$sha"):$(_scrub_inline "$file")"
|
||||||
|
elif [ -n "$snap_sha" ] && [ -n "$path" ]; then
|
||||||
|
reverify="git show $(_scrub_inline "$snap_sha"):$(_scrub_inline "$path")"
|
||||||
elif [ -n "$repo" ] && [ "$issue" != "" ]; then
|
elif [ -n "$repo" ] && [ "$issue" != "" ]; then
|
||||||
reverify="issue $(_scrub_inline "$repo")#$(_scrub_inline "$issue")"
|
reverify="issue $(_scrub_inline "$repo")#$(_scrub_inline "$issue")"
|
||||||
elif [ -n "$sha" ]; then
|
elif [ -n "$sha" ]; then
|
||||||
|
|||||||
@@ -240,8 +240,50 @@
|
|||||||
# guard/witness-marker are REMOVED — T9/T11 now RUN and ASSERT their failure
|
# guard/witness-marker are REMOVED — T9/T11 now RUN and ASSERT their failure
|
||||||
# paths in every environment including non-priv CI. Changed: _wake-common.sh
|
# paths in every environment including non-priv CI. Changed: _wake-common.sh
|
||||||
# (seam), test-wake-store-ack.sh (T9/T11 conversion).
|
# (seam), test-wake-store-ack.sh (T9/T11 conversion).
|
||||||
|
# 0.6.12 #940 snapshot-datable digests — the adapter-contract fd-3 snapshot-
|
||||||
|
# metadata channel (wake-pilot finding fw-wake-digest-snapshot-lag:
|
||||||
|
# a digest's locator carried observed_hash + emit_ts but nothing
|
||||||
|
# DATING the snapshot, so a consumer could not tell a fresh
|
||||||
|
# snapshot from one already superseded at delivery without a tool
|
||||||
|
# call). ADDITIVE + backward-compatible: (a) detector.sh invokes
|
||||||
|
# the W4 source adapter with fd 3 redirected to a temp file; the
|
||||||
|
# adapter MAY write one JSON object {"snapshot_sha": "<git commit
|
||||||
|
# sha>", "snapshot_ts": <epoch>} there. OUT-OF-BAND is load-
|
||||||
|
# bearing: everything on stdout is hashed by the delta gate, so an
|
||||||
|
# in-band tip-commit sha would advance observed_hash on every
|
||||||
|
# unrelated push (spurious delta wake per watched file). Metadata
|
||||||
|
# is ADVISORY and validated (sha ^[0-9a-f]{7,64}$, ts number):
|
||||||
|
# malformed metadata is dropped with a LOUD stderr diagnostic but
|
||||||
|
# NEVER fails the poll or suppresses the wake — the obligation
|
||||||
|
# never depends on optional dating. Valid fields join the enqueue
|
||||||
|
# locators; an adapter that never writes fd 3 is byte-identical
|
||||||
|
# legacy behavior. (b) digest.sh _locator_line renders
|
||||||
|
# snapshot_sha=/snapshot_ts= (scrubbed) beside observed_hash=, and
|
||||||
|
# snapshot_sha+path upgrades the one-call re-verify hint to
|
||||||
|
# `git show <snapshot_sha>:<path>` (snapshot_sha IS a commit sha,
|
||||||
|
# unlike observed_hash, so it may feed the git hint). With emit_ts
|
||||||
|
# already in the header, snapshot age becomes local arithmetic for
|
||||||
|
# the consumer — zero round trips. Watch-list schema UNTOUCHED
|
||||||
|
# ([1,1] unchanged — adapter contract + locator vocabulary, not
|
||||||
|
# watch-list config). store.sh/reconcile.sh/beacon.sh UNCHANGED.
|
||||||
|
# Review hardening (#941 §2): snapshot_ts additionally requires a
|
||||||
|
# VALID snapshot_sha (a bare number with no revision to re-verify
|
||||||
|
# against is the weakest attestation — dropped loudly), must be a
|
||||||
|
# sane positive epoch (^[0-9]{1,12}$ — validated BEFORE the shell
|
||||||
|
# integer comparison so an absurd value cannot error past it), and
|
||||||
|
# must not sit beyond a future-skew allowance
|
||||||
|
# (WAKE_SNAPSHOT_TS_FUTURE_SLACK, default 300 s): a future ts
|
||||||
|
# yields a NEGATIVE age — stale-reads-fresher-than-fresh, the
|
||||||
|
# exact failure class #940 fixes. NOTE for consumers: these fields
|
||||||
|
# are ADVISORY and their ABSENCE IS DELIBERATELY NOT DIAGNOSTIC —
|
||||||
|
# a pre-#940 adapter and a dropped-as-malformed attestation render
|
||||||
|
# identically (no snapshot_* fields); the drop is loud only in the
|
||||||
|
# detector's own stderr. Do not build load-bearing logic on the
|
||||||
|
# absence of these fields.
|
||||||
|
# Changed: detector.sh, digest.sh (+ test-wake-detector.sh
|
||||||
|
# D10/D11/D12, test-wake-digest-quarantine.sh Q10).
|
||||||
component=wake
|
component=wake
|
||||||
version=0.6.11
|
version=0.6.12
|
||||||
|
|
||||||
# Watch-list schema this component consumes, and the INCLUSIVE range of
|
# Watch-list schema this component consumes, and the INCLUSIVE range of
|
||||||
# schema_version values it supports. A wake-watch-list.json whose schema_version
|
# schema_version values it supports. A wake-watch-list.json whose schema_version
|
||||||
|
|||||||
@@ -445,6 +445,149 @@ EOF
|
|||||||
echo "$err" | grep -qi 'range' || fail_msg "D9: the out-of-range failure must still state it is out of the supported range [$err]"
|
echo "$err" | grep -qi 'range' || fail_msg "D9: the out-of-range failure must still state it is out of the supported range [$err]"
|
||||||
) && ok
|
) && ok
|
||||||
|
|
||||||
|
echo "== D10: snapshot metadata (fd 3, #940) — adapter-attested sha/ts land in the enqueued locators =="
|
||||||
|
(
|
||||||
|
WAKE_STATE_HOME="$(fresh_state d10)"
|
||||||
|
export WAKE_STATE_HOME
|
||||||
|
unset WAKE_AGENT
|
||||||
|
stub="$TMP_ROOT/d10stub"
|
||||||
|
mkdir -p "$stub"
|
||||||
|
# Stub adapter that ALSO writes snapshot metadata out-of-band on fd 3.
|
||||||
|
cat >"$stub/adapter.sh" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
kind="\$1"; id="\$2"
|
||||||
|
base="$stub/\${kind}_\${id}"
|
||||||
|
[ -f "\$base" ] && cat "\$base"
|
||||||
|
[ -f "\$base.meta" ] && { cat "\$base.meta" >&3; } 2>/dev/null
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$stub/adapter.sh"
|
||||||
|
export WAKE_DETECTOR_SOURCE_CMD="$stub/adapter.sh"
|
||||||
|
wl="$TMP_ROOT/d10.json"
|
||||||
|
write_watchlist "$wl" 1
|
||||||
|
export WAKE_WATCH_LIST="$wl"
|
||||||
|
printf 'SHA-AAA\n' >"$stub/repo_r1"
|
||||||
|
printf '## LANE-X\ndecision: hold\n' >"$stub/board_file_b1"
|
||||||
|
printf '{"snapshot_sha":"0123abc4567890def0123abc4567890def012345","snapshot_ts":1753850000}\n' >"$stub/repo_r1.meta"
|
||||||
|
"$DET" poll-once >/dev/null || fail_msg "D10: baseline pass failed"
|
||||||
|
printf 'SHA-BBB\n' >"$stub/repo_r1"
|
||||||
|
"$DET" poll-once >/dev/null || fail_msg "D10: change pass failed"
|
||||||
|
entry="$("$STORE" drain)"
|
||||||
|
echo "$entry" | jq -e 'select(.locators.snapshot_sha=="0123abc4567890def0123abc4567890def012345" and .locators.snapshot_ts==1753850000)' >/dev/null \
|
||||||
|
|| fail_msg "D10: enqueued locators must carry the adapter-attested snapshot_sha + snapshot_ts [$entry]"
|
||||||
|
# And the metadata must NOT have leaked into the hashed content: an unchanged
|
||||||
|
# source with CHANGED metadata is still a no-op (delta gate intact).
|
||||||
|
d0="$(det_seq)"
|
||||||
|
printf '{"snapshot_sha":"ffff111122223333444455556666777788889999","snapshot_ts":1753860000}\n' >"$stub/repo_r1.meta"
|
||||||
|
"$DET" poll-once >/dev/null || fail_msg "D10: metadata-only pass failed"
|
||||||
|
[ "$(det_seq)" = "$d0" ] || fail_msg "D10: metadata is OUT-OF-BAND — a metadata-only change must NOT be a delta, got seq $(det_seq)"
|
||||||
|
) && ok
|
||||||
|
|
||||||
|
echo "== D11: snapshot metadata is ADVISORY — malformed metadata is dropped LOUDLY, the wake still fires, no fields emitted =="
|
||||||
|
(
|
||||||
|
WAKE_STATE_HOME="$(fresh_state d11)"
|
||||||
|
export WAKE_STATE_HOME
|
||||||
|
unset WAKE_AGENT
|
||||||
|
stub="$TMP_ROOT/d11stub"
|
||||||
|
mkdir -p "$stub"
|
||||||
|
cat >"$stub/adapter.sh" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
kind="\$1"; id="\$2"
|
||||||
|
base="$stub/\${kind}_\${id}"
|
||||||
|
[ -f "\$base" ] && cat "\$base"
|
||||||
|
[ -f "\$base.meta" ] && { cat "\$base.meta" >&3; } 2>/dev/null
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$stub/adapter.sh"
|
||||||
|
export WAKE_DETECTOR_SOURCE_CMD="$stub/adapter.sh"
|
||||||
|
wl="$TMP_ROOT/d11.json"
|
||||||
|
write_watchlist "$wl" 1
|
||||||
|
export WAKE_WATCH_LIST="$wl"
|
||||||
|
printf 'SHA-AAA\n' >"$stub/repo_r1"
|
||||||
|
printf '## LANE-X\ndecision: hold\n' >"$stub/board_file_b1"
|
||||||
|
"$DET" poll-once >/dev/null 2>&1 || fail_msg "D11: baseline pass failed"
|
||||||
|
# (a) non-JSON garbage on fd 3.
|
||||||
|
printf 'this is not json\n' >"$stub/repo_r1.meta"
|
||||||
|
printf 'SHA-BBB\n' >"$stub/repo_r1"
|
||||||
|
err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$?
|
||||||
|
[ "$rc" -eq 0 ] || fail_msg "D11: malformed metadata must NEVER fail the poll (rc=$rc)"
|
||||||
|
echo "$err" | grep -qi 'snapshot' || fail_msg "D11: dropping malformed metadata must be LOUD on stderr [$err]"
|
||||||
|
entry="$("$STORE" drain | tail -1)"
|
||||||
|
echo "$entry" | jq -e '.locators | has("snapshot_sha") or has("snapshot_ts")' >/dev/null 2>&1 \
|
||||||
|
&& fail_msg "D11: malformed metadata must emit NO snapshot fields [$entry]"
|
||||||
|
# (b) valid JSON but a non-hex snapshot_sha -> same: loud drop, wake fires, no fields.
|
||||||
|
printf '{"snapshot_sha":"NOT-A-HEX-SHA","snapshot_ts":"also-not-a-number"}\n' >"$stub/repo_r1.meta"
|
||||||
|
printf 'SHA-CCC\n' >"$stub/repo_r1"
|
||||||
|
err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$?
|
||||||
|
[ "$rc" -eq 0 ] || fail_msg "D11: rejected snapshot_sha must NEVER fail the poll (rc=$rc)"
|
||||||
|
echo "$err" | grep -qi 'snapshot' || fail_msg "D11: rejecting a bad snapshot_sha must be LOUD on stderr [$err]"
|
||||||
|
entry="$("$STORE" drain | tail -1)"
|
||||||
|
echo "$entry" | jq -e '.locators | has("snapshot_sha") or has("snapshot_ts")' >/dev/null 2>&1 \
|
||||||
|
&& fail_msg "D11: rejected metadata must emit NO snapshot fields [$entry]"
|
||||||
|
[ "$(det_seq)" = "2" ] || fail_msg "D11: both real deltas must still have enqueued (advisory metadata never suppresses a wake), got seq $(det_seq)"
|
||||||
|
) && ok
|
||||||
|
|
||||||
|
echo "== D12: snapshot_ts guards (#940 review §2) — ts requires a sha, a future ts is dropped, an absurd ts cannot reach the comparison =="
|
||||||
|
(
|
||||||
|
WAKE_STATE_HOME="$(fresh_state d12)"
|
||||||
|
export WAKE_STATE_HOME
|
||||||
|
unset WAKE_AGENT
|
||||||
|
stub="$TMP_ROOT/d12stub"
|
||||||
|
mkdir -p "$stub"
|
||||||
|
cat >"$stub/adapter.sh" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
kind="\$1"; id="\$2"
|
||||||
|
base="$stub/\${kind}_\${id}"
|
||||||
|
[ -f "\$base" ] && cat "\$base"
|
||||||
|
[ -f "\$base.meta" ] && { cat "\$base.meta" >&3; } 2>/dev/null
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$stub/adapter.sh"
|
||||||
|
export WAKE_DETECTOR_SOURCE_CMD="$stub/adapter.sh"
|
||||||
|
wl="$TMP_ROOT/d12.json"
|
||||||
|
write_watchlist "$wl" 1
|
||||||
|
export WAKE_WATCH_LIST="$wl"
|
||||||
|
printf 'SHA-AAA\n' >"$stub/repo_r1"
|
||||||
|
printf '## LANE-X\ndecision: hold\n' >"$stub/board_file_b1"
|
||||||
|
"$DET" poll-once >/dev/null 2>&1 || fail_msg "D12: baseline pass failed"
|
||||||
|
goodsha="0123abc4567890def0123abc4567890def012345"
|
||||||
|
# (a) ts WITHOUT sha — the weakest attestation: an unverifiable number with no
|
||||||
|
# revision to re-verify against. Dropped loudly; the wake still fires.
|
||||||
|
printf '{"snapshot_ts":1753850000}\n' >"$stub/repo_r1.meta"
|
||||||
|
printf 'SHA-BBB\n' >"$stub/repo_r1"
|
||||||
|
err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$?
|
||||||
|
[ "$rc" -eq 0 ] || fail_msg "D12: ts-without-sha must NEVER fail the poll (rc=$rc)"
|
||||||
|
echo "$err" | grep -qi 'without a valid snapshot_sha' || fail_msg "D12: dropping ts-without-sha must be LOUD on stderr [$err]"
|
||||||
|
entry="$("$STORE" drain | tail -1)"
|
||||||
|
echo "$entry" | jq -e '.locators | has("snapshot_sha") or has("snapshot_ts")' >/dev/null 2>&1 \
|
||||||
|
&& fail_msg "D12: ts-without-sha must emit NO snapshot fields [$entry]"
|
||||||
|
# (b) valid sha + FUTURE ts — a negative age would read fresher-than-fresh,
|
||||||
|
# wrong in the reassuring direction. ts dropped, sha kept (independently verifiable).
|
||||||
|
future=$(( $(date +%s) + 9999 ))
|
||||||
|
printf '{"snapshot_sha":"%s","snapshot_ts":%s}\n' "$goodsha" "$future" >"$stub/repo_r1.meta"
|
||||||
|
printf 'SHA-CCC\n' >"$stub/repo_r1"
|
||||||
|
err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$?
|
||||||
|
[ "$rc" -eq 0 ] || fail_msg "D12: future ts must NEVER fail the poll (rc=$rc)"
|
||||||
|
echo "$err" | grep -qi 'future-skew' || fail_msg "D12: dropping a future ts must be LOUD on stderr [$err]"
|
||||||
|
entry="$("$STORE" drain | tail -1)"
|
||||||
|
echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts") | not' >/dev/null 2>&1 \
|
||||||
|
|| fail_msg "D12: future ts must drop ts but KEEP the sha [$entry]"
|
||||||
|
# (c) valid sha + absurdly large ts — must be rejected by the sanity regex BEFORE
|
||||||
|
# the shell integer comparison (which would error out and silently keep it).
|
||||||
|
printf '{"snapshot_sha":"%s","snapshot_ts":99999999999999999999}\n' "$goodsha" >"$stub/repo_r1.meta"
|
||||||
|
printf 'SHA-DDD\n' >"$stub/repo_r1"
|
||||||
|
err="$("$DET" poll-once 2>&1 >/dev/null)"; rc=$?
|
||||||
|
[ "$rc" -eq 0 ] || fail_msg "D12: absurd ts must NEVER fail the poll (rc=$rc)"
|
||||||
|
echo "$err" | grep -qi 'sane positive epoch' || fail_msg "D12: rejecting an absurd ts must be LOUD on stderr [$err]"
|
||||||
|
entry="$("$STORE" drain | tail -1)"
|
||||||
|
echo "$entry" | jq -e --arg s "$goodsha" 'select(.locators.snapshot_sha==$s) | .locators | has("snapshot_ts") | not' >/dev/null 2>&1 \
|
||||||
|
|| fail_msg "D12: absurd ts must drop ts but KEEP the sha [$entry]"
|
||||||
|
[ "$(det_seq)" = "3" ] || fail_msg "D12: all three real deltas must still have enqueued, got seq $(det_seq)"
|
||||||
|
) && ok
|
||||||
|
|
||||||
echo
|
echo
|
||||||
if [ -s "$FAILFILE" ]; then
|
if [ -s "$FAILFILE" ]; then
|
||||||
echo "wake detector harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
echo "wake detector harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||||
|
|||||||
@@ -301,6 +301,42 @@ echo "== Q9 (d): WAKE_ALARM_SINK_CMD unconfigured OR unreachable -> FAIL LOUD (n
|
|||||||
true
|
true
|
||||||
) && ok
|
) && ok
|
||||||
|
|
||||||
|
echo "== Q10: snapshot metadata (#940) — snapshot_sha/snapshot_ts render on the locator line and feed the re-verify hint =="
|
||||||
|
(
|
||||||
|
home="$(fresh_home q10)"
|
||||||
|
export WAKE_STATE_HOME="$home"
|
||||||
|
unset WAKE_AGENT
|
||||||
|
f="$TMP_ROOT/q10.jsonl"
|
||||||
|
{
|
||||||
|
# (a) a digest-class ORIENTATION pointer WITH adapter-attested snapshot
|
||||||
|
# metadata — the fields must render on the pointer line (the tier where
|
||||||
|
# the snapshot-lag class actually bit; re-verify hints are actionable-
|
||||||
|
# tier-only by design and are asserted via (c) below).
|
||||||
|
printf '%s\n' '{"observed_seq":21,"class":"digest","locators":{"kind":"board_file","id":"SNAP-A","path":"BOARD.md","observed_hash":"aaaa1111","snapshot_sha":"0123abc4567890def0123abc4567890def012345","snapshot_ts":1753850000},"emit_ts":2}'
|
||||||
|
# (b) a sibling WITHOUT the fields — must render no snapshot vestige.
|
||||||
|
printf '%s\n' '{"observed_seq":22,"class":"digest","locators":{"kind":"board_file","id":"SNAP-B","path":"OTHER.md","observed_hash":"bbbb2222"},"emit_ts":2}'
|
||||||
|
# (c) an ACTIONABLE entry (hard locator: repo+issue) carrying path +
|
||||||
|
# snapshot_sha — its re-verify hint must upgrade to the one-call
|
||||||
|
# `git show <snapshot_sha>:<path>` (most-specific-first).
|
||||||
|
printf '%s\n' '{"observed_seq":23,"class":"actionable","locators":{"repo":"example/repo","issue":7,"path":"BOARD.md","observed_hash":"cccc3333","snapshot_sha":"0123abc4567890def0123abc4567890def012345","snapshot_ts":1753850000},"emit_ts":2}'
|
||||||
|
} >"$f"
|
||||||
|
out="$("$DIGEST" render --from-file "$f" --agent default 2>/dev/null)"
|
||||||
|
rc=$?
|
||||||
|
[ "$rc" -eq 0 ] || fail_msg "Q10: render must succeed (rc=$rc)"
|
||||||
|
snapa_line="$(printf '%s\n' "$out" | grep 'id=SNAP-A' || true)"
|
||||||
|
printf '%s' "$snapa_line" | grep -q 'snapshot_sha=0123abc4567890def0123abc4567890def012345' \
|
||||||
|
|| fail_msg "Q10: snapshot_sha must render on the ORIENTATION pointer line [$snapa_line]"
|
||||||
|
printf '%s' "$snapa_line" | grep -q 'snapshot_ts=1753850000' \
|
||||||
|
|| fail_msg "Q10: snapshot_ts must render on the ORIENTATION pointer line (age = emit_ts - snapshot_ts, local arithmetic) [$snapa_line]"
|
||||||
|
printf '%s' "$out" | grep -q 'git show 0123abc4567890def0123abc4567890def012345:BOARD.md' \
|
||||||
|
|| fail_msg "Q10: snapshot_sha+path must upgrade the actionable re-verify hint to a one-call git show"
|
||||||
|
# The sibling without metadata must not grow empty snapshot_ fields.
|
||||||
|
snapb_line="$(printf '%s\n' "$out" | grep 'id=SNAP-B' || true)"
|
||||||
|
printf '%s' "$snapb_line" | grep -q 'snapshot_' \
|
||||||
|
&& fail_msg "Q10: an entry without metadata must render NO snapshot_ fields [$snapb_line]"
|
||||||
|
true
|
||||||
|
) && ok
|
||||||
|
|
||||||
echo
|
echo
|
||||||
if [ -s "$FAILFILE" ]; then
|
if [ -s "$FAILFILE" ]; then
|
||||||
echo "wake digest-quarantine harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
echo "wake digest-quarantine harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2
|
||||||
|
|||||||
Reference in New Issue
Block a user