windows-lifecycle.test.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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. http_check() {
  72. local port="$1"
  73. node -e "
  74. const http = require('http');
  75. http.get('http://localhost:$port/', (res) => {
  76. process.exit(res.statusCode === 200 ? 0 : 1);
  77. }).on('error', () => process.exit(1));
  78. " 2>/dev/null
  79. }
  80. # ========== Platform Detection ==========
  81. echo ""
  82. echo "=== Brainstorm Server Windows Lifecycle Tests ==="
  83. echo "Platform: ${OSTYPE:-unknown}"
  84. echo "MSYSTEM: ${MSYSTEM:-unset}"
  85. echo "Node: $(node --version 2>/dev/null || echo 'not found')"
  86. echo ""
  87. is_windows="false"
  88. case "${OSTYPE:-}" in
  89. msys*|cygwin*|mingw*) is_windows="true" ;;
  90. esac
  91. if [[ -n "${MSYSTEM:-}" ]]; then
  92. is_windows="true"
  93. fi
  94. if [[ "$is_windows" != "true" ]]; then
  95. echo "NOTE: Not running on Windows/MSYS2 (OSTYPE=${OSTYPE:-unset})."
  96. echo "Windows-specific tests will be skipped. Tests 4-6 still run."
  97. echo ""
  98. fi
  99. mkdir -p "$TEST_DIR"
  100. SERVER_PID=""
  101. CONTROL_PID=""
  102. STOP_TEST_PID=""
  103. # ========== Test 1: OWNER_PID is empty on Windows ==========
  104. echo "--- Owner PID Resolution ---"
  105. if [[ "$is_windows" == "true" ]]; then
  106. # Replicate the PID resolution logic from start-server.sh lines 104-112
  107. TEST_OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ' || true)"
  108. if [[ -z "$TEST_OWNER_PID" || "$TEST_OWNER_PID" == "1" ]]; then
  109. TEST_OWNER_PID="$PPID"
  110. fi
  111. # The fix: clear on Windows
  112. case "${OSTYPE:-}" in
  113. msys*|cygwin*|mingw*) TEST_OWNER_PID="" ;;
  114. esac
  115. if [[ -z "$TEST_OWNER_PID" ]]; then
  116. pass "OWNER_PID is empty on Windows after fix"
  117. else
  118. fail "OWNER_PID is empty on Windows after fix" \
  119. "Expected empty, got '$TEST_OWNER_PID'"
  120. fi
  121. else
  122. skip "OWNER_PID is empty on Windows" "not on Windows"
  123. fi
  124. # ========== Test 2: start-server.sh passes empty BRAINSTORM_OWNER_PID ==========
  125. if [[ "$is_windows" == "true" ]]; then
  126. # Use a fake 'node' that captures the env var and exits
  127. FAKE_NODE_DIR="$TEST_DIR/fake-bin"
  128. mkdir -p "$FAKE_NODE_DIR"
  129. cat > "$FAKE_NODE_DIR/node" <<'FAKENODE'
  130. #!/usr/bin/env bash
  131. echo "CAPTURED_OWNER_PID=${BRAINSTORM_OWNER_PID:-__UNSET__}"
  132. exit 0
  133. FAKENODE
  134. chmod +x "$FAKE_NODE_DIR/node"
  135. captured=$(PATH="$FAKE_NODE_DIR:$PATH" bash "$START_SCRIPT" --project-dir "$TEST_DIR/session" --foreground 2>/dev/null || true)
  136. owner_pid_value=$(echo "$captured" | grep "CAPTURED_OWNER_PID=" | head -1 | sed 's/CAPTURED_OWNER_PID=//')
  137. if [[ "$owner_pid_value" == "" || "$owner_pid_value" == "__UNSET__" ]]; then
  138. pass "start-server.sh passes empty BRAINSTORM_OWNER_PID on Windows"
  139. else
  140. fail "start-server.sh passes empty BRAINSTORM_OWNER_PID on Windows" \
  141. "Expected empty or unset, got '$owner_pid_value'"
  142. fi
  143. rm -rf "$FAKE_NODE_DIR" "$TEST_DIR/session"
  144. else
  145. skip "start-server.sh passes empty BRAINSTORM_OWNER_PID" "not on Windows"
  146. fi
  147. # ========== Test 3: Auto-foreground detection on Windows ==========
  148. echo ""
  149. echo "--- Foreground Mode Detection ---"
  150. if [[ "$is_windows" == "true" ]]; then
  151. FAKE_NODE_DIR="$TEST_DIR/fake-bin"
  152. mkdir -p "$FAKE_NODE_DIR"
  153. cat > "$FAKE_NODE_DIR/node" <<'FAKENODE'
  154. #!/usr/bin/env bash
  155. echo "FOREGROUND_MODE=true"
  156. exit 0
  157. FAKENODE
  158. chmod +x "$FAKE_NODE_DIR/node"
  159. # Run WITHOUT --foreground flag — Windows should auto-detect
  160. captured=$(PATH="$FAKE_NODE_DIR:$PATH" bash "$START_SCRIPT" --project-dir "$TEST_DIR/session2" 2>/dev/null || true)
  161. if echo "$captured" | grep -q "FOREGROUND_MODE=true"; then
  162. pass "Windows auto-detects foreground mode"
  163. else
  164. fail "Windows auto-detects foreground mode" \
  165. "Expected foreground code path, output: $captured"
  166. fi
  167. rm -rf "$FAKE_NODE_DIR" "$TEST_DIR/session2"
  168. else
  169. skip "Windows auto-detects foreground mode" "not on Windows"
  170. fi
  171. # ========== Test 4: Server survives past 60-second lifecycle check ==========
  172. echo ""
  173. echo "--- Server Survival (lifecycle check) ---"
  174. mkdir -p "$TEST_DIR/survival"
  175. echo " Starting server (will wait ~75s to verify survival past lifecycle check)..."
  176. BRAINSTORM_DIR="$TEST_DIR/survival" \
  177. BRAINSTORM_HOST="127.0.0.1" \
  178. BRAINSTORM_URL_HOST="localhost" \
  179. BRAINSTORM_OWNER_PID="" \
  180. BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
  181. node "$SERVER_SCRIPT" > "$TEST_DIR/survival/.server.log" 2>&1 &
  182. SERVER_PID=$!
  183. if ! wait_for_server_info "$TEST_DIR/survival"; then
  184. fail "Server starts successfully" "Server did not write state/server-info within 5 seconds"
  185. kill "$SERVER_PID" 2>/dev/null || true
  186. SERVER_PID=""
  187. else
  188. pass "Server starts successfully with empty OWNER_PID"
  189. SERVER_PORT=$(get_port_from_info "$TEST_DIR/survival")
  190. sleep 75
  191. if kill -0 "$SERVER_PID" 2>/dev/null; then
  192. pass "Server is still alive after 75 seconds"
  193. else
  194. fail "Server is still alive after 75 seconds" \
  195. "Server died. Log tail: $(tail -5 "$TEST_DIR/survival/.server.log" 2>/dev/null)"
  196. fi
  197. if http_check "$SERVER_PORT"; then
  198. pass "Server responds to HTTP after lifecycle check window"
  199. else
  200. fail "Server responds to HTTP after lifecycle check window" \
  201. "HTTP request to port $SERVER_PORT failed"
  202. fi
  203. if grep -q "owner process exited" "$TEST_DIR/survival/.server.log" 2>/dev/null; then
  204. fail "No 'owner process exited' in logs" \
  205. "Found spurious owner-exit shutdown in log"
  206. else
  207. pass "No 'owner process exited' in logs"
  208. fi
  209. kill "$SERVER_PID" 2>/dev/null || true
  210. wait "$SERVER_PID" 2>/dev/null || true
  211. SERVER_PID=""
  212. fi
  213. # ========== Test 5: Dead-at-startup OWNER_PID is logged but does not kill the server ==========
  214. #
  215. # The server validates BRAINSTORM_OWNER_PID at startup. If it's already dead,
  216. # the PID resolution was wrong (common on WSL, Tailscale SSH, cross-user
  217. # scenarios). The server logs 'owner-pid-invalid', disables owner monitoring,
  218. # and continues running. The idle timeout becomes the only shutdown trigger.
  219. echo ""
  220. echo "--- Dead-at-startup OWNER_PID: server survives, logs owner-pid-invalid ---"
  221. mkdir -p "$TEST_DIR/control"
  222. # Find a PID that does not exist
  223. BAD_PID=99999
  224. while kill -0 "$BAD_PID" 2>/dev/null; do
  225. BAD_PID=$((BAD_PID + 1))
  226. done
  227. BRAINSTORM_DIR="$TEST_DIR/control" \
  228. BRAINSTORM_HOST="127.0.0.1" \
  229. BRAINSTORM_URL_HOST="localhost" \
  230. BRAINSTORM_OWNER_PID="$BAD_PID" \
  231. BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
  232. node "$SERVER_SCRIPT" > "$TEST_DIR/control/.server.log" 2>&1 &
  233. CONTROL_PID=$!
  234. if ! wait_for_server_info "$TEST_DIR/control"; then
  235. fail "Control server starts" "Server did not write state/server-info within 5 seconds"
  236. kill "$CONTROL_PID" 2>/dev/null || true
  237. CONTROL_PID=""
  238. else
  239. pass "Control server starts with dead-at-startup OWNER_PID=$BAD_PID"
  240. echo " Waiting ~75s to verify server survives past lifecycle check..."
  241. sleep 75
  242. if kill -0 "$CONTROL_PID" 2>/dev/null; then
  243. pass "Server survives with dead-at-startup OWNER_PID (owner monitoring disabled)"
  244. else
  245. fail "Server survives with dead-at-startup OWNER_PID" \
  246. "Server died unexpectedly. Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
  247. fi
  248. if grep -q "owner-pid-invalid" "$TEST_DIR/control/.server.log" 2>/dev/null; then
  249. pass "Server logs 'owner-pid-invalid' for dead-at-startup PID"
  250. else
  251. fail "Server logs 'owner-pid-invalid' for dead-at-startup PID" \
  252. "Log tail: $(tail -5 "$TEST_DIR/control/.server.log" 2>/dev/null)"
  253. fi
  254. if grep -q "owner process exited" "$TEST_DIR/control/.server.log" 2>/dev/null; then
  255. fail "No spurious 'owner process exited' log" \
  256. "Found 'owner process exited' but owner monitoring should be disabled"
  257. else
  258. pass "No spurious 'owner process exited' log"
  259. fi
  260. kill "$CONTROL_PID" 2>/dev/null || true
  261. fi
  262. wait "$CONTROL_PID" 2>/dev/null || true
  263. CONTROL_PID=""
  264. # ========== Test 6: stop-server.sh cleanly stops the server ==========
  265. echo ""
  266. echo "--- Clean Shutdown ---"
  267. mkdir -p "$TEST_DIR/stop-test/state"
  268. BRAINSTORM_DIR="$TEST_DIR/stop-test" \
  269. BRAINSTORM_HOST="127.0.0.1" \
  270. BRAINSTORM_URL_HOST="localhost" \
  271. BRAINSTORM_OWNER_PID="" \
  272. BRAINSTORM_PORT=$((49152 + RANDOM % 16383)) \
  273. node "$SERVER_SCRIPT" > "$TEST_DIR/stop-test/.server.log" 2>&1 &
  274. STOP_TEST_PID=$!
  275. echo "$STOP_TEST_PID" > "$TEST_DIR/stop-test/state/server.pid"
  276. if ! wait_for_server_info "$TEST_DIR/stop-test"; then
  277. fail "Stop-test server starts" "Server did not start"
  278. kill "$STOP_TEST_PID" 2>/dev/null || true
  279. STOP_TEST_PID=""
  280. else
  281. bash "$STOP_SCRIPT" "$TEST_DIR/stop-test" >/dev/null 2>&1 || true
  282. sleep 1
  283. if ! kill -0 "$STOP_TEST_PID" 2>/dev/null; then
  284. pass "stop-server.sh cleanly stops the server"
  285. else
  286. fail "stop-server.sh cleanly stops the server" \
  287. "Server PID $STOP_TEST_PID is still alive after stop"
  288. kill "$STOP_TEST_PID" 2>/dev/null || true
  289. fi
  290. fi
  291. wait "$STOP_TEST_PID" 2>/dev/null || true
  292. STOP_TEST_PID=""
  293. # ========== Summary ==========
  294. echo ""
  295. echo "=== Results: $passed passed, $failed failed, $skipped skipped ==="
  296. if [[ $failed -gt 0 ]]; then
  297. exit 1
  298. fi
  299. exit 0