mirror of
https://github.com/obra/superpowers.git
synced 2026-04-16 02:02:41 +00:00
Separate brainstorm server content and state into peer directories
The session directory now contains two peers: content/ (HTML served to
the browser) and state/ (events, server-info, pid, log). Previously
all files shared a single directory, making server state and user
interaction data accessible over the /files/ HTTP route.
Also fixes stale test assertion ("Waiting for Claude" → "Waiting for
the agent").
Reported-By: 吉田仁
This commit is contained in:
@@ -18,6 +18,8 @@ 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');
|
||||
|
||||
function cleanup() {
|
||||
if (fs.existsSync(TEST_DIR)) {
|
||||
@@ -69,7 +71,6 @@ async function waitForServer(server) {
|
||||
|
||||
async function runTests() {
|
||||
cleanup();
|
||||
fs.mkdirSync(TEST_DIR, { recursive: true });
|
||||
|
||||
const server = startServer();
|
||||
let stdoutAccum = '';
|
||||
@@ -103,12 +104,14 @@ async function runTests() {
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await test('writes .server-info file', () => {
|
||||
const infoPath = path.join(TEST_DIR, '.server-info');
|
||||
assert(fs.existsSync(infoPath), '.server-info should exist');
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -118,7 +121,7 @@ async function runTests() {
|
||||
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 Claude'), 'Should show waiting message');
|
||||
assert(res.body.includes('Waiting for the agent'), 'Should show waiting message');
|
||||
});
|
||||
|
||||
await test('injects helper.js into waiting page', async () => {
|
||||
@@ -135,7 +138,7 @@ async function runTests() {
|
||||
|
||||
await test('serves full HTML documents as-is (not wrapped)', async () => {
|
||||
const fullDoc = '<!DOCTYPE html>\n<html><head><title>Custom</title></head><body><h1>Custom Page</h1></body></html>';
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'full-doc.html'), fullDoc);
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'full-doc.html'), fullDoc);
|
||||
await sleep(300);
|
||||
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
@@ -146,7 +149,7 @@ async function runTests() {
|
||||
|
||||
await test('wraps content fragments in frame template', async () => {
|
||||
const fragment = '<h2>Pick a layout</h2>\n<div class="options"><div class="option" data-choice="a"><div class="letter">A</div></div></div>';
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'fragment.html'), fragment);
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'fragment.html'), fragment);
|
||||
await sleep(300);
|
||||
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
@@ -157,9 +160,9 @@ async function runTests() {
|
||||
});
|
||||
|
||||
await test('serves newest file by mtime', async () => {
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'older.html'), '<h2>Older</h2>');
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'older.html'), '<h2>Older</h2>');
|
||||
await sleep(100);
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'newer.html'), '<h2>Newer</h2>');
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'newer.html'), '<h2>Newer</h2>');
|
||||
await sleep(300);
|
||||
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
@@ -168,7 +171,7 @@ async function runTests() {
|
||||
|
||||
await test('ignores non-html files for serving', async () => {
|
||||
// Write a newer non-HTML file — should still serve newest .html
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'data.json'), '{"not": "html"}');
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'data.json'), '{"not": "html"}');
|
||||
await sleep(300);
|
||||
|
||||
const res = await fetch(`http://localhost:${TEST_PORT}/`);
|
||||
@@ -206,9 +209,9 @@ async function runTests() {
|
||||
ws.close();
|
||||
});
|
||||
|
||||
await test('writes choice events to .events file', async () => {
|
||||
await test('writes choice events to state/events', async () => {
|
||||
// Clean up events from prior tests
|
||||
const eventsFile = path.join(TEST_DIR, '.events');
|
||||
const eventsFile = path.join(STATE_DIR, 'events');
|
||||
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
|
||||
|
||||
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
|
||||
@@ -225,8 +228,8 @@ async function runTests() {
|
||||
ws.close();
|
||||
});
|
||||
|
||||
await test('does NOT write non-choice events to .events file', async () => {
|
||||
const eventsFile = path.join(TEST_DIR, '.events');
|
||||
await test('does NOT write non-choice events to state/events', async () => {
|
||||
const eventsFile = path.join(STATE_DIR, 'events');
|
||||
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
|
||||
|
||||
const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
|
||||
@@ -257,7 +260,7 @@ async function runTests() {
|
||||
if (JSON.parse(data.toString()).type === 'reload') ws2Reload = true;
|
||||
});
|
||||
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'multi-client.html'), '<h2>Multi</h2>');
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'multi-client.html'), '<h2>Multi</h2>');
|
||||
await sleep(500);
|
||||
|
||||
assert(ws1Reload, 'Client 1 should receive reload');
|
||||
@@ -273,7 +276,7 @@ async function runTests() {
|
||||
await sleep(100);
|
||||
|
||||
// This should not throw even though ws1 is closed
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'after-close.html'), '<h2>After</h2>');
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'after-close.html'), '<h2>After</h2>');
|
||||
await sleep(300);
|
||||
// If we got here without error, the test passes
|
||||
});
|
||||
@@ -304,7 +307,7 @@ async function runTests() {
|
||||
if (JSON.parse(data.toString()).type === 'reload') gotReload = true;
|
||||
});
|
||||
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'watch-new.html'), '<h2>New</h2>');
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'watch-new.html'), '<h2>New</h2>');
|
||||
await sleep(500);
|
||||
|
||||
assert(gotReload, 'Should send reload on new file');
|
||||
@@ -312,7 +315,7 @@ async function runTests() {
|
||||
});
|
||||
|
||||
await test('sends reload on .html file change', async () => {
|
||||
const filePath = path.join(TEST_DIR, 'watch-change.html');
|
||||
const filePath = path.join(CONTENT_DIR, 'watch-change.html');
|
||||
fs.writeFileSync(filePath, '<h2>Original</h2>');
|
||||
await sleep(500);
|
||||
|
||||
@@ -340,35 +343,35 @@ async function runTests() {
|
||||
if (JSON.parse(data.toString()).type === 'reload') gotReload = true;
|
||||
});
|
||||
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'data.txt'), 'not html');
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'data.txt'), 'not html');
|
||||
await sleep(500);
|
||||
|
||||
assert(!gotReload, 'Should NOT reload for non-HTML files');
|
||||
ws.close();
|
||||
});
|
||||
|
||||
await test('clears .events on new screen', async () => {
|
||||
// Create an .events file
|
||||
const eventsFile = path.join(TEST_DIR, '.events');
|
||||
await test('clears state/events on new screen', async () => {
|
||||
// Create an events file
|
||||
const eventsFile = path.join(STATE_DIR, 'events');
|
||||
fs.writeFileSync(eventsFile, '{"choice":"a"}\n');
|
||||
assert(fs.existsSync(eventsFile));
|
||||
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'clear-events.html'), '<h2>New screen</h2>');
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'clear-events.html'), '<h2>New screen</h2>');
|
||||
await sleep(500);
|
||||
|
||||
assert(!fs.existsSync(eventsFile), '.events should be cleared on new screen');
|
||||
assert(!fs.existsSync(eventsFile), 'state/events should be cleared on new screen');
|
||||
});
|
||||
|
||||
await test('logs screen-added on new file', async () => {
|
||||
stdoutAccum = '';
|
||||
fs.writeFileSync(path.join(TEST_DIR, 'log-test.html'), '<h2>Log</h2>');
|
||||
fs.writeFileSync(path.join(CONTENT_DIR, 'log-test.html'), '<h2>Log</h2>');
|
||||
await sleep(500);
|
||||
|
||||
assert(stdoutAccum.includes('screen-added'), 'Should log screen-added');
|
||||
});
|
||||
|
||||
await test('logs screen-updated on file change', async () => {
|
||||
const filePath = path.join(TEST_DIR, 'log-update.html');
|
||||
const filePath = path.join(CONTENT_DIR, 'log-update.html');
|
||||
fs.writeFileSync(filePath, '<h2>V1</h2>');
|
||||
await sleep(500);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user