windows-lifecycle.test.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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. exit 0
  141. FAKENODE
  142. chmod +x "$FAKE_NODE_DIR/node"
  143. captured=$(PATH="$FAKE_NODE_DIR:$PATH" bash "$START_SCRIPT" --project-dir "$TEST_DIR/session" --foreground 2>/dev/null || true)
  144. owner_pid_value=$(echo "$captured" | grep "CAPTURED_OWNER_PID=" | head -1 | sed 's/CAPTURED_OWNER_PID=//')
  145. if [[ "$owner_pid_value" == "" || "$owner_pid_value" == "__UNSET__" ]]; then
  146. pass "start-server.sh passes empty BRAINSTORM_OWNER_PID on Windows"
  147. else
  148. fail "start-server.sh passes empty BRAINSTORM_OWNER_PID on Windows" \
  149. "Expected empty or unset, got '$owner_pid_value'"
  150. fi
  151. rm -rf "$FAKE_NODE_DIR" "$TEST_DIR/session"
  152. else
  153. skip "start-server.sh passes empty BRAINSTORM_OWNER_PID" "not on Windows"
  154. fi
  155. # ========== Test 3: Auto-foreground detection on Windows ==========
  156. echo ""
  157. echo "--- Foreground Mode Detection ---"
  158. if [[ "$is_windows" == "true" ]]; then
  159. FAKE_NODE_DIR="$TEST_DIR/fake-bin"
  160. mkdir -p "$FAKE_NODE_DIR"
  161. cat > "$FAKE_NODE_DIR/node" <<'FAKENODE'
  162. #!/usr/bin/env bash
  163. echo "FOREGROUND_MODE=true"
  164. exit 0
  165. FAKENODE
  166. chmod +x "$FAKE_NODE_DIR/node"
  167. # Run WITHOUT --foreground flag — Windows should auto-detect
  168. captured=$(PATH="$FAKE_NODE_DIR:$PATH" bash "$START_SCRIPT" --project-dir "$TEST_DIR/session2" 2>/dev/null || true)
  169. if echo "$captured" | grep -q "FOREGROUND_MODE=true"; then
  170. pass "Windows auto-detects foreground mode"
  171. else
  172. fail "Windows auto-detects foreground mode" \
  173. "Expected foreground code path, output: $captured"
  174. fi
  175. rm -rf "$FAKE_NODE_DIR" "$TEST_DIR/session2"
  176. else
  177. skip "Windows auto-detects foreground mode" "not on Windows"
  178. fi
  179. # ========== Test 4: Server survives past 60-second lifecycle check ==========
  180. echo ""
  181. echo "--- Server Survival (lifecycle check) ---"
  182. mkdir -p "$TEST_DIR/survival"
  183. echo " Starting server (will wait ~75s to verify survival past lifecycle check)..."
  184. BRAINSTORM_DIR="$TEST_DIR/survival" \
  185. BRAINSTORM_HOST="127.0.0.1" \
  186. BRAINSTORM_URL_HOST="localhost" \
  187. BRAINSTORM_OWNER_PID="" \
  188. BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
  189. node "$SERVER_SCRIPT" > "$TEST_DIR/survival/.server.log" 2>&1 &
  190. SERVER_PID=$!
  191. if ! wait_for_server_info "$TEST_DIR/survival"; then
  192. fail "Server starts successfully" "Server did not write state/server-info within 5 seconds"
  193. kill "$SERVER_PID" 2>/dev/null || true
  194. SERVER_PID=""
  195. else
  196. pass "Server starts successfully with empty OWNER_PID"
  197. SERVER_PORT=$(get_port_from_info "$TEST_DIR/survival")
  198. SERVER_KEY=$(get_key_from_info "$TEST_DIR/survival")
  199. sleep 75
  200. if kill -0 "$SERVER_PID" 2>/dev/null; then
  201. pass "Server is still alive after 75 seconds"
  202. else
  203. fail "Server is still alive after 75 seconds" \
  204. "Server died. Log tail: $(tail -5 "$TEST_DIR/survival/.server.log" 2>/dev/null)"
  205. fi
  206. if http_check "$SERVER_PORT" "$SERVER_KEY"; then
  207. pass "Server responds to HTTP after lifecycle check window"
  208. else
  209. fail "Server responds to HTTP after lifecycle check window" \
  210. "Authenticated HTTP request to port $SERVER_PORT failed"
  211. fi
  212. if grep -q "owner process exited" "$TEST_DIR/survival/.server.log" 2>/dev/null; then
  213. fail "No 'owner process exited' in logs" \
  214. "Found spurious owner-exit shutdown in log"
  215. else
  216. pass "No 'owner process exited' in logs"
  217. fi
  218. kill "$SERVER_PID" 2>/dev/null || true
  219. wait "$SERVER_PID" 2>/dev/null || true
  220. SERVER_PID=""
  221. fi
  222. # ========== Test 5: Dead-at-startup OWNER_PID is logged but does not kill the server ==========
  223. #
  224. # The server validates BRAINSTORM_OWNER_PID at startup. If it's already dead,
  225. # the PID resolution was wrong (common on WSL, Tailscale SSH, cross-user
  226. # scenarios). The server logs 'owner-pid-invalid', disables owner monitoring,
  227. # and continues running. The idle timeout becomes the only shutdown trigger.
  228. echo ""
  229. echo "--- Dead-at-startup OWNER_PID: server survives, logs owner-pid-invalid ---"
  230. mkdir -p "$TEST_DIR/control"
  231. # Find a PID that does not exist
  232. BAD_PID=99999
  233. while kill -0 "$BAD_PID" 2>/dev/null; do
  234. BAD_PID=$((BAD_PID + 1))
  235. done
  236. BRAINSTORM_DIR="$TEST_DIR/control" \
  237. BRAINSTORM_HOST="127.0.0.1" \
  238. BRAINSTORM_URL_HOST="localhost" \
  239. BRAINSTORM_OWNER_PID="$BAD_PID" \
  240. BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
  241. node "$SERVER_SCRIPT" > "$TEST_DIR/control/.server.log" 2>&1 &
  242. CONTROL_PID=$!
  243. if ! wait_for_server_info "$TEST_DIR/control"; then
  244. fail "Control server starts" "Server did not write state/server-info within 5 seconds"
  245. kill "$CONTROL_PID" 2>/dev/null || true
  246. CONTROL_PID=""
  247. else
  248. pass "Control server starts with dead-at-startup OWNER_PID=$BAD_PID"
  249. echo " Waiting ~75s to verify server survives past lifecycle check..."
  250. sleep 75
  251. if kill -0 "$CONTROL_PID" 2>/dev/null; then
  252. pass "Server survives with dead-at-startup OWNER_PID (owner monitoring disabled)"
  253. else
  254. fail "Server survives with dead-at-startup OWNER_PID" \
  255. "Server died unexpectedly. Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
  256. fi
  257. if grep -q "owner-pid-invalid" "$TEST_DIR/control/.server.log" 2>/dev/null; then
  258. pass "Server logs 'owner-pid-invalid' for dead-at-startup PID"
  259. else
  260. fail "Server logs 'owner-pid-invalid' for dead-at-startup PID" \
  261. "Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
  262. fi
  263. if grep -q "owner process exited" "$TEST_DIR/control/.server.log" 2>/dev/null; then
  264. fail "No spurious 'owner process exited' log" \
  265. "Found 'owner process exited' but owner monitoring should be disabled"
  266. else
  267. pass "No spurious 'owner process exited' log"
  268. fi
  269. kill "$CONTROL_PID" 2>/dev/null || true
  270. fi
  271. wait "$CONTROL_PID" 2>/dev/null || true
  272. CONTROL_PID=""
  273. # ========== Test 6: stop-server.sh cleanly stops the server ==========
  274. echo ""
  275. echo "--- Clean Shutdown ---"
  276. mkdir -p "$TEST_DIR/stop-test/state"
  277. STOP_TEST_ID="$(printf 'windowsstop%021d\n' "$RANDOM")"
  278. printf '%s\n' "$STOP_TEST_ID" > "$TEST_DIR/stop-test/state/server-instance-id"
  279. BRAINSTORM_DIR="$TEST_DIR/stop-test" \
  280. BRAINSTORM_HOST="127.0.0.1" \
  281. BRAINSTORM_URL_HOST="localhost" \
  282. BRAINSTORM_OWNER_PID="" \
  283. BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
  284. node "$SERVER_SCRIPT" "--brainstorm-server-id=$STOP_TEST_ID" > "$TEST_DIR/stop-test/.server.log" 2>&1 &
  285. STOP_TEST_PID=$!
  286. disown "$STOP_TEST_PID" 2>/dev/null || true
  287. echo "$STOP_TEST_PID" > "$TEST_DIR/stop-test/state/server.pid"
  288. if ! wait_for_server_info "$TEST_DIR/stop-test"; then
  289. fail "Stop-test server starts" "Server did not start"
  290. kill "$STOP_TEST_PID" 2>/dev/null || true
  291. STOP_TEST_PID=""
  292. else
  293. bash "$STOP_SCRIPT" "$TEST_DIR/stop-test" >/dev/null 2>&1 || true
  294. for _ in $(seq 1 10); do
  295. if ! kill -0 "$STOP_TEST_PID" 2>/dev/null; then
  296. wait "$STOP_TEST_PID" 2>/dev/null || true
  297. break
  298. fi
  299. sleep 0.1
  300. done
  301. if ! kill -0 "$STOP_TEST_PID" 2>/dev/null; then
  302. pass "stop-server.sh cleanly stops the server"
  303. else
  304. fail "stop-server.sh cleanly stops the server" \
  305. "Server PID $STOP_TEST_PID is still alive after stop"
  306. kill "$STOP_TEST_PID" 2>/dev/null || true
  307. fi
  308. fi
  309. wait "$STOP_TEST_PID" 2>/dev/null || true
  310. STOP_TEST_PID=""
  311. # ========== Summary ==========
  312. echo ""
  313. echo "=== Results: $passed passed, $failed failed, $skipped skipped ==="
  314. if [[ $failed -gt 0 ]]; then
  315. exit 1
  316. fi
  317. exit 0