69 lines
4.8 KiB
JavaScript
69 lines
4.8 KiB
JavaScript
// Row 278 (#1783): Console-owned read adapter and the listener boundary.
|
|
// All listeners and config are fixtures; no live bus or host configuration.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmp, fixture } from '../../cli/tests/helpers.mjs';
|
|
import { startServer } from '../src/serve.mjs';
|
|
import { startTesterSocket } from '../src/tester-socket.mjs';
|
|
import { testerRequest } from '../../cli/src/tester-http.mjs';
|
|
import { BusReadError } from '../src/bus.mjs';
|
|
import { close } from './fixture.mjs';
|
|
|
|
const rows = [{ thread: '11111111-1111-4111-8111-111111111111', instance: 'assistant', holder_run: 'thread-run', harness: 'pi', at: '2026-01-01T00:00:00Z' }];
|
|
|
|
test('row278 HTTP read returns held claims without send and refuses arguments before the bus', async t => {
|
|
const calls = [];
|
|
const bus = { 'thread.claims': async arg => { calls.push(arg); return rows; } };
|
|
const web = await startServer({ port: 0, board: 'http://127.0.0.1:9', bus });
|
|
t.after(() => close(web));
|
|
const url = `http://127.0.0.1:${web.address().port}/api/bus/thread.claims`;
|
|
const result = await fetch(url);
|
|
assert.equal(result.status, 200); assert.equal(result.headers.get('cache-control'), 'no-store');
|
|
const body = await result.json(); assert.deepEqual(body.rows, rows); assert.ok(body.at);
|
|
assert.deepEqual(calls, [undefined]);
|
|
for (const query of ['?thread=one', '?subject=one', '?limit=1', '?unknown=']) {
|
|
const r = await fetch(url + query); assert.equal(r.status, 400); assert.equal((await r.json()).error, 'invalid-request');
|
|
}
|
|
assert.equal((await fetch(url, { method: 'POST' })).status, 405);
|
|
assert.equal(calls.length, 1);
|
|
});
|
|
|
|
test('row278 HTTP read preserves bus refusal and rejects a malformed result', async t => {
|
|
let value = new BusReadError('human-required');
|
|
const web = await startServer({ port: 0, bus: { 'thread.claims': async () => { if (value instanceof Error) throw value; return value; } } });
|
|
t.after(() => close(web));
|
|
const url = `http://127.0.0.1:${web.address().port}/api/bus/thread.claims`;
|
|
let r = await fetch(url); assert.equal(r.status, 403); assert.equal((await r.json()).error, 'human-required');
|
|
value = {};
|
|
r = await fetch(url); assert.equal(r.status, 502); assert.equal((await r.json()).error, 'invalid-response');
|
|
value = [];
|
|
r = await fetch(url); assert.equal(r.status, 200); assert.deepEqual((await r.json()).rows, []);
|
|
});
|
|
|
|
test('row278 tester socket refuses thread claims explicitly with zero bus calls', async t => {
|
|
const dataRoot = tmp(t, 'claims-tester-'); let calls = 0;
|
|
const web = await startTesterSocket({ dataRoot, bus: { 'thread.claims': async () => { calls++; return rows; } } });
|
|
t.after(() => close(web));
|
|
const r = await testerRequest({ dataRoot, path: '/api/bus/thread.claims' });
|
|
assert.equal(r.status, 403); assert.equal(r.body.error, 'tester-refused'); assert.equal(calls, 0);
|
|
});
|
|
|
|
test('row278 Console adapter calls argument-free thread.claims through the normal transport', t => {
|
|
const root = tmp(t, 'claims-cli-'), f = fixture(root), log = join(root, 'calls.jsonl'), hook = join(root, 'hooks.mjs');
|
|
writeFileSync(log, '');
|
|
const serveSource = `export const DEFAULT_BOARD='http://127.0.0.1:9';export const previewOptions=()=>null;export const proofOptions=()=>null;export async function startServer(o){await o.bus['thread.claims']();return {address:()=>({port:0}),close:()=>{}};}`;
|
|
const busSource = `import {appendFileSync} from 'node:fs';export class BusReadError extends Error{};export const busCaller=o=>async(verb,...args)=>{appendFileSync(${JSON.stringify(log)},JSON.stringify({verb,args,tester:!!o.cli})+'\\n');return [];};export const organizationConsole=()=>{throw Error('organizationConsole outside dev-owner')};export const busBusiness=()=>{throw Error('busBusiness outside --sso')};export const userCaller=()=>{throw Error('userCaller outside --sso')};export const consoleCall=()=>{throw Error('consoleCall outside --sso')};`;
|
|
writeFileSync(hook, `import {registerHooks} from 'node:module';registerHooks({load(url,context,next){
|
|
if(url.endsWith('/webui/src/serve.mjs'))return {format:'module',shortCircuit:true,source:${JSON.stringify(serveSource)}};
|
|
if(url.endsWith('/webui/src/bus.mjs'))return {format:'module',shortCircuit:true,source:${JSON.stringify(busSource)}};
|
|
return next(url,context);
|
|
}});`);
|
|
const r = spawnSync(process.execPath, [new URL('../src/cli.mjs', import.meta.url).pathname, 'serve', '--business', 'acme', '--port', '0'],
|
|
{ env: { ...f.env, NODE_OPTIONS: `--import=${hook}` }, encoding: 'utf8', timeout: 10000 });
|
|
assert.equal(r.status, 0, r.stdout + r.stderr);
|
|
assert.deepEqual(readFileSync(log, 'utf8').trim().split('\n').map(JSON.parse), [{ verb: 'thread.claims', args: [], tester: false }]);
|
|
});
|