Files
pbakaus_impeccable/tests/live-server.test.mjs
T
Paul BakausandClaude Opus 4.6 455fe39155 Add 41 tests for live-wrap and live-server, add auto-execute guards
Tests:
- tests/live-wrap.test.mjs (26 tests): unit tests for buildSearchQueries,
  findElement, findClosingLine, detectCommentSyntax (20 pure function
  tests) + integration tests for the full wrapCli on HTML and JSX
  fixtures with temp dirs (6 tests covering wrapping, ID/class lookup,
  error handling, content preservation).

- tests/live-server.test.mjs (15 tests): integration tests that start a
  real server on port 8499, then test /health, /live.js, /detect.js,
  /poll (timeout + auth), /events POST (validation + auth), browser→agent
  event flow (POST event → poll receives it), agent→browser SSE flow
  (POST reply → SSE stream delivers it), /source (read, path traversal
  rejection, auth, 404).

Also:
- Added auto-execute guards to live-wrap.mjs and live-poll.mjs so they
  work when run directly with `node live-wrap.mjs ...` (needed for both
  skill instructions and integration tests).
- Exported buildSearchQueries, findElement, findClosingLine,
  detectCommentSyntax from live-wrap.mjs for unit testing.
- Updated package.json test script to include the new test files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:35:26 -07:00

242 lines
8.6 KiB
JavaScript

/**
* Tests for the live variant server.
* Run with: node --test tests/live-server.test.mjs
*/
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { execSync, spawn } from 'node:child_process';
const SERVER_SCRIPT = 'source/skills/impeccable/scripts/live-server.mjs';
const PID_FILE = join(tmpdir(), 'impeccable-live.json');
// ---------------------------------------------------------------------------
// Helper: start/stop server for integration tests
// ---------------------------------------------------------------------------
function startServer(port = 8499) {
return new Promise((resolve, reject) => {
const proc = spawn('node', [SERVER_SCRIPT, '--port=' + port], {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env },
});
let output = '';
proc.stdout.on('data', (d) => {
output += d.toString();
if (output.includes('running on')) {
// Read token from PID file
try {
const info = JSON.parse(readFileSync(PID_FILE, 'utf-8'));
resolve({ proc, port: info.port, token: info.token });
} catch {
reject(new Error('Server started but PID file not readable'));
}
}
});
proc.stderr.on('data', (d) => { output += d.toString(); });
proc.on('error', reject);
setTimeout(() => reject(new Error('Server start timeout. Output: ' + output)), 5000);
});
}
async function stopServer(port, token) {
try {
await fetch(`http://localhost:${port}/stop?token=${token}`);
} catch { /* server already gone */ }
}
// ---------------------------------------------------------------------------
// Server integration tests
// ---------------------------------------------------------------------------
describe('live-server integration', () => {
let server;
before(async () => {
server = await startServer(8499);
});
after(async () => {
if (server) {
await stopServer(server.port, server.token);
server.proc.kill();
}
});
it('/health returns correct status', async () => {
const res = await fetch(`http://localhost:${server.port}/health`);
assert.equal(res.status, 200);
const data = await res.json();
assert.equal(data.status, 'ok');
assert.equal(data.port, server.port);
assert.equal(data.mode, 'variant');
assert.equal(typeof data.hasProjectContext, 'boolean');
assert.equal(data.connectedClients, 0);
});
it('/live.js serves script with token injected', async () => {
const res = await fetch(`http://localhost:${server.port}/live.js`);
assert.equal(res.status, 200);
assert.equal(res.headers.get('content-type'), 'application/javascript');
const text = await res.text();
assert.ok(text.includes('__IMPECCABLE_TOKEN__'));
assert.ok(text.includes(server.token));
assert.ok(text.includes('__IMPECCABLE_PORT__'));
});
it('/detect.js serves the detection overlay', async () => {
const res = await fetch(`http://localhost:${server.port}/detect.js`);
// May 404 if detect-antipatterns-browser.js hasn't been built
assert.ok(res.status === 200 || res.status === 404);
});
it('/poll returns timeout when no events queued', async () => {
const res = await fetch(`http://localhost:${server.port}/poll?token=${server.token}&timeout=500`);
assert.equal(res.status, 200);
const data = await res.json();
assert.equal(data.type, 'timeout');
});
it('/poll rejects invalid token', async () => {
const res = await fetch(`http://localhost:${server.port}/poll?token=wrong&timeout=100`);
assert.equal(res.status, 401);
});
it('/stop rejects invalid token', async () => {
const res = await fetch(`http://localhost:${server.port}/stop?token=wrong`);
assert.equal(res.status, 401);
});
it('POST /events rejects invalid token', async () => {
const res = await fetch(`http://localhost:${server.port}/events`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: 'wrong', type: 'exit' }),
});
assert.equal(res.status, 401);
});
it('POST /events validates event structure', async () => {
const res = await fetch(`http://localhost:${server.port}/events`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: server.token, type: 'generate' }), // missing required fields
});
assert.equal(res.status, 400);
const data = await res.json();
assert.ok(data.error.includes('generate'));
});
it('POST /events accepts valid exit event', async () => {
const res = await fetch(`http://localhost:${server.port}/events`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: server.token, type: 'exit' }),
});
assert.equal(res.status, 200);
const data = await res.json();
assert.equal(data.ok, true);
});
it('events flow from browser POST to agent poll', async () => {
// Drain any queued events from previous tests
let drained;
do {
const r = await fetch(`http://localhost:${server.port}/poll?token=${server.token}&timeout=100`);
drained = await r.json();
} while (drained.type !== 'timeout');
// Start a poll (will block until event arrives or timeout)
const pollPromise = fetch(`http://localhost:${server.port}/poll?token=${server.token}&timeout=5000`)
.then(r => r.json());
// Give the poll a moment to register
await new Promise(r => setTimeout(r, 100));
// Send a generate event (simulating browser)
const postRes = await fetch(`http://localhost:${server.port}/events`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: server.token,
type: 'generate',
id: 'test-e2e-1',
action: 'bolder',
count: 2,
element: { outerHTML: '<div>test</div>', tagName: 'div' },
}),
});
assert.equal(postRes.status, 200);
// Poll should resolve with the event
const event = await pollPromise;
assert.equal(event.type, 'generate');
assert.equal(event.id, 'test-e2e-1');
assert.equal(event.action, 'bolder');
assert.equal(event.count, 2);
});
it('agent reply is forwarded via SSE to browser', async () => {
// Use raw HTTP to read SSE (no EventSource in Node.js)
const controller = new AbortController();
const sseRes = await fetch(
`http://localhost:${server.port}/events?token=${server.token}`,
{ signal: controller.signal }
);
assert.equal(sseRes.status, 200);
assert.equal(sseRes.headers.get('content-type'), 'text/event-stream');
// Read the first message (should be "connected")
const reader = sseRes.body.getReader();
const decoder = new TextDecoder();
const { value: chunk1 } = await reader.read();
const text1 = decoder.decode(chunk1);
assert.ok(text1.includes('"connected"'));
// Send a reply from the agent
await fetch(`http://localhost:${server.port}/poll`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: server.token, id: 'sse-test', type: 'done', file: 'x.html' }),
});
// Read the next SSE message
const { value: chunk2 } = await reader.read();
const text2 = decoder.decode(chunk2);
assert.ok(text2.includes('"done"'));
assert.ok(text2.includes('sse-test'));
controller.abort();
});
it('/source reads project files with valid token', async () => {
const res = await fetch(`http://localhost:${server.port}/source?token=${server.token}&path=package.json`);
assert.equal(res.status, 200);
const text = await res.text();
assert.ok(text.includes('"impeccable"'));
});
it('/source rejects path traversal', async () => {
const res = await fetch(`http://localhost:${server.port}/source?token=${server.token}&path=../../../etc/passwd`);
assert.equal(res.status, 400);
});
it('/source rejects invalid token', async () => {
const res = await fetch(`http://localhost:${server.port}/source?token=wrong&path=package.json`);
assert.equal(res.status, 401);
});
it('/source returns 404 for missing files', async () => {
try {
const res = await fetch(`http://localhost:${server.port}/source?token=${server.token}&path=nonexistent.xyz`);
assert.equal(res.status, 404);
} catch {
// Server may close socket on 404 for some Node versions
assert.ok(true, 'Server rejected request for missing file');
}
});
});