// Row 252 (#1753): the Console decision endpoint, driven with a fake request, response and bus // transport. The real bus side is in packages/bus/tests/console-decide.test.mjs and the page side // in the browser tests. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BusReadError } from '../src/bus.mjs'; import { DECISION_REFUSALS, createConfirmations, decisionHTTP } from '../src/decision-http.mjs'; const ORIGIN = 'https://console.test'; const DIGEST = 'a'.repeat(64); const PREVIEW = { id: 'd1', business: 'demo', question: 'Enroll host?', digest: DIGEST, approve: { key: 'yes', text: 'Yes, enroll it' }, deny: { key: 'no', text: 'No' }, grant: { method: 'host-enroll', host: 'h1' }, }; function request({ method = 'POST', body = {}, headers = {}, raw } = {}) { const h = { origin: [ORIGIN], 'sec-fetch-site': ['same-origin'], 'content-type': ['application/json'], ...headers }; for (const k of Object.keys(h)) if (h[k] === null) delete h[k]; const data = Buffer.from(raw ?? JSON.stringify(body)); return { method, headersDistinct: h, async *[Symbol.asyncIterator]() { yield data; } }; } function response() { const r = { status: null, headers: null, text: '' }; r.writeHead = (s, h) => { r.status = s; r.headers = h; }; r.end = (t = '') => { r.text += t; }; r.json = () => JSON.parse(r.text); return r; } function setup({ answer = () => ({ ...PREVIEW }), resolve = (a) => ({ id: a.id, op: 'resolved', choice: a.choice, grant: 'started' }), now } = {}) { const session = { name: 'jason', provider: 'fake-oidc', iss: 'https://idp.test', sub: 'owner-1' }; const calls = [], lines = []; const s = { session, calls, lines, current: session, confirmations: createConfirmations(now ? { now } : {}), decide: { origin: ORIGIN, call: async (verb, args, ctx) => { calls.push({ verb, args, ctx }); return ['decision.preview.console','hire.preview.console'].includes(verb) ? answer(args) : resolve(args); }, }, }; s.run = async (path, req) => { const res = response(); await decisionHTTP(req, res, { path, decide: s.decide, session: s.session, live: () => s.current, confirmations: s.confirmations, log: (l) => lines.push(l) }); return res; }; s.confirm = (choice = 'yes', id = 'd1') => s.run(`/api/decisions/${id}/confirmation`, request({ body: { choice } })); s.resolve = (body, id = 'd1') => s.run(`/api/decisions/${id}/resolve`, request({ body })); return s; } const refused = (res, code) => { assert.equal(res.status, DECISION_REFUSALS[code][0], `${code}: ${res.text}`); assert.deepEqual(res.json(), { error: code, message: DECISION_REFUSALS[code][1] }); assert.equal(res.headers['cache-control'], 'no-store'); }; test('a confirmation then a resolve answers once with the held choice and digest, and a second resolve refuses', async () => { const s = setup(); const c = await s.confirm('yes'); assert.equal(c.status, 200); const body = c.json(); assert.match(body.confirmation, /^[a-f0-9]{64}$/); assert.deepEqual({ ...body, confirmation: 'x' }, { id: 'd1', question: 'Enroll host?', action: 'approve', choice: 'yes', text: 'Yes, enroll it', grant: PREVIEW.grant, confirmation: 'x' }); assert.deepEqual(s.calls[0], { verb: 'decision.preview.console', args: { id: 'd1' }, ctx: { subject: { provider: 'fake-oidc', iss: 'https://idp.test', sub: 'owner-1' } } }); const r = await s.resolve({ choice: 'yes', confirmation: body.confirmation }); assert.equal(r.status, 200); assert.deepEqual(r.json(), { id: 'd1', op: 'resolved', choice: 'yes', grant: 'started' }); assert.deepEqual(s.calls[1], { verb: 'decision.resolve.console', args: { id: 'd1', choice: 'yes', digest: DIGEST }, ctx: { subject: { provider: 'fake-oidc', iss: 'https://idp.test', sub: 'owner-1' }, business: 'demo' } }); // J12: the confirmation is spent, so a repeat never reaches the bus. refused(await s.resolve({ choice: 'yes', confirmation: body.confirmation }), 'confirmation-required'); assert.equal(s.calls.length, 2); assert.equal(s.lines.length, 3); assert.match(s.lines[0], / decide user="jason" decision="d1" step=confirmation action=approve$/); assert.match(s.lines[1], / step=resolve choice="yes" grant=started$/); assert.doesNotMatch(s.lines.join('\n'), new RegExp(body.confirmation)); }); test('deny resolves without a grant field', async () => { const s = setup({ resolve: (a) => ({ id: a.id, op: 'resolved', choice: a.choice }) }); const c = (await s.confirm('no')).json(); assert.equal(c.action, 'deny'); assert.equal(c.text, 'No'); const r = await s.resolve({ choice: 'no', confirmation: c.confirmation }); assert.deepEqual(r.json(), { id: 'd1', op: 'resolved', choice: 'no' }); }); const ORDINARY = { id: 'd1', business: 'demo', question: 'How should we handle this request?', digest: DIGEST, kind: 'decision', options: [{ key: 'new/autonomous', text: 'New project — autonomous' }, { key: 'new/guided', text: 'New project — guided' }, { key: 'existing:project', text: 'Add to existing project' }], }; test('ordinary confirmation carries the selected third key/text and decision context, then resolves once in its business', async () => { const s = setup({ answer: () => ORDINARY, resolve: a => ({ id: a.id, op: 'resolved', choice: a.choice }) }); const c = await s.confirm('existing:project'); assert.equal(c.status, 200, c.text); assert.deepEqual({ ...c.json(), confirmation: 'x' }, { id: 'd1', kind: 'decision', question: ORDINARY.question, action: 'answer', choice: 'existing:project', text: 'Add to existing project', confirmation: 'x' }); const body = { choice: 'existing:project', confirmation: c.json().confirmation }; assert.deepEqual((await s.resolve(body)).json(), { id: 'd1', op: 'resolved', choice: 'existing:project' }); assert.equal(s.calls[1].ctx.business, 'demo'); assert.equal(s.calls[1].args.digest, DIGEST); refused(await s.resolve(body), 'confirmation-required'); assert.equal(s.calls.length, 2); }); test('ordinary options are validated authoritatively at confirmation, including two and nine choices', async () => { for (const options of [ORDINARY.options.slice(0, 2), Array.from({ length: 9 }, (_, i) => ({ key: `key:${i}`, text: `Answer ${i}` }))]) { const s = setup({ answer: () => ({ ...ORDINARY, options }) }); assert.equal((await s.confirm(options.at(-1).key)).status, 200); refused(await s.confirm('not-listed'), 'decision-stale'); } for (const options of [[], ORDINARY.options.slice(0, 1), Array(10).fill(ORDINARY.options[0]), [ORDINARY.options[0], ORDINARY.options[0]], [{ key: 1, text: 'x' }, ORDINARY.options[1]], [{ key: 'x' }, ORDINARY.options[1]]]) { const s = setup({ answer: () => ({ ...ORDINARY, options }) }); refused(await s.confirm('new/autonomous'), 'confirmation-unavailable'); assert.equal(s.confirmations.size, 0); } }); test('ordinary stale/closed and uncertain outcomes spend their confirmation and never replay', async () => { for (const code of ['decision-stale', 'decision-closed', 'outcome-unknown']) { const s = setup({ answer: () => ORDINARY, resolve: () => { throw new BusReadError(code); } }); const c = await s.confirm('new/guided'); assert.equal(c.status, 200, c.text); const body = { choice: 'new/guided', confirmation: c.json().confirmation }; refused(await s.resolve(body), code); refused(await s.resolve(body), 'confirmation-required'); assert.equal(s.calls.length, 2); } }); test('ordinary confirmations stay bound to the issuing session, choice, decision and business', async () => { const s = setup({ answer: () => ORDINARY, resolve: a => ({ id: a.id, op: 'resolved', choice: a.choice }) }); let c = (await s.confirm('new/guided')).json(); refused(await s.resolve({ choice: 'existing:project', confirmation: c.confirmation }), 'confirmation-required'); c = (await s.confirm('new/guided')).json(); refused(await s.resolve({ choice: 'new/guided', confirmation: c.confirmation }, 'd2'), 'confirmation-required'); c = (await s.confirm('new/guided')).json(); s.current = null; refused(await s.resolve({ choice: 'new/guided', confirmation: c.confirmation }), 'sign-in-required'); s.current = s.session; refused(await s.resolve({ choice: 'new/guided', confirmation: c.confirmation }), 'confirmation-required'); const other = setup({ answer: () => ({ ...ORDINARY, business: 'other' }) }); other.confirmations = s.confirmations; c = (await s.confirm('new/guided')).json(); refused(await other.resolve({ choice: 'new/guided', confirmation: c.confirmation }), 'confirmation-required'); assert.equal(s.calls.filter(x => x.verb === 'decision.resolve.console').length, 0); assert.equal(other.calls.length, 0); }); test('the gates before the body: method, read-only, sign-in and same origin', async () => { const s = setup(); refused(await s.run('/api/decisions/d1/confirmation', request({ method: 'GET' })), 'method-not-allowed'); const res = response(); await decisionHTTP(request({ body: { choice: 'yes' } }), res, { path: '/api/decisions/d1/confirmation', decide: null, session: s.session, live: () => s.session, confirmations: s.confirmations, log: () => {} }); refused(res, 'read-only'); const out = response(); await decisionHTTP(request({ body: { choice: 'yes' } }), out, { path: '/api/decisions/d1/confirmation', decide: s.decide, session: null, live: () => null, confirmations: s.confirmations, log: () => {} }); refused(out, 'sign-in-required'); for (const headers of [{ origin: ['https://evil.test'] }, { origin: null }, { origin: [ORIGIN, ORIGIN] }, { 'sec-fetch-site': ['same-site'] }, { 'sec-fetch-site': null }]) { refused(await s.run('/api/decisions/d1/confirmation', request({ body: { choice: 'yes' }, headers })), 'cross-origin'); } assert.equal(s.calls.length, 0); }); test('a bad body, id or content type refuses before the bus', async () => { const s = setup(); const path = '/api/decisions/d1/confirmation'; refused(await s.run(path, request({ body: { choice: 'yes' }, headers: { 'content-type': ['text/plain'] } })), 'invalid-request'); refused(await s.run(path, request({ raw: '{' })), 'invalid-request'); refused(await s.run(path, request({ raw: '[]' })), 'invalid-request'); refused(await s.run(path, request({ body: { choice: 'yes', extra: 1 } })), 'invalid-request'); refused(await s.run(path, request({ body: { choice: 1 } })), 'invalid-request'); refused(await s.run(path, request({ raw: JSON.stringify({ choice: 'y'.repeat(5000) }) })), 'request-too-large'); refused(await s.run('/api/decisions/..%2Fx/confirmation', request({ body: { choice: 'yes' } })), 'decision-not-found'); refused(await s.run('/api/decisions/%E0%A4%A/confirmation', request({ body: { choice: 'yes' } })), 'decision-not-found'); refused(await s.resolve({ choice: 'yes' }), 'invalid-request'); refused(await s.resolve({ choice: 'yes', confirmation: 'a'.repeat(64), more: 1 }), 'invalid-request'); assert.equal(s.calls.length, 0); }); test('a choice that is neither key is decision-stale, and a malformed preview is confirmation-unavailable', async () => { refused(await setup().confirm('maybe'), 'decision-stale'); for (const bad of [{ ...PREVIEW, id: 'd2' }, { ...PREVIEW, digest: 'short' }, { ...PREVIEW, deny: { key: 'yes', text: 'No' } }, { ...PREVIEW, grant: null }, null]) { const s = setup({ answer: () => bad }); refused(await s.confirm('yes'), 'confirmation-unavailable'); assert.equal(s.confirmations.size, 0); } }); test('bus refusals map by code, and an unknown or missing code is unavailable at preview and outcome-unknown at resolve', async () => { const cases = [ ['human-required', 'human-required'], ['agent-required', 'human-required'], ['access-revoked', 'human-required'], ['peercred-required', 'peercred-required'], ['decision-not-eligible', 'decision-not-eligible'], ['decision-closed', 'decision-closed'], ['decision-stale', 'decision-stale'], ['invalid-choice', 'decision-stale'], ['decision-not-found', 'decision-not-found'], ['outcome-unknown', 'confirmation-unavailable'], ['weird', 'confirmation-unavailable'], ]; for (const [bus, code] of cases) { const s = setup({ answer: () => { throw new BusReadError(bus); } }); refused(await s.confirm('yes'), code); } refused(await setup({ answer: () => { throw new Error('boom'); } }).confirm('yes'), 'confirmation-unavailable'); for (const [bus, code] of [['decision-closed', 'decision-closed'], ['outcome-unknown', 'outcome-unknown'], ['weird', 'outcome-unknown']]) { const s = setup({ resolve: () => { throw new BusReadError(bus); } }); const c = (await s.confirm('yes')).json(); refused(await s.resolve({ choice: 'yes', confirmation: c.confirmation }), code); // Nothing retries, and the confirmation stays spent. assert.equal(s.calls.length, 2); refused(await s.resolve({ choice: 'yes', confirmation: c.confirmation }), 'confirmation-required'); } for (const reply of [null, { id: 'd2', op: 'resolved', choice: 'yes' }, { id: 'd1', op: 'refused', choice: 'yes' }, { id: 'd1', op: 'resolved', choice: 'no' }]) { const s = setup({ resolve: () => reply }); const c = (await s.confirm('yes')).json(); refused(await s.resolve({ choice: 'yes', confirmation: c.confirmation }), 'outcome-unknown'); } }); test('a confirmation binds the session, decision and choice, and any mismatch spends it', async () => { const s = setup(); let c = (await s.confirm('yes')).json(); refused(await s.resolve({ choice: 'no', confirmation: c.confirmation }), 'confirmation-required'); refused(await s.resolve({ choice: 'yes', confirmation: c.confirmation }), 'confirmation-required'); c = (await s.confirm('yes')).json(); refused(await s.resolve({ choice: 'yes', confirmation: c.confirmation }, 'd2'), 'confirmation-required'); c = (await s.confirm('yes')).json(); const other = setup(); other.confirmations = s.confirmations; other.session = { ...s.session }; other.current = other.session; refused(await other.resolve({ choice: 'yes', confirmation: c.confirmation }), 'confirmation-required'); refused(await s.resolve({ choice: 'yes', confirmation: 'not-a-token' }), 'confirmation-required'); assert.equal(s.calls.filter((x) => x.verb === 'decision.resolve.console').length, 0); assert.equal(other.calls.length, 0); }); test('a session that ends between the steps refuses before the bus', async () => { const s = setup(); s.current = null; refused(await s.confirm('yes'), 'sign-in-required'); assert.equal(s.calls.length, 0); s.current = s.session; const c = (await s.confirm('yes')).json(); s.current = { ...s.session }; refused(await s.resolve({ choice: 'yes', confirmation: c.confirmation }), 'sign-in-required'); assert.equal(s.calls.length, 1); // A sign-out during the preview also refuses, and no confirmation is issued. const t = setup({ answer: () => { t.current = null; return { ...PREVIEW }; } }); refused(await t.confirm('yes'), 'sign-in-required'); assert.equal(t.confirmations.size, 0); }); test('confirmations expire after the TTL, cap at the limit without dropping a live one, and are one use', () => { let t = 0; const store = createConfirmations({ ttlMs: 1000, max: 2, now: () => t }); const bind = { session: 's', business: 'demo', decision: 'd1', choice: 'yes', digest: DIGEST }; const a = store.issue(bind), b = store.issue(bind); assert.equal(store.issue(bind), null); assert.equal(store.size, 2); assert.equal(store.consume(a, bind).digest, DIGEST); assert.equal(store.consume(a, bind), null); t = 1000; assert.equal(store.consume(b, bind), null); assert.equal(store.size, 0); assert.match(store.issue(bind), /^[a-f0-9]{64}$/); assert.equal(store.consume(undefined, bind), null); assert.equal(store.consume('A'.repeat(64), bind), null); }); test('A4 hire confirmation carries computed lines and resolves through org.hire, never decide',async()=>{ const flagged=[{field:'tools',value:'bash',reasons:['above-chief']}],credentialLines=[{service:'gitea',scope:'read:repository'}]; const s=setup({answer:()=>({id:'d1',business:'demo',kind:'hire',question:'hire scout?',digest:DIGEST,options:[{key:'yes',text:'Approve'},{key:'no',text:'Deny'}],flagged,credentialLines,expiresAt:'2099-01-01T00:00:00.000Z'}),resolve:a=>({id:a.id,op:'resolved',choice:a.choice})}); const c=await s.confirm('yes');assert.equal(c.status,200); assert.deepEqual(c.json().flagged,flagged);assert.deepEqual(c.json().credentialLines,credentialLines); const r=await s.resolve({choice:'yes',confirmation:c.json().confirmation});assert.equal(r.status,200); assert.equal(s.calls[1].verb,'hire.resolve.console'); }); test('A4 Console start carries the signed-in subject and org, records once and never retries',async()=>{ const s=setup({resolve:()=>({id:'new-hire',op:'resolved',choice:'yes'})}); const hire={op:'hire',level:'org',name:'scout',reason:'Need help',role:{template:'coder',sha256:DIGEST},route:{harness:'pi',model:'claude-opus-5-5'}}; const r=await s.run('/api/decisions/hire/start',request({body:{business:'demo',hire}})); assert.equal(r.status,200,r.text);assert.equal(s.calls.length,1); assert.equal(s.calls[0].verb,'hire.start.console');assert.deepEqual(s.calls[0].args,hire);assert.equal(s.calls[0].ctx.business,'demo'); refused(await s.run('/api/decisions/hire/start',request({body:{business:'demo',hire},headers:{origin:['https://elsewhere.test']}})),'cross-origin'); assert.equal(s.calls.length,1); }); test('A4 confirmation holds the exact human adjustment, so resolve cannot substitute it',async()=>{ const adjust={role:{tools:['read']},credentials:[]}; const s=setup({answer:()=>({...ORDINARY,kind:'hire',flagged:[],credentialLines:[],expiresAt:'2099-01-01T00:00:00.000Z'}),resolve:a=>({id:a.id,op:'resolved',choice:a.choice})}); const c=await s.run('/api/decisions/d1/confirmation',request({body:{choice:'new/autonomous',adjust}})); assert.equal(c.status,200,c.text);assert.deepEqual(s.calls[0].args,{id:'d1',adjust}); assert.equal(s.calls[0].verb,'hire.preview.console'); const r=await s.resolve({choice:'new/autonomous',confirmation:c.json().confirmation}); assert.equal(r.status,200,r.text);assert.deepEqual(s.calls[1].args.adjust,adjust); }); // D4 (#1873, P1-2): a hire start the bus definitely refused before it recorded the decision keeps its // own code and a next step; every other code, a lost or unreadable reply among them, is still // outcome-unknown, and the other steps keep their mapping. const START_HIRE={op:'hire',level:'org',name:'scout',reason:'Need help',role:{template:'coder',sha256:DIGEST},route:{harness:'pi',model:'gone-model'}}; const startWith=async err=>{ const s=setup({resolve:()=>{throw err;}}); const r=await s.run('/api/decisions/hire/start',request({body:{business:'demo',hire:START_HIRE}})); assert.equal(s.calls.length,1,'one bus call, never retried'); return {r,s}; }; test('D4 hire start: model-provider-unavailable keeps its code and names Settings > Model proxy',async()=>{ const {r,s}=await startWith(new BusReadError('model-provider-unavailable')); assert.equal(r.status,409,r.text); const body=r.json(); assert.equal(body.error,'model-provider-unavailable'); assert.match(body.message,/Settings > Model proxy/); assert.match(body.message,/Nothing was applied/); assert.equal(r.headers['cache-control'],'no-store'); assert.match(s.lines.at(-1),/step=start error=model-provider-unavailable$/); }); test('D4 hire start: the other model route codes keep their code with a next step',async()=>{ for(const [code,status] of [['model-catalogue-unavailable',503],['model-harness-incompatible',409],['thinking-unsupported',409]]) { const {r}=await startWith(new BusReadError(code)); assert.equal(r.status,status,`${code}: ${r.text}`);assert.equal(r.json().error,code);assert.match(r.json().message,/Nothing was applied/); } }); test('D4 hire start: the other proposal checks that run before the decision insert keep their code',async()=>{ for(const code of ['definition-not-hireable','definition-shared','no-chief-of-staff']) { const {r}=await startWith(new BusReadError(code)); assert.equal(r.status,409,`${code}: ${r.text}`);assert.equal(r.json().error,code);assert.match(r.json().message,/Nothing was applied/); assert.doesNotMatch(r.json().message,/\bhire was\b/,'worded for hire, fire and change alike'); } }); // Review P3: a code the broker can raise after the decision exists or is consumed, or one the bus // server raises about the request itself, is not a definite refusal of the start. test('D4 hire start: a code that can follow the decision insert, or a transport code, stays outcome-unknown',async()=>{ for(const code of ['revision-conflict-revoked','request-timeout','invalid-envelope','incomplete-request','instance-kept','organization-instance-exists','organization-unknown-instance','root-fire-shared','fire-in-progress','some-future-code']) refused((await startWith(new BusReadError(code))).r,'outcome-unknown'); }); test('D4 hire start: a lost or unreadable reply is still outcome-unknown',async()=>{ for(const err of [new BusReadError('outcome-unknown'),new BusReadError('invalid-response'),new BusReadError('response-too-large'),new BusReadError('outcome-unknown-revoked'),new BusReadError('Not A Code'),new BusReadError(''),new Error('socket closed'),Object.assign(new Error('x'),{code:'model-provider-unavailable'})]) refused((await startWith(err)).r,'outcome-unknown'); // A reply that does not say the hire resolved yes is not a refusal either. const s=setup({resolve:()=>({id:'new-hire',op:'resolved',choice:'no'})}); refused(await s.run('/api/decisions/hire/start',request({body:{business:'demo',hire:START_HIRE}})),'outcome-unknown'); }); test('D4 hire start: the bus codes the endpoint already names keep their refusal',async()=>{ refused((await startWith(new BusReadError('unauthenticated'))).r,'human-required'); refused((await startWith(new BusReadError('right-required'))).r,'right-required'); refused((await startWith(new BusReadError('hire-owner-required'))).r,'hire-owner-required'); }); test('D4 templates, confirmation and resolve keep their open mapping for a definite unnamed code',async()=>{ const t=setup({resolve:()=>{throw new BusReadError('model-provider-unavailable');}}); refused(await t.run('/api/decisions/hire/templates',request({body:{business:'demo'}})),'confirmation-unavailable'); const c=setup({answer:()=>{throw new BusReadError('model-provider-unavailable');}}); refused(await c.confirm('yes'),'confirmation-unavailable'); const s=setup({resolve:()=>{throw new BusReadError('model-provider-unavailable');}}); const ok=await s.confirm('yes');assert.equal(ok.status,200); refused(await s.resolve({choice:'yes',confirmation:ok.json().confirmation}),'outcome-unknown'); });