ci/woodpecker/push/publish Pipeline was canceled
Co-authored-by: veronica <[email protected]>
217 lines
8.4 KiB
Bash
Executable File
217 lines
8.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# CI-fit regression suite for the #1408 legacy-socket guard in
|
|
# start-agent-session.sh.
|
|
#
|
|
# Same hermeticity contract as test-agent-session-broker-preflight.sh: a fake
|
|
# tmux on PATH that scripts its own answers, a real unix socket in a tmpdir so
|
|
# the broker preflight passes, env -i with a fake HOME. No case depends on host
|
|
# state.
|
|
#
|
|
# The failure this suite is written down to catch: during a socket cutover a
|
|
# seat's session still lives on the DEFAULT tmux socket while the launcher
|
|
# targets the named one. The declared-socket has-session check cannot see the
|
|
# legacy session (measured 2026-08-24: rc=1, script proceeds), so launch
|
|
# creates a same-name duplicate — and comms delivery, which addresses sessions
|
|
# by NAME, cannot tell the two apart. The guard refuses with its own code
|
|
# (exit 76, after 75 broker-absent) BEFORE any tmux mutation.
|
|
#
|
|
# Cases:
|
|
# 1. legacy session present -> exit 76, message names seat-on-legacy-socket
|
|
# + both sockets' roles, and NO tmux session was created.
|
|
# 2. legacy session absent -> proceeds PAST the guard (the run then stops at
|
|
# a later precondition; asserted: exit != 76, stderr lacks the guard's
|
|
# code, proving the guard was not the refusal).
|
|
# 3. MOSAIC_TMUX_SOCKET empty (single-socket host) -> guard is inert: the
|
|
# default-socket probe must not fire at all.
|
|
#
|
|
# Sabotage control, run by the developer (not in-suite): remove the guard
|
|
# block, re-run — case 1 fails (exit is not 76), cases 2-3 still pass;
|
|
# restore byte-identically.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/agent-session-legacy-socket-guard}"
|
|
FAKE_HOME="$WORK_DIR/home"
|
|
BIN_DIR="$WORK_DIR/bin"
|
|
SOCK_DIR="$WORK_DIR/sockets"
|
|
LOG_FILE="$WORK_DIR/tmux-calls.log"
|
|
LEGACY_FLAG="$WORK_DIR/legacy-session-present"
|
|
|
|
rm -rf "$WORK_DIR"
|
|
mkdir -p "$FAKE_HOME/.config/mosaic/fleet/agents" "$BIN_DIR" "$SOCK_DIR"
|
|
chmod 700 "$FAKE_HOME/.config/mosaic" "$FAKE_HOME/.config/mosaic/fleet/agents"
|
|
chmod 750 "$FAKE_HOME/.config/mosaic/fleet"
|
|
cat > "$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-test.env.generated" <<'ENVEOF'
|
|
MOSAIC_AGENT_NAME=lsguard-test
|
|
MOSAIC_GIT_IDENTITY=lsguard-test
|
|
MOSAIC_AGENT_CLASS=worker
|
|
MOSAIC_AGENT_RUNTIME=pi
|
|
MOSAIC_AGENT_MODEL=
|
|
MOSAIC_AGENT_REASONING=
|
|
MOSAIC_AGENT_TOOL_POLICY=code
|
|
MOSAIC_AGENT_WORKDIR=/tmp
|
|
MOSAIC_TMUX_SOCKET=mosaic-fleet
|
|
ENVEOF
|
|
chmod 600 "$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-test.env.generated"
|
|
|
|
# A projection with NO named socket, for case 3. Same file minus the socket line.
|
|
sed '/^MOSAIC_TMUX_SOCKET=/d; s/lsguard-test/lsguard-nosock/' \
|
|
"$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-test.env.generated" \
|
|
> "$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-nosock.env.generated"
|
|
echo 'MOSAIC_TMUX_SOCKET=' >> "$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-nosock.env.generated"
|
|
chmod 600 "$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-nosock.env.generated"
|
|
|
|
# Ownership identity the launcher validates before anything touches tmux:
|
|
# a 0600 uuid file plus a tmux global environment that matches it exactly.
|
|
mkdir -p "$FAKE_HOME/.config/mosaic/fleet/run"
|
|
chmod 750 "$FAKE_HOME/.config/mosaic/fleet/run"
|
|
OWNER_UUID="aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
|
|
printf '%s' "$OWNER_UUID" > "$FAKE_HOME/.config/mosaic/fleet/run/holder-owner"
|
|
chmod 600 "$FAKE_HOME/.config/mosaic/fleet/run/holder-owner"
|
|
|
|
# The exact env block assert_owned_tmux_server expects; the socket value differs
|
|
# per case, so cases rewrite it via write_tmux_env before each run.
|
|
write_tmux_env() {
|
|
printf '%s\n' \
|
|
"HOME=$FAKE_HOME" \
|
|
'PATH=/usr/bin:/bin' \
|
|
"PWD=$FAKE_HOME" \
|
|
"MOSAIC_FLEET_OWNER=$OWNER_UUID" \
|
|
'MOSAIC_TMUX_HOLDER=_holder' \
|
|
"MOSAIC_TMUX_SOCKET=$1" > "$WORK_DIR/tmux-env"
|
|
}
|
|
|
|
# ─── Fake tmux ──────────────────────────────────────────────────────────────
|
|
# Scripted answers: a DEFAULT-socket has-session (argv carries no -L) answers
|
|
# by the flag file; every named-socket call succeeds (holder present, no
|
|
# existing session is fine for these cases since refusal happens first).
|
|
cat > "$BIN_DIR/tmux" <<SH
|
|
#!/usr/bin/env bash
|
|
printf 'tmux %s\n' "\$*" >> "$LOG_FILE"
|
|
if [[ "\$*" == *new-session* ]]; then
|
|
echo "TMUX-NEW-SESSION-INVOKED" >> "$LOG_FILE"
|
|
fi
|
|
if [[ "\$*" == *show-environment* ]]; then
|
|
cat "$WORK_DIR/tmux-env"
|
|
exit 0
|
|
fi
|
|
if [[ "\$*" == *has-session* ]]; then
|
|
# holder session always present; the seat's DEFAULT-socket presence is the
|
|
# flag file; the seat is never already-running on the NAMED socket.
|
|
[[ "\$*" == *_holder* ]] && exit 0
|
|
if [[ "\$1" == "-L" ]]; then exit 1; fi
|
|
[[ -e "$LEGACY_FLAG" ]] && exit 0 || exit 1
|
|
fi
|
|
exit 0
|
|
SH
|
|
chmod +x "$BIN_DIR/tmux"
|
|
|
|
for bin in mosaic pi claude; do
|
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$BIN_DIR/$bin"
|
|
chmod +x "$BIN_DIR/$bin"
|
|
done
|
|
|
|
# Real socket so the #1292 broker preflight passes and the run reaches the guard.
|
|
# Same idiom as the broker-preflight suite: AF_UNIX binds cap at 108 path bytes,
|
|
# so the socket lives at a SHORT /tmp path held by a detached python holder (a
|
|
# foreground bind would close on exit; -S on a closed-but-unlinked path fails).
|
|
LIVE_SOCK="/tmp/mosaic-lsguard-$RANDOM-$$.sock"
|
|
trap 'rm -f "$LIVE_SOCK"' EXIT
|
|
rm -f "$LIVE_SOCK"
|
|
cat > "$SOCK_DIR/holder.py" <<'PY'
|
|
import socket, sys, time
|
|
path = sys.argv[1]
|
|
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
s.bind(path)
|
|
s.listen(1)
|
|
time.sleep(120)
|
|
PY
|
|
python3 "$SOCK_DIR/holder.py" "$LIVE_SOCK" >/dev/null 2>"$SOCK_DIR/holder.err" &
|
|
for _ in $(seq 1 50); do
|
|
[ -S "$LIVE_SOCK" ] && break
|
|
sleep 0.1
|
|
done
|
|
[ -S "$LIVE_SOCK" ] || { echo "FAIL: could not create live socket" >&2; exit 1; }
|
|
|
|
run_session_script() {
|
|
local agent="$1"; shift
|
|
(
|
|
cd "$WORK_DIR"
|
|
env -i HOME="$FAKE_HOME" PATH="$BIN_DIR:/usr/bin:/bin" \
|
|
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
|
|
MOSAIC_HOME="$FAKE_HOME/.config/mosaic" \
|
|
MOSAIC_LEASE_BROKER_SOCKET="$LIVE_SOCK" \
|
|
"$@" \
|
|
bash "$SCRIPT_DIR/start-agent-session.sh" "$agent"
|
|
)
|
|
}
|
|
|
|
fail=0
|
|
assert() {
|
|
local desc="$1" expected="$2" actual="$3"
|
|
[[ "$expected" == "$actual" ]] || { echo "FAIL: $desc — expected '$expected', got '$actual'" >&2; fail=1; }
|
|
}
|
|
assert_contains() {
|
|
local desc="$1" haystack="$2" needle="$3"
|
|
[[ "$haystack" == *"$needle"* ]] || { echo "FAIL: $desc — missing '$needle'" >&2; fail=1; }
|
|
}
|
|
assert_not_contains() {
|
|
local desc="$1" haystack="$2" needle="$3"
|
|
if [[ "$haystack" == *"$needle"* ]]; then
|
|
echo "FAIL: $desc — must not contain '$needle'" >&2
|
|
fail=1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# ─── 1. Legacy session present → exit 76, no tmux mutation. ─────────────────
|
|
write_tmux_env "mosaic-fleet"
|
|
: > "$LOG_FILE"; touch "$LEGACY_FLAG"
|
|
stderr_file="$WORK_DIR/stderr-1.tmp"
|
|
set +e
|
|
run_session_script lsguard-test >/dev/null 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
err=$(cat "$stderr_file")
|
|
assert "legacy present exit code" "76" "$rc"
|
|
assert_contains "names the failure" "$err" "FAIL_LAUNCH seat-on-legacy-socket"
|
|
assert_contains "names the agent" "$err" "lsguard-test"
|
|
assert_contains "names the target socket" "$err" "mosaic-fleet"
|
|
assert_not_contains "no session created" "$(cat "$LOG_FILE")" "TMUX-NEW-SESSION-INVOKED"
|
|
|
|
# ─── 2. Legacy session absent → guard is not the refusal. ───────────────────
|
|
write_tmux_env "mosaic-fleet"
|
|
: > "$LOG_FILE"; rm -f "$LEGACY_FLAG"
|
|
stderr_file="$WORK_DIR/stderr-2.tmp"
|
|
set +e
|
|
run_session_script lsguard-test >/dev/null 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
err=$(cat "$stderr_file")
|
|
if [[ "$rc" == "76" ]]; then
|
|
echo "FAIL: legacy absent must not exit 76" >&2; fail=1
|
|
fi
|
|
assert_not_contains "guard code absent from stderr" "$err" "seat-on-legacy-socket"
|
|
|
|
# ─── 3. Empty MOSAIC_TMUX_SOCKET → guard inert, no default-socket probe. ────
|
|
write_tmux_env ""
|
|
: > "$LOG_FILE"; touch "$LEGACY_FLAG" # even with a legacy session present
|
|
stderr_file="$WORK_DIR/stderr-3.tmp"
|
|
set +e
|
|
run_session_script lsguard-nosock >/dev/null 2>"$stderr_file"
|
|
rc=$?
|
|
set -e
|
|
err=$(cat "$stderr_file")
|
|
if [[ "$rc" == "76" ]]; then
|
|
echo "FAIL: empty socket must never exit 76 (single-socket host)" >&2; fail=1
|
|
fi
|
|
assert_not_contains "guard code absent on single-socket host" "$err" "seat-on-legacy-socket"
|
|
|
|
rm -f "$LEGACY_FLAG"
|
|
if [[ "$fail" -ne 0 ]]; then
|
|
echo "start-agent-session legacy-socket guard regression FAILED" >&2
|
|
exit 1
|
|
fi
|
|
echo "start-agent-session legacy-socket guard regression passed"
|