273 lines
9.5 KiB
Bash
Executable File
273 lines
9.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# test-agent-watch-socket.sh — P5-C03 independent B1 contract validation.
|
|
#
|
|
# Uses real tmux servers on a private TMUX_TMPDIR. systemctl and systemd-run
|
|
# are stubs because this suite validates start-time socket selection, not
|
|
# systemd scheduling. HOME, watch state, and all test sockets are isolated.
|
|
# Run with --no-red-controls only for the mutation child runs.
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
|
TOOL="${AGENT_WATCH_TOOL_OVERRIDE:-$HERE/agent-watch.sh}"
|
|
REAL_TMUX="$(command -v tmux 2>/dev/null || true)"
|
|
BASH_BIN="$(command -v bash)"
|
|
ORIG_PATH="$PATH"
|
|
|
|
[ -x "$REAL_TMUX" ] || { echo "SKIP: no tmux binary (live fixtures impossible)"; exit 77; }
|
|
[ -x "$TOOL" ] || { echo "FAIL: agent-watch tool not found/executable: $TOOL" >&2; exit 1; }
|
|
|
|
ROOT="$(mktemp -d)"
|
|
ROOT="$(cd "$ROOT" && pwd)"
|
|
HOME_DIR="$ROOT/home"
|
|
TMUX_TMPDIR="$ROOT/tmux"
|
|
BIN="$ROOT/bin"
|
|
STATE="$ROOT/state"
|
|
mkdir -p "$HOME_DIR" "$TMUX_TMPDIR" "$BIN" "$STATE"
|
|
chmod 700 "$ROOT" "$HOME_DIR" "$TMUX_TMPDIR" "$BIN" "$STATE"
|
|
unset MOSAIC_TMUX_SOCKET
|
|
|
|
# These stubs satisfy agent-watch's user-bus and transient-unit checks while
|
|
# leaving real tmux semantics, including socket discovery, under test.
|
|
cat > "$BIN/systemctl" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
case "${*:--}" in
|
|
"--user show-environment") exit 0 ;;
|
|
"--user is-active"*) exit 1 ;;
|
|
"--user start"*) exit 1 ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
STUB
|
|
cat > "$BIN/systemd-run" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
printf '%s\n' "$*" >> "${AGENT_WATCH_TEST_SYSTEMD_RUN_LOG:?}"
|
|
exit 0
|
|
STUB
|
|
cat > "$BIN/sender-stub" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
exit 0
|
|
STUB
|
|
chmod 700 "$BIN/systemctl" "$BIN/systemd-run" "$BIN/sender-stub"
|
|
|
|
SOCKETS=()
|
|
cleanup() {
|
|
local socket
|
|
for socket in "${SOCKETS[@]}"; do
|
|
TMUX_TMPDIR="$TMUX_TMPDIR" "$REAL_TMUX" -L "$socket" kill-server >/dev/null 2>&1 || true
|
|
done
|
|
rm -rf "$ROOT"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
failures=0
|
|
check() {
|
|
local label="$1" result="$2"
|
|
if [ "$result" -eq 0 ]; then
|
|
printf 'ok %s\n' "$label"
|
|
else
|
|
printf 'FAIL %s\n' "$label" >&2
|
|
failures=$((failures + 1))
|
|
fi
|
|
}
|
|
|
|
mk_server() {
|
|
local socket="$1" session="$2"
|
|
SOCKETS+=("$socket")
|
|
TMUX_TMPDIR="$TMUX_TMPDIR" "$REAL_TMUX" -L "$socket" new-session -d -s "$session" -c "$HOME_DIR" \
|
|
'exec bash --noprofile --norc -i' >/dev/null 2>&1
|
|
}
|
|
|
|
run_start() {
|
|
local socket_env=(-u MOSAIC_TMUX_SOCKET)
|
|
if [ -n "${MOSAIC_TMUX_SOCKET:-}" ]; then
|
|
socket_env=(MOSAIC_TMUX_SOCKET="$MOSAIC_TMUX_SOCKET")
|
|
fi
|
|
env "${socket_env[@]}" \
|
|
PATH="$BIN:$ORIG_PATH" HOME="$HOME_DIR" TMUX_TMPDIR="$TMUX_TMPDIR" \
|
|
MOSAIC_WATCH_STATE="$STATE" MOSAIC_AGENT_SEND="$BIN/sender-stub" \
|
|
AGENT_WATCH_TEST_SYSTEMD_RUN_LOG="$ROOT/systemd-run.log" \
|
|
"$BASH_BIN" "$TOOL" start "$@"
|
|
}
|
|
|
|
run_start_env() {
|
|
local socket="$1"
|
|
shift
|
|
(
|
|
MOSAIC_TMUX_SOCKET="$socket"
|
|
run_start "$@"
|
|
)
|
|
}
|
|
|
|
config_has() {
|
|
local name="$1" line="$2"
|
|
grep -qxF "$line" "$STATE/$name/config" 2>/dev/null
|
|
}
|
|
|
|
check_config() {
|
|
local label="$1" name="$2" line="$3"
|
|
if config_has "$name" "$line"; then
|
|
check "$label" 0
|
|
else
|
|
check "$label" 1
|
|
fi
|
|
}
|
|
|
|
# C1: explicit --socket wins over the environment and discovery.
|
|
FLAG_SOCKET="aw-c03-flag-$$"
|
|
mk_server "$FLAG_SOCKET" flag-target
|
|
if run_start_env "not-the-flag" --name c03-flag --session flag-target --socket "$FLAG_SOCKET" \
|
|
--when false --message "socket contract" --interval 10 >/dev/null 2>&1; then
|
|
check_config "C1 explicit --socket wins over MOSAIC_TMUX_SOCKET" c03-flag "SOCKET=$FLAG_SOCKET"
|
|
else
|
|
check "C1 explicit --socket start succeeds" 1
|
|
fi
|
|
|
|
# C2: the launcher-exported environment pin wins before discovery. Both
|
|
# sockets intentionally contain the target, so selecting discovery first
|
|
# would hit the ambiguity guard instead of producing an env-pinned config.
|
|
ENV_SOCKET="aw-c03-env-$$"
|
|
ENV_DISCOVERY_SOCKET="aw-c03-env-discovery-$$"
|
|
mk_server "$ENV_SOCKET" env-target
|
|
mk_server "$ENV_DISCOVERY_SOCKET" env-target
|
|
if run_start_env "$ENV_SOCKET" --name c03-env --session env-target \
|
|
--when false --message "socket contract" --interval 10 >/dev/null 2>&1; then
|
|
check_config "C2 MOSAIC_TMUX_SOCKET wins before discovery" c03-env "SOCKET=$ENV_SOCKET"
|
|
else
|
|
check "C2 environment pin start succeeds without discovery ambiguity" 1
|
|
fi
|
|
BAD_ENV_SOCKET="aw-c03-bad-env-$$"
|
|
bad_env_rc=0
|
|
run_start_env "$BAD_ENV_SOCKET" --name c03-bad-env --session env-target \
|
|
--when false --message "socket contract" --interval 10 >/dev/null 2>"$ROOT/bad-env.err" || bad_env_rc=$?
|
|
if [ "$bad_env_rc" -eq 2 ]; then
|
|
check "C2 stale environment socket is rejected" 0
|
|
else
|
|
check "C2 stale environment socket is rejected" 1
|
|
fi
|
|
if [ ! -e "$STATE/c03-bad-env/config" ]; then
|
|
check "C2 stale environment socket writes no config" 0
|
|
else
|
|
check "C2 stale environment socket writes no config" 1
|
|
fi
|
|
|
|
# C3: with no pin, one exact session hit is discovered from TMUX_TMPDIR.
|
|
DEFAULT_SOCKET="aw-c03-default-$$"
|
|
mk_server "$DEFAULT_SOCKET" default-target
|
|
if run_start --name c03-default --session default-target --when false \
|
|
--message "socket contract" --interval 10 >/dev/null 2>&1; then
|
|
check_config "C3 unique TMUX_TMPDIR hit is selected" c03-default "SOCKET=$DEFAULT_SOCKET"
|
|
else
|
|
check "C3 unique discovery start succeeds" 1
|
|
fi
|
|
|
|
# C4: duplicate session names refuse with the documented rc 4 and identify
|
|
# both sockets. The compound form also proves the session component is pinned
|
|
# exactly while resolving, rather than being treated as a prefix.
|
|
DUP_SOCKET_A="aw-c03-dup-a-$$"
|
|
DUP_SOCKET_B="aw-c03-dup-b-$$"
|
|
mk_server "$DUP_SOCKET_A" duplicate-target
|
|
mk_server "$DUP_SOCKET_B" duplicate-target
|
|
DUP_ERR="$ROOT/duplicate.err"
|
|
dup_rc=0
|
|
run_start --name c03-duplicate --session duplicate-target:0.0 --when false \
|
|
--message "socket contract" --interval 10 >/dev/null 2>"$DUP_ERR" || dup_rc=$?
|
|
if [ "$dup_rc" -eq 4 ]; then
|
|
check "C4 compound duplicate refuses with rc 4" 0
|
|
else
|
|
check "C4 compound duplicate refuses with rc 4" 1
|
|
fi
|
|
if grep -qF "$DUP_SOCKET_A" "$DUP_ERR" && grep -qF "$DUP_SOCKET_B" "$DUP_ERR"; then
|
|
check "C4 refusal names both duplicate sockets" 0
|
|
else
|
|
check "C4 refusal names both duplicate sockets" 1
|
|
fi
|
|
if [ ! -e "$STATE/c03-duplicate/config" ]; then
|
|
check "C4 duplicate refusal leaves no watcher config" 0
|
|
else
|
|
check "C4 duplicate refusal leaves no watcher config" 1
|
|
fi
|
|
|
|
# C5: exact matching rejects a prefix-only session, then accepts the exact
|
|
# name when a prefix sibling is also present.
|
|
PREFIX_SOCKET="aw-c03-prefix-$$"
|
|
EXACT_SOCKET="aw-c03-exact-$$"
|
|
mk_server "$PREFIX_SOCKET" exact-target-old
|
|
prefix_rc=0
|
|
run_start --name c03-prefix --session exact-target --when false \
|
|
--message "socket contract" --interval 10 >/dev/null 2>"$ROOT/prefix.err" || prefix_rc=$?
|
|
if [ "$prefix_rc" -eq 2 ]; then
|
|
check "C5 prefix-only session is not an exact hit" 0
|
|
else
|
|
check "C5 prefix-only session is not an exact hit" 1
|
|
fi
|
|
|
|
mk_server "$EXACT_SOCKET" exact-target
|
|
if run_start --name c03-exact --session =exact-target --when false \
|
|
--message "socket contract" --interval 10 >/dev/null 2>&1; then
|
|
check_config "C5 =name selects the exact session despite prefix sibling" c03-exact "SOCKET=$EXACT_SOCKET"
|
|
check_config "C5 config retains the exact =name target" c03-exact 'SESSION==exact-target'
|
|
else
|
|
check "C5 exact =name start succeeds" 1
|
|
fi
|
|
|
|
# C6: compound input without a leading '=' is normalized to '=session:rest'
|
|
# and discovered. This is the regression that catches constructing '==...'.
|
|
COMPOUND_SOCKET="aw-c03-compound-$$"
|
|
mk_server "$COMPOUND_SOCKET" compound-target
|
|
if run_start --name c03-compound --session compound-target:0.0 --when false \
|
|
--message "socket contract" --interval 10 >/dev/null 2>&1; then
|
|
check_config "C6 compound session discovery pins the session component" c03-compound 'SESSION==compound-target:0.0'
|
|
check_config "C6 compound session selects its unique socket" c03-compound "SOCKET=$COMPOUND_SOCKET"
|
|
else
|
|
check "C6 compound session discovery succeeds" 1
|
|
fi
|
|
|
|
run_mutation() {
|
|
local label="$1" mutation="$2"
|
|
local mutant="$ROOT/$label-mutant.sh" output="$ROOT/$label.out"
|
|
python3 - "$TOOL" "$mutant" "$mutation" <<'PY'
|
|
import sys
|
|
source, destination, mutation = sys.argv[1:]
|
|
text = open(source, encoding="utf-8").read()
|
|
if mutation == "env":
|
|
old = 'if [[ -z "$socket" && -n "${MOSAIC_TMUX_SOCKET:-}" ]]; then\n'
|
|
new = 'if false; then\n'
|
|
else:
|
|
old = 'tmux -L "$sname" has-session -t "$session" 2>/dev/null'
|
|
new = 'tmux -L "$sname" has-session -t "${session#=}" 2>/dev/null'
|
|
assert text.count(old) == 1, f"mutation anchor count for {mutation}: {text.count(old)}"
|
|
mutant_text = text.replace(old, new)
|
|
assert mutant_text != text
|
|
open(destination, "w", encoding="utf-8").write(mutant_text)
|
|
PY
|
|
chmod 700 "$mutant"
|
|
if cmp -s "$TOOL" "$mutant"; then
|
|
echo "FAIL $label mutation was a no-op" >&2
|
|
failures=$((failures + 1))
|
|
return
|
|
fi
|
|
if AGENT_WATCH_TOOL_OVERRIDE="$mutant" "$BASH_BIN" "$0" --no-red-controls >"$output" 2>&1; then
|
|
printf 'FAIL %s mutant survived (suite did not go red)\n' "$label" >&2
|
|
failures=$((failures + 1))
|
|
else
|
|
printf 'ok %s mutant makes the behavioral suite red\n' "$label"
|
|
fi
|
|
}
|
|
|
|
if [ "${1:-}" != "--no-red-controls" ]; then
|
|
# R1: moving env precedence out of its pre-discovery branch must be caught.
|
|
run_mutation red-precedence env
|
|
# R2: removing '=' from discovery must be caught by the exact/prefix arms.
|
|
run_mutation red-exact exact
|
|
fi
|
|
|
|
if [ "$failures" -eq 0 ]; then
|
|
if [ "${1:-}" = "--no-red-controls" ]; then
|
|
echo "agent-watch socket contract: contract arms OK (C1-C6)"
|
|
else
|
|
echo "agent-watch socket contract: all arms OK (C1-C6 + 2 red controls)"
|
|
fi
|
|
exit 0
|
|
fi
|
|
printf 'agent-watch socket contract: %d failure(s)\n' "$failures" >&2
|
|
exit 1
|