Skip to content

fix(watch): harden hash_pane() fallback chain against PATH gaps - #21

Merged
trillium merged 2 commits into
mainfrom
fm/watch-md5-harden-fm-watch-sh-hash-pane-against-pat-d7
Aug 1, 2026
Merged

fix(watch): harden hash_pane() fallback chain against PATH gaps#21
trillium merged 2 commits into
mainfrom
fm/watch-md5-harden-fm-watch-sh-hash-pane-against-pat-d7

Conversation

@trillium

@trillium trillium commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Intent

Harden bin/fm-watch.sh's hash_pane() against PATH gaps so it never hard-errors when md5/md5sum aren't on PATH. Root cause: the herdr secondmate hit repeated watcher FAILED cycles because its watcher process's PATH lacked md5/md5sum, so hash_pane() errored on every poll cycle, needing ~3 manual watcher restarts that session. On the dev machine md5, md5sum, and openssl all exist at /opt/homebrew/bin (and in a clean login shell md5 is /sbin/md5, md5sum is /run/current-system/sw/bin/md5sum, openssl is /usr/bin/openssl) - the root cause is a PATH gap in the watcher's runtime environment, not a missing binary. Fix: extended hash_pane()'s fallback chain past md5/(hardcoded /sbin/md5)/md5sum to add openssl dgst -md5, then shasum, then cksum, then a wc -c last resort that cannot itself be absent - so hash_pane always returns a stable hex-ish token and never hard-errors. Also made the existing hardcoded /sbin/md5 check overridable via FM_MD5_SBIN_OVERRIDE so it can be exercised deterministically in tests. hash_pane's output is only compared for poll-to-poll change detection, so any tool returning a stable token in its first whitespace-delimited field is interchangeable - exact hash algorithm/format does not need to match across tools within a single watcher process, since the tool selection is stable for the life of that process. Constraint: this collides with a standing refactor 'Slice 4' (task-tozf) that will rewrite this same watcher script, so the change was kept deliberately minimal and localized to hash_pane() only - no changes to polling logic, beacon, or lifecycle. Added tests/fm-hash-pane.test.sh (4 cases) covering: stable/distinct hashing via the sbin md5 tier, falling back to openssl when md5/md5sum are absent from PATH, falling back further to shasum when openssl is also absent, and never hard-erroring even with no hash tool at all on PATH (verified against a known pre-existing dev-machine quirk where the ambient PATH's 'md5' resolves to a homebrew symlink of GNU gmd5sum lacking -q support, unrelated to this fix). Registered the new test file in bin/fm-test-run.sh's watcher-wake-lock family. Known pre-existing, out-of-scope issue: tests/fm-watch-triage.test.sh fails on at least one dev machine due to that same ambient 'md5 -q' shadowing quirk - reproduced identically on the unmodified base branch before this change, so it is not a regression introduced here and should not be fixed as part of this task.

What Changed

  • Extended hash_pane() in bin/fm-watch.sh with a longer fallback chain (past md5/hardcoded /sbin/md5/md5sum) to try openssl dgst -md5, then shasum, then cksum, then a wc -c last resort that can never itself be absent, so the watcher never hard-errors on a poll cycle when no hash tool is on PATH.
  • Made the hardcoded /sbin/md5 check overridable via a new FM_MD5_SBIN_OVERRIDE environment variable, documented in docs/configuration.md, so the fallback chain can be exercised deterministically in tests.
  • Added tests/fm-hash-pane.test.sh (4 cases covering stable/distinct hashing and successive fallback tiers) and registered it in bin/fm-test-run.sh's watcher-wake-lock test family.

Risk Assessment

✅ Low: Change is minimal and localized to hash_pane()'s tool-resolution fallback chain; the hash value is only ever compared for byte-equality against a persisted previous value (no callers assume a particular format/length), the override env var preserves default behavior (/sbin/md5) when unset, no set -e/pipefail issues introduced, and the new test file exercises each fallback tier plus the never-hard-error guarantee.

Testing

Ran the new tests/fm-hash-pane.test.sh directly and via bin/fm-test-run.sh (all 4 cases pass, confirming hash_pane's fallback chain through sbin-md5, openssl, shasum, and a wc-based last resort never hard-errors and stays stable/input-sensitive), verified the test is properly registered in the watcher-wake-lock family, and confirmed via a base-commit worktree comparison that the unrelated fm-watch-triage.test.sh failure (ambient PATH md5 -q shadowing quirk) pre-dates this change and is not a regression.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-hash-pane.test.sh (direct run, 4/4 pass)
  • bin/fm-test-run.sh tests/fm-hash-pane.test.sh (via runner, exit=0)
  • bin/fm-test-run.sh --family watcher-wake-lock --list (confirms fm-hash-pane.test.sh registration)
  • bash tests/fm-watch-triage.test.sh on target commit a7d6124 (reproduces pre-existing md5 -q failure)
  • bash tests/fm-watch-triage.test.sh on base commit f988287 in isolated worktree (reproduces identical failure, confirming not a regression)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Summary by CodeRabbit

  • Bug Fixes

    • Improved watcher compatibility across systems with additional checksum fallbacks.
    • Prevented failures when standard hashing utilities are unavailable.
    • Added support for configuring the system MD5 utility path.
  • Documentation

    • Documented the new configuration option and checksum fallback order.
  • Tests

    • Added coverage for checksum selection, input changes, and environments without hashing tools.

hash_pane() hard-errored on every poll cycle when a watcher's PATH lacked
both md5 and md5sum (observed on a secondmate whose runtime PATH omitted
both, needing ~3 manual watcher restarts). Add an FM_MD5_SBIN_OVERRIDE test
seam for the existing hardcoded /sbin/md5 check, and extend the fallback
chain past md5sum to openssl dgst -md5, then shasum, then cksum, then a
wc -c last resort that cannot itself be absent. The hash is only compared
for change-detection between polls, so any tool that returns a stable token
in its first whitespace-delimited field is interchangeable.

Covered by tests/fm-hash-pane.test.sh.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

hash_pane() now supports configurable and additional hashing fallbacks, including a non-failing byte-count fallback. New tests cover tool selection and output behavior. Documentation describes the configuration variable and fallback order.

Changes

Hash pane fallback support

Layer / File(s) Summary
Hash fallback implementation
bin/fm-watch.sh, docs/configuration.md
hash_pane() uses FM_MD5_SBIN_OVERRIDE, adds checksum fallbacks, and documents the fallback order.
Hash fallback validation
tests/fm-hash-pane.test.sh, bin/fm-test-run.sh
Tests validate /sbin/md5, OpenSSL, shasum, and the no-tool fallback. The test runner maps the new tests to watcher-wake-lock.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change to harden hash_pane() against missing commands on PATH.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fm/watch-md5-harden-fm-watch-sh-hash-pane-against-pat-d7

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bin/fm-watch.sh`:
- Around line 197-198: Update the final fallback in hash_pane within
bin/fm-watch.sh at lines 197-198 to produce a content-sensitive token, or
explicitly return and handle an unavailable result instead of using wc -c;
update docs/configuration.md at line 442 to accurately describe the selected
behavior, and extend tests/fm-hash-pane.test.sh at lines 113-123 to verify that
equal-length, different inputs yield distinct results when checksum tools are
unavailable.

In `@tests/fm-hash-pane.test.sh`:
- Around line 96-124: Add a dedicated test alongside the existing hash_pane
fallback tests that constructs a fake PATH containing only cksum, invokes
hash_pane with the md5 override unavailable, and verifies successful execution
with the expected normalized checksum output produced by cksum. Keep the test
focused on the cksum tier and mirror the setup and assertions used by
test_hash_pane_falls_back_past_openssl_to_shasum.
- Around line 25-26: Update the sbin-md5 test setup around REAL_SBIN_MD5 and the
FM_MD5_SBIN_OVERRIDE seam so it does not depend on host availability or behavior
of /sbin/md5 or md5. Provide a test-local executable that supports -q, or
feature-probe the resolved executable and skip the affected test when
unavailable; ensure the setup used by the test at lines 66-70 never fails the
entire file on unsupported hosts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d18c426-b332-4718-8d1d-3be26154ab0a

📥 Commits

Reviewing files that changed from the base of the PR and between 21f08b4 and 68e6533.

📒 Files selected for processing (4)
  • bin/fm-test-run.sh
  • bin/fm-watch.sh
  • docs/configuration.md
  • tests/fm-hash-pane.test.sh

Comment thread bin/fm-watch.sh
Comment on lines +197 to +198
else
wc -c | tr -d ' '

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Use a content-sensitive final fallback.

wc -c returns only the byte count. Two different pane states with the same length produce the same token, so hash_pane() can miss a poll-to-poll change and suppress required watcher handling.

  • bin/fm-watch.sh#L197-L198: replace the byte-count fallback with a content-sensitive token, or define and handle an explicit unavailable result.
  • docs/configuration.md#L442-L442: document the selected behavior accurately. Do not describe a byte count as reliable pane hashing.
  • tests/fm-hash-pane.test.sh#L113-L123: assert that two different equal-length inputs produce distinct results when checksum tools are unavailable.
📍 Affects 3 files
  • bin/fm-watch.sh#L197-L198 (this comment)
  • docs/configuration.md#L442-L442
  • tests/fm-hash-pane.test.sh#L113-L123
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/fm-watch.sh` around lines 197 - 198, Update the final fallback in
hash_pane within bin/fm-watch.sh at lines 197-198 to produce a content-sensitive
token, or explicitly return and handle an unavailable result instead of using wc
-c; update docs/configuration.md at line 442 to accurately describe the selected
behavior, and extend tests/fm-hash-pane.test.sh at lines 113-123 to verify that
equal-length, different inputs yield distinct results when checksum tools are
unavailable.

Comment on lines +25 to +26
REAL_SBIN_MD5=/sbin/md5
[ -x "$REAL_SBIN_MD5" ] || REAL_SBIN_MD5=$(command -v md5 || true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make the sbin-md5 test independent of host tools.

On hosts without /sbin/md5 or md5, Line 66 fails the whole test file. A resolved md5 path also does not prove that the executable supports -q.

Use a test-local executable that accepts -q, or feature-probe a real executable and skip this case when unavailable. The FM_MD5_SBIN_OVERRIDE seam supports a local fixture.

Also applies to: 66-70

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/fm-hash-pane.test.sh` around lines 25 - 26, Update the sbin-md5 test
setup around REAL_SBIN_MD5 and the FM_MD5_SBIN_OVERRIDE seam so it does not
depend on host availability or behavior of /sbin/md5 or md5. Provide a
test-local executable that supports -q, or feature-probe the resolved executable
and skip the affected test when unavailable; ensure the setup used by the test
at lines 66-70 never fails the entire file on unsupported hosts.

Comment on lines +96 to +124
test_hash_pane_falls_back_past_openssl_to_shasum() (
local home fakebin out expected status
home="$TMP_ROOT/no-md5-no-openssl"
mkdir -p "$home"
source_watch "$home"
[ -n "$REAL_SHASUM" ] || fail "shasum not available to exercise the fallback tier"
fakebin=$(make_fakebin "$home/fakebin" "shasum=$REAL_SHASUM")
status=0
out=$(PATH=$fakebin FM_MD5_SBIN_OVERRIDE="$home/no-such-md5" hash_pane <<<"pane text a") || status=$?
expect_code 0 "$status" "hash_pane exit with md5/md5sum/openssl absent from PATH"
[ -n "$out" ] || fail "hash_pane produced empty output with md5/md5sum/openssl absent from PATH"
expected=$(printf 'pane text a\n' | "$REAL_SHASUM" | "$REAL_CUT" -d' ' -f1)
[ "$out" = "$expected" ] \
|| fail "hash_pane did not use the shasum fallback tier past openssl (got '$out', wanted '$expected')"
pass "hash_pane falls back past openssl to shasum and still exits cleanly"
)

test_hash_pane_never_hard_errors_with_no_hash_tool_at_all() (
local home fakebin out status
home="$TMP_ROOT/no-hash-tool"
mkdir -p "$home"
source_watch "$home"
fakebin=$(make_fakebin "$home/fakebin")
status=0
out=$(PATH=$fakebin FM_MD5_SBIN_OVERRIDE="$home/no-such-md5" hash_pane <<<"pane text a") || status=$?
expect_code 0 "$status" "hash_pane exit with no md5/md5sum/openssl/shasum/cksum on PATH"
[ -n "$out" ] || fail "hash_pane produced empty output with no hash tool at all on PATH"
pass "hash_pane never hard-errors even with no hashing tool at all on PATH"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add direct coverage for the cksum tier.

Lines 195-196 in bin/fm-watch.sh add a distinct output conversion path. The tests exercise openssl, shasum, and no-tool behavior, but they never provide only cksum on PATH.

Add a fake-bin case with only cksum available. Assert that hash_pane() returns the expected normalized checksum.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/fm-hash-pane.test.sh` around lines 96 - 124, Add a dedicated test
alongside the existing hash_pane fallback tests that constructs a fake PATH
containing only cksum, invokes hash_pane with the md5 override unavailable, and
verifies successful execution with the expected normalized checksum output
produced by cksum. Keep the test focused on the cksum tier and mirror the setup
and assertions used by test_hash_pane_falls_back_past_openssl_to_shasum.

@trillium
trillium merged commit 1350b11 into main Aug 1, 2026
12 checks passed
trillium added a commit that referenced this pull request Aug 1, 2026
* fix(watch): make hash_pane's last-resort tier content-sensitive

Address 3 CodeRabbit findings on merged PR #21 (hash_pane PATH-gap
fallback chain):

- MAJOR: the last-resort tier returned wc -c's raw byte count, so two
  panes of equal size but different content collided and change
  detection missed real updates. Replace it with a pure-shell content
  digest (od byte dump piped through an awk rolling hash) so same-size
  differing content can no longer collide.
- MAJOR: the sbin-md5 test depended on the runner's ambient /sbin/md5
  or PATH md5, which is absent on non-BSD hosts. Replace it with a
  self-contained fake md5 -q stand-in script so the test no longer
  depends on host tooling.
- MINOR: add direct test coverage for the cksum tier.

make_fakebin's baseline symlink set grows to include od/awk/cat, which
the new last-resort tier and the fake sbin-md5 script both need.

* no-mistakes(review): fix stale hash_pane doc and drop unused wc test fixture
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant