/** * Integration tests for the brainstorm server. * * Tests the full server behavior: HTTP serving, WebSocket communication, * file watching, and the brainstorming workflow. * * Uses the `ws` npm package as a test client (test-only dependency, * not shipped to end users). */ const { spawn } = require('child_process'); const http = require('http'); const WebSocket = require('ws'); const fs = require('fs'); const path = require('path'); const assert = require('assert'); const SERVER_PATH = path.join(__dirname, '../../skills/brainstorming/scripts/server.cjs'); const TEST_PORT = 3334; const TEST_DIR = '/tmp/brainstorm-test'; const CONTENT_DIR = path.join(TEST_DIR, 'content'); const STATE_DIR = path.join(TEST_DIR, 'state'); // Fixed session key so the test client can authenticate (see auth.test.js for // the security behavior itself; here we just need authorized requests). const TOKEN = 'testtoken-server-0123456789abcdef'; function cleanup() { if (fs.existsSync(TEST_DIR)) { fs.rmSync(TEST_DIR, { recursive: true }); } } async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function fetch(url) { return new Promise((resolve, reject) => { const headers = { Cookie: `brainstorm-key-${TEST_PORT}=${TOKEN}` }; http.get(url, { headers }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data })); }).on('error', reject); }); } function startServer() { return spawn('node', [SERVER_PATH], { env: { ...process.env, BRAINSTORM_PORT: TEST_PORT, BRAINSTORM_DIR: TEST_DIR, BRAINSTORM_TOKEN: TOKEN } }); } async function waitForServer(server) { let stdout = ''; let stderr = ''; return new Promise((resolve, reject) => { server.stdout.on('data', (data) => { stdout += data.toString(); if (stdout.includes('server-started')) { resolve({ stdout, stderr, getStdout: () => stdout }); } }); server.stderr.on('data', (data) => { stderr += data.toString(); }); server.on('error', reject); setTimeout(() => reject(new Error(`Server didn't start. stderr: ${stderr}`)), 5000); }); } class SkipTest extends Error { constructor(message) { super(message); this.skip = true; } } function skip(message) { throw new SkipTest(message); } function serverStartedMessage(out) { const line = out.trim().split('\n').find(l => l.includes('server-started')); assert(line, 'server-started JSON should be present'); return JSON.parse(line); } function assertStartedOnExpectedPort(out) { const msg = serverStartedMessage(out); assert.strictEqual( msg.port, TEST_PORT, `server.test.js expected fixed port ${TEST_PORT}, got ${msg.port}; fixed-port tests must not run through fallback` ); return msg; } function ensureSymlinkWorks(target, link) { try { fs.symlinkSync(target, link); fs.unlinkSync(link); } catch (e) { try { fs.unlinkSync(link); } catch (ignore) {} skip(`symlink creation unavailable on this host: ${e.message}`); } } async function runTests() { cleanup(); const server = startServer(); let stdoutAccum = ''; server.stdout.on('data', (data) => { stdoutAccum += data.toString(); }); let initialStdout = ''; let passed = 0; let failed = 0; let skipped = 0; function test(name, fn) { return fn().then(() => { console.log(` PASS: ${name}`); passed++; }).catch(e => { if (e.skip) { console.log(` SKIP: ${name}`); console.log(` ${e.message}`); skipped++; return; } console.log(` FAIL: ${name}`); console.log(` ${e.message}`); failed++; }); } try { const { stdout } = await waitForServer(server); initialStdout = stdout; assertStartedOnExpectedPort(initialStdout); // ========== Server Startup ========== console.log('\n--- Server Startup ---'); await test('outputs server-started JSON on startup', () => { const msg = serverStartedMessage(initialStdout); assert.strictEqual(msg.type, 'server-started'); assert.strictEqual(msg.port, TEST_PORT); assert(msg.url, 'Should include URL'); assert(msg.screen_dir, 'Should include screen_dir'); return Promise.resolve(); }); await test('writes server-info to state/', () => { const infoPath = path.join(STATE_DIR, 'server-info'); assert(fs.existsSync(infoPath), 'state/server-info should exist'); const info = JSON.parse(fs.readFileSync(infoPath, 'utf-8').trim()); assert.strictEqual(info.type, 'server-started'); assert.strictEqual(info.port, TEST_PORT); assert.strictEqual(info.screen_dir, CONTENT_DIR, 'screen_dir should point to content/'); assert.strictEqual(info.state_dir, STATE_DIR, 'state_dir should point to state/'); return Promise.resolve(); }); // ========== HTTP Serving ========== console.log('\n--- HTTP Serving ---'); await test('serves waiting page when no screens exist', async () => { const res = await fetch(`http://localhost:${TEST_PORT}/`); assert.strictEqual(res.status, 200); assert(res.body.includes('Waiting for the agent'), 'Should show waiting message'); }); await test('injects helper.js into waiting page', async () => { const res = await fetch(`http://localhost:${TEST_PORT}/`); assert(res.body.includes('WebSocket'), 'Should have helper.js injected'); assert(res.body.includes('toggleSelect'), 'Should have toggleSelect from helper'); assert(res.body.includes('brainstorm'), 'Should have brainstorm API from helper'); }); await test('returns Content-Type text/html', async () => { const res = await fetch(`http://localhost:${TEST_PORT}/`); assert(res.headers['content-type'].includes('text/html'), 'Should be text/html'); }); await test('serves full HTML documents as-is (not wrapped)', async () => { const fullDoc = '\n