diff --git a/skills/brainstorming/scripts/stop-server.sh b/skills/brainstorming/scripts/stop-server.sh index e440bb98..355c8e0e 100755 --- a/skills/brainstorming/scripts/stop-server.sh +++ b/skills/brainstorming/scripts/stop-server.sh @@ -40,7 +40,7 @@ command_has_server_id() { local expected_arg="--brainstorm-server-id=$expected" if [[ -r "/proc/$pid/cmdline" ]]; then local arg - while IFS= read -r -d '' arg; do + while IFS= read -r -d '' arg || [[ -n "$arg" ]]; do [[ "$arg" == "$expected_arg" ]] && return 0 done < "/proc/$pid/cmdline" return 1 diff --git a/tests/brainstorm-server/lifecycle.test.js b/tests/brainstorm-server/lifecycle.test.js index ef0d2fcd..236c51f8 100644 --- a/tests/brainstorm-server/lifecycle.test.js +++ b/tests/brainstorm-server/lifecycle.test.js @@ -78,6 +78,48 @@ function httpStatus(port, key) { }); } +function isWindowsLikeShell() { + return process.platform === 'win32' || + /^msys|^cygwin|^mingw/i.test(process.env.OSTYPE || '') || + !!process.env.MSYSTEM; +} + +async function waitForStartedOutput(child, timeoutMs = 5000) { + let stdout = ''; + let stderr = ''; + child.stdout.on('data', d => { stdout += d.toString(); }); + child.stderr.on('data', d => { stderr += d.toString(); }); + + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline && !stdout.includes('server-started') && child.exitCode === null) { + await sleep(50); + } + + if (!stdout.includes('server-started')) { + throw new Error(`start-server.sh did not report server-started. exit=${child.exitCode} stdout=${stdout} stderr=${stderr}`); + } + return stdout; +} + +function makeShellTempDir(prefix) { + return execFileSync('bash', ['-lc', `mktemp -d "\${TMPDIR:-/tmp}/${prefix}-XXXXXX"`], { encoding: 'utf8' }).trim(); +} + +function removeShellPath(p) { + execFileSync('bash', ['-lc', 'rm -rf "$1"', 'bash', p], { stdio: 'ignore' }); +} + +function newestSessionDir(projectDir) { + const sessionDir = execFileSync('bash', [ + '-lc', + 'find "$1/.superpowers/brainstorm" -mindepth 1 -maxdepth 1 -type d -print | sort | tail -1', + 'bash', + projectDir + ], { encoding: 'utf8' }).trim(); + assert(sessionDir, `expected at least one session dir under ${projectDir}/.superpowers/brainstorm`); + return sessionDir; +} + async function runTests() { let passed = 0, failed = 0; async function test(name, fn) { @@ -125,15 +167,26 @@ async function runTests() { }); await test('start-server.sh --idle-timeout-minutes sets the timeout', async () => { - const dir = fs.mkdtempSync('/tmp/bs-life-'); - let info; - const out = execFileSync('bash', [START, '--project-dir', dir, '--idle-timeout-minutes', '5', '--background'], { encoding: 'utf8' }); - info = firstServerStarted(out); + const dir = makeShellTempDir('bs-life'); + let info = null; + let startProcess = null; + let sessionDir = null; try { + if (isWindowsLikeShell()) { + startProcess = spawn('bash', [START, '--project-dir', dir, '--idle-timeout-minutes', '5']); + info = firstServerStarted(await waitForStartedOutput(startProcess)); + } else { + const out = execFileSync('bash', [START, '--project-dir', dir, '--idle-timeout-minutes', '5', '--background'], { encoding: 'utf8' }); + info = firstServerStarted(out); + } + sessionDir = newestSessionDir(dir); assert.strictEqual(info.idle_timeout_ms, 5 * 60 * 1000, '5 minutes -> 300000 ms'); } finally { - execFileSync('bash', [STOP, path.dirname(info.state_dir)], { stdio: 'ignore' }); - fs.rmSync(dir, { recursive: true, force: true }); + if (sessionDir) execFileSync('bash', [STOP, sessionDir], { stdio: 'ignore' }); + if (startProcess && !await waitForExit(startProcess, 3000)) { + await killAndWait(startProcess); + } + removeShellPath(dir); } });