diff --git a/bin/fm-backend.sh b/bin/fm-backend.sh index 2882f4a6af2..60516489a55 100644 --- a/bin/fm-backend.sh +++ b/bin/fm-backend.sh @@ -595,6 +595,13 @@ fm_backend_expected_label_of_selector() { # fm_backend_source() { # local name=$1 fm_backend_validate "$name" || return 1 + # A missing adapter file must be a guardable refusal, never a fatal source + # error: under `set -e` (fm-teardown.sh and other lifecycle consumers) a + # failed `.` on a nonexistent file exits the shell even inside `||`/`if` + # guards on bash 3.2, which silently reports success instead of reaching the + # caller's "prerequisites unavailable" refusal. Precheck existence so the + # source only ever runs on a present file. + [ -f "$FM_BACKEND_LIB_DIR/backends/$name.sh" ] || return 1 case "$name" in tmux) if [ -z "${_FM_BACKEND_TMUX_SOURCED:-}" ]; then diff --git a/tests/fm-backend.test.sh b/tests/fm-backend.test.sh index ece981b1222..294f7792e5c 100755 --- a/tests/fm-backend.test.sh +++ b/tests/fm-backend.test.sh @@ -503,6 +503,29 @@ test_backend_source_shell_portable() { pass "bash: fm_backend_source recognizes known backends and rejects unknown ones" } +test_backend_source_missing_adapter_refuses_guardably() { + local out rc tmpbin + # The teardown preflight relies on fm_backend_source returning 1 when an + # adapter file is absent. Under `set -e`, a failed `.` on a nonexistent file + # is a fatal special-builtin error on bash 3.2, even inside `||`/`if` guards, + # so the refusal must come from an existence precheck instead of the source + # itself failing - otherwise lifecycle callers silently exit as if they + # succeeded. This mirrors the missing-adapter teardown fixture at the + # library boundary for every backend and caller. + tmpbin="$TMP_ROOT/missing-adapter-lib/backends" + mkdir -p "$tmpbin" + rc=0 + out=$(set -eu; cd "$ROOT" \ + && . bin/fm-backend.sh \ + && FM_BACKEND_LIB_DIR="$tmpbin" \ + && if fm_backend_source herdr; then echo sourced; else echo refused; fi) || rc=$? + [ "$rc" -eq 0 ] \ + || fail "fm_backend_source missing adapter: the caller shell died (rc=$rc) instead of refusing" + [ "$out" = "refused" ] \ + || fail "fm_backend_source missing adapter: expected 'refused', got: $out" + pass "fm_backend_source refuses a missing adapter guardably under set -e instead of exiting the caller" +} + test_backend_validate_spawn_accepts_orca() { local out fm_backend_validate_spawn tmux 2>/dev/null || fail "fm_backend_validate_spawn should accept tmux" @@ -1126,6 +1149,7 @@ test_backend_name_autodetect_notice test_backend_name_explicit_beats_detection test_backend_validate_refuses_unknown test_backend_source_shell_portable +test_backend_source_missing_adapter_refuses_guardably test_backend_validate_spawn_accepts_orca test_meta_get_and_backend_of_meta test_resolve_selector_three_forms