windows-lifecycle.test.sh 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. #!/usr/bin/env bash
  2. # Windows lifecycle tests for the brainstorm server.
  3. #
  4. # Verifies brainstorm server lifecycle behavior, including:
  5. # - Windows/MSYS2 foreground mode and empty OWNER_PID handling
  6. # - Server survival past the 60-second lifecycle check window
  7. # - Dead-at-startup OWNER_PID validation (logged, monitoring disabled)
  8. # - Clean stop-server.sh shutdown
  9. #
  10. # Requirements:
  11. # - Node.js in PATH
  12. # - Run from the repository root, or set SUPERPOWERS_ROOT
  13. # - On Windows: Git Bash (OSTYPE=msys*)
  14. #
  15. # Usage:
  16. # bash tests/brainstorm-server/windows-lifecycle.test.sh
  17. set -uo pipefail
  18. # ========== Configuration ==========
  19. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  20. REPO_ROOT="${SUPERPOWERS_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
  21. START_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/start-server.sh"
  22. STOP_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/stop-server.sh"
  23. SERVER_SCRIPT="$REPO_ROOT/skills/brainstorming/scripts/server.cjs"
  24. TEST_DIR="${TMPDIR:-/tmp}/brainstorm-win-test-$$"
  25. passed=0
  26. failed=0
  27. skipped=0
  28. # ========== Helpers ==========
  29. cleanup() {
  30. # Kill any server processes we started
  31. for pidvar in SERVER_PID CONTROL_PID STOP_TEST_PID; do
  32. pid="${!pidvar:-}"
  33. if [[ -n "$pid" ]]; then
  34. kill "$pid" 2>/dev/null || true
  35. wait "$pid" 2>/dev/null || true
  36. fi
  37. done
  38. if [[ -n "${TEST_DIR:-}" && -d "$TEST_DIR" ]]; then
  39. rm -rf "$TEST_DIR"
  40. fi
  41. }
  42. trap cleanup EXIT
  43. pass() {
  44. echo " PASS: $1"
  45. passed=$((passed + 1))
  46. }
  47. fail() {
  48. echo " FAIL: $1"
  49. echo " $2"
  50. failed=$((failed + 1))
  51. }
  52. skip() {
  53. echo " SKIP: $1 ($2)"
  54. skipped=$((skipped + 1))
  55. }
  56. wait_for_server_info() {
  57. local dir="$1"
  58. for _ in $(seq 1 50); do
  59. if [[ -f "$dir/state/server-info" ]]; then
  60. return 0
  61. fi
  62. sleep 0.1
  63. done
  64. return 1
  65. }
  66. get_port_from_info() {
  67. # Read the port from state/server-info. Use grep/sed instead of Node.js
  68. # to avoid MSYS2-to-Windows path translation issues.
  69. grep -o '"port":[0-9]*' "$1/state/server-info" | head -1 | sed 's/"port"://'
  70. }
  71. get_key_from_info() {
  72. grep -o '"url":"[^"]*key=[^"]*' "$1/state/server-info" | head -1 | sed 's/.*key=//'
  73. }
  74. http_check() {
  75. local port="$1"
  76. local key="${2:-}"
  77. node - "$port" "$key" <<'NODE'
  78. const http = require('http');
  79. const port = Number(process.argv[2]);
  80. const key = process.argv[3] || '';
  81. const path = key ? '/?key=' + encodeURIComponent(key) : '/';
  82. http.get({ hostname: '127.0.0.1', port, path }, (res) => {
  83. res.resume();
  84. process.exit(res.statusCode === 200 ? 0 : 1);
  85. }).on('error', () => process.exit(1));
  86. NODE
  87. }
  88. # ========== Platform Detection ==========
  89. echo ""
  90. echo "=== Brainstorm Server Windows Lifecycle Tests ==="
  91. echo "Platform: ${OSTYPE:-unknown}"
  92. echo "MSYSTEM: ${MSYSTEM:-unset}"
  93. echo "Node: $(node --version 2>/dev/null || echo 'not found')"
  94. echo ""
  95. is_windows="false"
  96. case "${OSTYPE:-}" in
  97. msys*|cygwin*|mingw*) is_windows="true" ;;
  98. esac
  99. if [[ -n "${MSYSTEM:-}" ]]; then
  100. is_windows="true"
  101. fi
  102. if [[ "$is_windows" != "true" ]]; then
  103. echo "NOTE: Not running on Windows/MSYS2 (OSTYPE=${OSTYPE:-unset})."
  104. echo "Windows-specific tests will be skipped. Tests 4-6 still run."
  105. echo ""
  106. fi
  107. mkdir -p "$TEST_DIR"
  108. SERVER_PID=""
  109. CONTROL_PID=""
  110. STOP_TEST_PID=""
  111. # ========== Test 1: OWNER_PID is empty on Windows ==========
  112. echo "--- Owner PID Resolution ---"
  113. if [[ "$is_windows" == "true" ]]; then
  114. # Replicate the PID resolution logic from start-server.sh lines 104-112
  115. TEST_OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ' || true)"
  116. if [[ -z "$TEST_OWNER_PID" || "$TEST_OWNER_PID" == "1" ]]; then
  117. TEST_OWNER_PID="$PPID"
  118. fi
  119. # The fix: clear on Windows
  120. case "${OSTYPE:-}" in
  121. msys*|cygwin*|mingw*) TEST_OWNER_PID="" ;;
  122. esac
  123. if [[ -z "$TEST_OWNER_PID" ]]; then
  124. pass "OWNER_PID is empty on Windows after fix"
  125. else
  126. fail "OWNER_PID is empty on Windows after fix" \
  127. "Expected empty, got '$TEST_OWNER_PID'"
  128. fi
  129. else
  130. skip "OWNER_PID is empty on Windows" "not on Windows"
  131. fi
  132. # ========== Test 2: start-server.sh passes empty BRAINSTORM_OWNER_PID ==========
  133. if [[ "$is_windows" == "true" ]]; then
  134. # Use a fake 'node' that captures the env var and exits
  135. FAKE_NODE_DIR="$TEST_DIR/fake-bin"
  136. mkdir -p "$FAKE_NODE_DIR"
  137. cat > "$FAKE_NODE_DIR/node" <<'FAKENODE'
  138. #!/usr/bin/env bash
  139. echo "CAPTURED_OWNER_PID=${BRAINSTORM_OWNER_PID:-__UNSET__}"
  140. printf 'CAPTURED_ARGV=%s\n' "$@"
  141. exit 0
  142. FAKENODE
  143. chmod +x "$FAKE_NODE_DIR/node"
  144. captured=$(PATH="$FAKE_NODE_DIR:$PATH" bash "$START_SCRIPT" --project-dir "$TEST_DIR/session" --foreground 2>/dev/null || true)
  145. owner_pid_value=$(echo "$captured" | grep "CAPTURED_OWNER_PID=" | head -1 | sed 's/CAPTURED_OWNER_PID=//')
  146. if [[ "$owner_pid_value" == "" || "$owner_pid_value" == "__UNSET__" ]]; then
  147. pass "start-server.sh passes empty BRAINSTORM_OWNER_PID on Windows"
  148. else
  149. fail "start-server.sh passes empty BRAINSTORM_OWNER_PID on Windows" \
  150. "Expected empty or unset, got '$owner_pid_value'"
  151. fi
  152. if echo "$captured" | grep -Eq '^CAPTURED_ARGV=--brainstorm-server-id=[A-Za-z0-9_-]{32,64}$'; then
  153. pass "start-server.sh passes server instance id argv on Windows"
  154. else
  155. fail "start-server.sh passes server instance id argv on Windows" \
  156. "Expected --brainstorm-server-id=<safe id>, output: $captured"
  157. fi
  158. rm -rf "$FAKE_NODE_DIR" "$TEST_DIR/session"
  159. else
  160. skip "start-server.sh passes empty BRAINSTORM_OWNER_PID" "not on Windows"
  161. fi
  162. # ========== Test 3: Auto-foreground detection on Windows ==========
  163. echo ""
  164. echo "--- Foreground Mode Detection ---"
  165. if [[ "$is_windows" == "true" ]]; then
  166. FAKE_NODE_DIR="$TEST_DIR/fake-bin"
  167. mkdir -p "$FAKE_NODE_DIR"
  168. cat > "$FAKE_NODE_DIR/node" <<'FAKENODE'
  169. #!/usr/bin/env bash
  170. echo "FOREGROUND_MODE=true"
  171. exit 0
  172. FAKENODE
  173. chmod +x "$FAKE_NODE_DIR/node"
  174. # Run WITHOUT --foreground flag — Windows should auto-detect
  175. captured=$(PATH="$FAKE_NODE_DIR:$PATH" bash "$START_SCRIPT" --project-dir "$TEST_DIR/session2" 2>/dev/null || true)
  176. if echo "$captured" | grep -q "FOREGROUND_MODE=true"; then
  177. pass "Windows auto-detects foreground mode"
  178. else
  179. fail "Windows auto-detects foreground mode" \
  180. "Expected foreground code path, output: $captured"
  181. fi
  182. rm -rf "$FAKE_NODE_DIR" "$TEST_DIR/session2"
  183. else
  184. skip "Windows auto-detects foreground mode" "not on Windows"
  185. fi
  186. # ========== Test 4: Server survives past 60-second lifecycle check ==========
  187. echo ""
  188. echo "--- Server Survival (lifecycle check) ---"
  189. mkdir -p "$TEST_DIR/survival"
  190. echo " Starting server (will wait ~75s to verify survival past lifecycle check)..."
  191. BRAINSTORM_DIR="$TEST_DIR/survival" \
  192. BRAINSTORM_HOST="127.0.0.1" \
  193. BRAINSTORM_URL_HOST="localhost" \
  194. BRAINSTORM_OWNER_PID="" \
  195. BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
  196. node "$SERVER_SCRIPT" > "$TEST_DIR/survival/.server.log" 2>&1 &
  197. SERVER_PID=$!
  198. if ! wait_for_server_info "$TEST_DIR/survival"; then
  199. fail "Server starts successfully" "Server did not write state/server-info within 5 seconds"
  200. kill "$SERVER_PID" 2>/dev/null || true
  201. SERVER_PID=""
  202. else
  203. pass "Server starts successfully with empty OWNER_PID"
  204. SERVER_PORT=$(get_port_from_info "$TEST_DIR/survival")
  205. SERVER_KEY=$(get_key_from_info "$TEST_DIR/survival")
  206. sleep 75
  207. if kill -0 "$SERVER_PID" 2>/dev/null; then
  208. pass "Server is still alive after 75 seconds"
  209. else
  210. fail "Server is still alive after 75 seconds" \
  211. "Server died. Log tail: $(tail -5 "$TEST_DIR/survival/.server.log" 2>/dev/null)"
  212. fi
  213. if http_check "$SERVER_PORT" "$SERVER_KEY"; then
  214. pass "Server responds to HTTP after lifecycle check window"
  215. else
  216. fail "Server responds to HTTP after lifecycle check window" \
  217. "Authenticated HTTP request to port $SERVER_PORT failed"
  218. fi
  219. if grep -q "owner process exited" "$TEST_DIR/survival/.server.log" 2>/dev/null; then
  220. fail "No 'owner process exited' in logs" \
  221. "Found spurious owner-exit shutdown in log"
  222. else
  223. pass "No 'owner process exited' in logs"
  224. fi
  225. kill "$SERVER_PID" 2>/dev/null || true
  226. wait "$SERVER_PID" 2>/dev/null || true
  227. SERVER_PID=""
  228. fi
  229. # ========== Test 5: Dead-at-startup OWNER_PID is logged but does not kill the server ==========
  230. #
  231. # The server validates BRAINSTORM_OWNER_PID at startup. If it's already dead,
  232. # the PID resolution was wrong (common on WSL, Tailscale SSH, cross-user
  233. # scenarios). The server logs 'owner-pid-invalid', disables owner monitoring,
  234. # and continues running. The idle timeout becomes the only shutdown trigger.
  235. echo ""
  236. echo "--- Dead-at-startup OWNER_PID: server survives, logs owner-pid-invalid ---"
  237. mkdir -p "$TEST_DIR/control"
  238. # Find a PID that does not exist
  239. BAD_PID=99999
  240. while kill -0 "$BAD_PID" 2>/dev/null; do
  241. BAD_PID=$((BAD_PID + 1))
  242. done
  243. BRAINSTORM_DIR="$TEST_DIR/control" \
  244. BRAINSTORM_HOST="127.0.0.1" \
  245. BRAINSTORM_URL_HOST="localhost" \
  246. BRAINSTORM_OWNER_PID="$BAD_PID" \
  247. BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
  248. node "$SERVER_SCRIPT" > "$TEST_DIR/control/.server.log" 2>&1 &
  249. CONTROL_PID=$!
  250. if ! wait_for_server_info "$TEST_DIR/control"; then
  251. fail "Control server starts" "Server did not write state/server-info within 5 seconds"
  252. kill "$CONTROL_PID" 2>/dev/null || true
  253. CONTROL_PID=""
  254. else
  255. pass "Control server starts with dead-at-startup OWNER_PID=$BAD_PID"
  256. echo " Waiting ~75s to verify server survives past lifecycle check..."
  257. sleep 75
  258. if kill -0 "$CONTROL_PID" 2>/dev/null; then
  259. pass "Server survives with dead-at-startup OWNER_PID (owner monitoring disabled)"
  260. else
  261. fail "Server survives with dead-at-startup OWNER_PID" \
  262. "Server died unexpectedly. Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
  263. fi
  264. if grep -q "owner-pid-invalid" "$TEST_DIR/control/.server.log" 2>/dev/null; then
  265. pass "Server logs 'owner-pid-invalid' for dead-at-startup PID"
  266. else
  267. fail "Server logs 'owner-pid-invalid' for dead-at-startup PID" \
  268. "Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
  269. fi
  270. if grep -q "owner process exited" "$TEST_DIR/control/.server.log" 2>/dev/null; then
  271. fail "No spurious 'owner process exited' log" \
  272. "Found 'owner process exited' but owner monitoring should be disabled"
  273. else
  274. pass "No spurious 'owner process exited' log"
  275. fi
  276. kill "$CONTROL_PID" 2>/dev/null || true
  277. fi
  278. wait "$CONTROL_PID" 2>/dev/null || true
  279. CONTROL_PID=""
  280. # ========== Test 6: stop-server.sh cleanly stops the server ==========
  281. echo ""
  282. echo "--- Clean Shutdown ---"
  283. mkdir -p "$TEST_DIR/stop-test/state"
  284. STOP_TEST_ID="$(printf 'windowsstop%021d\n' "$RANDOM")"
  285. printf '%s\n' "$STOP_TEST_ID" > "$TEST_DIR/stop-test/state/server-instance-id"
  286. BRAINSTORM_DIR="$TEST_DIR/stop-test" \
  287. BRAINSTORM_HOST="127.0.0.1" \
  288. BRAINSTORM_URL_HOST="localhost" \
  289. BRAINSTORM_OWNER_PID="" \
  290. BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
  291. node "$SERVER_SCRIPT" "--brainstorm-server-id=$STOP_TEST_ID" > "$TEST_DIR/stop-test/.server.log" 2>&1 &
  292. STOP_TEST_PID=$!
  293. disown "$STOP_TEST_PID" 2>/dev/null || true
  294. echo "$STOP_TEST_PID" > "$TEST_DIR/stop-test/state/server.pid"
  295. if ! wait_for_server_info "$TEST_DIR/stop-test"; then
  296. fail "Stop-test server starts" "Server did not start"
  297. kill "$STOP_TEST_PID" 2>/dev/null || true
  298. wait "$STOP_TEST_PID" 2>/dev/null || true
  299. STOP_TEST_PID=""
  300. else
  301. bash "$STOP_SCRIPT" "$TEST_DIR/stop-test" >/dev/null 2>&1 || true
  302. for _ in $(seq 1 10); do
  303. if ! kill -0 "$STOP_TEST_PID" 2>/dev/null; then
  304. wait "$STOP_TEST_PID" 2>/dev/null || true
  305. break
  306. fi
  307. sleep 0.1
  308. done
  309. if ! kill -0 "$STOP_TEST_PID" 2>/dev/null; then
  310. pass "stop-server.sh cleanly stops the server"
  311. else
  312. fail "stop-server.sh cleanly stops the server" \
  313. "Server PID $STOP_TEST_PID is still alive after stop"
  314. kill "$STOP_TEST_PID" 2>/dev/null || true
  315. fi
  316. fi
  317. wait "$STOP_TEST_PID" 2>/dev/null || true
  318. STOP_TEST_PID=""
  319. # ========== Summary ==========
  320. echo ""
  321. echo "=== Results: $passed passed, $failed failed, $skipped skipped ==="
  322. if [[ $failed -gt 0 ]]; then
  323. exit 1
  324. fi
  325. exit 0