diff --git a/bin/fm-test-run.sh b/bin/fm-test-run.sh index 5d958eab35..65b88545ab 100755 --- a/bin/fm-test-run.sh +++ b/bin/fm-test-run.sh @@ -131,7 +131,8 @@ family_for_basename() { fm-test-run.test.sh|fm-test-isolation-proof.test.sh) printf '%s\n' pure-contract-unit ;; - fm-daemon.test.sh|fm-guard-stale-banner.test.sh|fm-pi-watch-extension.test.sh|\ + fm-daemon.test.sh|fm-guard-stale-banner.test.sh|fm-hash-pane.test.sh|\ + fm-pi-watch-extension.test.sh|\ fm-supervision-events.test.sh|fm-turnend-guard.test.sh|fm-wake-daemon-lifecycle-e2e.test.sh|\ fm-wake-queue.test.sh|fm-watch-checkpoint.test.sh|fm-watch-triage.test.sh|\ fm-watcher-lock.test.sh) diff --git a/bin/fm-watch.sh b/bin/fm-watch.sh index 766023008c..9947e261b5 100755 --- a/bin/fm-watch.sh +++ b/bin/fm-watch.sh @@ -174,14 +174,28 @@ _event_cap_fails=0 afk_present() { [ -e "$STATE/.afk" ]; } hash_pane() { + local sbin_md5="${FM_MD5_SBIN_OVERRIDE:-/sbin/md5}" + # Only used for change-detection between polls, so any tool that reliably + # returns the checksum in its first whitespace-delimited field is + # interchangeable here. The chain degrades through progressively more + # universal tools rather than hard-erroring when a watcher's PATH is + # missing md5/md5sum (observed on a secondmate whose runtime PATH omitted + # both): openssl and cksum are near-universal fallbacks, and wc -c is a + # last-resort that cannot itself be absent. if command -v md5 >/dev/null 2>&1; then md5 -q - elif [ -x /sbin/md5 ]; then - /sbin/md5 -q + elif [ -x "$sbin_md5" ]; then + "$sbin_md5" -q elif command -v md5sum >/dev/null 2>&1; then md5sum | cut -d' ' -f1 - else + elif command -v openssl >/dev/null 2>&1; then + openssl dgst -md5 -r | cut -d' ' -f1 + elif command -v shasum >/dev/null 2>&1; then shasum | cut -d' ' -f1 + elif command -v cksum >/dev/null 2>&1; then + printf '%x\n' "$(cksum | cut -d' ' -f1)" + else + wc -c | tr -d ' ' fi } diff --git a/docs/configuration.md b/docs/configuration.md index a5add630d8..4d10f7d7a2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -439,6 +439,7 @@ FM_BOOTSTRAP_DETECT_ONLY=0 # internal/read-only session-start mode: skip boots FM_GUARD_READ_ONLY=0 # internal/read-only guard mode: keep alarms but suppress drain, supervision repair, and checkout repair commands FM_GUARD_CONTINUE_LINE='This is a supervision warning only; the guarded operation WILL still run.' # banner continuation line; fm-send.sh overrides it to name the requested message specifically FM_POLL=15 # seconds between watcher poll cycles +FM_MD5_SBIN_OVERRIDE=/sbin/md5 # override for hash_pane()'s hardcoded /sbin/md5 fallback tier, mainly for tests; hash_pane also falls back through md5sum, openssl, shasum, and cksum before a wc -c last resort that cannot itself be absent, so a watcher's poll-to-poll pane hashing never hard-errors when PATH lacks md5/md5sum FM_HEARTBEAT=600 # base seconds between heartbeat scans; no-change heartbeats are absorbed while idle FM_HEARTBEAT_MAX=7200 # heartbeat backoff cap FM_CHECK_INTERVAL=300 # seconds between slow checks (authenticated merge polls, custom checks, or X-mode dispatch) diff --git a/tests/fm-hash-pane.test.sh b/tests/fm-hash-pane.test.sh new file mode 100755 index 0000000000..a12aedcdc4 --- /dev/null +++ b/tests/fm-hash-pane.test.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# tests/fm-hash-pane.test.sh - bin/fm-watch.sh's hash_pane() tool-resolution +# fallback chain. hash_pane() only feeds poll-to-poll change detection, so any +# tool that yields a stable token is interchangeable; these tests confirm the +# fallback chain never hard-errors when a watcher's PATH is missing md5 and +# md5sum (observed on a secondmate whose runtime PATH omitted both, causing +# repeated watcher FAILED cycles that needed manual restarts). +# +# Each case builds a minimal PATH from symlinks to individually resolved +# binaries rather than trimming directories out of the ambient PATH: on at +# least one dev machine md5, md5sum, and openssl are all symlinked from the +# same homebrew bin directory, so removing "the directory containing md5" +# would silently remove openssl too and defeat the tier being tested. +set -u + +# shellcheck source=tests/lib.sh +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +TMP_ROOT=$(fm_test_tmproot fm-hash-pane) + +# Resolve real tool paths once, before any test narrows PATH. +REAL_CUT=$(command -v cut) || fail "cut not found on the test host" +REAL_TR=$(command -v tr) || fail "tr not found on the test host" +REAL_WC=$(command -v wc) || fail "wc not found on the test host" +REAL_SBIN_MD5=/sbin/md5 +[ -x "$REAL_SBIN_MD5" ] || REAL_SBIN_MD5=$(command -v md5 || true) +REAL_OPENSSL=$(command -v openssl || true) +REAL_SHASUM=$(command -v shasum || true) + +# make_fakebin