diff --git a/bin/fm-backend.sh b/bin/fm-backend.sh index e8dc7fc559..ac4480d4e3 100644 --- a/bin/fm-backend.sh +++ b/bin/fm-backend.sh @@ -709,7 +709,6 @@ fm_backend_target_exists() { # [expected-label] [recorded-sc case "$backend" in tmux) fm_backend_source tmux || return 1 - fm_backend_tmux_expected_label_matches "$target" "$expected_label" "$recorded_scoped_target" || return 1 # The probe was `tmux display-message -p -t '#{pane_id}'`, which # exits 0 for a window that no longer exists (tmux silently falls back to # the session's current window - see fm_backend_tmux_target_exists), so @@ -718,12 +717,20 @@ fm_backend_target_exists() { # [expected-label] [recorded-sc # fm-teardown.sh refuse to release the task forever, leaking its worktree # lease. # - # The probe used to fall back to the recorded scoped target for a - # @window-id. That indirection is gone: the guard above already proves a - # @window-id resolves AND that the session and window it resolves to are - # the recorded ones, which is strictly stronger than probing the recorded - # session:window on its own. - fm_backend_tmux_target_exists "$target" + # ONE tmux round-trip either way. When the caller supplies an expectation + # the identity guard already RESOLVES the target to prove that identity - + # exactly for a session:window, and by reading the id back for a + # @window-id - so its success is itself the existence proof and a second + # probe would only re-ask a question already answered. With no + # expectation there is nothing to verify and the probe is the whole check. + # This path runs in a bounded retry loop in fm-teardown.sh's + # managed_endpoint_is_gone, so a redundant probe there is a doubled + # process spawn per iteration on a machine that may already be loaded. + if [ -n "$expected_label" ] || [ -n "$recorded_scoped_target" ]; then + fm_backend_tmux_expected_label_matches "$target" "$expected_label" "$recorded_scoped_target" + else + fm_backend_tmux_target_exists "$target" + fi ;; herdr) fm_backend_source herdr || return 1 diff --git a/tests/fm-teardown-suite.sh b/tests/fm-teardown-suite.sh index b97a1ef9ca..8a0bfaf9f6 100644 --- a/tests/fm-teardown-suite.sh +++ b/tests/fm-teardown-suite.sh @@ -6415,6 +6415,13 @@ test_secondmate_registry_updates_are_locked_and_literal() { case_dir=$(make_case secondmate-registry-locked-literal) id='foo.bar' prepare_secondmate_home_fixture "$case_dir" "$id" + # This case is about registry locking, and a secondmate teardown legitimately + # requires an already-quiesced endpoint, so model one the way the rest of this + # suite does. It previously read as gone only by accident: the stub's + # list-windows answers with a hardcoded fm-task-x1, which never matched this + # task's fm-foo.bar window, so the bare-name lookup missed it whatever the + # live marker said. + rm -f "$case_dir/fakebin/.tmux-live" fm_write_meta "$case_dir/state/$id.meta" \ "window=fm-$id" \ "tmux_session_target=firstmate:fm-$id" \ diff --git a/tests/secondmate-helpers.sh b/tests/secondmate-helpers.sh index f2a6f3568c..7ac91b39d7 100644 --- a/tests/secondmate-helpers.sh +++ b/tests/secondmate-helpers.sh @@ -54,7 +54,15 @@ case "${1:-}" in prev=$arg format=$arg done - if [ -n "$target" ] && [ -f "$FM_FAKE_TMUX_LOG.killed" ] && grep -qxF "$target" "$FM_FAKE_TMUX_LOG.killed"; then + # Compare against the killed log in tmux's PLAIN target spelling. Firstmate + # resolves a window with tmux's exact-match syntax ("=session:=window") so a + # name can never resolve by fnmatch, and real tmux treats that spelling and + # the plain one identically - both fail once the window is killed. This stub + # matches the recorded kill-window target literally, so without normalising + # the "=" markers it would report a killed window as still alive. + probe_target=$(printf '%s' "$target" | tr -d '=') + if [ -n "$probe_target" ] && [ -f "$FM_FAKE_TMUX_LOG.killed" ] \ + && grep -qxF "$probe_target" "$FM_FAKE_TMUX_LOG.killed"; then exit 1 fi case "$format" in