Combined node --test runs hung in the org browser files. Two causes: - org-quick: a page's GET /api/config reached the server on a kept-alive connection after close(server) had stopped the listener. The handler threw on server.address().port, the request was never answered, and server.close() waited for it forever. close() in fixture.mjs now cuts every connection and fails the hook after 5 s with a message. - org-delete: t.after hooks run in registration order, so the data directory was removed while the server still wrote there. The rm failed with ENOTEMPTY, node:test skipped the later hooks, and the open server and chromium kept the file running. The three org files and project-archive-style now register the removal last. browser.mjs: close() kills a browser that does not answer Browser.close within 5 s, a call to an exited browser fails at once, and an unclosed chromium no longer holds the event loop and is killed when the test process exits. teardown.test.mjs reproduces each case: 0/4 pass on the old helpers, 4/4 on the new ones.
49 lines
3.8 KiB
JavaScript
49 lines
3.8 KiB
JavaScript
// Same session-line fixture shape as control-board/tests/serve.test.mjs.
|
|
// Run the real board scanner/server against temporary data only.
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { startServer as startBoard } from '../../control-board/src/serve.mjs';
|
|
import { makeRegistration, writeRegistration, updateTask } from '../../seat/src/seat.mjs';
|
|
import { startServer } from '../src/serve.mjs';
|
|
// P19 #865: teardown cuts every connection, not only idle ones. A page can send a request on a
|
|
// kept-alive connection after server.close() has stopped the listener; that request can go
|
|
// unanswered, and server.close() would then wait for it forever. A server that still has not
|
|
// closed after `ms` fails the hook with a message instead of hanging the test file.
|
|
export const close = (server, ms = 5000) => new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => reject(new Error(`server did not close within ${ms} ms (${server.address() ? 'still listening' : 'connections still open'})`)), ms);
|
|
server.close(() => { clearTimeout(timer); resolve(); });
|
|
server.closeAllConnections();
|
|
});
|
|
export async function fixture() {
|
|
const root = mkdtempSync(join(tmpdir(), 'webui-fixture-'));
|
|
const seatsDir = join(root, 'seats'), specs = [];
|
|
let exitCode = 0;
|
|
const captures = [];
|
|
for (const [agent, project, state] of [['agent1', 'proj', 'waiting'], ['agent2', 'proj', 'working'], ['agent3', 'other', 'error'], ['agent4', 'other', 'offline']]) {
|
|
const sessionsDir = join(root, agent); mkdirSync(sessionsDir);
|
|
const assistant = { role: 'assistant', model: 'fixture-model', provider: 'fixture-provider', stopReason: state === 'error' ? 'error' : 'stop', content: [{ type: 'text', text: (state === 'waiting' ? 'Input needed: ' : '') + 'done? <script>window.injected=true</script>' }] };
|
|
const lines = [
|
|
{ type: 'session', id: agent, timestamp: '2026-09-01T00:00:00Z', cwd: '/fixture/workspace' },
|
|
{ type: 'message', timestamp: '2026-09-01T00:00:01Z', message: { role: 'user', content: [{ type: 'text', text: 'Build the fixture task' }] } },
|
|
{ type: 'message', timestamp: '2026-09-01T00:00:02Z', message: assistant },
|
|
];
|
|
if (state === 'working') lines.push({ type: 'message', timestamp: '2026-09-01T00:00:03Z', message: { role: 'user', content: [{ type: 'text', text: 'continue' }] } });
|
|
writeFileSync(join(sessionsDir, 's.jsonl'), lines.map(l => JSON.stringify(l)).join('\n') + '\n');
|
|
specs.push({ agent, project, sessionsDir, tmux: { session: agent } });
|
|
if (agent === 'agent1') {
|
|
writeRegistration(seatsDir, makeRegistration({ resolved: { seat: agent, project, sessionsDir, seatDir: sessionsDir, launchScript: join(root, 'unused.sh'), layout: 'repo', defaultWorkspace: '/fixture/workspace' }, task: 'Registered fixture task', tmux: { session: agent, socket: null }, pid: process.pid }));
|
|
// #1511: the same path `mosaic seat task --by` takes, on the fixture record only.
|
|
updateTask(seatsDir, agent, 'Registered fixture task', { layout: 'repo', setBy: 'fixture-setter' });
|
|
}
|
|
}
|
|
const board = await startBoard({ port: 0, specs, seatsDir, configDirectory: root, boardDir: join(root, 'board'), isAlive: tmux => tmux.session !== 'agent4', isPidAlive: () => true,
|
|
exec: (file, args) => { captures.push({ file, args }); return { status: exitCode, stdout: 'fixture transport', stderr: exitCode ? 'fixture refusal <unsafe>' : '' }; },
|
|
});
|
|
const boardURL = `http://127.0.0.1:${board.address().port}`;
|
|
const web = await startServer({ port: 0, board: boardURL });
|
|
return { root, board, web, boardURL, base: `http://127.0.0.1:${web.address().port}`, captures, failReply: () => { exitCode = 4; },
|
|
async close() { await close(web); await close(board); rmSync(root, { recursive: true, force: true }); },
|
|
};
|
|
}
|