Files
stack/packages/webui/tests/thread-claims.test.mjs

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 }]);
});