scripts/agent-environment.test.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
# Lifecycle of scripts/agent-environment.sh against a stub dev-env.sh: who
# stops a component, who destroys the shared environment, and that exactly one
# of them does whatever order or timing the stops come in.
set -euo pipefail
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmp_dir="$(mktemp -d)"
pids=()
cleanup() {
local pid
for pid in ${pids[@]+"${pids[@]}"}; do kill "$pid" 2>/dev/null || true; done
wait 2>/dev/null || true
rm -rf "$tmp_dir"
}
trap cleanup EXIT
fail() {
echo "FAIL: $*" >&2
[ ! -f "$repo/calls.log" ] || sed 's/^/ calls: /' "$repo/calls.log" >&2
exit 1
}
free_port() {
node -e 'const s = require("node:net").createServer().listen(0, "127.0.0.1", () => { console.log(s.address().port); s.close(); });'
}
# A fresh fake checkout: the real script next to a dev-env.sh that records
# each verb, keeps "registered" as the environment's existence and
# "<component>.running" as each component's state.
new_repo() {
repo="$tmp_dir/repo-$1"
mkdir -p "$repo/scripts"
cp "$root_dir/scripts/agent-environment.sh" "$repo/scripts/"
component_port="$(free_port)"
cat > "$repo/scripts/dev-env.sh" <<EOF
#!/usr/bin/env bash
set -eu
cd "$repo"
state() { [ -f "\$1.running" ] && echo running || echo stopped; }
case "\$1" in
up)
echo "\$*" >> calls.log; touch registered
case "\$3" in *web*) touch web.running ;; *desktop*) touch desktop.running ;; esac ;;
status)
[ -f registered ] || exit 1
printf '{"frontend_port":%s,"desktop_renderer_port":%s,"workspace_slug":"dev","components":{"web":{"state":"%s"},"desktop":{"state":"%s"}}}\n' \
"$component_port" "$component_port" "\$(state web)" "\$(state desktop)" ;;
down) sleep 0.3; echo "down \$3" >> calls.log; rm -f "\$3.running" ;;
destroy) sleep 0.3; echo destroy >> calls.log; rm -f registered web.running desktop.running ;;
esac
EOF
: > "$repo/calls.log"
}
count() { grep -cx "$1" "$repo/calls.log" || true; }
# A process that stands in for a running start: its pid is what a claim names.
live_claim() {
mkdir -p "$repo/.multica/agent-environments"
sleep 300 &
pids+=("$!")
printf '%s\n' "$!" > "$repo/.multica/agent-environments/$1.claim"
}
# Kills and reaps a background job, so bash does not report it as terminated.
stop_job() {
kill "$1" 2>/dev/null || true
wait "$1" 2>/dev/null || true
}
serve_component() {
node -e 'require("node:http").createServer((_, res) => res.end("ok")).listen(Number(process.argv[1]), "127.0.0.1")' "$component_port" &
component_pid=$!
pids+=("$component_pid")
}
start_in_background() {
MULTICA_PORT="$(free_port)" bash "$repo/scripts/agent-environment.sh" start "$1" > "$repo/start.log" 2>&1 &
start_pid=$!
pids+=("$start_pid")
local i
for i in $(seq 1 100); do
grep -q "redirects to\|watching its renderer" "$repo/start.log" 2>/dev/null && return 0
sleep 0.1
done
fail "start $1 never came up: $(cat "$repo/start.log")"
}
wait_for_exit() {
local i
for i in $(seq 1 300); do
kill -0 "$1" 2>/dev/null || { wait "$1" 2>/dev/null || true; return 0; }
sleep 0.1
done
fail "process $1 did not exit"
}
# 1. The component going away is cleaned up by start itself: the daemon
# neither runs stop nor expires an environment whose start has exited.
new_repo exit-last
serve_component
start_in_background web
stop_job "$component_pid"
wait_for_exit "$start_pid"
[ "$(count "down web")" = 1 ] && [ "$(count destroy)" = 1 ] || fail "a component that exited was not cleaned up"
[ ! -f "$repo/registered" ] || fail "the environment outlived its last component"
# 2. ...but what the other component still uses is kept.
new_repo exit-shared
live_claim web
serve_component
start_in_background desktop
stop_job "$component_pid"
wait_for_exit "$start_pid"
[ "$(count "down desktop")" = 1 ] && [ "$(count destroy)" = 0 ] || fail "an exiting component destroyed what web still uses"
# 3. Two stops at once: both components stop, exactly one destroys.
new_repo concurrent
touch "$repo/registered"
live_claim web
live_claim desktop
bash "$repo/scripts/agent-environment.sh" stop web > /dev/null &
first=$!
bash "$repo/scripts/agent-environment.sh" stop desktop > /dev/null &
second=$!
wait "$first" || fail "stop web failed"
wait "$second" || fail "stop desktop failed"
[ "$(count "down web")" = 1 ] && [ "$(count "down desktop")" = 1 ] || fail "concurrent stops did not stop both components"
[ "$(count destroy)" = 1 ] || fail "concurrent stops destroyed $(count destroy) times, want once"
# 4. The daemon signals start after running stop. start's own cleanup then
# finds nothing left to do.
new_repo signalled
serve_component
start_in_background web
bash "$repo/scripts/agent-environment.sh" stop web > /dev/null
stop_job "$start_pid"
[ "$(count "down web")" = 1 ] && [ "$(count destroy)" = 1 ] || fail "stop followed by the daemon's signal cleaned up more than once"
# 5. A claim whose start was killed outright does not keep the environment.
new_repo stale
touch "$repo/registered"
mkdir -p "$repo/.multica/agent-environments"
sleep 0 & dead=$!
wait "$dead"
printf '%s\n' "$dead" > "$repo/.multica/agent-environments/desktop.claim"
bash "$repo/scripts/agent-environment.sh" stop web > /dev/null
[ "$(count destroy)" = 1 ] || fail "a stale claim kept the environment alive"
# 6. A component started with a plain `make up` has no claim, but is in use:
# stopping or losing the managed one next to it keeps what it uses. Both
# orders, through both ways a managed component ends.
for pair in "desktop web" "web desktop"; do
set -- $pair
unmanaged=$1 managed=$2
new_repo "unmanaged-$unmanaged-exit"
touch "$repo/registered" "$repo/$unmanaged.running"
serve_component
start_in_background "$managed"
stop_job "$component_pid"
wait_for_exit "$start_pid"
[ "$(count "down $managed")" = 1 ] && [ "$(count destroy)" = 0 ] \
|| fail "$managed exiting destroyed the environment $unmanaged runs in"
new_repo "unmanaged-$unmanaged-stop"
touch "$repo/registered" "$repo/$unmanaged.running"
serve_component
start_in_background "$managed"
bash "$repo/scripts/agent-environment.sh" stop "$managed" > /dev/null
stop_job "$start_pid"
[ "$(count "down $managed")" = 1 ] && [ "$(count destroy)" = 0 ] \
|| fail "stopping $managed destroyed the environment $unmanaged runs in"
done
# 7. An environment that existed before the first claim is someone else's:
# start keeps its owner and TTL, and the last stop leaves it in place.
new_repo borrowed
touch "$repo/registered"
serve_component
start_in_background web
grep -q -- "--ttl" "$repo/calls.log" && fail "start gave an existing environment a TTL"
bash "$repo/scripts/agent-environment.sh" stop web > /dev/null
stop_job "$start_pid"
[ "$(count "down web")" = 1 ] && [ "$(count destroy)" = 0 ] || fail "the last stop destroyed an environment it did not create"
# 8. ...while one start created fresh still gets the TTL.
new_repo fresh
serve_component
start_in_background web
grep -q -- "--ttl 25" "$repo/calls.log" || fail "start did not give a new environment its TTL"
stop_job "$start_pid"
echo "✓ agent-environment.sh lifecycle verified"