Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bin/backends/orca.sh
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ EOF
}

fm_backend_orca_quiesce_terminal() { # <terminal-id> [expected-worktree-id] [expected-label]
local terminal=$1 expected_worktree_id=${2:-} expected_label=${3:-} attempt state
local terminal=$1 expected_worktree_id=${2:-} expected_label=${3:-} state
[ -n "$terminal" ] || return 1
if [ -n "$expected_worktree_id" ] || [ -n "$expected_label" ]; then
state=$(fm_backend_orca_terminal_state "$terminal" "$expected_worktree_id" "$expected_label")
Expand All @@ -424,7 +424,7 @@ fm_backend_orca_quiesce_terminal() { # <terminal-id> [expected-worktree-id] [ex
echo "error: failed to close Orca terminal $terminal" >&2
return 1
}
for attempt in 1 2 3 4 5; do
for _ in 1 2 3 4 5; do
state=$(fm_backend_orca_terminal_state "$terminal" "$expected_worktree_id" "$expected_label")
[ "$state" != absent ] || return 0
sleep 0.1
Expand Down
1 change: 1 addition & 0 deletions bin/fm-account-routing-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,7 @@ fm_secondmate_registry_lock_acquire() {

fm_secondmate_registry_query() {
local registry=$1 mode=${2:-validate} expected_id=${3:-} key=${4:-}
# shellcheck disable=SC2016 # Perl source is intentionally a literal shell argument.
fm_account_system_perl -MErrno=ENOENT -e '
my ($registry, $mode, $expected_id, $key) = @ARGV;
if (!lstat($registry)) {
Expand Down
1 change: 1 addition & 0 deletions bin/fm-fleet-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,7 @@ run_sync_project_bounded() (
echo "$project: skipped: refresh lock ownership cannot be proved"
return 0
}
# shellcheck disable=SC2031 # This whole function is a subshell; the value is intentionally local to it.
export FM_PROCESS_TREE_GUARD_FILE="$lock_owner_dir/process-group"
trap 'fm_lock_release "$checkout_lock"' EXIT
if fm_run_bounded "$FLEET_SYNC_TIMEOUT" \
Expand Down
2 changes: 0 additions & 2 deletions bin/fm-home-seed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,6 @@ SEED_HOME_CREATED=0
SEED_HOME_BACKED_UP=0
SEED_BACKUP_DIR=
SEED_CREATED_PROJECTS_FILE=
SEED_PARENT_REG_EXISTED=0
SEED_PARENT_BRIEF=
SEED_PARENT_BRIEF_CREATED=0
SEED_PARENT_BRIEF_DIR_CREATED=0
Expand Down Expand Up @@ -923,7 +922,6 @@ seed_home() {
SEED_BACKUP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/fm-home-seed.XXXXXX")
SEED_CREATED_PROJECTS_FILE="$SEED_BACKUP_DIR/created-projects"
: > "$SEED_CREATED_PROJECTS_FILE"
SEED_PARENT_REG_EXISTED=0
SEED_PARENT_BRIEF="$DATA/$id/brief.md"
SEED_PARENT_BRIEF_CREATED=0
SEED_PARENT_BRIEF_DIR_CREATED=0
Expand Down
31 changes: 17 additions & 14 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@
# Before a secondmate launch, the home must fast-forward safely to the primary
# default-branch commit and independently match the live default tip.
# Any unproven freshness state refuses launch.
# Ship/scout spawns refresh the primary checkout before Treehouse acquisition,
# surface dirty pool entries, and durably lease one available worktree before
# creating the endpoint. They refuse to create that endpoint unless the leased
# Ship/scout spawns canonicalize a symlinked project path before exact-root
# refresh and pool preflight, surface dirty pool entries, and durably lease
# one available worktree before creating the endpoint. They refuse to create
# that endpoint unless the leased
# path is a clean isolated worktree from the requested repository whose HEAD
# matches its live upstream or local default-branch tip. Dirty acquisitions
# remain under their durable lease for manual recovery. Other pre-commit
Expand Down Expand Up @@ -2728,9 +2729,11 @@ fi
# 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
# once here so exact-root preflight and every downstream comparison use the
# same physical form
# (docs/herdr-backend.md "Known gaps").
PROJ_ABS_REAL=$(cd "$PROJ_ABS" 2>/dev/null && pwd -P) || PROJ_ABS_REAL="$PROJ_ABS"
PROJ_ABS=$PROJ_ABS_REAL

real_path_or_raw() { # <path>
local path=$1 real
Expand Down Expand Up @@ -3393,11 +3396,11 @@ EOF
fi
WORKTREE_CREATED=1
fi
[ "$(fm_backend_orca_terminal_state "$ORCA_TERMINAL" "$ORCA_WORKTREE_ID" "$W")" = present ] \
&& fm_backend_orca_worktree_terminal_contains "$ORCA_WORKTREE_ID" "$W" "$ORCA_TERMINAL" || {
echo "error: Orca terminal is not authoritatively bound to worktree $ORCA_WORKTREE_ID and task $W" >&2
exit 1
}
if [ "$(fm_backend_orca_terminal_state "$ORCA_TERMINAL" "$ORCA_WORKTREE_ID" "$W")" != present ] \
|| ! fm_backend_orca_worktree_terminal_contains "$ORCA_WORKTREE_ID" "$W" "$ORCA_TERMINAL"; then
echo "error: Orca terminal is not authoritatively bound to worktree $ORCA_WORKTREE_ID and task $W" >&2
exit 1
fi
T="$ORCA_TERMINAL"
ENDPOINT_CREATED=1
;;
Expand Down Expand Up @@ -3618,11 +3621,11 @@ fi
# process (go build, go test, ...) inherit it. Sent before the launch command so
# the env is set when the agent starts; the brief sleep lets the export land.
if [ "$BACKEND" = orca ]; then
[ "$(fm_backend_orca_terminal_state "$T" "$ORCA_WORKTREE_ID" "$W")" = present ] \
&& fm_backend_orca_worktree_terminal_contains "$ORCA_WORKTREE_ID" "$W" "$T" || {
echo "error: Orca terminal authority changed before launch for $ID" >&2
exit 1
}
if [ "$(fm_backend_orca_terminal_state "$T" "$ORCA_WORKTREE_ID" "$W")" != present ] \
|| ! fm_backend_orca_worktree_terminal_contains "$ORCA_WORKTREE_ID" "$W" "$T"; then
echo "error: Orca terminal authority changed before launch for $ID" >&2
exit 1
fi
validate_orca_abort_worktree_identity || {
echo "error: Orca worktree authority changed before launch for $ID" >&2
exit 1
Expand Down
7 changes: 6 additions & 1 deletion bin/fm-teardown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
# 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.
# Per-task temp cleanup accepts only the exact /tmp/fm-<id> root, treats that
# exact root as already clean when absent, and still refuses wrong paths,
# symlinks, or traversal.
# Uncommitted changes are never landed.
# Ordinary teardown first proves that metadata names the exact registered project,
# worktree, and task lease, then quiesces the endpoint before its final safety checks.
Expand Down Expand Up @@ -1820,6 +1823,7 @@ if not stat.S_ISDIR(os.lstat(base).st_mode):
print(base)
PY
) || return 1
[ -e "$base/fm-$ID" ] || [ -L "$base/fm-$ID" ] || return 0
removal_tree_operation "$base/fm-$ID" "task temp root" remove
}

Expand Down Expand Up @@ -4152,7 +4156,8 @@ fi
remove_grok_turnend_auth "$STATE" "$ID"
fm_backend_clear_transition "$BACKEND" "$STATE" "$T" || true
# Remove the per-task temp root (/tmp/fm-<id>/, incl. its gotmp/) recorded by spawn.
# Read before the state-file rm below; empty (pre-fix tasks without tasktmp=) is a no-op.
# Read before the state-file rm below; empty metadata and an already-absent
# exact root are no-ops.
[ -z "$TASK_TMP" ] || safe_remove_task_tmp "$TASK_TMP" || exit 1
rm -f "$STATE/$ID.status" "$STATE/$ID.turn-ended" "$STATE/$ID.check.sh" "$STATE/$ID.meta" "$STATE/$ID.pi-ext.ts" "$STATE/$ID.grok-turnend-token"
[ -z "$ACCOUNT_DELETE_LOCK" ] || fm_account_lifecycle_lock_release "$ACCOUNT_DELETE_LOCK" >/dev/null 2>&1 || true
Expand Down
2 changes: 1 addition & 1 deletion docs/herdr-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ Covered by the unit cases in `tests/fm-afk-launch.test.sh` (clear-on-fresh-entry
- **RESOLVED: worktree-discovery isolation guard's symlinked-project-prefix false refusal.** Originally discovered while building the runtime-backend-auto-detection real smoke test (`tests/fm-backend-autodetect-smoke.test.sh`), which needed a scratch project.
`fm-spawn.sh`'s `PROJ_ABS` was a LOGICAL `cd && pwd` (symlink components kept), while herdr's `foreground_cwd` (and real tmux's `pane_current_path`, on the same OS-level cwd primitive) report the PHYSICALLY resolved path.
When the project itself lived under a symlinked directory (e.g. macOS's `/tmp` -> `/private/tmp`), the very first worktree-discovery poll saw two different strings for the identical starting directory and the isolation guard false-refused the spawn as "not isolated" before `treehouse get` ever moved the pane - backend-agnostic, not specific to herdr.
Fixed 2026-07-06 (backlog `fm-spawn-symlink-guard-s8`): `bin/fm-spawn.sh` now canonicalizes once into `PROJ_ABS_REAL` (`cd "$PROJ_ABS" && pwd -P`) right after `PROJ_ABS` is resolved, canonicalizes each observed pane cwd for the worktree-discovery comparison, and uses `PROJ_ABS_REAL` in `validate_spawn_worktree`'s own primary-vs-worktree comparison instead of recomputing from the still-symlinked `PROJ_ABS`.
Fixed 2026-07-06 (backlog `fm-spawn-symlink-guard-s8`) and completed for exact-root preflight on 2026-07-25: `bin/fm-spawn.sh` canonicalizes `PROJ_ABS` once with `cd "$PROJ_ABS" && pwd -P` before checkout refresh and Treehouse pool preflight, canonicalizes each observed pane cwd for the worktree-discovery comparison, and reuses that physical project root in `validate_spawn_worktree`.
This removes both failure directions: a symlinked prefix can no longer false-refuse an isolated spawn, and, since both sides are physically resolved for comparison, a genuinely tangled spawn (worktree resolves to the same physical directory as the project) still correctly refuses.
Verified with GNU bash 5.3.9(1)-release (aarch64-apple-darwin25.3.0) and git 2.53.0 on macOS (Darwin 25.5.0): added `tests/fm-backend.test.sh:test_spawn_symlinked_project_prefix_avoids_false_refusal`, which drives the real `bin/fm-spawn.sh` against fake-tmux panes whose first `pane_current_path` poll returns both the project's `pwd -P`-resolved physical path and its logical symlink-preserving path while `PROJ_ABS` is reached through a synthetic symlinked prefix (`ln -s <real> <link>`, project passed as `<link>/proj`).
Confirmed the test reproduces the original bug against the pre-fix script (`git stash` the `bin/fm-spawn.sh` change and rerun: `not ok - fm-spawn.sh should succeed for a project reached through a symlinked prefix` / `error: treehouse get did not yield an isolated worktree ...`), and passes against the fix (`bash tests/fm-backend.test.sh` reports `ok - fm-spawn.sh: a project reached through a symlinked prefix (e.g. macOS /tmp -> /private/tmp) does not trip the isolation guard's false refusal`, with the rest of that suite's assertions unaffected).
Expand Down
62 changes: 45 additions & 17 deletions tests/fm-account-directory.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ FAKEBIN=$(fm_fakebin "$TMP_ROOT")
QUOTA_LOG="$TMP_ROOT/quota.log"
HERDR_LOG="$TMP_ROOT/herdr.log"
TREEHOUSE_LOG="$TMP_ROOT/treehouse.log"
export FM_TREEHOUSE_ROOT="$TMP_ROOT/treehouse"
export FM_CHECKOUT_REFRESH_STATE_BASE="$TMP_ROOT/checkout-refresh"

mkdir -p "$ACCOUNT_ROOT/codex" "$ACCOUNT_ROOT/claude"
mkdir -p "$ACCOUNT_ROOT/codex" "$ACCOUNT_ROOT/claude" "$FM_TREEHOUSE_ROOT"

cat > "$FAKEBIN/quota-axi" <<'SH'
#!/usr/bin/env bash
Expand Down Expand Up @@ -70,7 +72,10 @@ case "${3:-}" in
*) exit 67 ;;
esac
if [ -n "${FM_FAKE_HERDR_DRIFT_WORKTREE:-}" ]; then
git -C "$FM_FAKE_HERDR_DRIFT_WORKTREE" switch --quiet --detach || exit 68
git -C "$FM_FAKE_HERDR_DRIFT_WORKTREE" \
-c user.name='Firstmate Test' -c user.email='[email protected]' \
commit --allow-empty --quiet \
-m 'fixture identity drift' || exit 68
fi
SH
chmod +x "$FAKEBIN/herdr"
Expand Down Expand Up @@ -242,6 +247,7 @@ case "${1:-}" in
fi
prev=$argument
done
[ "${FM_FAKE_TMUX_SEND_FAIL:-0}" != 1 ] || exit 73
exit 0
;;
esac
Expand All @@ -251,11 +257,28 @@ SH
cat > "$fakebin/treehouse" <<'SH'
#!/usr/bin/env bash
set -u
printf '%s\n' "$*" >> "${FM_FAKE_TREEHOUSE_LOG:?}"
[ "${1:-}" = return ] || exit 0
[ "${FM_FAKE_TREEHOUSE_RETURN_FAIL:-0}" != 1 ] || exit 71
target=${@: -1}
git worktree remove --force "$target"
case "${1:-}" in
get)
[ "${2:-}" = --lease ] || exit 2
worktree=${FM_FAKE_TREEHOUSE_WORKTREE:?}
printf '%s\n' "$*" >> "${FM_FAKE_TREEHOUSE_LOG:?}"
git -C "$worktree" checkout --detach --quiet || exit 1
printf '%s\n' "$worktree"
;;
return)
[ "${FM_FAKE_TREEHOUSE_RETURN_FAIL:-0}" != 1 ] || exit 71
target=${@: -1}
[ "$target" != . ] || target=$(pwd -P)
[ "$target" = "${FM_FAKE_TREEHOUSE_WORKTREE:?}" ] || exit 72
cd "${FM_TREEHOUSE_RETURN_PROJECT:?}" || exit 1
git worktree remove --force "$target" || exit 1
git worktree prune || exit 1
[ ! -e "$target" ] || rm -rf -- "$target"
[ ! -e "$target" ] || exit 1
printf 'return --force %s\n' "$target" >> "${FM_FAKE_TREEHOUSE_LOG:?}"
;;
*) exit 2 ;;
esac
SH
chmod +x "$fakebin/treehouse"
cat > "$fakebin/forbidden-agent-fleet" <<'SH'
Expand All @@ -276,8 +299,10 @@ run_direct_spawn() {
FM_SPAWN_NO_GUARD=1 FM_FAKE_PANE_PATH="$worktree" TMUX="fake,1,0" \
FM_FAKE_LAUNCH_LOG="$launch_log" FM_FAKE_ENDPOINT_FILE="$home/state/.fake-endpoint" \
FM_FAKE_ENDPOINT_LABEL="fm-${1:-unknown}" FM_FAKE_KILL_RETAIN="${FM_FAKE_KILL_RETAIN:-0}" \
FM_FAKE_TMUX_SEND_FAIL="${FM_FAKE_TMUX_SEND_FAIL:-0}" \
FM_FAKE_HERDR_DRIFT_WORKTREE="${FM_FAKE_HERDR_DRIFT_WORKTREE:-}" \
FM_FAKE_TREEHOUSE_LOG="$TREEHOUSE_LOG" \
FM_FAKE_TREEHOUSE_WORKTREE="$worktree" \
FM_FAKE_TREEHOUSE_RETURN_FAIL="${FM_FAKE_TREEHOUSE_RETURN_FAIL:-0}" \
PATH="$FAKEBIN:$PATH" \
FM_ACCOUNT_DIRECTORY_TEST_LAB=firstmate-account-directory-test-lab-v1 \
Expand Down Expand Up @@ -541,8 +566,10 @@ test_direct_recovery_rejects_worktree_from_another_project() {
status=$?
fi
[ "$status" -ne 0 ] || fail "direct recovery launched in a worktree from another project"
assert_contains "$out" "does not belong to recorded project" \
"direct recovery project-identity refusal was not actionable"
case "$out" in
*"does not belong to recorded project"*|*"returned redirected or unprovable Git metadata"*) ;;
*) fail "direct recovery project-identity refusal was not actionable"$'\n'"$out" ;;
esac
[ ! -e "$SPAWN_HOME/state/.fake-endpoint" ] || fail "project-identity mismatch created a replacement endpoint"
[ ! -s "$QUOTA_LOG" ] || fail "project-identity mismatch read account quota before refusing recovery"
[ ! -s "$HERDR_LOG" ] || fail "project-identity mismatch installed a profile hook before refusing recovery"
Expand Down Expand Up @@ -686,7 +713,8 @@ test_direct_recovery_rechecks_identity_after_account_prepare() {
else
status=$?
fi
[ "$status" -ne 0 ] || fail "direct recovery ignored worktree identity drift during account preparation"
[ "$status" -ne 0 ] \
|| fail "direct recovery ignored worktree identity drift during account preparation"$'\n'"$out"
assert_contains "$out" "changed branch identity" \
"post-prepare identity drift refusal was not actionable"
[ ! -e "$SPAWN_HOME/state/.fake-endpoint" ] || fail "post-prepare identity drift created a replacement endpoint"
Expand All @@ -704,6 +732,7 @@ test_direct_recovery_tracks_retained_replacement_endpoint() {
id=direct-retained-endpoint-z7
record=$(make_spawn_case direct-retained-endpoint codex "$id")
read_spawn_case "$record"
rm -rf "/tmp/fm-$id"

run_direct_spawn "$SPAWN_HOME" "$SPAWN_WORKTREE" "$SPAWN_LAUNCH_LOG" \
"$id" "$SPAWN_PROJECT" --account-pool legacy-codex-pool >/dev/null 2>&1
Expand Down Expand Up @@ -807,10 +836,10 @@ test_failed_new_direct_spawn_returns_worktree_after_endpoint_cleanup() {
read_spawn_case "$record"
recorded_worktree=$(cd "$SPAWN_WORKTREE" && pwd -P)
: > "$TREEHOUSE_LOG"
: > "/tmp/fm-$id"

if out=$(run_direct_spawn "$SPAWN_HOME" "$SPAWN_WORKTREE" "$SPAWN_LAUNCH_LOG" \
"$id" "$SPAWN_PROJECT" --account-pool legacy-codex-pool 2>&1); then
if out=$(FM_FAKE_TMUX_SEND_FAIL=1 \
run_direct_spawn "$SPAWN_HOME" "$SPAWN_WORKTREE" "$SPAWN_LAUNCH_LOG" \
"$id" "$SPAWN_PROJECT" --account-pool legacy-codex-pool 2>&1); then
status=0
else
status=$?
Expand All @@ -822,7 +851,7 @@ test_failed_new_direct_spawn_returns_worktree_after_endpoint_cleanup() {
[ ! -e "$SPAWN_WORKTREE" ] || fail "failed new direct spawn left its worktree registered"
[ ! -e "$SPAWN_HOME/state/$id.meta" ] || fail "successful direct rollback left task metadata"
[ ! -e "$SPAWN_HOME/state/.fake-endpoint" ] || fail "successful direct rollback left its endpoint"
rm -f "/tmp/fm-$id"
[ ! -e "/tmp/fm-$id" ] || fail "successful direct rollback left its task temp root"
pass "failed new direct spawn removes its endpoint and returns its worktree"
}

Expand All @@ -834,9 +863,8 @@ test_failed_new_direct_spawn_retains_cleanup_when_worktree_return_fails() {
record=$(make_spawn_case direct-new-return-fail codex "$id")
read_spawn_case "$record"
: > "$TREEHOUSE_LOG"
: > "/tmp/fm-$id"

if out=$(FM_FAKE_TREEHOUSE_RETURN_FAIL=1 \
if out=$(FM_FAKE_TMUX_SEND_FAIL=1 FM_FAKE_TREEHOUSE_RETURN_FAIL=1 \
run_direct_spawn "$SPAWN_HOME" "$SPAWN_WORKTREE" "$SPAWN_LAUNCH_LOG" \
"$id" "$SPAWN_PROJECT" --account-pool legacy-codex-pool 2>&1); then
status=0
Expand All @@ -851,7 +879,7 @@ test_failed_new_direct_spawn_retains_cleanup_when_worktree_return_fails() {
assert_grep "rollback_pending=1" "$meta" \
"direct return failure did not fail closed"
[ ! -e "$SPAWN_HOME/state/.fake-endpoint" ] || fail "direct return failure retained an already-removed endpoint"
rm -f "/tmp/fm-$id"
rm -rf "/tmp/fm-$id"
pass "direct spawn persists cleanup state when worktree return cannot be confirmed"
}

Expand Down
16 changes: 8 additions & 8 deletions tests/fm-account-routing.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ test_off_is_byte_compatible_and_never_calls_agent_fleet() {

test_failed_freshness_proof_rolls_back_unmanaged_resources() {
local id rec out status default_branch
id=checkout-freshness-rollback-z1a
id='checkout-freshness-rollback-z1a'
rec=$(make_case checkout-freshness-rollback claude "$id")
read_case "$rec"
default_branch=$(git -C "$PROJ_DIR" branch --show-current)
Expand Down Expand Up @@ -536,7 +536,7 @@ test_failed_freshness_proof_rolls_back_unmanaged_resources() {

test_local_only_spawn_uses_local_default_tip() {
local id rec out status
id=checkout-local-only-z1b
id='checkout-local-only-z1b'
rec=$(make_case checkout-local-only claude "$id")
read_case "$rec"
git -C "$PROJ_DIR" remote remove origin
Expand All @@ -555,7 +555,7 @@ test_local_only_spawn_uses_local_default_tip() {

test_dirty_acquisition_is_retained_without_force_return() {
local id rec draft out status
id=checkout-dirty-retain-z1c
id='checkout-dirty-retain-z1c'
rec=$(make_case checkout-dirty-retain claude "$id")
read_case "$rec"
draft="$WT_DIR/.agents/skills/unlanded/SKILL.md"
Expand Down Expand Up @@ -587,7 +587,7 @@ test_dirty_acquisition_is_retained_without_force_return() {

test_treehouse_acquisition_timeout_is_bounded_before_endpoint_creation() {
local id rec out status
id=checkout-acquire-timeout-z1f
id='checkout-acquire-timeout-z1f'
rec=$(make_case checkout-acquire-timeout claude "$id")
read_case "$rec"

Expand All @@ -609,7 +609,7 @@ test_treehouse_acquisition_timeout_is_bounded_before_endpoint_creation() {

test_changed_acquisition_is_retained_during_unmanaged_rollback() {
local id rec marker release out_file spawn_pid
id=checkout-changed-rollback-z1g
id='checkout-changed-rollback-z1g'
rec=$(make_case checkout-changed-rollback pi "$id")
read_case "$rec"
marker="$CASE_DIR/gotmp-send-started"
Expand Down Expand Up @@ -645,7 +645,7 @@ test_changed_acquisition_is_retained_during_unmanaged_rollback() {

test_unmanaged_postinstall_failure_restores_prior_state() {
local id rec expected out status artifact common key expected_lock lock_marker
id=checkout-unmanaged-restore-z1d
id='checkout-unmanaged-restore-z1d'
rec=$(make_case checkout-unmanaged-restore pi "$id")
read_case "$rec"
fm_write_meta "$HOME_DIR/state/$id.meta" \
Expand Down Expand Up @@ -695,7 +695,7 @@ test_unmanaged_postinstall_failure_restores_prior_state() {

test_spawn_rollback_relays_unverified_treehouse_cleanup() {
local id rec out status real_ps common key lock group owner child_pid
id=checkout-unverified-return-z1h
id='checkout-unverified-return-z1h'
rec=$(make_case checkout-unverified-return pi "$id")
read_case "$rec"
real_ps=$(command -v ps)
Expand Down Expand Up @@ -752,7 +752,7 @@ SH

test_unmanaged_rollback_waits_for_metadata_lock() {
local id rec marker release out_file spawn_pid held
id=checkout-unmanaged-rollback-lock-z1e
id='checkout-unmanaged-rollback-lock-z1e'
rec=$(make_case checkout-unmanaged-rollback-lock pi "$id")
read_case "$rec"
fm_write_meta "$HOME_DIR/state/$id.meta" \
Expand Down
Loading
Loading