#!/usr/bin/env bash # Real SSH forwards in an offline VM; listeners belong only to this test. set -euo pipefail launcher=$1 tmp=$(mktemp -d) export HOME=$tmp/home DSH_HOME=$tmp/dsh DSH_AGENTS_HOME=$tmp/agents XDG_STATE_HOME=$tmp/state mkdir -p "$HOME" "$tmp/project" cd "$tmp/project" state="$XDG_STATE_HOME/agent-vm/$(printf %s "$PWD" | sha256sum | cut -c1-16)" vm_pid='' listener='' cleanup() { status=$? if (( status )); then grep -h . "$tmp/launcher.log" "$tmp/listeners.log" "$state/console.log" | tail -100 || true fi "$launcher" stop >/dev/null 2>&1 || true if [[ -n $vm_pid ]]; then kill "$vm_pid" 2>/dev/null || true; wait "$vm_pid" 2>/dev/null || true; fi if [[ -n $listener ]]; then kill "$listener" 2>/dev/null || true; wait "$listener" 2>/dev/null || true; fi rm -rf "$tmp" return "$status" } trap cleanup EXIT reserve_through() { rm -f "$tmp/listeners-ready" node - "$1" "$tmp/listeners-ready" > "$tmp/listeners.log" 2>&1 <<'JS' & const http = require('node:http'); const fs = require('node:fs'); const listeners = []; for (let port = 3080; port <= Number(process.argv[2]); port++) { listeners.push(new Promise((resolve, reject) => { http.createServer((req, res) => res.end('occupied\n')) .on('error', reject).listen(port, '127.0.0.1', resolve); })); } Promise.all(listeners).then(() => fs.writeFileSync(process.argv[3], 'ready')); JS listener=$! for ((i=0; i<100; i++)); do [[ ! -f $tmp/listeners-ready ]] || return 0 kill -0 "$listener" sleep 0.1 done echo 'Port fixture startup timed out' >&2; return 1 } release_listeners() { kill "$listener"; wait "$listener" || true; listener=''; } expect_port() { local expected=$1 login='' "$launcher" run > "$tmp/launcher.log" 2>&1 & vm_pid=$! for ((i=0; i<120; i++)); do kill -0 "$vm_pid" if login=$("$launcher" url 2>/dev/null); then break; fi sleep 1 done [[ $login == "http://127.0.0.1:$expected/?token=offline-test" ]] [[ $(< "$state/web-port") == "$expected" ]] [[ $(curl --fail --silent --max-time 5 "$login") == agent-vm-test ]] if [[ -n $listener ]]; then [[ $(curl --fail --silent --max-time 5 http://127.0.0.1:3080/) == occupied ]] fi "$launcher" stop wait "$vm_pid" vm_pid='' [[ ! -e $state/web-port && ! -e $state/web.sock ]] if "$launcher" url >/dev/null 2>&1; then echo 'Stale URL after shutdown' >&2; return 1; fi } # Pick the first hole, then exercise the inclusive upper endpoint. reserve_through 3082 expect_port 3083 release_listeners reserve_through 3099 expect_port 3100 release_listeners # Exhaustion must fail, shut down its VM, and not leave a remembered URL. reserve_through 3100 if "$launcher" run > "$tmp/launcher.log" 2>&1; then echo 'Accepted a full port range' >&2; exit 1; fi grep -Fq 'No available Web UI port on 127.0.0.1 in 3080-3100' "$tmp/launcher.log" [[ ! -e $state/web-port && ! -e $state/web.sock ]] if "$launcher" ssh true >/dev/null 2>&1; then echo 'VM survived failed launch' >&2; exit 1; fi release_listeners # A new run starts searching at 3080, not at the previously selected port. expect_port 3080 echo 'PASS: first available port, inclusive 3100 endpoint, full-range failure, real HTTP forwarding, URL persistence and cleanup, restart from 3080'