// Row 194 (#1692, D7, T16): the Console's Agents routes. Launch, terminate, regenerate and veto each // have instanceSend's boundary and make one call through agentWrite; delete joins add and change on // /api/instances; the page follows a request at /api/agent/request. The tester listener lets launch, // terminate and the request read through (D4.1) and refuses regenerate and veto with no bus call. // Every write here uses a fixture writer; the bus verbs' own checks are in the bus tests. import test from 'node:test'; import assert from 'node:assert/strict'; import { request } from 'node:http'; import { mkdtempSync, rmSync, readFileSync } from 'node:fs'; import { runInNewContext } from 'node:vm'; import { THREAD_DEFINITIONS } from '../../organization/src/vocabulary.mjs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; 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 { createSsoSession } from './helpers/sso-session.mjs'; const REV = 'a'.repeat(64); const ID = '0b5e8f1c-3a2d-4c6e-9f10-1234567890ab'; const read = (extra = {}) => ({ business: 'acme', revision: 'b'.repeat(64), launch: true, instances: { coder: { definition: 'coder', vars: {}, launched: true, launcher: false, protected: false, kept: false } }, definitions: {}, noHire: [], retired: [], ...extra }); const ANSWERS = { 'instance.launch': () => ({ request: ID }), 'instance.regenerate': () => ({ request: ID }), 'instance.end': () => ({ request: ID, runs: ['r-1'], cancelled: [], taken: [] }), 'veto.set': () => ({ ...read(), transaction: 't-1', restart: false }), 'instance.request': args => ({ request: args.request, action: 'launch', instance: 'coder', by: 'jason', at: '2026-10-20T00:00:00.000Z', state: 'closed', done: { run: 'r-2', transaction: null, refusal: null }, cancelled: null }), }; async function setup(t, { on = true, agent = async (verb, args) => ANSWERS[verb](args), instances = async args => ({ request: ID, args }), tester = false } = {}) { const calls = [], lines = []; const options = { log: line => lines.push(line), agentWrite: agent ? async (verb, args) => { calls.push([verb, args]); return agent(verb, args); } : null, instancesWrite: instances ? async args => { calls.push(['instances.set', args]); return instances(args); } : null, }; // Row 196 (#1694): the tester listener serves only on its private socket under the data root. if (tester) { const dataRoot = mkdtempSync(join(tmpdir(), 'g194-tester-')); t.after(() => rmSync(dataRoot, { recursive: true, force: true })); const server = await startTesterSocket({ dataRoot, ...options }); t.after(() => new Promise(r => server.close(r))); return { calls, lines, dataRoot }; } const auth = await createSsoSession(t, { host: 'console.example' }); const server = await startServer({ port: 0, sso: auth.sso, send: on ? async () => ({}) : null, ...options }); t.after(() => new Promise(r => server.close(r))); const session = await auth.signIn(server.address().port); return { calls, lines, port: server.address().port, headers: auth.writeHeaders(session) }; } const send = (s, path, data, hs = s.headers, method = 'POST') => new Promise((resolve, reject) => { const req = request({ hostname: '127.0.0.1', port: s.port, path, method, headers: hs }, r => { let text = ''; r.on('data', c => text += c); r.on('end', () => resolve({ status: r.statusCode, type: r.headers['content-type'], text, body: (() => { try { return JSON.parse(text); } catch { return null; } })() })); }); req.on('error', reject); req.end(data === undefined ? undefined : typeof data === 'string' ? data : JSON.stringify(data)); }); const get = (s, path, hs = s.headers) => send(s, path, undefined, hs, 'GET'); test('row 194 T16: each agent action makes one call with only its keys, logs the user and answers the request', async t => { const s = await setup(t); const launched = await send(s, '/api/agent/launch', { instance: 'coder' }); assert.equal(launched.status, 200, launched.text); assert.equal(launched.body.request, ID); assert.match(launched.body.at, /^\d{4}-/); const regenerated = await send(s, '/api/agent/regenerate', { instance: 'coder' }); assert.equal(regenerated.status, 200, regenerated.text); const ended = await send(s, '/api/agent/terminate', { instance: 'coder', reason: 'secret reason text' }); assert.equal(ended.status, 200, ended.text); assert.deepEqual(ended.body.runs, ['r-1']); const bare = await send(s, '/api/agent/terminate', { instance: 'coder' }); assert.equal(bare.status, 200, bare.text); const kept = await send(s, '/api/veto', { revision: REV, op: 'keep', name: 'coder' }); assert.equal(kept.status, 200, kept.text); assert.equal(kept.body.transaction, 't-1'); assert.deepEqual(s.calls, [ ['instance.launch', { instance: 'coder' }], ['instance.regenerate', { instance: 'coder' }], ['instance.end', { instance: 'coder', reason: 'secret reason text' }], ['instance.end', { instance: 'coder' }], ['veto.set', { revision: REV, op: 'keep', name: 'coder' }], ]); assert.deepEqual(s.lines.map(l => l.replace(/^\S+ /, '')), [ `launch user="jason" instance="coder" done request=${ID}`, `regenerate user="jason" instance="coder" done request=${ID}`, `terminate user="jason" instance="coder" done request=${ID}`, `terminate user="jason" instance="coder" done request=${ID}`, 'veto user="jason" instance="coder" done', ]); assert.ok(s.lines.every(l => !l.includes('secret reason text')), 'a reason is never logged'); }); test('row 194 T16: the boundary refuses before any bus call', async t => { const s = await setup(t); const cases = [ ['/api/agent/launch', { instance: 'coder' }, { ...s.headers, cookie: '__Host-mosaic_session=invalid' }, 401], ['/api/agent/launch', { instance: 'coder' }, { ...s.headers, origin: 'https://evil.example' }, 403], ['/api/agent/launch', { instance: 'coder' }, { ...s.headers, 'sec-fetch-site': 'cross-site' }, 403], ['/api/agent/launch', { instance: 'coder', extra: 'x' }, s.headers, 400], ['/api/agent/launch', {}, s.headers, 400], ['/api/agent/launch', { instance: 7 }, s.headers, 400], ['/api/agent/launch', ['coder'], s.headers, 400], ['/api/agent/launch', 'not json', s.headers, 400], ['/api/agent/launch', { instance: 'x'.repeat(17000) }, s.headers, 400], ['/api/agent/terminate', { instance: 'coder', reason: 3 }, s.headers, 400], ['/api/agent/regenerate', { instance: 'coder', reason: 'no' }, s.headers, 400], ['/api/veto', { revision: REV, op: 'fire', name: 'coder' }, s.headers, 400], ['/api/veto', { revision: 'nope', op: 'keep', name: 'coder' }, s.headers, 400], ['/api/veto', { op: 'keep', name: 'coder' }, s.headers, 400], ]; for (const [path, body, hs, status] of cases) { const r = await send(s, path, body, hs); assert.equal(r.status, status, `${path} ${JSON.stringify(body).slice(0, 60)}: ${r.text}`); } for (const path of ['/api/agent/launch', '/api/agent/terminate', '/api/agent/regenerate', '/api/veto']) { assert.equal((await get(s, path)).status, 405, path); } assert.equal((await send(s, '/api/agent/request', { request: ID })).status, 405); assert.deepEqual(s.calls, []); }); test('row 194 T16: read-only when send is off or no writer is wired; no route has a --send exception', async t => { for (const options of [{ on: false }, { agent: null }]) { const s = await setup(t, options); for (const path of ['/api/agent/launch', '/api/agent/terminate', '/api/agent/regenerate', '/api/veto']) { const r = await send(s, path, { instance: 'coder' }); assert.equal(r.status, 403, path); assert.equal(r.body.error, 'read-only'); } const r = await get(s, `/api/agent/request?id=${ID}`); assert.equal(r.status, 403); assert.equal(r.body.error, 'read-only'); assert.deepEqual(s.calls, []); } }); test('row 194 T16: a bus refusal names its reason and nothing changed; an unknown outcome or a bad shape is never retried', async t => { let answer; const s = await setup(t, { agent: async () => answer() }); answer = () => { throw new BusReadError('no-route'); }; const refused = await send(s, '/api/agent/launch', { instance: 'coder' }); assert.equal(refused.body.sent, false); assert.equal(refused.body.error, 'no-route'); assert.match(refused.body.message, /^no-route: .+\. Nothing changed\.$/); assert.doesNotMatch(refused.body.message, /refused by the bus/, 'no-route has its own reason'); answer = () => { throw new BusReadError('instance-running'); }; const running = await send(s, '/api/agent/regenerate', { instance: 'coder' }); assert.equal(running.body.error, 'instance-running'); assert.equal(running.body.sent, false); answer = () => { throw new BusReadError('outcome-unknown'); }; const unknown = await send(s, '/api/agent/launch', { instance: 'coder' }); assert.deepEqual([unknown.status, unknown.body.error, unknown.body.sent], [503, 'outcome-unknown', 'unknown']); for (const [path, body, bad] of [ ['/api/agent/launch', { instance: 'coder' }, { request: 'not-an-id' }], ['/api/agent/terminate', { instance: 'coder' }, { request: ID, runs: 'r-1', cancelled: [], taken: [] }], ['/api/veto', { revision: REV, op: 'nohire', name: 'coder' }, { revision: REV }], // Row 194 b6 (webby-0094 B1): a veto answer without restart can't say whether the reload took. ['/api/veto', { revision: REV, op: 'nohire', name: 'coder' }, { ...read(), revision: REV, transaction: 't-1' }], ]) { answer = () => bad; const r = await send(s, path, body); assert.deepEqual([r.status, r.body.error, r.body.sent], [503, 'outcome-unknown', 'unknown'], path); } assert.equal(s.calls.length, 7, 'one call per POST, nothing retried'); }); test('row 194 T16: the request read takes one id, adds the refusal reason and refuses an unexpected shape', async t => { let answer = ANSWERS['instance.request']; const s = await setup(t, { agent: async (verb, args) => answer(args) }); const closed = await get(s, `/api/agent/request?id=${ID}`); assert.equal(closed.status, 200, closed.text); assert.deepEqual([closed.body.state, closed.body.done.run, closed.body.reason], ['closed', 'r-2', undefined]); answer = args => ({ ...ANSWERS['instance.request'](args), done: { run: null, transaction: null, refusal: 'no-route' } }); assert.match((await get(s, `/api/agent/request?id=${ID}`)).body.reason, /route/); // Row 194 deviation 11 (sage-1620): a failed revoke says the file is unchanged and a retry is safe. answer = args => ({ ...ANSWERS['instance.request'](args), done: { run: null, transaction: null, refusal: 'credential-revoke-failed' } }); assert.match((await get(s, `/api/agent/request?id=${ID}`)).body.reason, /business file is unchanged\. Tokens already revoked stay revoked .* delete again, which is safe$/); answer = args => ({ ...ANSWERS['instance.request'](args), done: null, cancelled: { by: 'jason' } }); assert.ok((await get(s, `/api/agent/request?id=${ID}`)).body.reason); answer = args => ({ ...ANSWERS['instance.request'](args), state: 'taken', done: null }); assert.equal((await get(s, `/api/agent/request?id=${ID}`)).body.state, 'taken'); assert.deepEqual(s.calls.map(c => c[0]), Array(5).fill('instance.request')); for (const query of ['', '?id=nope', `?id=${ID}&id=${ID}`, `?id=${ID}&x=1`, `?id=${ID.toUpperCase()}`]) { const r = await get(s, `/api/agent/request${query}`); assert.equal(r.status, 400, query); assert.equal(r.body.error, 'invalid-request'); } assert.equal(s.calls.length, 5, 'a bad id makes no call'); answer = () => { throw new BusReadError('unknown-request'); }; const unknown = await get(s, `/api/agent/request?id=${ID}`); assert.equal(unknown.body.error, 'unknown-request'); for (const bad of [args => ({ ...ANSWERS['instance.request'](args), request: '0'.repeat(36) }), args => ({ ...ANSWERS['instance.request'](args), state: 'done' }), () => null]) { answer = bad; const r = await get(s, `/api/agent/request?id=${ID}`); assert.deepEqual([r.status, r.body.error], [502, 'invalid-response']); } }); test('row 194 T16: delete joins add and change on /api/instances with only revision, instance and op', async t => { const s = await setup(t); const deleted = await send(s, '/api/instances', { revision: REV, instance: 'coder', op: 'delete' }); assert.equal(deleted.status, 200, deleted.text); assert.equal(deleted.body.request, ID); assert.deepEqual(s.calls, [['instances.set', { revision: REV, instance: 'coder', op: 'delete' }]]); assert.match(s.lines.at(-1), / instance user="jason" instance="coder" done request=/); for (const extra of [{ definition: 'coder' }, { vars: {} }, { launch: false }]) { const r = await send(s, '/api/instances', { revision: REV, instance: 'coder', op: 'delete', ...extra }); assert.equal(r.status, 400, JSON.stringify(extra)); } assert.equal(s.calls.length, 1); }); test('row 194 T16 D4.1: the tester listener launches, terminates and reads requests; regenerate, veto and delete are refused with no call', async t => { const s = await setup(t, { tester: true }); const send = (s, path, body) => testerRequest({ dataRoot: s.dataRoot, path, method: 'POST', body }); assert.equal((await send(s, '/api/agent/launch', { instance: 'coder' })).status, 200); assert.equal((await send(s, '/api/agent/terminate', { instance: 'coder' })).status, 200); assert.equal((await testerRequest({ dataRoot: s.dataRoot, path: `/api/agent/request?id=${ID}` })).status, 200); for (const [path, body] of [['/api/agent/regenerate', { instance: 'coder' }], ['/api/veto', { revision: REV, op: 'keep', name: 'coder' }], ['/api/instances', { revision: REV, instance: 'coder', op: 'delete' }]]) { const r = await send(s, path, body); assert.equal(r.status, 403, path); assert.equal(r.body.sent, false, path); } assert.deepEqual(s.calls.map(c => c[0]), ['instance.launch', 'instance.end', 'instance.request']); assert.ok(s.lines.every(l => l.includes('user="tester"'))); }); test('row 194 N5: agents.js is served as JavaScript and index.html loads it once', async t => { const s = await setup(t); const js = await get(s, '/agents.js'); assert.equal(js.status, 200); assert.match(js.type, /^text\/javascript/); // F3 adds the agent restart receipt and the thread test that Settings > Routing uses. assert.match(js.text, /window\.MosaicAgents = Object\.freeze\(\{ section, restartMarkup, isThread: definition => threadDefinitions\.has\(definition\) \}\)/); assert.match(js.text, /Agent actions are unavailable here\. Use mosaic org instance from the instance CLI\./); const html = await get(s, '/'); assert.equal(html.text.match(/