#!/usr/bin/env bash
# daemon-sh.test.sh — gate H5 (docs/home-world-design.md): the dev scripts start a
# keet-daemon when none is up, REUSE a live one, and stop only the one they started.
#
# scripts/test/daemon-sh.test.sh
#
# NO REAL DAEMON, NO REAL IDENTITY. `bare` is a stub that listens on a scratch
# socket (HYPER_TPS_BARE + KEET_SOCK), and Godot is a stub that records whether the
# socket answered while the "editor" was open (HYPER_TPS_GODOT). A real daemon would
# open ~/.config/keet-cli -- the user's identity -- which a test must never touch.
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# Short paths: an AF_UNIX path over 108 bytes cannot be bound at all.
T="$(mktemp -d "${XDG_RUNTIME_DIR:-/tmp}/htps-dsh.XXXXXX")"
export KEET_SOCK="$T/k.sock"
export HYPER_TPS_DEV_LOG="$T/daemon.log"
export HYPER_TPS_DEV_PIDFILE="$T/daemon.pid"
export STUB_STARTS="$T/starts"
: > "$STUB_STARTS"
FAIL=0
pass() { echo " PASS $*"; }
fail() { echo " FAIL $*"; FAIL=1; }
alive() {
python3 -c "
import socket,sys
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.settimeout(2)
try:
s.connect('$1'); s.close(); sys.exit(0)
except Exception:
sys.exit(1)
" 2>/dev/null
}
# A stub `bare`: counts its starts and serves the socket until killed. Arguments
# (index.js --daemon-only --no-game) are ignored, as is the cwd.
cat > "$T/bare" <<'EOF'
#!/usr/bin/env bash
echo "$*" >> "$STUB_STARTS"
exec python3 -c "
import socket, os
p = os.environ['KEET_SOCK']
# Replace a stale socket node, as a real daemon does: a killed listener leaves
# the FILE behind, and bind() on it fails with EADDRINUSE.
if os.path.exists(p): os.unlink(p)
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.bind(p); s.listen(8)
while True:
c, _ = s.accept(); c.close()
"
EOF
chmod +x "$T/bare"
export HYPER_TPS_BARE="$T/bare"
# A stub engine: records whether the daemon answered while it ran, then exits.
cat > "$T/godot" <<EOF
#!/usr/bin/env bash
python3 -c "
import socket,sys
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.settimeout(2)
try:
s.connect('$KEET_SOCK'); print('up')
except Exception:
print('down')
" > "$T/editor_saw"
EOF
chmod +x "$T/godot"
export HYPER_TPS_GODOT="$T/godot"
_cleanup() {
[ -f "$T/ext.pid" ] && kill "$(cat "$T/ext.pid")" 2>/dev/null
[ -f "$HYPER_TPS_DEV_PIDFILE" ] && kill -TERM "-$(cat "$HYPER_TPS_DEV_PIDFILE")" 2>/dev/null
}
trap _cleanup EXIT
if [ ! -f "$REPO/scripts/daemon.sh" ]; then
fail "scripts/daemon.sh does not exist"
echo "daemon-sh.test: FAIL"; exit 1
fi
echo "T1 ensure_daemon with none running starts one"
(
. "$REPO/scripts/daemon.sh"
ensure_daemon
[ "$started_daemon" = 1 ] && pass "started_daemon=1" || fail "started_daemon=$started_daemon"
alive "$KEET_SOCK" && pass "socket answers" || fail "socket does not answer"
[ "$(wc -l < "$STUB_STARTS")" = 1 ] && pass "one start" || fail "starts=$(wc -l < "$STUB_STARTS")"
[ "$(cat "$STUB_STARTS")" = "index.js --daemon-only --no-game" ] && pass "argv reaches bare intact" \
|| fail "bare got argv: '$(cat "$STUB_STARTS")'"
stop_daemon_if_started
sleep 0.5
alive "$KEET_SOCK" && fail "T3 own daemon still answers after stop" || pass "T3 own daemon stopped"
)
echo "T2 ensure_daemon with one already running reuses it"
: > "$STUB_STARTS"
( "$T/bare" & echo $! > "$T/ext.pid"; wait ) &
for _ in $(seq 1 40); do alive "$KEET_SOCK" && break; sleep 0.1; done
: > "$STUB_STARTS" # the external start is not ensure_daemon's
(
. "$REPO/scripts/daemon.sh"
ensure_daemon
[ "$started_daemon" = 0 ] && pass "started_daemon=0" || fail "started_daemon=$started_daemon"
[ "$(wc -l < "$STUB_STARTS")" = 0 ] && pass "no second start" || fail "starts=$(wc -l < "$STUB_STARTS")"
stop_daemon_if_started
sleep 0.5
alive "$KEET_SOCK" && pass "T3 someone else's daemon left running" || fail "T3 stopped a daemon it did not start"
)
kill "$(cat "$T/ext.pid")" 2>/dev/null; rm -f "$T/ext.pid"
for _ in $(seq 1 40); do alive "$KEET_SOCK" || break; sleep 0.1; done
echo "T4 dev-editor.sh starts a daemon for the editor session and stops it after"
: > "$STUB_STARTS"; rm -f "$T/editor_saw"
"$REPO/scripts/dev-editor.sh" > "$T/editor.log" 2>&1
[ "$(cat "$T/editor_saw" 2>/dev/null)" = up ] && pass "daemon up while the editor ran" || fail "editor saw: $(cat "$T/editor_saw" 2>/dev/null || echo nothing) (log: $T/editor.log)"
sleep 0.5
alive "$KEET_SOCK" && fail "daemon still running after the editor exited" || pass "daemon stopped after the editor"
echo "T5 dev-editor.sh --no-daemon starts none"
: > "$STUB_STARTS"; rm -f "$T/editor_saw"
"$REPO/scripts/dev-editor.sh" --no-daemon > "$T/editor2.log" 2>&1
[ "$(wc -l < "$STUB_STARTS")" = 0 ] && pass "no start" || fail "starts=$(wc -l < "$STUB_STARTS")"
[ "$(cat "$T/editor_saw" 2>/dev/null)" = down ] && pass "editor ran without a daemon" || fail "editor saw: $(cat "$T/editor_saw" 2>/dev/null || echo nothing)"
echo "T6 dev-run.sh starts a daemon for the game and stops it after"
: > "$STUB_STARTS"; rm -f "$T/editor_saw"
HYPER_TPS_GAME_LOG="$T/game.log" "$REPO/scripts/dev-run.sh" > "$T/run.log" 2>&1
[ "$(cat "$T/editor_saw" 2>/dev/null)" = up ] && pass "daemon up while the game ran" || fail "game saw: $(cat "$T/editor_saw" 2>/dev/null || echo nothing) (log: $T/run.log)"
sleep 0.5
alive "$KEET_SOCK" && fail "daemon still running after the game exited" || pass "daemon stopped after the game"
echo "T7 dev-run.sh --daemon-only --pin-room=<id> leaves the daemon running, pinned"
: > "$STUB_STARTS"
"$REPO/scripts/dev-run.sh" --daemon-only --pin-room=room-a > "$T/run2.log" 2>&1
[ "$(cat "$STUB_STARTS")" = "index.js --daemon-only --no-game --pin-room room-a" ] && pass "pin reaches bare" \
|| fail "bare got argv: '$(cat "$STUB_STARTS")'"
grep -q "pinning 1 room(s)" "$T/run2.log" && pass "pin count says 1" || fail "pin message: $(grep pinning "$T/run2.log")"
sleep 0.5
alive "$KEET_SOCK" && pass "daemon outlives the script" || fail "daemon gone (log: $T/run2.log)"
# _cleanup stops it via the pidfile.
if [ "$FAIL" = 0 ]; then
_cleanup; trap - EXIT
rm -rf "$T" # this test's own mktemp dir; kept on FAIL for the logs
echo "daemon-sh.test: PASS"
else
echo "daemon-sh.test: FAIL (scratch: $T)"
fi
exit "$FAIL"