fix(bin): keep remote home seeding portable on Bash 3.2 and fail closed on missing backend adapters - #72
Merged
Conversation
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix the two pre-existing baseline test failures found on 2026-08-24 in the firstmate repository's own suite on clean current main using bin/fm-test-run.sh: reproduce and properly root-cause the remote-lifecycle E2E seed failure and its sibling baseline failure, fixing defects rather than weakening assertions. If a failure is environment-dependent, make the test deterministic or properly gated and disclose that in the PR body. Scope was expanded after clean-main proof to include the fm-pending-reply and fm-wake-queue regressions. The fm-tool-update-check regression already had a fix in flight on fm/fm-lock-steal-recursion-fix-x2, so do not duplicate that fix. The resulting PR must make the relevant baseline suite green together with the x2 branch. Preserve Firstmate's shared tracked-material coding guidelines and do not broaden into unrelated baseline or environment failures.
What Changed
bin/fm-remote-home-seed.shnow walksPROJECT_NAMESby index instead of expanding"${PROJECT_NAMES[@]}", so a--no-projectsseed no longer aborts underset -uon Bash 3.2 (the baseline/bin/bashon macOS).bin/fm-backend.sh'sfm_backend_sourceprechecks that the adapter file exists and is readable, printingerror: backend adapter is unavailable: <path>and returning 1, instead of letting a dot-source of an absent file abort the whole non-interactive shell before the per-adapter|| return 1can run.tests/fm-remote-secondmate-lifecycle-e2e.test.shreplaces thecp -R+diff -ruunchanged-tree checks with a sharedtree_snapshothelper that records each entry by its own type (symlink by literal target, regular file by digest), removing the platform-dependent handling of symlinks and non-regular entries; the helper fails closed when the directory is missing so a deleted tree can no longer read as unchanged, and every call site fails with a distinct message. Failure output in this test and intests/fm-teardown.test.shnow includes the captured stderr/stdout of the failing step.Risk Assessment
✅ Low: Both source fixes are narrow, root-caused corrections I reproduced directly (bash 3.2 empty-array expansion under set -u, and
.on a missing file aborting an errexit shell), the test changes strengthen rather than weaken the residue assertions, and the round-2 fail-closed fix genuinely closes the empty-snapshot hole at every call site.Testing
Reproduced both reported baseline failures by reverting only the two bin/ source files to the base commit and re-running the affected scripts under bin/fm-test-run.sh, then confirmed the branch turns them green (5/5 across the remote-lifecycle E2E, teardown, backend-dispatch, pending-reply and wake-queue scripts). Root-caused each to a real defect — an empty-array expansion under
set -uon macOS bash 3.2, and a failing adaptersourcethatset -eplus an EXIT trap silently converts into a successful teardown that skips its safety preflight — and captured before/after CLI transcripts at both user-facing surfaces plus the suite summaries as evidence. No visual artifact applies: this change is entirely shell CLI behavior with no rendered surface. fm-pending-reply and fm-wake-queue, named in the expanded scope, pass with base sources too and never reproduced here; worktree left clean with all temporary checkouts removed.Evidence: fm-remote-home-seed.sh --no-projects: before/after CLI transcript
Source: fm-remote-home-seed.sh --no-projects: before/after CLI transcript
--- BEFORE (base bac5c84) --- $ FM_HOME=<home> bin/fm-remote-home-seed.sh demo remote-mac <repo> <remote-home> --no-projects <repo>/bin/fm-remote-home-seed.sh: line 190: PROJECT_NAMES[@]: unbound variable exit=0 --- AFTER (branch 30f2a9b) --- $ FM_HOME=<home> bin/fm-remote-home-seed.sh demo remote-mac <repo> <remote-home> --no-projects error: remote command is not tracked by this Firstmate checkout: fm-remote-doctor.sh error: remote runtime preflight failed; nothing was provisioned. Close the gaps listed above, or update the remote code root if it predates the current fm-remote-doctor.sh exit=1Evidence: fm-teardown herdr preflight with a missing adapter: before/after
Source: fm-teardown herdr preflight with a missing adapter: before/after
--- BEFORE (base bac5c84) --- <checkout>/bin/fm-backend.sh: line 609: <checkout>/bin/backends/herdr.sh: No such file or directory teardown exit status: 0 --- AFTER (branch 30f2a9b) --- error: backend adapter is unavailable: <checkout>/bin/backends/herdr.sh error: herdr teardown prerequisites are unavailable for task-x1; nothing was changed - restore the adapter and rerun teardown teardown exit status: 1Evidence: bin/fm-test-run.sh before/after summaries
Source: bin/fm-test-run.sh before/after summaries
BEFORE (branch tests + base bin/ sources): not ok - failing seed exited before remote provisioning bin/fm-remote-home-seed.sh: line 190: PROJECT_NAMES[@]: unbound variable not ok - herdr-preflight-missing-adapter: teardown continued without its required preflight AFTER (branch 30f2a9b): FM_TEST_SUMMARY total=5 failed=0 skipped_gate=0 duration_ms=514956Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
tests/fm-remote-secondmate-lifecycle-e2e.test.sh:1273- tree_snapshot returns empty output for a non-existent directory (cd "$dir" 2>/dev/null || exit 0at line 447), soassert_retirement_refusednow compares "" to "" and passes silently if $REMOTE_HOME is absent. The replacedcp -R+diff -rupair failed loudly in that case (cp errored, diff on two missing paths returned non-zero -> "refusal changed the remote directory"). Concrete trace: if a preceding case leaves $REMOTE_HOME deleted, home_before="", teardown refuses, second snapshot is "", the equality holds and the assertion reports success without ever inspecting a remote home. Add[ -d "$REMOTE_HOME" ] || fail "$label refusal ran without a remote home"before capturing home_before (assert_retirement_completed already distinguishes this via itsexistedflag).tests/fm-remote-secondmate-lifecycle-e2e.test.sh:1- The intent states "Scope was expanded after clean-main proof to include the fm-pending-reply and fm-wake-queue regressions" and "The resulting PR must make the relevant baseline suite green together with the x2 branch". The branch diff (bac5c84..d68565a) contains nothing that touches those paths: neither tests/fm-pending-reply.test.sh nor tests/fm-wake-queue.test.sh is modified, neither contains the empty-array pattern fixed in fm-remote-home-seed.sh, and fm-wake-queue.test.sh does not reference the backend adapter path fixed in fm-backend.sh. Either those two regressions are already resolved by the base commit bac5c84 (the merged non-recursive steal-mutex lock fix, which both tests exercise heavily) - in which case say so and disclose it - or the expanded scope is unmet. This needs the author's confirmation rather than a reviewer guess.bin/fm-backend.sh:598- The new guard closes the missing-adapter case, but the same bash 3.2 hazard (.on an unopenable file terminates the shell, defeating|| return 1) still exists one level down: bin/backends/tmux.sh:22-26 sources fm-tmux-lib.sh / fm-session-lock-lib.sh / fm-cursor-lib.sh unguarded, and bin/fm-teardown.sh:2088 sources fm-wake-lib.sh unguarded. Not reachable from any current test mode (the missing-parser and missing-explicit-close-helper cases keep the adapter file present), so this is not a blocker - noting it as the residual reach of the same class if a partial bin/ deployment ever occurs.🔧 Fix: make tree_snapshot fail closed on missing directories
1 info still open:
bin/fm-backend.sh:598- The adapter-existence guard is evaluated before the_FM_BACKEND_<NAME>_SOURCEDcache check, so a secondfm_backend_source herdrin a process that already sourced the adapter now returns 1 if the file disappeared in the meantime, where it previously succeeded as a no-op from in-memory functions. Concrete path: a long-livedbin/fm-watch.shsources herdr, a non-atomicbin/redeploy or branch checkout removesbin/backends/herdr.sh, and the nextteardown_herdr_require_prerequisites(bin/fm-teardown.sh:2072) refuses with "restore the adapter and rerun teardown" despite every prerequisite function still being defined. Failing closed here is defensible (the process is running against a torn tree), so this is a note on the behavior change rather than a defect; moving the guard inside eachif [ -z "${_FM_BACKEND_*_SOURCED:-}" ]block would preserve the old cached-success semantics if that is preferred.tests/fm-pending-reply.test.sh- fm-pending-reply and fm-wake-queue were named in the expanded intent scope, but they pass on this machine both with base (bac5c84) bin/ sources and with the branch — the regressions did not reproduce here, and nothing on this branch changes their code paths. Worth disclosing in the PR body so reviewers do not read them as fixed by this change.bin/fm-test-run.sh tests/fm-remote-secondmate-lifecycle-e2e.test.sh tests/fm-teardown.test.sh tests/fm-pending-reply.test.sh tests/fm-wake-queue.test.sh tests/fm-backend.test.shon the branch — 5/5 passRegression proof: temporarily restoredbin/fm-remote-home-seed.shandbin/fm-backend.shto bac5c84 and re-ran the same scripts — reproducednot ok - failing seed exited before remote provisioning(PROJECT_NAMES[@]: unbound variable) andnot ok - herdr-preflight-missing-adapter: teardown continued without its required preflight; source files restored, worktree verified cleanManual CLI transcript:bin/fm-remote-home-seed.sh demo remote-mac <root> <home> --no-projectsrun against base and branch checkouts (git archive into /tmp) — base crashes and exits 0, branch advances past project assembly to the remote-runtime preflight and exits 1Manual CLI transcript:fm_backend_source herdrwithbin/backends/herdr.shremoved, driven under fm-teardown.sh's exact shell contract (set -eu+ EXIT trap) — base exits 0 with only raw shell noise, branch printserror: backend adapter is unavailable: ...plus teardown'snothing was changedrefusal and exits 1grep -n '^set -' bin/fm-teardown.shandgrep -n 'trap .* EXIT' bin/fm-teardown.shto confirm the set -e / EXIT-trap masking mechanism✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.