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