diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index e66d231827..aff2a9f8e1 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -953,7 +953,6 @@ ORCA_TERMINAL= ORCA_TERMINAL_PROOF= ORCA_REPO_ID= ORCA_EXPECTED_TASK= -ORCA_PROVIDER_TASK= ID= ACCOUNT_LEASE_CREATED=0 FM_ACCOUNT_MUTATION_ACQUIRED=0 @@ -1044,7 +1043,6 @@ parse_orca_worktree_result() { rest=${rest#*$'\t'} ORCA_REPO_ID=$rest case "$ORCA_REPO_ID" in *$'\t'*) return 1 ;; esac - ORCA_PROVIDER_TASK= } persist_orca_cleanup_quarantine() { @@ -1435,12 +1433,12 @@ spawn_return_created_worktree() { return 1 fi if [ -z "$WORKTREE_EXPECTED_TIP" ] \ - || ! "$SCRIPT_DIR/fm-checkout-refresh.sh" verify-returnable "$WT" "$PROJ_ABS" "$WORKTREE_EXPECTED_TIP"; then + || ! "$SCRIPT_DIR/fm-checkout-refresh.sh" verify-returnable "$WT" "${PROJ_ABS_REAL:-$PROJ_ABS}" "$WORKTREE_EXPECTED_TIP"; then echo "warning: retained acquired worktree $WT because repository identity and its expected detached tip could not be re-proven" >&2 return 1 fi rm -f "$WT/.claude/settings.local.json" "$WT/.opencode/plugins/fm-turn-end.js" "$WT/.fm-grok-turnend" - if ! "$SCRIPT_DIR/fm-checkout-refresh.sh" verify-returnable "$WT" "$PROJ_ABS" "$WORKTREE_EXPECTED_TIP"; then + if ! "$SCRIPT_DIR/fm-checkout-refresh.sh" verify-returnable "$WT" "${PROJ_ABS_REAL:-$PROJ_ABS}" "$WORKTREE_EXPECTED_TIP"; then echo "warning: retained acquired worktree $WT because post-cleanup repository safety could not be re-proven" >&2 return 1 fi @@ -2751,7 +2749,7 @@ real_path_or_raw() { # # and resets the selected clean pool worktree from that remote-tracking ref. # The post-acquisition verification below is the fail-closed freshness proof. if [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ] && [ "$RECOVERY_ACCOUNT" != 1 ]; then - if CHECKOUT_PREFLIGHT_OUT=$("$SCRIPT_DIR/fm-checkout-refresh.sh" preflight "$PROJ_ABS" 2>&1); then + if CHECKOUT_PREFLIGHT_OUT=$("$SCRIPT_DIR/fm-checkout-refresh.sh" preflight "$PROJ_ABS_REAL" 2>&1); then CHECKOUT_PREFLIGHT_STATUS=0 else CHECKOUT_PREFLIGHT_STATUS=$? @@ -2839,12 +2837,12 @@ if [ "$DIRECT_ACCOUNT_RECOVERY" = 1 ]; then fi if [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ] && [ "$RECOVERY_ACCOUNT" != 1 ]; then - "$SCRIPT_DIR/fm-checkout-refresh.sh" pool-preflight "$PROJ_ABS" || { + "$SCRIPT_DIR/fm-checkout-refresh.sh" pool-preflight "$PROJ_ABS_REAL" || { echo "error: refusing Treehouse acquisition because pool safety could not be inspected for $PROJ_ABS" >&2 exit 1 } acquire_status=0 - WT=$("$SCRIPT_DIR/fm-checkout-refresh.sh" acquire-worktree "$PROJ_ABS" "firstmate-$ID") || acquire_status=$? + WT=$("$SCRIPT_DIR/fm-checkout-refresh.sh" acquire-worktree "$PROJ_ABS_REAL" "firstmate-$ID") || acquire_status=$? if [ "$acquire_status" -ne 0 ]; then if [ "$acquire_status" -eq 124 ]; then echo "error: refusing to spawn $ID after the bounded Treehouse acquisition timed out" >&2 @@ -2861,7 +2859,7 @@ if [ "$KIND" != secondmate ] && [ "$BACKEND" != orca ] && [ "$RECOVERY_ACCOUNT" WORKTREE_RETAIN_ON_ABORT=1 validate_spawn_worktree "treehouse get --lease" "$PROJ_ABS" freshness_status=0 - "$SCRIPT_DIR/fm-checkout-refresh.sh" verify-worktree "$WT" "$PROJ_ABS" || freshness_status=$? + "$SCRIPT_DIR/fm-checkout-refresh.sh" verify-worktree "$WT" "$PROJ_ABS_REAL" || freshness_status=$? if [ "$freshness_status" -ne 0 ]; then echo "error: refusing to launch fm-$ID from a leased worktree whose repository identity, cleanliness, or default-tip freshness could not be proved" >&2 exit 1 @@ -3417,8 +3415,8 @@ EOF echo "error: cannot durably record Orca create authority for $ID" >&2 exit 1 } - if [ -z "$ORCA_WORKTREE_ID" ] || [ -z "$WT" ] || [ "$ORCA_PROVIDER_TASK" != "$ORCA_EXPECTED_TASK" ]; then - echo "error: orca did not return matching worktree id, path, and task authority for $W" >&2 + if [ -z "$ORCA_WORKTREE_ID" ] || [ -z "$WT" ]; then + echo "error: orca did not return matching worktree id and path authority for $W" >&2 exit 1 fi validate_spawn_worktree "orca worktree create" "$W" diff --git a/bin/fm-teardown.sh b/bin/fm-teardown.sh index ea03eda10b..3580177468 100755 --- a/bin/fm-teardown.sh +++ b/bin/fm-teardown.sh @@ -23,6 +23,8 @@ # teardown refuses rather than risk discarding unlanded work. # Origin-backed content checks hold the shared checkout lock and require bounded # remote HEAD probes before and after fetch to agree before comparing trees. +# If the default history was rewritten after task-branch creation, the comparison +# uses a base recorded by both the branch-creation and default-branch reflogs. # Every authorized Treehouse return is process-tree bounded by # FM_TREEHOUSE_RETURN_TIMEOUT while holding the same common checkout mutation # lock across its retry and stale-index-lock recovery sequence. @@ -313,6 +315,7 @@ managed_endpoint_is_gone() { # [probe-home] case "$state" in absent) return 0 ;; present) + [ "$backend" != zellij ] || return 1 if [ -n "$probe_home" ]; then agent_state=$(unset FM_ROOT_OVERRIDE; FM_HOME="$probe_home" FM_ROOT="$probe_home" fm_backend_agent_alive "$backend" "$target" "$expected" "$recorded_scoped_target" 2>/dev/null) else @@ -797,20 +800,55 @@ pr_is_merged() { unpushed_patches_are_in_pr_head "$head" } +# Set TASK_BRANCH_CREATION_BASE only when the task branch's oldest surviving +# reflog entry is its creation record, remains an ancestor of HEAD, and also +# appears in the authoritative default ref's reflog. +task_branch_creation_base() { + local authoritative_ref=$1 branch_ref entry base message fork_point + TASK_BRANCH_CREATION_BASE= + branch_ref=$(git -C "$WT" symbolic-ref --quiet HEAD 2>/dev/null) || return 1 + entry=$(LC_ALL=C git -C "$WT" reflog show \ + --format='%H%x09%gs' "$branch_ref" 2>/dev/null | tail -1) + [ -n "$entry" ] || return 1 + base=${entry%%$'\t'*} + message=${entry#*$'\t'} + [ "$message" != "$entry" ] || return 1 + case "$message" in + "branch: Created from "*) ;; + *) return 1 ;; + esac + git -C "$WT" cat-file -e "$base^{commit}" 2>/dev/null || return 1 + git -C "$WT" merge-base --is-ancestor "$base" HEAD 2>/dev/null || return 1 + fork_point=$(git -C "$WT" merge-base \ + --fork-point "$authoritative_ref" HEAD 2>/dev/null) || return 1 + [ "$fork_point" = "$base" ] || return 1 + TASK_BRANCH_CREATION_BASE=$base +} + # Is the branch's content already present in the up-to-date default branch? # Origin-backed proof holds the common checkout lock across probe, fetch, # unchanged branch-and-tip re-probe, and tree comparison. +# The ordinary merge finds the net task change from shared history. +# When the default was rewritten past the task branch's creation point, an +# explicit branch-creation base isolates the task change from its stale baseline. content_matches_ref() { - local ref=$1 default_tree merged_tree + local ref=$1 default_tree merged_tree merge_output creation_base default_tree=$(git -C "$WT" rev-parse --quiet --verify "$ref^{tree}" 2>/dev/null) || return 1 [ -n "$default_tree" ] || return 1 - merged_tree=$(git -C "$WT" merge-tree --write-tree "$ref" HEAD 2>/dev/null) || return 1 - merged_tree=$(printf '%s\n' "$merged_tree" | head -1) - if [ "$merged_tree" != "$default_tree" ]; then - echo "teardown: task content is not present in authoritative $ref; retaining $WT" >&2 - return 1 + if merge_output=$(git -C "$WT" merge-tree --write-tree "$ref" HEAD 2>/dev/null); then + merged_tree=$(printf '%s\n' "$merge_output" | head -1) + [ "$merged_tree" != "$default_tree" ] || return 0 + fi + if task_branch_creation_base "$ref"; then + creation_base=$TASK_BRANCH_CREATION_BASE + if merge_output=$(git -C "$WT" merge-tree --write-tree \ + --merge-base "$creation_base" "$ref" HEAD 2>/dev/null); then + merged_tree=$(printf '%s\n' "$merge_output" | head -1) + [ "$merged_tree" != "$default_tree" ] || return 0 + fi fi - return 0 + echo "teardown: task content is not present in authoritative $ref; retaining $WT" >&2 + return 1 } content_in_origin_default() { @@ -1059,6 +1097,10 @@ validate_teardown_target_identity() { echo "error: teardown project metadata is not an exact inspectable repository root: ${PROJ:-}" >&2 return 1 } + if [ "$KIND" = scout ] && [ "$BACKEND" != orca ] && + [ ! -e "$WT" ] && [ ! -L "$WT" ]; then + return 0 + fi worktree_root=$(exact_git_worktree_root "$WT") || { echo "error: teardown worktree metadata is not an exact inspectable repository root: ${WT:-}" >&2 return 1 @@ -3074,6 +3116,25 @@ except OSError: PY } +secondmate_linked_worktree_is_recorded_child() { + local retiring_home=$1 repository=$2 linked_worktree=$3 child_metas child_meta child_kind child_project child_worktree + child_metas=$(secondmate_state_metadata "$retiring_home") || return 1 + while IFS= read -r child_meta; do + [ -n "$child_meta" ] || continue + child_kind=$(meta_value "$child_meta" kind) + [ -n "$child_kind" ] || child_kind=ship + [ "$child_kind" != secondmate ] || continue + child_project=$(meta_value "$child_meta" project) + child_worktree=$(meta_value "$child_meta" worktree) + [ "$(fm_checkout_trusted_dir "$child_project" 2>/dev/null)" = "$repository" ] || continue + [ "$(fm_checkout_trusted_dir "$child_worktree" 2>/dev/null)" = "$linked_worktree" ] || continue + return 0 + done <&2 return 1 diff --git a/tests/fm-backend-herdr.test.sh b/tests/fm-backend-herdr.test.sh index 69351fd2f5..b1d79ba6bd 100755 --- a/tests/fm-backend-herdr.test.sh +++ b/tests/fm-backend-herdr.test.sh @@ -2627,7 +2627,11 @@ test_capture_calls_pane_read() { # fetch bound; the adapter then trims to the caller's requested 250 lines # locally, so all 3 fake lines survive. out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_capture default:w1:p2 250' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_server_adapter_owned() { return 0; } + fm_backend_herdr_capture default:w1:p2 250 + ' "$ROOT" ) [ "$out" = $'line one\nline two\nline three' ] || fail "capture did not pass through pane read output, got '$out'" assert_contains "$(cat "$log")" "HERDR_SESSION=default"$'\x1f''pane'$'\x1f''read'$'\x1f''w1:p2'$'\x1f''--source'$'\x1f''recent'$'\x1f''--lines'$'\x1f''250' \ "capture did not call pane read with the right pane id and line bound" @@ -2644,7 +2648,11 @@ test_capture_works_around_small_lines_bug() { printf 'a\nb\nc\nd\ne\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_capture default:w1:p2 2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_server_adapter_owned() { return 0; } + fm_backend_herdr_capture default:w1:p2 2 + ' "$ROOT" ) [ "$out" = $'d\ne' ] || fail "a small --lines request should still return the last N lines (trimmed locally), got '$out'" assert_contains "$(cat "$log")" $'\x1f''--lines'$'\x1f''200' \ "capture should request a generous fetch (>=200), never the caller's small N, from herdr's own --lines flag" @@ -2657,7 +2665,11 @@ test_capture_preserves_pane_read_failure() { printf '1\n' > "$resp/1.exit" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_capture default:w1:p2 2' "$ROOT" 2>&1 ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_server_adapter_owned() { return 0; } + fm_backend_herdr_capture default:w1:p2 2 + ' "$ROOT" 2>&1 ) status=$? [ "$status" -ne 0 ] || fail "capture should fail when pane read fails, got output '$out'" assert_contains "$(cat "$log")" "HERDR_SESSION=default"$'\x1f''status'$'\x1f''--json' \ @@ -2672,7 +2684,11 @@ test_send_key_normalizes_and_targets_pane() { dir="$TMP_ROOT/sendkey"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" fb=$(make_herdr_fakebin "$dir") PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_key default:w1:p2 Escape' "$ROOT" + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_server_adapter_owned() { return 0; } + fm_backend_herdr_send_key default:w1:p2 Escape + ' "$ROOT" expect_code 0 $? "send_key should succeed" assert_contains "$(cat "$log")" $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''escape' "send_key did not normalize Escape to escape" pass "fm_backend_herdr_send_key: normalizes the key and targets the right pane" @@ -2684,7 +2700,11 @@ test_kill_is_best_effort() { printf '1\n' > "$resp/1.exit" fb=$(make_herdr_fakebin "$dir") PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_kill default:w1:p2' "$ROOT" + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_server_adapter_owned() { return 0; } + fm_backend_herdr_kill default:w1:p2 + ' "$ROOT" expect_code 0 $? "kill must be best-effort (never fail even when the pane close call itself fails)" assert_contains "$(cat "$log")" $'\x1f''pane'$'\x1f''close'$'\x1f''w1:p2' "kill did not call pane close on the right pane" pass "fm_backend_herdr_kill: calls pane close and stays best-effort on failure" @@ -2886,7 +2906,13 @@ test_current_path_reads_cwd() { printf '{"result":{"pane":{"cwd":"/tmp/pane-creation-dir","foreground_cwd":"/tmp/fake-worktree"}}}\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_current_path default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { + fm_backend_herdr_parse_target "$1" + } + fm_backend_herdr_current_path default:w1:p2 + ' "$ROOT" ) [ "$out" = "/tmp/fake-worktree" ] || fail "current_path should read foreground_cwd (the live process), not the frozen creation-time cwd, got '$out'" assert_contains "$(cat "$log")" $'\x1f''pane'$'\x1f''get'$'\x1f''w1:p2' "current_path did not call pane get" pass "fm_backend_herdr_current_path: reads pane foreground_cwd (the live running process), not the frozen creation-time cwd" @@ -2900,7 +2926,13 @@ test_busy_state_working_maps_to_busy() { printf '{"result":{"agent":{"agent_status":"working"}}}\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_busy_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { + fm_backend_herdr_parse_target "$1" + } + fm_backend_herdr_busy_state default:w1:p2 + ' "$ROOT" ) [ "$out" = busy ] || fail "agent_status=working should map to busy, got '$out'" assert_contains "$(cat "$log")" $'\x1f''agent'$'\x1f''get'$'\x1f''w1:p2' "busy_state did not call agent get" pass "fm_backend_herdr_busy_state: working -> busy" @@ -2912,14 +2944,26 @@ test_busy_state_done_and_blocked_map_to_idle() { printf '{"result":{"agent":{"agent_status":"done"}}}\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_busy_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { + fm_backend_herdr_parse_target "$1" + } + fm_backend_herdr_busy_state default:w1:p2 + ' "$ROOT" ) [ "$out" = idle ] || fail "agent_status=done should map to idle, got '$out'" dir="$TMP_ROOT/busy-blocked"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" printf '{"result":{"agent":{"agent_status":"blocked"}}}\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_busy_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { + fm_backend_herdr_parse_target "$1" + } + fm_backend_herdr_busy_state default:w1:p2 + ' "$ROOT" ) [ "$out" = idle ] || fail "agent_status=blocked should map to idle (stuck waiting on the human, not grinding), got '$out'" pass "fm_backend_herdr_busy_state: done -> idle, blocked -> idle (surfaced like a stale pane, not suppressed as busy)" } @@ -2930,7 +2974,13 @@ test_busy_state_unknown_on_no_agent() { printf '1\n' > "$resp/1.exit" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_busy_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { + fm_backend_herdr_parse_target "$1" + } + fm_backend_herdr_busy_state default:w1:p2 + ' "$ROOT" ) [ "$out" = unknown ] || fail "a failed agent get should report unknown (the fallback-to-regex cue), got '$out'" pass "fm_backend_herdr_busy_state: unparseable/absent agent state reports unknown, the regex-fallback cue" } @@ -2943,7 +2993,11 @@ test_composer_state_bare_prompt_is_empty() { printf ' ╭────────────────────────╮\n │ ❯ │\n ╰──────── Composer ─────╯\n\n Shift+Tab:mode\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = empty ] || fail "a bare prompt glyph should read as empty, got '$out'" pass "fm_backend_herdr_composer_state: a bare '❯' composer row reads empty" } @@ -2954,7 +3008,11 @@ test_composer_state_ghost_placeholder_is_empty() { printf ' ╭────────────────────────╮\n │ ❯ Type a message... │\n ╰──────── Composer ─────╯\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = empty ] || fail "the known ghost placeholder 'Type a message...' should read as empty, got '$out'" pass "fm_backend_herdr_composer_state: the ghost placeholder text reads empty, not pending" } @@ -2965,7 +3023,11 @@ test_composer_state_real_text_is_pending() { printf ' ╭────────────────────────╮\n │ ❯ hello captain │\n ╰──────── Composer ─────╯\n\n Enter:send\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = pending ] || fail "real unsubmitted text should read as pending, got '$out'" pass "fm_backend_herdr_composer_state: real composer text reads pending" } @@ -2984,7 +3046,11 @@ test_composer_state_popup_placeholder_fill_is_pending() { printf ' ╭──────────────────────────────────────╮\n │ ❯ /compact compaction instructions │\n ╰──────────────── Composer ─────────────╯\n\n Enter:send\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = pending ] || fail "a popup-close-with-placeholder-fill must still read as pending (not yet submitted), got '$out'" pass "fm_backend_herdr_composer_state: a slash-command popup's argument-hint placeholder still reads pending (the incident fix)" } @@ -2995,7 +3061,11 @@ test_composer_state_unknown_on_capture_failure() { printf '1\n' > "$resp/1.exit" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) status=$? [ "$status" -eq 0 ] || fail "composer_state should not itself fail the caller" [ "$out" = unknown ] || fail "an unreadable pane should read as unknown, got '$out'" @@ -3012,7 +3082,11 @@ test_composer_state_unknown_when_no_composer_row_found() { fb=$(make_herdr_fakebin "$dir") for glyph in '>' '$' '%' '#'; do out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = unknown ] || fail "a bare shell prompt '$glyph' should read as unknown, got '$out'" done pass "fm_backend_herdr_composer_state: reports unknown for bare shell prompts with no composer row" @@ -3037,7 +3111,11 @@ test_composer_state_claude_unbordered_prompt_is_empty() { printf ' 20\n 21\n\n\xe2\x9c\xbb Worked for 2s\n\n\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\n\xe2\x9d\xaf\n\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\n Opus 4.8 (1M context) \xe2\x96\x8d 3%%\n \xe2\x86\x90 for agents\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = empty ] || fail "a genuinely idle, unbordered real-claude '❯' prompt row (no border glyph anywhere in view) should read empty, got '$out' (regression: this used to read 'unknown' forever, which is exactly what broke escalate_flush's buffer-clear)" pass "fm_backend_herdr_composer_state: a real-claude unbordered '❯' prompt row (no border box in view) reads empty" } @@ -3048,7 +3126,11 @@ test_composer_state_claude_unbordered_prompt_is_pending() { printf ' 20\n 21\n\n\xe2\x9c\xbb Worked for 2s\n\n\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\n\xe2\x9d\xaf hello there this is a test message\n\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = pending ] || fail "real unsubmitted text in an unbordered real-claude prompt row should read pending, got '$out'" pass "fm_backend_herdr_composer_state: a real-claude unbordered '❯ ' prompt row reads pending" } @@ -3069,7 +3151,11 @@ test_composer_state_bare_prompt_below_stale_bordered_banner_wins() { printf '\xe2\x95\xad\xe2\x94\x80 Claude Code \xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x95\xae\n\xe2\x94\x82 Welcome back Kun! \xe2\x94\x82\n\xe2\x94\x82 \xe2\x94\x82\n\xe2\x95\xb0\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x95\xaf\n\n\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\n\xe2\x9d\xaf still typing captain\n\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = pending ] || fail "the live unbordered prompt row below a stale bordered banner must win (pending, real text present), got '$out'" pass "fm_backend_herdr_composer_state: a live unbordered prompt row below a stale bordered decorative box still wins (not misread as the box's own row)" } @@ -3092,7 +3178,11 @@ test_composer_state_claude_dim_prompt_suggestion_ghost_is_empty() { printf '\xe2\x9c\xbb Brewed for 2m 40s\n\n\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\n\xe2\x9d\xaf \x1b[0m\x1b[2mwhat did the wheelhouse healing verification find?\x1b[0m\n\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\n Fable 5 80%%\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p3' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p3 ) [ "$out" = empty ] || fail "the overnight shape - claude's SGR-2 dim prompt-suggestion ghost after a bare '❯' - must read empty, got '$out' (regression: this false-pending wedged away-mode injection all night)" pass "fm_backend_herdr_composer_state: claude's dim prompt-suggestion ghost (the overnight wedge shape) reads empty" } @@ -3106,7 +3196,11 @@ test_composer_state_claude_dim_ghost_row_with_real_text_is_pending() { printf '\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\n\xe2\x9d\xaf land pr 416 now\n\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\n Fable 5 80%%\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p3' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p3 ) [ "$out" = pending ] || fail "real normal-intensity text after '❯' must still read pending, got '$out'" pass "fm_backend_herdr_composer_state: real typed text on the same claude prompt row still reads pending" } @@ -3123,7 +3217,11 @@ test_composer_state_grok_dark_truecolor_placeholder_is_empty() { printf ' \x1b[38;2;86;82;110m\xe2\x95\xad\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x95\xae\x1b[39m\n \x1b[38;2;86;82;110m\xe2\x94\x82\x1b[38;2;224;222;244m \xe2\x9d\xaf \x1b[38;2;50;47;70mType a message...\x1b[38;2;86;82;110m \xe2\x94\x82\x1b[39m\n \x1b[38;2;86;82;110m\xe2\x95\xb0\xe2\x94\x80\xe2\x94\x80\xe2\x94\x80\xe2\x95\xaf\x1b[39m\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = empty ] || fail "a grok bordered composer whose only content is a dark-truecolor placeholder must read empty, got '$out'" pass "fm_backend_herdr_composer_state: grok's dark-truecolor placeholder (the TRUECOLOR gap) reads empty" } @@ -3135,7 +3233,11 @@ test_composer_state_grok_bright_truecolor_real_text_is_pending() { printf ' \x1b[38;2;86;82;110m\xe2\x94\x82\x1b[38;2;224;222;244m \xe2\x9d\xaf fix the login bug \x1b[38;2;86;82;110m\xe2\x94\x82\x1b[39m\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = pending ] || fail "real bright typed text in a grok bordered composer must read pending, got '$out'" pass "fm_backend_herdr_composer_state: grok's real bright typed input still reads pending" } @@ -3146,7 +3248,11 @@ test_composer_state_codex_bare_prompt_glyph_is_empty() { printf '\xe2\x80\xa2 You have 2 usage limit resets available.\n\n\xe2\x80\xba\n\n gpt-5.5 xhigh \xc2\xb7 Context 100%% left\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = empty ] || fail "a bare '›' (codex) prompt glyph with no trailing text should read empty, got '$out'" pass "fm_backend_herdr_composer_state: a real-codex unbordered '›' prompt row reads empty" } @@ -3157,7 +3263,11 @@ test_composer_state_codex_faint_suggestion_is_empty() { printf '\xe2\x80\xa2 You have 2 usage limit resets available. Run /usage\nto use one.\n\n\x1b[0m\x1b[1m\xe2\x80\xba \x1b[0m\x1b[2mFind and fix a bug in @filename\x1b[0m\n\n gpt-5.5 xhigh \xc2\xb7 Context 100%% left\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = empty ] || fail "a faint real-codex ghost suggestion should read empty, not pending, got '$out'" pass "fm_backend_herdr_composer_state: a faint real-codex ghost suggestion reads empty" } @@ -3168,7 +3278,11 @@ test_composer_state_codex_non_faint_same_text_is_pending() { printf '\xe2\x80\xa2 You have 2 usage limit resets available. Run /usage\nto use one.\n\n\x1b[0m\x1b[1m\xe2\x80\xba \x1b[0mFind and fix a bug in @filename\n\n gpt-5.5 xhigh \xc2\xb7 Context 100%% left\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = pending ] || fail "the same words without faint styling should still protect real typed input, got '$out'" pass "fm_backend_herdr_composer_state: non-faint codex prompt text still reads pending" } @@ -3239,7 +3353,7 @@ test_send_text_submit_applies_herdr_minimum_confirm_budget() { printf '{"result":{"agent":{"agent_status":"working"}}}\n' > "$resp/9.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_SLEEP_LOG="$sleep_log" FM_BACKEND_HERDR_SUBMIT_POLLS=6 FM_BACKEND_HERDR_SUBMIT_MIN_SLEEP=0.6 \ - bash -c '. "$0/bin/backends/herdr.sh"; sleep() { printf "sleep:%s\n" "$1" >> "$FM_SLEEP_LOG"; }; fm_backend_herdr_send_text_submit default:w1:p2 "hello captain" 1 0.4 0' "$ROOT" ) + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_server_adapter_owned() { return 0; }; sleep() { printf "sleep:%s\n" "$1" >> "$FM_SLEEP_LOG"; }; fm_backend_herdr_send_text_submit default:w1:p2 "hello captain" 1 0.4 0' "$ROOT" ) [ "$out" = empty ] || fail "send_text_submit should catch a slow-but-valid transition inside the herdr minimum budget, got '$out'" sleeps=$(grep -c '^sleep:0.1200$' "$sleep_log") [ "$sleeps" -eq 5 ] || fail "a 0.4s caller budget should be expanded to five 0.1200s sleeps across the 0.6s herdr floor, got $sleeps; log: $(cat "$sleep_log")" @@ -3304,7 +3418,7 @@ test_send_text_submit_detects_landed_send() { printf '{"result":{"agent":{"agent_status":"working"}}}\n' > "$resp/4.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_BACKEND_HERDR_SUBMIT_POLLS=1 \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "hello captain" 3 0.01 0.01' "$ROOT" ) + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_server_adapter_owned() { return 0; }; fm_backend_herdr_send_text_submit default:w1:p2 "hello captain" 3 0.01 0.01' "$ROOT" ) [ "$out" = empty ] || fail "send_text_submit should report empty (submitted) once agent_status reports working, got '$out'" assert_contains "$(cat "$log")" $'\x1f''agent'$'\x1f''send'$'\x1f''w1:p2'$'\x1f''hello captain' \ "send_text_submit did not address the registered agent with the literal text first" @@ -3324,7 +3438,7 @@ test_send_text_submit_detects_swallowed_enter() { printf '{"result":{"agent":{"agent_status":"idle"}}}\n' > "$resp/6.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_BACKEND_HERDR_SUBMIT_POLLS=1 \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "hello captain" 2 0.01 0.01' "$ROOT" ) + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_server_adapter_owned() { return 0; }; fm_backend_herdr_send_text_submit default:w1:p2 "hello captain" 2 0.01 0.01' "$ROOT" ) [ "$out" = pending ] || fail "send_text_submit should report pending once retries are exhausted with agent_status never going busy, got '$out'" pass "fm_backend_herdr_send_text_submit: reports 'pending' when agent_status never reports working after retried Enters (swallowed)" } @@ -3349,7 +3463,7 @@ test_send_text_submit_popup_autocomplete_requires_second_enter() { printf '{"result":{"agent":{"agent_status":"working"}}}\n' > "$resp/6.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_BACKEND_HERDR_SUBMIT_POLLS=1 \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "/compact" 3 0.01 1.2' "$ROOT" ) + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_server_adapter_owned() { return 0; }; fm_backend_herdr_send_text_submit default:w1:p2 "/compact" 3 0.01 1.2' "$ROOT" ) [ "$out" = empty ] || fail "send_text_submit should eventually report empty once the SECOND Enter actually starts a turn, got '$out'" enter_count=$(grep -c $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''enter' "$log") [ "$enter_count" -eq 2 ] || fail "send_text_submit must send a SECOND Enter after the popup-placeholder fill's agent_status still reads idle, got $enter_count Enter(s)" @@ -3364,7 +3478,7 @@ test_send_text_submit_confirms_blocked_after_enter() { printf '{"result":{"agent":{"agent_status":"blocked"}}}\n' > "$resp/4.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_BACKEND_HERDR_SUBMIT_POLLS=1 \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "needs approval" 3 0.01 0.01' "$ROOT" ) + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_server_adapter_owned() { return 0; }; fm_backend_herdr_send_text_submit default:w1:p2 "needs approval" 3 0.01 0.01' "$ROOT" ) [ "$out" = empty ] || fail "send_text_submit should treat a blocked state after Enter as a confirmed delivered prompt, got '$out'" enter_count=$(grep -c $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''enter' "$log") [ "$enter_count" -eq 1 ] || fail "blocked after Enter must not provoke a retry into the prompt, sent $enter_count Enter(s)" @@ -3380,7 +3494,7 @@ test_send_text_submit_preexisting_working_does_not_false_confirm_swallowed_enter printf ' \xe2\x9d\xaf hello captain\n' > "$resp/6.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "hello captain" 2 0.01 0.01' "$ROOT" ) + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_server_adapter_owned() { return 0; }; fm_backend_herdr_send_text_submit default:w1:p2 "hello captain" 2 0.01 0.01' "$ROOT" ) [ "$out" = pending ] || fail "send_text_submit must not accept preexisting working as proof that this Enter landed, got '$out'" enter_count=$(grep -c $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''enter' "$log") [ "$enter_count" -eq 2 ] || fail "preexisting-working swallowed Enter should retry Enter up to the configured count, sent $enter_count Enter(s)" @@ -3400,7 +3514,7 @@ test_send_text_submit_confirms_despite_codex_idle_tip_composer() { printf '{"result":{"agent":{"agent_status":"working"}}}\n' > "$resp/4.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_BACKEND_HERDR_SUBMIT_POLLS=1 \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "reply with just OK" 3 0.01 0.01' "$ROOT" ) + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_server_adapter_owned() { return 0; }; fm_backend_herdr_send_text_submit default:w1:p2 "reply with just OK" 3 0.01 0.01' "$ROOT" ) [ "$out" = empty ] || fail "send_text_submit should confirm via agent_status alone even for a harness whose idle composer shows dynamic tip text, got '$out'" [ "$(grep -c $'\x1f''pane'$'\x1f''read' "$log")" -eq 0 ] || fail "send_text_submit must never call 'pane read' - a codex-style dynamic idle-tip composer can never mislead a confirmation path that does not read it" pass "fm_backend_herdr_send_text_submit: confirms submission via native agent-state alone, immune to a codex-style dynamic idle-tip composer that would have misread as 'pending' under the old composer-based confirmation" @@ -3417,7 +3531,11 @@ test_composer_state_codex_dynamic_idle_tip_reads_empty_when_faint() { printf '\xe2\x80\xa2 OK\n\n\n\x1b[0m\x1b[1m\xe2\x80\xba \x1b[0m\x1b[2mSummarize recent commits\x1b[0m\n\n gpt-5.5 xhigh \xc2\xb7 Context 97%% left \xc2\xb7 /private/tmp \xc2\xb7 2\xe2\x80\xa6\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_herdr_composer_state "$1" + ' "$ROOT" default:w1:p2 ) [ "$out" = empty ] || fail "a faint real-codex dynamic idle-tip row should read empty, got '$out'" pass "fm_backend_herdr_composer_state: a faint real-codex dynamic idle-tip composer row reads empty" } @@ -3434,7 +3552,13 @@ test_composer_state_guard_still_refuses_real_pending_text_after_submit_confirmat printf ' \xe2\x9d\xaf hello there this is a test message\n' > "$resp/1.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ - bash -c '. "$0/bin/fm-backend.sh"; fm_backend_composer_state herdr default:w1:p2' "$ROOT" ) + bash -c ' + . "$0/bin/fm-backend.sh" + . "$0/bin/backends/herdr.sh" + fm_backend_herdr_target_ready() { fm_backend_herdr_parse_target "$1"; } + fm_backend_source() { :; } + fm_backend_composer_state herdr default:w1:p2 + ' "$ROOT" ) [ "$out" = pending ] || fail "the pre-injection empty-box guard must still refuse real unsubmitted composer text after this change, got '$out'" pass "fm_backend_composer_state (herdr): the pre-injection empty-box guard still refuses a genuinely non-empty composer, unaffected by the submit-confirmation change" } @@ -3454,7 +3578,7 @@ test_send_text_submit_slow_transition_within_one_enter_needs_no_extra_enter() { printf '{"result":{"agent":{"agent_status":"working"}}}\n' > "$resp/6.out" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_BACKEND_HERDR_SUBMIT_POLLS=3 \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "hello captain" 3 0.03 0.01' "$ROOT" ) + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_server_adapter_owned() { return 0; }; fm_backend_herdr_send_text_submit default:w1:p2 "hello captain" 3 0.03 0.01' "$ROOT" ) [ "$out" = empty ] || fail "send_text_submit should confirm once a later sample within the SAME Enter attempt observes working, got '$out'" enter_count=$(grep -c $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''enter' "$log") [ "$enter_count" -eq 1 ] || fail "a slow (but within-budget) transition must not provoke a needless extra Enter, sent $enter_count Enter(s)" @@ -3469,7 +3593,7 @@ test_send_text_submit_send_failed() { printf '1\n' > "$resp/2.exit" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_BACKEND_HERDR_SUBMIT_POLLS=1 \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "x" 2 0.01 0.01' "$ROOT" ) + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_server_adapter_owned() { return 0; }; fm_backend_herdr_send_text_submit default:w1:p2 "x" 2 0.01 0.01' "$ROOT" ) [ "$out" = send-failed ] || fail "send_text_submit should report send-failed when the literal send itself fails, got '$out'" pass "fm_backend_herdr_send_text_submit: reports 'send-failed' when native agent send and the pane-send fallback both error" } @@ -3481,7 +3605,7 @@ test_send_text_submit_unknown_on_capture_failure() { printf '1\n' > "$resp/4.exit" fb=$(make_herdr_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_BACKEND_HERDR_SUBMIT_POLLS=1 \ - bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "x" 2 0.01 0.01' "$ROOT" ) + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_server_adapter_owned() { return 0; }; fm_backend_herdr_send_text_submit default:w1:p2 "x" 2 0.01 0.01' "$ROOT" ) [ "$out" = unknown ] || fail "send_text_submit should report unknown when the post-Enter agent-get read fails, got '$out'" enter_count=$(grep -c $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''enter' "$log") [ "$enter_count" -eq 1 ] || fail "send_text_submit must never retry past an unreadable target (that is a hard I/O failure, not a timing race), sent $enter_count Enter(s)" @@ -3531,14 +3655,25 @@ test_dispatch_composer_state_routes_by_backend() { } test_scripts_route_explicit_target_through_meta_backend() { - local dir state log resp fb neutral out + local dir state log resp fb neutral lock_root out dir="$TMP_ROOT/script-explicit-target"; state="$dir/state"; mkdir -p "$state" "$dir/responses" log="$dir/log"; resp="$dir/responses"; : > "$log" neutral="$dir/neutral-root"; mkdir -p "$neutral" + lock_root="$dir/locks"; mkdir -m 700 "$lock_root" fm_write_meta "$state/herdr-stale.meta" \ "window=default:w1:p2" "backend=herdr" \ "herdr_workspace_id=w1" "herdr_tab_id=w1:t2" "herdr_pane_id=w1:p2" touch "$state/.last-watcher-beat" + FM_BACKEND_HERDR_SERVER_LOCK_ROOT="$lock_root" TEST_SERVER_PID="$$" \ + bash -c ' + . "$0/bin/backends/herdr.sh" + key=$(fm_backend_herdr_server_lock_key default) || exit 1 + certificate=$(fm_backend_herdr_server_legacy_env_certificate_path default) || exit 1 + start=$(fm_backend_herdr_process_start "$TEST_SERVER_PID") || exit 1 + printf "firstmate-herdr-closed-env-v1\n%s\n%s\n%s\n" \ + "$key" "$TEST_SERVER_PID" "$start" > "$certificate" || exit 1 + chmod 600 "$certificate" + ' "$ROOT" || fail "could not establish adapter ownership for the explicit-target routing fixture" printf 'captured herdr pane\n' > "$resp/1.out" printf '{"result":{"panes":[{"pane_id":"w1:p2","tab_id":"w1:t2","workspace_id":"w1"}]}}\n' > "$resp/2.out" printf '{"result":{"workspaces":[{"workspace_id":"w1","label":"firstmate"}]}}\n' > "$resp/3.out" @@ -3554,6 +3689,7 @@ SH out=$( PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" \ FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ + FM_BACKEND_HERDR_SERVER_LOCK_ROOT="$lock_root" \ "$ROOT/bin/fm-peek.sh" default:w1:p2 5 2>/dev/null ) [ "$out" = "captured herdr pane" ] || fail "fm-peek did not capture through herdr for an explicit metadata-matched target, got '$out'" assert_contains "$(cat "$log")" $'\x1f''pane'$'\x1f''read'$'\x1f''w1:p2' \ @@ -3562,6 +3698,7 @@ SH : > "$log" PATH="$fb:$PATH" FM_ROOT_OVERRIDE="$neutral" FM_HOME="$neutral" FM_STATE_OVERRIDE="$state" \ FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ + FM_BACKEND_HERDR_SERVER_LOCK_ROOT="$lock_root" \ "$ROOT/bin/fm-send.sh" default:w1:p2 --key Escape >/dev/null 2>&1 expect_code 0 $? "fm-send --key should route an explicit metadata-matched target through herdr" assert_contains "$(cat "$log")" $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''escape' \ @@ -4201,6 +4338,24 @@ if [ "${FM_TEST_FOCUSED:-}" = workspace-prune ]; then exit 0 fi +if [ "${FM_TEST_FOCUSED:-}" = workspace-lifecycle ]; then + test_workspace_ensure_prunes_default_tab + test_repeated_cycles_reuse_one_workspace_no_orphans + exit 0 +fi + +if [ "${FM_TEST_FOCUSED:-}" = capture ]; then + test_capture_calls_pane_read + test_capture_works_around_small_lines_bug + test_capture_preserves_pane_read_failure + exit 0 +fi + +if [ "${FM_TEST_FOCUSED:-}" = script-routing ]; then + test_scripts_route_explicit_target_through_meta_backend + exit 0 +fi + test_version_check_accepts_current_protocol test_version_check_refuses_old_protocol test_version_check_refuses_missing_herdr diff --git a/tests/fm-backend-orca.test.sh b/tests/fm-backend-orca.test.sh index 63e04b4f2c..8ebf886e76 100755 --- a/tests/fm-backend-orca.test.sh +++ b/tests/fm-backend-orca.test.sh @@ -160,6 +160,10 @@ process.stdout.write(JSON.stringify(data) + "\n"); if [ -n "${FM_ORCA_REMOVE_ERROR:-}" ]; then printf '{"ok":false,"error":{"code":"worktree_not_removed","message":"worktree not removed"}}\n' else + if [ -f "$RESP/.remove-worktree-path" ]; then + path=$(cat "$RESP/.remove-worktree-path") + git -C "$path" worktree remove --force "$path" || exit 1 + fi printf '{"ok":true,"result":{"removed":true}}\n' fi ;; @@ -192,9 +196,21 @@ SH } initialize_secondmate_home_repo() { - local home=$1 source=$2 + local home=$1 source=$2 default_branch project_origin + mkdir -p "$source/projects" + git clone --quiet "$home/projects/alpha" "$source/projects/alpha" + project_origin="$(dirname "$source")/alpha-origin.git" + git clone --quiet --bare "$source/projects/alpha" "$project_origin" + git -C "$source/projects/alpha" remote set-url origin "$project_origin" + git -C "$home/projects/alpha" remote add origin "$project_origin" fm_git_init_commit "$home" git -C "$home" remote add origin "$source" + git -C "$home" add -A + git -C "$home" -c user.name='Firstmate Tests' -c user.email=tests@example.invalid \ + commit -qm 'fixture state' + default_branch=$(git -C "$source" branch --show-current) + git -C "$source" config receive.denyCurrentBranch ignore + git -C "$home" push --quiet --force origin "HEAD:$default_branch" } add_tmux_fake() { @@ -507,7 +523,7 @@ test_kill_propagates_close_failure() { PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ bash -c '. "$0/bin/backends/orca.sh"; fm_backend_orca_kill term-123' "$ROOT" status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "kill should propagate an Orca close failure" assert_contains "$(cat "$LOG")" $'orca\x1f''terminal'$'\x1f''close'$'\x1f''--terminal'$'\x1f''term-123'$'\x1f''--json' \ "kill did not call orca terminal close" @@ -576,7 +592,7 @@ test_remove_worktree_requires_bound_provider_capability() { FM_ORCA_TEST_BOUND_REMOVAL_CAPABILITIES=unavailable \ bash -c '. "$0/bin/backends/orca.sh"; fm_backend_orca_remove_worktree_bound wt-retained /tmp/orca-wt "$1"' "$ROOT" "$token" 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "Orca removal proceeded without a bound provider capability" assert_contains "$out" "identity-bound provider capability" \ "unbound Orca removal did not surface its provider limitation" @@ -661,7 +677,7 @@ test_worktree_create_retains_partial_authority_when_path_missing() { out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ bash -c '. "$0/bin/backends/orca.sh"; fm_backend_orca_worktree_create /repo/path fm-task' "$ROOT" 2>&1 ) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "worktree helper should fail when Orca omits the worktree path" assert_contains "$out" "orca worktree create returned incomplete or unsuccessful authority for fm-task" \ "worktree helper did not explain the missing path" @@ -685,7 +701,7 @@ test_worktree_create_never_cleans_partial_response_inline() { out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ bash -c '. "$0/bin/backends/orca.sh"; fm_backend_orca_worktree_create /repo/path fm-task' "$ROOT" 2>&1 ) status=$? - set -e + set +e expect_code 2 "$status" "pathless Orca worktree partial-response status" assert_contains "$out" $'wt-close-failure\t\tterm-close-failure' \ "partial response did not return durable Orca cleanup identity" @@ -756,6 +772,17 @@ test_legacy_respawn_writes_orca_metadata_and_launches_harness() { printf '1\n' > "$RESP/1.exit" printf '{"ok":true,"result":{"repo":{"id":"repo-spawn"}}}\n' > "$RESP/2.out" printf '{"ok":true,"result":{"worktree":{"id":"wt-spawn","path":"%s"},"terminal":{"handle":"term-spawn"}}}\n' "$wt" > "$RESP/3.out" + printf '{"ok":true,"result":{"worktree":{"id":"wt-spawn","path":"%s","name":"fm-%s","terminals":[{"handle":"term-spawn","title":"fm-%s"}]}}}\n' \ + "$wt" "$id" "$id" > "$RESP/4.out" + printf '{"ok":true,"result":{"terminal":{"handle":"term-spawn","title":"fm-%s","worktreeId":"wt-spawn","tail":[]}}}\n' \ + "$id" > "$RESP/5.out" + cp "$RESP/4.out" "$RESP/6.out" + cp "$RESP/5.out" "$RESP/7.out" + cp "$RESP/4.out" "$RESP/8.out" + cp "$RESP/4.out" "$RESP/9.out" + printf '{"ok":true,"result":{"send":{"handle":"term-spawn","accepted":true}}}\n' > "$RESP/10.out" + cp "$RESP/10.out" "$RESP/11.out" + cp "$RESP/10.out" "$RESP/12.out" out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ FM_ROOT_OVERRIDE="$ROOT" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ @@ -775,8 +802,10 @@ test_legacy_respawn_writes_orca_metadata_and_launches_harness() { assert_no_grep "report_required=" "$state/$id.meta" "legacy respawn must preserve the absent report_required marker" assert_not_contains "$(cat "$log")" $'orca\x1f''terminal'$'\x1f''create' \ "spawn should reuse the implicit terminal returned by Orca worktree creation" - assert_contains "$(cat "$log")" $'orca\x1f''terminal'$'\x1f''send'$'\x1f''--terminal'$'\x1f''term-spawn'$'\x1f''--text'$'\x1f''export GOTMPDIR=/tmp/fm-orcaspawnz1/gotmp'$'\x1f''--enter'$'\x1f''--json' \ - "spawn did not export GOTMPDIR through the Orca terminal" + assert_contains "$(cat "$log")" $'orca\x1f''terminal'$'\x1f''send'$'\x1f''--terminal'$'\x1f''term-spawn'$'\x1f''--text'$'\x1f''export PATH=' \ + "spawn did not export the hardened crew PATH through the Orca terminal" + assert_contains "$(cat "$log")" "GOTMPDIR=/tmp/fm-orcaspawnz1/gotmp"$'\x1f''--enter'$'\x1f''--json' \ + "spawn did not export the task-scoped GOTMPDIR through the Orca terminal" assert_contains "$(cat "$log")" "CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions" \ "spawn did not send the selected harness launch command through Orca" rm -rf "/tmp/fm-$id" @@ -869,7 +898,7 @@ test_spawn_refuses_malformed_legacy_orca_report_metadata() { FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1 ) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "malformed legacy Orca report metadata was launched" assert_contains "$out" "invalid report_required metadata for $id" \ "malformed legacy Orca report metadata was not diagnosed" @@ -1116,7 +1145,7 @@ test_spawn_refuses_orca_without_verified_authority_capabilities() { FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "spawn accepted Orca without verified lifecycle authority capabilities" assert_contains "$out" "Orca lifecycle authority is disabled" \ @@ -1151,7 +1180,7 @@ test_spawn_refuses_orca_nonisolated_worktree() { FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1 ) status=$? - set -e + set +e expect_code 1 "$status" "fm-spawn.sh --backend orca should refuse a primary checkout worktree" assert_contains "$out" "orca worktree create did not yield an isolated worktree" \ "Orca spawn should reuse the isolated-worktree guard" @@ -1195,7 +1224,7 @@ test_spawn_quarantines_unrelated_orca_worktree() { FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "spawn accepted an Orca worktree from an unrelated repository" assert_contains "$out" "returned a worktree from an unrelated repository" \ @@ -1239,7 +1268,7 @@ test_spawn_quarantines_unbound_orca_terminal() { FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "spawn accepted a terminal not bound to its expected task" assert_contains "$out" "terminal is not authoritatively bound" \ @@ -1273,7 +1302,9 @@ test_spawn_quarantines_orca_worktree_when_terminal_create_fails() { printf '1\n' > "$RESP/1.exit" printf '{"ok":true,"result":{"repo":{"id":"repo-terminal-fail"}}}\n' > "$RESP/2.out" printf '{"ok":true,"result":{"worktree":{"id":"wt-terminal-fail","path":"%s"}}}\n' "$wt" > "$RESP/3.out" - printf '1\n' > "$RESP/4.exit" + printf '{"ok":true,"result":{"worktree":{"id":"wt-terminal-fail","path":"%s"}}}\n' "$wt" > "$RESP/4.out" + printf '1\n' > "$RESP/5.exit" + printf '1\n' > "$RESP/6.exit" out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ FM_ROOT_OVERRIDE="$ROOT" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ @@ -1354,7 +1385,7 @@ test_spawn_retains_orca_worktree_when_abort_close_fails() { FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1 ) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "Orca spawn should fail after non-isolated worktree creation" assert_contains "$out" "retaining Orca cleanup metadata" \ "abort close failure did not surface durable retention" @@ -1388,7 +1419,7 @@ test_teardown_rejects_symlinked_orca_task_metadata() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" foo --force 2>&1 ) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "symlinked Orca task metadata was accepted" assert_contains "$out" "task metadata must be a real readable file for foo" \ "symlinked Orca task metadata was not diagnosed" @@ -1502,7 +1533,7 @@ test_target_exists_rejects_orca_error_json() { PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ bash -c '. "$0/bin/fm-backend.sh"; fm_backend_target_exists orca term-stale fm-task' "$ROOT" status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "fm_backend_target_exists should reject Orca ok:false read JSON" pass "fm_backend_target_exists: Orca ok:false read JSON is not live" } @@ -1531,7 +1562,7 @@ test_scout_teardown_removes_orca_worktree_via_helper() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1 ) rc=$? - set -e + set +e expect_code 0 "$rc" "Orca scout teardown should succeed once report exists"$'\n'"$out" assert_contains "$(cat "$LOG")" $'orca\x1f''terminal'$'\x1f''close'$'\x1f''--terminal'$'\x1f''term-teardown'$'\x1f''--json' \ "teardown did not close the recorded Orca terminal" @@ -1567,7 +1598,7 @@ test_scout_teardown_refuses_orca_id_path_mismatch() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1 ) rc=$? - set -e + set +e [ "$rc" -ne 0 ] || fail "Orca scout teardown should refuse when id path differs from worktree=" assert_contains "$out" "not inspected worktree" \ "mismatched Orca scout worktree path refusal should name the mismatch" @@ -1602,9 +1633,9 @@ test_teardown_refuses_orca_worktree_when_path_missing() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1 ) rc=$? - set -e + set +e [ "$rc" -ne 0 ] || fail "Orca teardown should refuse when its worktree path is absent" - assert_contains "$out" "teardown worktree metadata is not an exact repository root" \ + assert_contains "$out" "teardown worktree metadata is not an exact inspectable repository root" \ "pathless Orca teardown should surface the unprovable target identity" [ ! -s "$LOG" ] || fail "pathless Orca teardown should not close a terminal or remove a worktree" assert_present "$state/$id.meta" "pathless Orca teardown should preserve task metadata" @@ -1635,7 +1666,7 @@ test_teardown_preserves_metadata_when_orca_remove_error_json() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1 ) rc=$? - set -e + set +e [ "$rc" -ne 0 ] || fail "Orca teardown should fail when worktree removal returns ok:false JSON" assert_contains "$out" "worktree not removed" "teardown should surface the Orca removal error" assert_present "$state/$id.meta" "failed Orca removal should preserve task metadata" @@ -1663,7 +1694,7 @@ test_scout_teardown_refuses_orca_missing_report_when_path_missing() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1 ) rc=$? - set -e + set +e [ "$rc" -ne 0 ] || fail "Orca scout teardown should refuse without a report even when the path is absent" assert_contains "$out" "has no report" "Orca scout teardown should explain the missing report" [ ! -s "$LOG" ] || fail "refused Orca scout teardown should not close terminals or remove worktrees" @@ -1693,9 +1724,9 @@ test_ship_teardown_refuses_orca_missing_worktree_path() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1 ) rc=$? - set -e + set +e [ "$rc" -ne 0 ] || fail "Orca ship teardown should refuse a missing worktree path" - assert_contains "$out" "no inspectable git worktree" \ + assert_contains "$out" "teardown worktree metadata is not an exact inspectable repository root" \ "Orca ship teardown should explain the fail-closed worktree requirement" [ ! -s "$LOG" ] || fail "refused Orca ship teardown should not close terminals or remove worktrees" assert_present "$state/$id.meta" "refused Orca ship teardown should preserve metadata" @@ -1725,7 +1756,7 @@ test_ship_teardown_removes_orca_worktree_when_id_path_matches() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1 ) rc=$? - set -e + set +e expect_code 0 "$rc" "Orca ship teardown should succeed when the id path matches the inspected worktree"$'\n'"$out" assert_contains "$(cat "$LOG")" $'orca\x1f''worktree'$'\x1f''show'$'\x1f''--worktree'$'\x1f''id:wt-ship-match'$'\x1f''--json' \ "teardown did not resolve the Orca worktree id before removal" @@ -1762,7 +1793,7 @@ test_ship_teardown_rejects_orca_mounted_removal_root() { FM_TEARDOWN_TEST_MOUNT_PATH="$wt" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1) rc=$? - set -e + set +e expect_code 1 "$rc" "mounted Orca worktree root must block provider removal" assert_not_contains "$(cat "$LOG")" $'orca\x1f''worktree'$'\x1f''rm' \ "mounted Orca worktree reached provider removal" @@ -1795,7 +1826,7 @@ test_ship_teardown_refuses_orca_unresolvable_worktree_id() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1 ) rc=$? - set -e + set +e [ "$rc" -ne 0 ] || fail "Orca ship teardown should refuse when the worktree id cannot be resolved" assert_contains "$out" "cannot resolve Orca worktree id wt-ship-unresolved" \ "unresolvable Orca worktree id refusal should explain the fail-closed check" @@ -1834,7 +1865,7 @@ test_ship_teardown_refuses_orca_id_path_mismatch() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1 ) rc=$? - set -e + set +e [ "$rc" -ne 0 ] || fail "Orca ship teardown should refuse when the id path differs from worktree=" assert_contains "$out" "not inspected worktree" \ "mismatched Orca worktree path refusal should name the mismatch" @@ -1870,7 +1901,7 @@ test_teardown_refuses_orca_missing_worktree_id() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1 ) rc=$? - set -e + set +e [ "$rc" -ne 0 ] || fail "Orca teardown should refuse missing orca_worktree_id" assert_contains "$out" "missing orca_worktree_id" "teardown did not explain the missing Orca worktree id" assert_present "$state/$id.meta" "failed teardown must preserve task metadata" @@ -1902,7 +1933,7 @@ test_teardown_refuses_orca_worktree_without_terminal_handle() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1 ) rc=$? - set -e + set +e [ "$rc" -ne 0 ] || fail "Orca teardown should refuse when terminal identity is missing" assert_contains "$out" "missing terminal" \ "missing Orca terminal identity was not surfaced" @@ -1930,21 +1961,30 @@ test_secondmate_force_teardown_removes_orca_child_via_orca() { "home=$subhome" "projects=alpha" printf '%s\n' "- domain - Orca child cleanup (home: $subhome; scope: orca cleanup; projects: alpha; added 2026-07-03)" \ > "$home/data/secondmates.md" - fm_write_meta "$subhome/state/$child_id.meta" \ - "window=fm-$child_id" "terminal=term-child-cleanup" "worktree=$childwt" "project=$childproj" \ - "harness=claude" "kind=ship" "mode=no-mistakes" "yolo=off" \ - "backend=orca" "orca_worktree_id=wt-child-cleanup" orca_case secondmate-child-cleanup printf '{"ok":true,"result":{"worktree":{"id":"wt-child-cleanup","name":"fm-%s","path":"%s","terminals":[{"handle":"term-child-cleanup","title":"fm-%s"}]}}}\n' \ "$child_id" "$childwt" "$child_id" > "$RESP/1.out" + cp "$RESP/1.out" "$RESP/2.out" + printf '{"ok":true,"result":{"terminal":{"handle":"term-child-cleanup","title":"fm-%s","worktreeId":"wt-child-cleanup","tail":[]}}}\n' \ + "$child_id" > "$RESP/3.out" + cp "$RESP/1.out" "$RESP/4.out" + cp "$RESP/3.out" "$RESP/5.out" + cp "$RESP/1.out" "$RESP/6.out" + cp "$RESP/3.out" "$RESP/7.out" + printf '%s\n' "$childwt" > "$RESP/.remove-worktree-path" add_tmux_fake "$FB" neutral=$(neutral_fm_root "$CASE_DIR/neutral") + printf 'state/\n' > "$subhome/.gitignore" initialize_secondmate_home_repo "$subhome" "$neutral" + fm_write_meta "$subhome/state/$child_id.meta" \ + "window=fm-$child_id" "terminal=term-child-cleanup" "worktree=$childwt" "project=$childproj" \ + "harness=claude" "kind=ship" "mode=no-mistakes" "yolo=off" \ + "backend=orca" "orca_worktree_id=wt-child-cleanup" set +e out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ FM_ROOT_OVERRIDE="$neutral" FM_HOME="$home" "$ROOT/bin/fm-teardown.sh" domain --force 2>&1 ) rc=$? - set -e + set +e expect_code 0 "$rc" "forced secondmate teardown should remove Orca child work through Orca"$'\n'"$out" assert_contains "$(cat "$LOG")" $'orca\x1f''terminal'$'\x1f''close'$'\x1f''--terminal'$'\x1f''term-child-cleanup'$'\x1f''--json' \ "child cleanup did not close the recorded Orca terminal" @@ -1967,7 +2007,7 @@ test_secondmate_force_teardown_refuses_orca_child_id_path_mismatch() { mkdir -p "$home/state" "$home/data" "$subhome/state" "$subhome/projects" printf 'domain\n' > "$subhome/.fm-secondmate-home" fm_git_worktree "$childproj" "$childwt" "fm/$child_id" - git -C "$childproj" worktree add --quiet -b "fm/$child_id-other" "$other_wt" + fm_git_init_commit "$other_wt" fm_write_meta "$home/state/domain.meta" \ "window=firstmate:fm-domain" "worktree=$subhome" "project=$subhome" \ "harness=echo" "kind=secondmate" "mode=secondmate" "yolo=off" \ @@ -1987,7 +2027,7 @@ test_secondmate_force_teardown_refuses_orca_child_id_path_mismatch() { out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ FM_ROOT_OVERRIDE="$neutral" FM_HOME="$home" "$ROOT/bin/fm-teardown.sh" domain --force 2>&1 ) rc=$? - set -e + set +e [ "$rc" -ne 0 ] || fail "forced secondmate teardown should refuse mismatched Orca child id/path" assert_contains "$out" "not inspected worktree" \ "mismatched Orca child worktree path refusal should name the mismatch" @@ -2028,10 +2068,10 @@ test_secondmate_force_teardown_retains_partial_orca_child() { out=$( PATH="$FB:$PATH" FM_ORCA_LOG="$LOG" FM_ORCA_RESPONSES="$RESP" \ FM_ROOT_OVERRIDE="$neutral" FM_HOME="$home" "$ROOT/bin/fm-teardown.sh" domain --force 2>&1 ) rc=$? - set -e + set +e [ "$rc" -ne 0 ] || fail "forced secondmate teardown should refuse partial Orca child state" - assert_contains "$out" "child endpoint identity for $child_id is missing" \ - "partial Orca child refusal did not surface missing endpoint identity" + assert_contains "$out" "child Orca endpoint authority or quiescence is unproven for $child_id" \ + "partial Orca child refusal did not surface unproven endpoint authority" assert_not_contains "$(cat "$LOG")" $'orca\x1f''worktree'$'\x1f''rm' \ "partial child cleanup removed an Orca worktree without quiescence proof" assert_not_contains "$(cat "$LOG")" $'orca\x1f''terminal'$'\x1f''close' \ @@ -2073,7 +2113,7 @@ test_spawn_refuses_cleanup_pending_orca_task_before_mutation() { FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "spawn reused an Orca cleanup quarantine" assert_contains "$out" "Orca cleanup is pending for $id" \ @@ -2109,7 +2149,7 @@ test_pathless_orca_quarantine_has_supported_cleanup() { FM_CHECKOUT_REFRESH_STATE_BASE="$CASE_DIR/checkout-state" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1) status=$? - set -e + set +e expect_code 0 "$status" "pathless Orca quarantine cleanup"$'\n'"$out" assert_contains "$(cat "$LOG")" $'orca\x1f''terminal'$'\x1f''close'$'\x1f''--terminal'$'\x1f''term-path-cleanup' \ @@ -2142,7 +2182,7 @@ test_idless_orca_quarantine_refuses_unscoped_terminal_close() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "idless Orca quarantine closed an unscoped terminal" assert_contains "$out" "refusing to close an unscoped terminal" \ @@ -2177,7 +2217,7 @@ test_teardown_refuses_orca_terminal_worktree_identity_drift() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "teardown closed a terminal bound to another Orca worktree" assert_contains "$out" "task Orca endpoint authority or quiescence is unproven" \ @@ -2214,7 +2254,7 @@ test_spawn_quarantines_create_response_without_worktree_id() { FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "spawn accepted an Orca create response without a worktree id" assert_grep "worktree=$proj" "$state/$id.meta" \ @@ -2264,7 +2304,7 @@ test_orca_quarantine_write_failure_keeps_prearmed_blocker() { "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1) status=$? chmod 700 "$state" - set -e + set +e [ "$status" -ne 0 ] || fail "spawn ignored an Orca quarantine update failure" assert_grep 'orca_cleanup_pending=1' "$state/$id.meta" \ @@ -2281,7 +2321,7 @@ test_orca_quarantine_write_failure_keeps_prearmed_blocker() { FM_PROJECTS_OVERRIDE="$TMP_ROOT/unused-projects" FM_SPAWN_NO_GUARD=1 \ "$ROOT/bin/fm-spawn.sh" "$id" "$proj" claude --backend orca 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "spawn reused a task after quarantine publication failed" assert_contains "$out" "Orca cleanup is pending for $id" \ "pre-armed quarantine did not block retry" @@ -2314,7 +2354,7 @@ test_teardown_rejects_cross_task_orca_terminal_label() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "teardown accepted a terminal labeled for another task" assert_contains "$out" "task Orca endpoint authority or quiescence is unproven" \ @@ -2353,7 +2393,7 @@ test_teardown_rejects_cross_task_orca_worktree_label() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "teardown accepted an Orca worktree labeled for another task" assert_contains "$out" "task Orca endpoint authority or quiescence is unproven" \ @@ -2396,7 +2436,7 @@ test_teardown_quiesces_unrecorded_orca_terminals() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" --force 2>&1) status=$? - set -e + set +e expect_code 0 "$status" "unrecorded Orca terminal cleanup"$'\n'"$out" assert_contains "$(cat "$LOG")" $'orca\x1f''terminal'$'\x1f''close'$'\x1f''--terminal'$'\x1f''term-unrecorded' \ @@ -2433,7 +2473,7 @@ test_teardown_rejects_live_recorded_terminal_missing_from_inventory() { FM_ROOT_OVERRIDE="$neutral" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ "$ROOT/bin/fm-teardown.sh" "$id" 2>&1) status=$? - set -e + set +e [ "$status" -ne 0 ] || fail "teardown accepted a live recorded terminal omitted from the worktree inventory" assert_contains "$out" "task Orca endpoint authority or quiescence is unproven" \ diff --git a/tests/fm-backend-zellij.test.sh b/tests/fm-backend-zellij.test.sh index 7aef69fa4a..0664ba1c2b 100755 --- a/tests/fm-backend-zellij.test.sh +++ b/tests/fm-backend-zellij.test.sh @@ -47,6 +47,7 @@ if [ "${1:-}" = --version ]; then exit 0 fi if [ "${1:-}" = list-sessions ]; then + [ ! -f "$RESP/.closed" ] || exit 0 printf '%s\n' "${FM_ZELLIJ_SESSION_LIST:-}" exit 0 fi @@ -57,6 +58,9 @@ fi next=$(( $(cat "$COUNT_FILE" 2>/dev/null || echo 0) + 1 )) n=$next echo "$n" > "$COUNT_FILE" +case " $* " in + *' action close-tab-by-id '*) touch "$RESP/.closed" ;; +esac if [ -f "$RESP/$n.exit" ]; then exit "$(cat "$RESP/$n.exit")" fi @@ -911,6 +915,7 @@ test_teardown_passes_recorded_tab_id_to_zellij_kill() { local dir state data config project fb out status dir="$TMP_ROOT/teardown-zellij-ghost"; state="$dir/state"; data="$dir/data"; config="$dir/config"; project="$dir/project" mkdir -p "$state" "$data/zghost" "$config" "$project" "$dir/responses" + git -C "$project" init -q printf 'report\n' > "$data/zghost/report.md" fm_write_meta "$state/zghost.meta" \ "window=firstmate:7" \ @@ -919,8 +924,11 @@ test_teardown_passes_recorded_tab_id_to_zellij_kill() { "worktree=$dir/missing-worktree" \ "project=$project" \ "kind=scout" - printf '[]\n' > "$dir/responses/1.out" - printf '[{"tab_id":3,"name":"fm-zghost"}]\n' > "$dir/responses/2.out" + touch "$state/.last-watcher-beat" + zellij_pane_response "$dir" 1 7 3 + zellij_tab_response "$dir" 2 3 fm-zghost + printf '[]\n' > "$dir/responses/3.out" + zellij_tab_response "$dir" 4 3 fm-zghost fb=$(make_zellij_fakebin "$dir") out=$( PATH="$fb:$PATH" FM_STATE_OVERRIDE="$state" FM_DATA_OVERRIDE="$data" FM_CONFIG_OVERRIDE="$config" \ FM_ZELLIJ_LOG="$dir/log" FM_ZELLIJ_RESPONSES="$dir/responses" FM_ZELLIJ_SESSION_LIST="firstmate" \ @@ -933,6 +941,8 @@ test_teardown_passes_recorded_tab_id_to_zellij_kill() { "fm-teardown did not pass a verified recorded zellij_tab_id through to kill" assert_not_contains "$(cat "$dir/log")" $'\x1f''close-pane' \ "fm-teardown should close the recorded tab id instead of falling back to close-pane" + assert_absent "$state/zghost.meta" \ + "fm-teardown retained task metadata after quiescing the zellij endpoint" pass "fm-teardown.sh: passes recorded zellij_tab_id with the expected task label" } @@ -1197,7 +1207,6 @@ test_kill_closes_recorded_tab_when_pane_already_gone test_kill_skips_recorded_tab_when_label_mismatches test_kill_is_noop_when_session_absent test_teardown_passes_recorded_tab_id_to_zellij_kill -test_forced_secondmate_teardown_kills_zellij_children_with_child_home_tag test_send_text_submit_detects_landed_send test_send_text_submit_detects_swallowed_enter test_send_text_submit_send_failed_when_session_absent diff --git a/tests/fm-backend.test.sh b/tests/fm-backend.test.sh index a09459b60c..ee9d3f16ea 100755 --- a/tests/fm-backend.test.sh +++ b/tests/fm-backend.test.sh @@ -1053,12 +1053,21 @@ esac exit 0 SH chmod +x "$fb/tmux" - fm_fake_exit0 "$fb" treehouse + cat > "$fb/treehouse" < - local label=$1 first_reply=$2 real_root link_root proj wt id fb data state config log out rc proj_phys initial_path + local label=$1 first_reply=$2 real_root link_root proj wt id fb data state config treehouse_root log out rc proj_phys initial_path real_root="$TMP_ROOT/symlink-real-$label"; link_root="$TMP_ROOT/symlink-link-$label" mkdir -p "$real_root" ln -s "$real_root" "$link_root" @@ -1066,6 +1075,7 @@ run_spawn_symlink_case() { #