/** * Integration tests for the Plan Canvas server (scripts/lib/plan-canvas/). * * Spins up the real HTTP server in-process and drives it exactly like the * browser chrome (fetch + SSE) and the agent CLI (long-poll) do. * * Run with: node tests/scripts/plan-canvas.test.js */ const assert = require('assert'); const fs = require('fs'); const http = require('http'); const os = require('os'); const path = require('path'); const { createSessionStore } = require('../../scripts/lib/plan-canvas/sessions'); const { createPlanCanvasServer } = require('../../scripts/lib/plan-canvas/server'); async function test(name, fn) { try { await fn(); console.log(` ✓ ${name}`); return true; } catch (err) { console.log(` ✗ ${name}`); console.log(` Error: ${err.stack || err.message}`); return false; } } function request(port, method, requestPath, { body = null, headers = {} } = {}) { return new Promise((resolve, reject) => { const payload = body === null ? null : JSON.stringify(body); const req = http.request( { host: '127.0.0.1', port, method, path: requestPath, agent: false, headers: payload ? { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload), ...headers } : headers }, res => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => resolve({ statusCode: res.statusCode, headers: res.headers, body: data })); } ); req.on('error', reject); if (payload) req.write(payload); req.end(); }); } function jsonBody(res) { return JSON.parse(res.body.trim()); } // Open an SSE stream and collect parsed events into `received`. function openSse(port, key) { const received = []; let close = () => {}; const ready = new Promise((resolve, reject) => { const req = http.get( { host: '127.0.0.1', port, path: `/events/${key}`, agent: false }, res => { let buffer = ''; res.on('data', chunk => { buffer += chunk; let idx; while ((idx = buffer.indexOf('\n\n')) >= 0) { const frame = buffer.slice(0, idx); buffer = buffer.slice(idx + 2); const eventMatch = frame.match(/^event: (.+)$/m); const dataMatch = frame.match(/^data: (.+)$/m); if (eventMatch && dataMatch) { received.push({ event: eventMatch[1], data: JSON.parse(dataMatch[1]) }); } } }); resolve(); } ); req.on('error', reject); close = () => req.destroy(); }); return { received, ready, close: () => close() }; } function waitFor(predicate, { timeoutMs = 3000, intervalMs = 20 } = {}) { return new Promise((resolve, reject) => { const startedAt = Date.now(); const timer = setInterval(() => { if (predicate()) { clearInterval(timer); resolve(); } else if (Date.now() - startedAt > timeoutMs) { clearInterval(timer); reject(new Error('waitFor timed out')); } }, intervalMs); }); } async function main() { console.log('\n=== Testing plan-canvas server ===\n'); let passed = 0; let failed = 0; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'plan-canvas-server-')); const artifact = path.join(tmp, 'demo.plan.md'); fs.writeFileSync(artifact, '# Plan: Demo\n\n## Files to Change\n\n| File | Action |\n|---|---|\n| `a.js` | UPDATE |\n'); const htmlArtifact = path.join(tmp, 'report.html'); fs.writeFileSync(htmlArtifact, '