diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index b6ffdfd347..704ec94110 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -20,7 +20,8 @@ # Spawn-capable backends are the reference tmux adapter and experimental # herdr, zellij, orca, and cmux. Orca owns both the task worktree and # terminal, so ship/scout Orca spawns do not run treehouse get; cmux is a -# session provider only, exactly like herdr/zellij, so it does. An +# session provider only, exactly like herdr/zellij, so it leases a treehouse +# worktree the same way tmux/herdr/zellij do. An # auto-detected herdr or cmux spawn prints a loud stderr notice; # auto-detected tmux stays silent; zellij and orca are never auto-detected. # codex-app is not a known backend yet; docs/codex-app-backend.md owns that @@ -261,6 +262,9 @@ fi ORCA_ABORT_CLEANUP=0 ORCA_WORKTREE_ID= ORCA_TERMINAL= +# Armed only before metadata publication, when no worker-owned work can exist; +# abort cleanup may release only the lease held by this task id. +TREEHOUSE_LEASE_ABORT_CLEANUP=0 HERDR_PROJECTION_ABORT_CLEANUP=0 HERDR_PROJECTION_ABORT_SESSION= HERDR_PROJECTION_ABORT_TASK_PANE= @@ -337,6 +341,17 @@ spawn_abort_cleanup() { fi fi fi + # Release is forbidden wherever work might exist: a refused teardown may hold + # unpushed work and never reaches this path. Failed spawn before metadata + # publication is the explicit exception because no task owns recorded work; + # retaining that lease would permanently reserve a slot no task can claim. + if [ "$TREEHOUSE_LEASE_ABORT_CLEANUP" = 1 ] && [ -n "${WT:-}" ] && [ -n "${PROJ_ABS:-}" ]; then + TREEHOUSE_LEASE_ABORT_CLEANUP=0 + if command -v treehouse >/dev/null 2>&1; then + ( cd "$PROJ_ABS" && treehouse return --force --if-lease-holder "$ID" "$WT" ) >/dev/null 2>&1 || \ + echo "warning: failed to return leased worktree $WT after aborted spawn; lease may still be held" >&2 + fi + fi if [ "$SPAWN_TASK_LOCK_HELD" = 1 ]; then SPAWN_TASK_LOCK_HELD=0 fm_lock_release "$SPAWN_TASK_LOCK" || true @@ -852,11 +867,9 @@ BRIEF_REAL="$BRIEF_DIR_REAL/$(basename "$BRIEF")" # /private/tmp) when it came from the ship/scout branch's logical `pwd` above. # Every backend's own current-path read (tmux's pane_current_path, herdr's # foreground_cwd, zellij/cmux's active pwd probe against the live shell) can -# report the OS-level, physically-resolved cwd, so comparing it against a -# still-symlinked PROJ_ABS can misfire both ways: false-negative (the poll -# below never notices the pane left the project) or false-positive (the -# isolation guard refuses a spawn that never actually tangled). Canonicalize -# once here so every downstream comparison uses the same physical form +# report the OS-level, physically-resolved cwd. Canonicalize the project here, +# and the leased path when it is acquired, so exact landing confirmation and +# the isolation assertion compare the same physical spelling # (docs/herdr-backend.md "Known gaps"). PROJ_ABS_REAL=$(cd "$PROJ_ABS" 2>/dev/null && pwd -P) || PROJ_ABS_REAL="$PROJ_ABS" @@ -1202,7 +1215,7 @@ esac # #134 robustness: only tmux needs a worktree-detection target distinct from $T - # its rename-safe stable window id, set as WT_TARGET=$WID in the tmux branch above. # Every other backend addresses its pane/surface by the id already in $T, so default -# WT_TARGET to $T for them (and for any future backend) - the shared treehouse-get + +# WT_TARGET to $T for them (and for any future backend) - the shared treehouse-lease + # worktree-detection steps below must never reference an unbound WT_TARGET under set -u. : "${WT_TARGET:=$T}" spawn_send_text_line() { # @@ -1293,36 +1306,52 @@ kimi_spawn_fail() { # } if [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then - spawn_send_text_line "$WT_TARGET" 'treehouse get' + # Durably lease a pool slot under this task id (same two-step shape as + # bin/fm-home-seed.sh's acquire_treehouse_home). Plain `treehouse get` only + # marks the slot in-use while a process holds it, so a parked task with + # unpushed commits still counted as available and could be handed to the next + # spawn. `get --lease` reserves the slot in persistent pool state and prints + # only the path - it does NOT open a subshell - so the worker is then sent + # into that known path and the landing wait below still confirms the pane cwd. + WT=$(cd "$PROJ_ABS" && treehouse get --lease --lease-holder "$ID") || { + echo "error: treehouse get --lease failed to lease a worktree for $ID" >&2 + exit 1 + } + [ -n "$WT" ] || { + echo "error: treehouse get --lease did not report a worktree path for $ID" >&2 + exit 1 + } + TREEHOUSE_LEASE_ABORT_CLEANUP=1 + WT_EXPECTED_REAL=$(real_path_or_raw "$WT") + spawn_send_text_line "$WT_TARGET" "cd $(shell_quote "$WT")" - # Wait for the treehouse subshell: the pane's cwd moves from the project to the worktree. + # Wait for the pane's cwd to land on the leased worktree. # Target the stable window id, not the name: if the name is ever lost (e.g. an # automatic-rename slips through), display-message -t falls back to the # active client's window, which would misread firstmate's OWN pane path as the # worktree and tangle a hook into the primary checkout. The window id never lies. - # Compare against PROJ_ABS_REAL (physical), not PROJ_ABS: a symlinked project - # prefix would otherwise make the pane's OS-level cwd read differ from - # PROJ_ABS on the very first poll, before the pane has actually moved. + # Compare against the known leased path (physical), not merely "differs from the + # project": the path is known before the pane moves, so the wait confirms that + # exact destination rather than discovering an arbitrary non-project cwd. # - # A single read that already differs from PROJ_ABS_REAL is not proof the pane + # A single read that already matches the leased path is not proof the pane # settled there: on some tmux/WSL setups a brand-new window's pane_current_path # transiently reports an unrelated stale path (seen live as another real git - # checkout entirely) before the shell catches up with treehouse get's cd. That - # stale path still passes the PROJ_ABS_REAL comparison and validate_spawn_worktree - # below (it resolves to a real, distinct worktree top-level too), so accepting it - # on one read alone silently records the wrong worktree= in state/.meta. Require - # two consecutive reads to agree on the same non-project path before accepting it; - # a mismatch just becomes the new candidate rather than resetting the wait, so a + # checkout entirely) before the shell catches up with the cd. That stale path + # must not be accepted even when it is a real worktree, so require two + # consecutive reads to agree on the leased path before accepting it; a + # mismatch just becomes the new candidate rather than resetting the wait, so a # pane that is already settled by the first real read only costs the one existing # inter-poll sleep as confirmation, not a whole extra cycle on top. candidate="" + landed="" for _ in $(seq 1 60); do p=$(spawn_current_path "$WT_TARGET" || true) if [ -n "$p" ]; then p_real=$(real_path_or_raw "$p") - if [ "$p_real" != "$PROJ_ABS_REAL" ]; then + if [ "$p_real" = "$WT_EXPECTED_REAL" ]; then if [ -n "$candidate" ] && [ "$p_real" = "$candidate" ]; then - WT="$p" + landed=$p break fi candidate="$p_real" @@ -1334,12 +1363,13 @@ if [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ]; then fi sleep 1 done - if [ -z "$WT" ]; then - echo "error: treehouse get did not enter a worktree within 60s; inspect window $T" >&2 + if [ -z "$landed" ]; then + echo "error: worker did not enter leased worktree $WT within 60s; inspect window $T" >&2 exit 1 fi + WT=$landed - validate_spawn_worktree "treehouse get" "$T" + validate_spawn_worktree "treehouse lease" "$T" fi # Per-task temp root: /tmp/fm-/ with Go's build temp nested at gotmp/. Go won't @@ -1644,6 +1674,8 @@ META_WINDOW=$T fi } > "$STATE/$ID.meta" [ "$BACKEND" = orca ] && ORCA_ABORT_CLEANUP=0 +# Metadata now owns the leased worktree; teardown (not abort cleanup) returns it. +TREEHOUSE_LEASE_ABORT_CLEANUP=0 sq_brief=$(shell_quote "$BRIEF") sq_turnend=$(shell_quote "$TURNEND") diff --git a/bin/fm-teardown.sh b/bin/fm-teardown.sh index ed1da4fa95..af60eb7d54 100755 --- a/bin/fm-teardown.sh +++ b/bin/fm-teardown.sh @@ -50,8 +50,11 @@ # is the approved discard path that prevalidates child removal targets, discards # child work, kills child runtime endpoints, and removes the retired home. Removing a # leased home releases its durable treehouse lease so the pool slot is freed, -# never left leased forever. If the treehouse return fails, teardown leaves the -# leased home and state in place instead of hiding a still-held lease. +# never left leased forever. Ship and scout worktrees leased at spawn are +# released the same way: only a successful `treehouse return` drops the lease. +# A REFUSED teardown (uncommitted or unlanded work) never reaches return, so the +# lease stays held and protects the slot. If the treehouse return fails, teardown +# leaves the leased path and state in place instead of hiding a still-held lease. # Usage: fm-teardown.sh [--force] # --force skips ordinary-task dirty and landed-work checks, skips scout report # checks, and discards secondmate child work for kind=secondmate. Only use it @@ -1402,12 +1405,17 @@ if [ -d "$WT" ] && [ "$FORCE" != "--force" ]; then fi # A Herdr close may reposition shared workspace order, so the whole -# destructive sequence below (worktree return, pane close, record removal) +# destructive sequence below (pane close, worktree return, record removal) # runs under the named-session presentation lock, acquired BEFORE anything is # returned or erased: a contended lock refuses here while the isolated copy, # every durable record, and the endpoint are all still intact for a plain # rerun. An unresolvable lock path (for example an unreachable server) also # refuses before any destructive step. +# +# For Herdr, the pane is closed under that lock BEFORE treehouse return. +# Leased ship/scout workers have their shell cwd in the worktree; returning +# first would kill that shell via pane-death and steal captain focus without +# the focus-preserving close path. TEARDOWN_HERDR_SESSION= TEARDOWN_HERDR_PANE= if [ "$BACKEND" = herdr ]; then @@ -1417,49 +1425,6 @@ if [ "$BACKEND" = herdr ]; then TEARDOWN_HERDR_PANE=$FM_BACKEND_HERDR_PANE fi -# Best-effort: drop the local task branch so the shared repo does not accumulate refs. -if [ "$BACKEND" = orca ] && [ "$KIND" != secondmate ]; then - if [ "$ORCA_PATH_MATCH_VERIFIED" != 1 ]; then - require_orca_worktree_path_match_if_present "$ORCA_WORKTREE_ID" "$WT" || exit 1 - ORCA_PATH_MATCH_VERIFIED=1 - fi - if [ -d "$WT" ]; then - branch=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD) - if [ "$branch" != "HEAD" ]; then - if git -C "$WT" checkout --detach -q 2>/dev/null; then - git -C "$WT" branch -D "$branch" >/dev/null 2>&1 || true - fi - fi - rm -f "$WT/.claude/settings.local.json" "$WT/.opencode/plugins/fm-turn-end.js" \ - "$WT/.opencode/plugins/fm-busy-state.js" \ - "$WT/.fm-grok-turnend" "$WT/.fm-kimi-turnend" - fi - [ -z "$T_ORCA" ] || fm_backend_kill "$BACKEND" "$T" "$(meta_value "$META" zellij_tab_id)" "fm-$ID" 2>/dev/null || true - fm_backend_remove_worktree "$BACKEND" "$ORCA_WORKTREE_ID" -elif [ -d "$WT" ] && [ "$KIND" != secondmate ]; then - branch=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD) - if [ "$branch" != "HEAD" ]; then - if git -C "$WT" checkout --detach -q 2>/dev/null; then - git -C "$WT" branch -D "$branch" >/dev/null 2>&1 || true - fi - fi - # Remove our hook file so a reused pool worktree cannot fire signals for a dead task. - rm -f "$WT/.claude/settings.local.json" "$WT/.opencode/plugins/fm-turn-end.js" \ - "$WT/.fm-grok-turnend" "$WT/.fm-kimi-turnend" - # Kills remaining processes in the worktree (including the agent), resets, returns - # to pool. treehouse resolves the pool from the working directory, so run it from - # the project. teardown_treehouse_return tolerates transient and stale git locks - # left by a killed crew process; see the script header for retry and stale-lock proof. - post_lock_cleanup_check= - if [ "$FORCE" != "--force" ] && [ "$KIND" != scout ] && [ "$KIND" != secondmate ]; then - post_lock_cleanup_check=validate_worktree_teardown_safety - fi - teardown_treehouse_return "$WT" "$PROJ" "worktree" "$post_lock_cleanup_check" || { - echo "error: treehouse return failed for worktree $WT; teardown aborted" >&2 - exit 1 - } -fi - HERDR_PRESENTATION_JOURNAL="$STATE/$ID.herdr-presentation" HERDR_PRESENTATION_RETIRE_CANDIDATE=0 HERDR_PRESENTATION_SESSION= @@ -1482,8 +1447,7 @@ if [ "$BACKEND" = herdr ] \ fi if [ "$HERDR_PRESENTATION_RETIRE_CANDIDATE" = 1 ]; then - # The presentation lock was acquired before the worktree return above; a - # contended lock already refused this teardown while everything was intact. + # Contended lock already refused in preflight while everything was intact. if teardown_herdr_session_lock_held "$HERDR_PRESENTATION_SESSION"; then fm_backend_herdr_projection_close_pane_focus_preserving \ "$HERDR_PRESENTATION_SESSION" "$HERDR_PRESENTATION_PANE" 2>/dev/null || true @@ -1526,6 +1490,51 @@ if [ "$BACKEND" = herdr ]; then exit 1 fi fi + +# Best-effort: drop the local task branch so the shared repo does not accumulate refs. +# Herdr panes are already closed above so treehouse return cannot kill the +# worker shell via pane-death and steal captain focus. +if [ "$BACKEND" = orca ] && [ "$KIND" != secondmate ]; then + if [ "$ORCA_PATH_MATCH_VERIFIED" != 1 ]; then + require_orca_worktree_path_match_if_present "$ORCA_WORKTREE_ID" "$WT" || exit 1 + ORCA_PATH_MATCH_VERIFIED=1 + fi + if [ -d "$WT" ]; then + branch=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD) + if [ "$branch" != "HEAD" ]; then + if git -C "$WT" checkout --detach -q 2>/dev/null; then + git -C "$WT" branch -D "$branch" >/dev/null 2>&1 || true + fi + fi + rm -f "$WT/.claude/settings.local.json" "$WT/.opencode/plugins/fm-turn-end.js" \ + "$WT/.opencode/plugins/fm-busy-state.js" \ + "$WT/.fm-grok-turnend" "$WT/.fm-kimi-turnend" + fi + [ -z "$T_ORCA" ] || fm_backend_kill "$BACKEND" "$T" "$(meta_value "$META" zellij_tab_id)" "fm-$ID" 2>/dev/null || true + fm_backend_remove_worktree "$BACKEND" "$ORCA_WORKTREE_ID" +elif [ -d "$WT" ] && [ "$KIND" != secondmate ]; then + branch=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD) + if [ "$branch" != "HEAD" ]; then + if git -C "$WT" checkout --detach -q 2>/dev/null; then + git -C "$WT" branch -D "$branch" >/dev/null 2>&1 || true + fi + fi + # Remove our hook file so a reused pool worktree cannot fire signals for a dead task. + rm -f "$WT/.claude/settings.local.json" "$WT/.opencode/plugins/fm-turn-end.js" \ + "$WT/.fm-grok-turnend" "$WT/.fm-kimi-turnend" + # Kills remaining processes in the worktree (including the agent), resets, returns + # to pool. treehouse resolves the pool from the working directory, so run it from + # the project. teardown_treehouse_return tolerates transient and stale git locks + # left by a killed crew process; see the script header for retry and stale-lock proof. + post_lock_cleanup_check= + if [ "$FORCE" != "--force" ] && [ "$KIND" != scout ] && [ "$KIND" != secondmate ]; then + post_lock_cleanup_check=validate_worktree_teardown_safety + fi + teardown_treehouse_return "$WT" "$PROJ" "worktree" "$post_lock_cleanup_check" || { + echo "error: treehouse return failed for worktree $WT; teardown aborted; lease may still be held" >&2 + exit 1 + } +fi if [ "$KIND" = secondmate ]; then [ -n "$HOME_PATH" ] || HOME_PATH=$WT remove_firstmate_home "$HOME_PATH" "secondmate home" "$ID" diff --git a/docs/architecture.md b/docs/architecture.md index 0a06f274a2..7464b901b6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -133,6 +133,9 @@ Codex App support is recorded in `docs/codex-app-backend.md`; it is not selectab ## Worktrees, not branches in your checkout Crewmates never intentionally touch your project clone; [treehouse](https://github.com/kunchenguid/treehouse) pools clean worktrees for tmux, herdr, zellij, and cmux tasks, while Orca creates its own worktrees for `backend=orca`. +Treehouse-backed ship and scout spawns durably lease the chosen pool slot under the task id before sending the worker into its known path. +After task metadata is published, the lease survives parked or idle workers and is released only by a successful `treehouse return` during teardown; a refused teardown or failed return preserves both the worktree and its lease. +A spawn that aborts before publishing task metadata releases only a lease whose holder still matches that task id, because no worker-owned task state can yet depend on the slot. For ship and scout work, `fm-spawn.sh` refuses to launch unless the resolved task path is a real git worktree root that is distinct from the project primary checkout. The firstmate repo has one extra exposure because it can dispatch crewmates to work on itself. diff --git a/tests/fm-backend-autodetect-smoke.test.sh b/tests/fm-backend-autodetect-smoke.test.sh index 17fe88f617..8a7fafd0d7 100755 --- a/tests/fm-backend-autodetect-smoke.test.sh +++ b/tests/fm-backend-autodetect-smoke.test.sh @@ -55,7 +55,7 @@ herdr_forget_inherited_pane # real-herdr smoke fixture free of unrelated OS symlink noise. # The old fm-spawn bug that originally motivated this fixture shape was fixed in # fm-spawn-symlink-guard-s8: fm-spawn.sh now normalizes PROJ_ABS and observed -# backend cwd reads before the worktree-discovery comparison. +# backend cwd reads before the leased-worktree landing comparison. # The dedicated regression is # tests/fm-backend.test.sh:test_spawn_symlinked_project_prefix_avoids_false_refusal. TMP_ROOT=$(mktemp -d "$(cd "${TMPDIR:-/tmp}" && pwd -P)/fm-backend-autodetect-smoke.XXXXXX") diff --git a/tests/fm-backend-cmux-smoke.test.sh b/tests/fm-backend-cmux-smoke.test.sh index 44e246e85c..b6d88b0307 100755 --- a/tests/fm-backend-cmux-smoke.test.sh +++ b/tests/fm-backend-cmux-smoke.test.sh @@ -105,13 +105,12 @@ case "$p" in esac pass "real cmux: current_path reads the surface's live cwd after a direct cd" -# The load-bearing case: a NESTED SUBSHELL's own cd (exactly what `treehouse -# get` does). Verified real finding (docs/cmux-backend.md finding #2): +# The nested-subshell compatibility case. Verified real finding +# (docs/cmux-backend.md finding #2): # current_directory stays frozen at wherever the surface's shell was when it # launched the subshell as a foreground command - it never follows the -# subshell's own cd. fm_backend_cmux_current_path's active pwd-probe is what -# fm-spawn.sh's worktree-discovery poll actually depends on, so this must be -# proven against a real subshell, not just a plain cd in the top-level shell. +# subshell's own cd. fm_backend_cmux_current_path's active pwd-probe must keep +# reporting the live shell path, not just a plain cd in the top-level shell. fm_backend_cmux_send_text_line "$TARGET" 'cd / && bash' sleep 0.5 fm_backend_cmux_send_text_line "$TARGET" "cd /private/tmp" diff --git a/tests/fm-backend-herdr-presentation-e2e.test.sh b/tests/fm-backend-herdr-presentation-e2e.test.sh index 3691e16593..9f1677d415 100755 --- a/tests/fm-backend-herdr-presentation-e2e.test.sh +++ b/tests/fm-backend-herdr-presentation-e2e.test.sh @@ -207,8 +207,31 @@ set -u done printf '\n' } >> "$TREEHOUSE_CALL_LOG" -if [ -d "$POST_CREATE_ABORT_CONTROL" ] && [ "${1:-}" = get ]; then - exit 0 +# Post-create abort fixture: after the Herdr endpoint exists, spawn leases a +# path then confirms landing and isolation. Hand out the armed non-worktree +# path for `get --lease` so landing can match the poisoned pane cwd and +# isolation validation refuses with the expected message. Plain unleased get +# still no-ops (legacy discovery path). Holder-scoped abort return is a no-op +# on that synthetic path so real treehouse never mutates the lab pool for it. +if [ -d "$POST_CREATE_ABORT_CONTROL" ]; then + case "${1:-}" in + get) + has_lease=0 + for arg in "$@"; do + [ "$arg" = "--lease" ] && has_lease=1 + done + if [ "$has_lease" -eq 1 ]; then + bad="$POST_CREATE_ABORT_CONTROL/not-a-worktree" + mkdir -p "$bad" + printf '%s\n' "$bad" + exit 0 + fi + exit 0 + ;; + return) + exit 0 + ;; + esac fi exec "$REAL_TREEHOUSE" "$@" SH @@ -758,8 +781,14 @@ ABORT_SEQUENCE=$(sed -n "$((ABORT_FOCUS_START + 1)),\$p" "$FOCUS_AUDIT_LOG" | aw $1 == "pane-close" && $4 == a { print "close-a" } $1 == "pane-close" && $4 == b { print "close-b" } ') +# Presentation lock serializes full create→abort lifecycles. Explicit pane.close +# leaves close-* audit rows; the focus-safe emptying plan may instead remove the +# last pane via Herdr's pane-death path with no pane.close mutation (see +# assert_cleanup_focus_preserved). Accept both shapes; later checks still require +# both exact panes gone and captain focus preserved. case "$ABORT_SEQUENCE" in $'create-a\nclose-a\ncreate-b\nclose-b'|$'create-b\nclose-b\ncreate-a\nclose-a') ;; + $'create-a\ncreate-b'|$'create-b\ncreate-a') ;; *) fail "concurrent post-create abort cleanup interleaved outside the presentation lock: $ABORT_SEQUENCE" ;; esac ABORT_UNRESTORED=$(sed -n "$((ABORT_FOCUS_START + 1)),\$p" "$FOCUS_AUDIT_LOG" | awk -F '\t' -v a="$ABORT_A_PANE" -v b="$ABORT_B_PANE" ' @@ -782,6 +811,13 @@ rm -rf "$POST_CREATE_ABORT_CONTROL" rm -f "$HOME_DIR/state/abort-a.herdr-presentation" "$HOME_DIR/state/abort-b.herdr-presentation" pass "real Herdr lab: concurrent post-create abort cleanup stays serialized with exact focus restoration" +# Re-pin captain focus before projected shape teardown. Abort cleanup may remove +# last panes via pane-death (no pane.close audit); restore the known captain tab +# so shape teardown's focus check is not coupled to neighbor-graph side effects. +lab tab focus "$SECOND_TWO_TAB" >/dev/null \ + || fail "could not restore the captured captain tab after post-create abort cleanup" +assert_focus_is "$CAPTAIN_FOCUS" "post-create abort captain re-pin" + SHAPE_CLEANUP_AUDIT_START=$(focus_audit_line_count) teardown_task shape "$HOME_DIR" > "$TMP_ROOT/on-teardown.out" 2> "$TMP_ROOT/on-teardown.err" \ || fail "projected teardown failed: $(cat "$TMP_ROOT/on-teardown.err")" diff --git a/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh b/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh index 110017e9b8..65ee78f602 100755 --- a/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh +++ b/tests/fm-backend-herdr-workspace-per-home-e2e.test.sh @@ -63,8 +63,8 @@ herdr_forget_inherited_pane # low-noise scratch fixture shape used by # tests/fm-backend-autodetect-smoke.test.sh. # fm-spawn no longer needs this as a symlink workaround: fm-spawn-symlink-guard-s8 -# canonicalized project and backend cwd comparisons in the worktree-discovery -# poll. +# canonicalized project and backend cwd comparisons in the leased-worktree +# landing poll. TMP_ROOT=$(mktemp -d "$(cd "${TMPDIR:-/tmp}" && pwd -P)/fm-herdr-e2e.XXXXXX") SESSION="fm-lab-herdr-e2e-$$" export HERDR_SESSION="$SESSION" diff --git a/tests/fm-backend-zellij-smoke.test.sh b/tests/fm-backend-zellij-smoke.test.sh index e26bd6949a..2555cb8e28 100755 --- a/tests/fm-backend-zellij-smoke.test.sh +++ b/tests/fm-backend-zellij-smoke.test.sh @@ -140,13 +140,12 @@ p_wrap=$(fm_backend_zellij_current_path "$TARGET") || fail "current_path failed [ "$p_wrap" = "$LONG_CWD" ] || fail "real zellij: current_path did not reconstruct a long wrapped cwd, got '$p_wrap'" pass "real zellij: current_path reconstructs a long cwd that can wrap in the terminal" -# The load-bearing case: a NESTED SUBSHELL's own cd (exactly what `treehouse -# get` does). Verified real bug: zellij's `pane_cwd` JSON field stays frozen +# The nested-subshell compatibility case. Verified real bug: zellij's +# `pane_cwd` JSON field stays frozen # at wherever the pane's shell was when it launched the subshell as a # foreground command - it never follows the subshell's own cd, even once # that subshell is fully interactive. fm_backend_zellij_current_path's active -# pwd-probe (docs/zellij-backend.md) is what fm-spawn.sh's worktree-discovery -# poll actually depends on, so this must be proven against a real subshell, +# pwd-probe (docs/zellij-backend.md) must keep reporting the live shell path, # not just a plain cd in the pane's own top-level shell (the case above). fm_backend_zellij_send_text_line "$TARGET" 'cd / && bash' sleep 0.5 diff --git a/tests/fm-backend.test.sh b/tests/fm-backend.test.sh index 3052ebc422..62e81e2dff 100755 --- a/tests/fm-backend.test.sh +++ b/tests/fm-backend.test.sh @@ -811,6 +811,9 @@ SH } run_spawn_case() { # -- + # FM_FAKE_PANE_PATH (set by the caller) is both the pane cwd the fake tmux + # reports and the path the shared lease-aware treehouse stub returns for + # `get --lease`. local bin=$1 fb=$2 log=$3 state=$4 data=$5 config=$6 proj=$7; shift 7 [ "${1:-}" = -- ] && shift : > "$log" @@ -818,6 +821,7 @@ run_spawn_case() { # FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" \ FM_SPAWN_NO_GUARD=1 TMUX="fake,1,0" FM_TMUX_LOG="$log" \ + FM_FAKE_PANE_PATH="${FM_FAKE_PANE_PATH:-}" \ "$bin/bin/fm-spawn.sh" "$@" } @@ -844,9 +848,9 @@ run_spawn_case() { # # itself lives under a symlinked prefix (e.g. macOS's /tmp -> /private/tmp), # fm-spawn.sh's PROJ_ABS - a logical `cd && pwd` - differs string-for-string # from that physical read even before treehouse moves the pane at all, so the -# worktree-discovery poll used to mistake an UNMOVED pane for one that had -# already left the project, handing validate_spawn_worktree the project's own -# directory as "the worktree" and tripping its false isolation refusal. +# old discovery poll could mistake an UNMOVED pane for one that had already +# left the project, handing validate_spawn_worktree the project's own directory +# as "the worktree" and tripping its false isolation refusal. # make_spawn_symlink_fakebin's tmux stub returns an unmoved project path on the # first pane_current_path poll, then the real worktree path from the second poll # onward, so this test fails loudly if the PROJ_ABS/PROJ_ABS_REAL @@ -908,7 +912,7 @@ run_spawn_symlink_case() { #