#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd) START="$SCRIPT_DIR/start-agent-session.sh" SOCKET="mosaic-agent-test-$RANDOM-$$" AGENT="agent-$RANDOM" WORKDIR=$(mktemp -d) # Keep a single cleanup trap that accumulates resources. CLEANUP_DIRS=("$WORKDIR") CLEANUP_SOCKETS=("$SOCKET") trap '_cleanup' EXIT _cleanup() { for s in "${CLEANUP_SOCKETS[@]:-}"; do tmux -L "$s" kill-server >/dev/null 2>&1 || true done for d in "${CLEANUP_DIRS[@]:-}"; do rm -rf "$d" done } fail() { echo "FAIL: $*" >&2 exit 1 } # ── Test 1: basic session creation with workdir check ───────────────────────── MOSAIC_TMUX_SOCKET="$SOCKET" \ MOSAIC_AGENT_WORKDIR="$WORKDIR" \ MOSAIC_AGENT_COMMAND='bash --noprofile --norc -i' \ "$START" "$AGENT" tmux -L "$SOCKET" has-session -t "=$AGENT:0.0" || fail "agent session was not created" actual_dir=$(tmux -L "$SOCKET" display-message -p -t "=$AGENT:0.0" '#{pane_current_path}') [ "$actual_dir" = "$WORKDIR" ] || fail "agent workdir mismatch: $actual_dir" # ── Test 2: idempotency (duplicate start prints 'already running') ───────────── MOSAIC_TMUX_SOCKET="$SOCKET" \ MOSAIC_AGENT_WORKDIR="$WORKDIR" \ MOSAIC_AGENT_COMMAND='bash --noprofile --norc -i' \ "$START" "$AGENT" >/tmp/mosaic-start-agent-idempotent.out grep -qF 'already running' /tmp/mosaic-start-agent-idempotent.out || fail "duplicate start was not idempotent" # ── Test 3: runtime-bin PATH prefix is baked into the pane command ──────────── # # We capture the command the script would hand to tmux by injecting a fake # 'tmux' shim into PATH. The shim: # - Intercepts 'new-session' calls and records its arguments to a file. # - For 'has-session' calls, exits 1 (session does not exist) so the script # proceeds to launch instead of printing "already running". # - For all other subcommands, exits 0. # # Assertions: # a) 'export PATH=' with the synthetic MOSAIC_RUNTIME_BIN prefix appears. # b) 'exec' appears so the runtime replaces the wrapper shell. # c) MOSAIC_AGENT_COMMAND with flags is forwarded intact. FAKE_BIN=$(mktemp -d) FAKE_RUNTIME_BIN=$(mktemp -d) TMUX_ARGS_FILE=$(mktemp) CLEANUP_DIRS+=("$FAKE_BIN" "$FAKE_RUNTIME_BIN") # Write the fake tmux shim (uses only positional args, no sourced vars). cat > "$FAKE_BIN/tmux" < ... if [ "\$subcmd" = "has-session" ]; then exit 1 # session not found → script will attempt new-session fi if [ "\$subcmd" = "new-session" ]; then printf '%s\n' "\$@" > "$TMUX_ARGS_FILE" exit 0 fi exit 0 SHIM chmod +x "$FAKE_BIN/tmux" SOCKET3="mosaic-agent-test3-$RANDOM-$$" AGENT3="agent3-$RANDOM" WORKDIR3=$(mktemp -d) CLEANUP_DIRS+=("$WORKDIR3") PATH="$FAKE_BIN:$PATH" \ MOSAIC_TMUX_SOCKET="$SOCKET3" \ MOSAIC_AGENT_WORKDIR="$WORKDIR3" \ MOSAIC_AGENT_RUNTIME="pi" \ MOSAIC_RUNTIME_BIN="$FAKE_RUNTIME_BIN" \ MOSAIC_AGENT_COMMAND="mosaic yolo pi --model openai-codex/gpt-5.5:high" \ "$START" "$AGENT3" all_args=$(cat "$TMUX_ARGS_FILE" 2>/dev/null || true) rm -f "$TMUX_ARGS_FILE" echo "--- captured tmux new-session args ---" echo "$all_args" echo "--- end args ---" # a) PATH prefix containing FAKE_RUNTIME_BIN must appear. echo "$all_args" | grep -qF "export PATH=" || fail "pane command does not export PATH" echo "$all_args" | grep -qF "$FAKE_RUNTIME_BIN" || fail "pane command does not include MOSAIC_RUNTIME_BIN in PATH prefix" # b) exec must appear so the runtime replaces the wrapper shell. echo "$all_args" | grep -qF "exec " || fail "pane command does not use exec" # c) Full MOSAIC_AGENT_COMMAND (with flags) must be forwarded. echo "$all_args" | grep -qF "mosaic yolo pi --model openai-codex/gpt-5.5:high" || \ fail "pane command does not forward MOSAIC_AGENT_COMMAND with flags intact" # ── Test 4: when no extra runtime-bin dirs exist, exec still appears ─────────── TMUX_ARGS_FILE2=$(mktemp) FAKE_BIN2=$(mktemp -d) CLEANUP_DIRS+=("$FAKE_BIN2") cat > "$FAKE_BIN2/tmux" < "$TMUX_ARGS_FILE2" exit 0 fi exit 0 SHIM2 chmod +x "$FAKE_BIN2/tmux" SOCKET4="mosaic-agent-test4-$RANDOM-$$" AGENT4="agent4-$RANDOM" WORKDIR4=$(mktemp -d) CLEANUP_DIRS+=("$WORKDIR4") # MOSAIC_RUNTIME_BIN points to a non-existent dir so prefix will be empty; # .npm-global/bin and .local/bin may or may not exist but we just want exec. PATH="$FAKE_BIN2:$PATH" \ MOSAIC_TMUX_SOCKET="$SOCKET4" \ MOSAIC_AGENT_WORKDIR="$WORKDIR4" \ MOSAIC_AGENT_RUNTIME="pi" \ MOSAIC_RUNTIME_BIN="/nonexistent-dir-$$" \ MOSAIC_AGENT_COMMAND="mosaic yolo pi" \ "$START" "$AGENT4" all_args4=$(cat "$TMUX_ARGS_FILE2" 2>/dev/null || true) rm -f "$TMUX_ARGS_FILE2" rm -rf "$WORKDIR4" echo "$all_args4" | grep -qF "exec " || fail "pane command (no prefix dirs) does not use exec" echo "$all_args4" | grep -qF "mosaic yolo pi" || fail "pane command does not include agent command when no prefix" # ── Test 5: candidate dir already in LAUNCHER $PATH is still baked into pane ── # # Regression guard for the bug where _build_runtime_bin_prefix() used to skip # a candidate because it was already present in the launcher process's $PATH. # That check was wrong: the pane inherits the tmux SERVER environment, not the # launcher's env. Even if a dir is on the launcher's PATH it must always be # baked into the pane's PATH export. # # We prove this by setting PATH to include FAKE_RUNTIME_BIN5 (the candidate), # then asserting the generated new-session command still exports it. TMUX_ARGS_FILE5=$(mktemp) FAKE_BIN5=$(mktemp -d) FAKE_RUNTIME_BIN5=$(mktemp -d) # this dir IS on the launcher's PATH below CLEANUP_DIRS+=("$FAKE_BIN5" "$FAKE_RUNTIME_BIN5") cat > "$FAKE_BIN5/tmux" < "$TMUX_ARGS_FILE5" exit 0 fi exit 0 SHIM5 chmod +x "$FAKE_BIN5/tmux" SOCKET5="mosaic-agent-test5-$RANDOM-$$" AGENT5="agent5-$RANDOM" WORKDIR5=$(mktemp -d) CLEANUP_DIRS+=("$WORKDIR5") CLEANUP_SOCKETS+=("$SOCKET5") # FAKE_RUNTIME_BIN5 is deliberately placed on the LAUNCHER PATH so that the # old (buggy) code would have skipped it. The correct code must still include # it in the pane PATH export. PATH="$FAKE_BIN5:$FAKE_RUNTIME_BIN5:$PATH" \ MOSAIC_TMUX_SOCKET="$SOCKET5" \ MOSAIC_AGENT_WORKDIR="$WORKDIR5" \ MOSAIC_AGENT_RUNTIME="pi" \ MOSAIC_RUNTIME_BIN="$FAKE_RUNTIME_BIN5" \ MOSAIC_AGENT_COMMAND="mosaic yolo pi" \ "$START" "$AGENT5" all_args5=$(cat "$TMUX_ARGS_FILE5" 2>/dev/null || true) rm -f "$TMUX_ARGS_FILE5" rm -rf "$WORKDIR5" echo "--- test 5: launcher-PATH candidate must still appear in pane export ---" echo "$all_args5" echo "--- end test 5 args ---" echo "$all_args5" | grep -qF "export PATH=" || \ fail "test5: pane command does not export PATH when candidate is on launcher PATH" echo "$all_args5" | grep -qF "$FAKE_RUNTIME_BIN5" || \ fail "test5: candidate dir (already on launcher PATH) was NOT baked into pane PATH — regression" echo "ok - start-agent-session"