// Walkthrough defect 1 (Jason, 7350 Quick Setup): an organization conversation showed "Request status unavailable." // although the message went through. The organization options built in cli.mjs carried no request read, so // /api/orgs//config never offered requests and the project page counted that as a failed read. // This runs the real cli.mjs serve --dev-local-owner against a real organization record. Only the bus // transports cli.mjs builds are replaced (loader hook), so no bus or human proof is involved. import test from 'node:test'; import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { mkdirSync, readFileSync, renameSync, chmodSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { tmp, fixture } from '../../cli/tests/helpers.mjs'; import { createOrg } from '../../organization/src/org-service.mjs'; import { orgPaths } from '../../cli/src/config.mjs'; import { DEV_OWNER } from '../../cli/src/dev-local-owner.mjs'; import { browser } from './browser.mjs'; const P = '10000000-0000-4000-8000-0000000000a1', T = '20000000-0000-4000-8000-0000000000a1', S = '30000000-0000-4000-8000-0000000000a1'; // The correlation id a sent message carries. The org's request intake has no request under it. const REQUEST = 'c0ffee00-0000-4000-8000-0000000000a1'; async function consoleFor(t) { const root = tmp(t, 'o-'), f = fixture(root), home = join(root, 'home'), configDir = join(home, '.config', 'mosaic-test'); mkdirSync(join(home, '.config'), { recursive: true }); renameSync(join(root, 'config'), configDir); chmodSync(configDir, 0o700); chmodSync(join(configDir, 'businesses'), 0o700); writeFileSync(join(f.dataRoot, 'instance.json'), JSON.stringify({ instanceVersion: 1, name: 'test' })); const doc = structuredClone(f.doc); doc.id = 'alpha'; doc.human = DEV_OWNER.sub; doc.projects.stack.root = '/__mosaic_org__/alpha/projects/stack'; const org = await createOrg({ configDir, dataRoot: f.dataRoot, instance: 'test', principal: DEV_OWNER }, 'alpha', doc); const log = join(root, 'calls.jsonl'); writeFileSync(log, ''); const busURL = new URL('../src/bus.mjs', import.meta.url).href, orgBusURL = new URL('../../cli/src/org-bus.mjs', import.meta.url).href; // Each transport answers from fixed rows and logs (kind, business, socket, verb). Everything else in bus.mjs is real. const busShim = join(root, 'bus-shim.mjs'), orgBusShim = join(root, 'org-bus-shim.mjs'), hook = join(root, 'hook.mjs'); writeFileSync(busShim, `import { appendFileSync } from 'node:fs'; import { BusReadError } from ${JSON.stringify(busURL)}; export * from ${JSON.stringify(busURL)}; const P=${JSON.stringify(P)}, T=${JSON.stringify(T)}, S=${JSON.stringify(S)}, REQUEST=${JSON.stringify(REQUEST)}; // The bus answers list verbs with bare arrays; the Console wraps them in rows. const human = (verb, args) => ({ plugins: { plugins: { channel: ['cli', 'webui'] } }, 'project.list': [{ id: P, name: 'work', repo: null }], 'thread.list': args?.shared ? [] : [{ id: T, project: P, name: 'init', instance: 'assistant' }], 'session.list': [{ id: S, name: 'Main', default: true, state: 'active', revision: 1 }], 'thread.messages': [ { id: '40000000-0000-4000-8000-0000000000a1', from_role: 'human', body: 'hello world!', request: REQUEST, at: '2026-11-08T10:00:00Z' }, { id: '40000000-0000-4000-8000-0000000000a2', from_role: 'assistant', body: 'PINEAPPLE', at: '2026-11-08T10:00:05Z' }, ], }[verb] ?? []); const record = (kind, o, verb, args) => appendFileSync(${JSON.stringify(log)}, JSON.stringify({ kind, business: o.business ?? null, socket: o.socket ?? null, verb, args }) + '\\n'); export const busCaller = o => async (verb, args) => { record('human', o, verb, args); return human(verb, args); }; export const consoleCall = o => async (verb, args) => { record('console', o, verb, args); if (verb === 'request.get.console') throw new BusReadError('request-not-found', 'no request under this id'); throw new BusReadError('unavailable', 'fixture: ' + verb); }; `); writeFileSync(orgBusShim, `export * from ${JSON.stringify(orgBusURL)};\nexport const busStatus = async () => ({ healthy: true });\n`); writeFileSync(hook, `import { registerHooks } from 'node:module'; registerHooks({ resolve(specifier, context, next) { if (context.parentURL?.endsWith('/webui/src/cli.mjs')) { if (specifier === './bus.mjs') return { url: ${JSON.stringify(pathToFileURL(busShim).href)}, shortCircuit: true }; if (specifier === '../../cli/src/org-bus.mjs') return { url: ${JSON.stringify(pathToFileURL(orgBusShim).href)}, shortCircuit: true }; } return next(specifier, context); } }); `); const env = { ...f.env, HOME: home, MOSAIC_CONFIG: join(configDir, 'config.json'), NODE_OPTIONS: `--import=${pathToFileURL(hook).href}` }; const child = spawn(process.execPath, [new URL('../src/cli.mjs', import.meta.url).pathname, 'serve', '--port', '0', '--board', 'http://127.0.0.1:9', '--dev-local-owner'], { env, stdio: ['ignore', 'pipe', 'pipe'] }); let out = ''; child.stdout.on('data', c => out += c); child.stderr.on('data', c => out += c); t.after(() => { if (child.exitCode === null) child.kill('SIGTERM'); }); for (let i = 0; i < 300 && !/Mosaic Console: (http:\/\/127\.0\.0\.1:\d+\/)/.test(out); i++) { if (child.exitCode !== null) break; await new Promise(r => setTimeout(r, 50)); } const base = /Mosaic Console: (http:\/\/127\.0\.0\.1:\d+\/)/.exec(out)?.[1]; assert.ok(base, `the Console did not start:\n${out}`); const calls = () => readFileSync(log, 'utf8').trim().split('\n').filter(Boolean).map(line => JSON.parse(line)); // A failed wait reports the Console output and the last transport calls with the page text. const diagnostics = () => `Console output:\n${out.slice(-1500)}\nLast calls:\n${readFileSync(log, 'utf8').trim().split('\n').slice(-12).join('\n')}`; return { base, calls, org, diagnostics }; } async function page(t, base, diagnostics) { const b = await browser({ machine: false }); t.after(() => b.close()); await b.viewport(1440, 900); const wait = async (expression, label = expression) => { for (let i = 0; i < 400; i++) { try { if (await b.evaluate(`Boolean(${expression})`)) return; } catch { /* navigation */ } await new Promise(r => setTimeout(r, 50)); } throw Error(`Timed out: ${label}\n${await b.evaluate('document.body.innerText.slice(0, 2000)').catch(() => 'page unavailable')}\n${diagnostics()}`); }; return { b, wait }; } test('an organization offers its request status, and a conversation whose message has no request says nothing is wrong', { timeout: 60000 }, async t => { const { base, calls, org, diagnostics } = await consoleFor(t); const { b, wait } = await page(t, base, diagnostics); await b.navigate(`${base}#/orgs/alpha/projects/threads/${T}`); await wait(`document.querySelector('.chat-log')?.textContent.includes('PINEAPPLE')`); const config = await b.evaluate(`fetch('/api/orgs/alpha/config').then(r => r.json())`); assert.equal(config.requests, true, `the organization config must offer request reads: ${JSON.stringify(config)}`); // The page settles when the request read answered: the Tasks tab gets its count, or the status line says it failed. const settled = `document.querySelector('.project-tabs a[href$="/tasks"] .project-count') || document.querySelector('[data-conversation-request-status]')?.textContent`; await wait(settled, 'request status settled'); assert.equal(await b.evaluate(`document.querySelector('[data-conversation-request-status]').textContent`), ''); assert.doesNotMatch(await b.evaluate(`document.querySelector('main').innerText`), /Request status unavailable|Status unavailable/); const reads = calls().filter(c => c.kind === 'console'); assert.ok(reads.length > 0, 'the request read reached the console transport'); for (const read of reads) { assert.equal(read.verb, 'request.get.console'); assert.deepEqual(read.args, { request: REQUEST }); assert.equal(read.business, 'alpha', 'the read goes to the selected organization'); assert.equal(read.socket, orgPaths(org.root).socket, 'the read uses the organization bus socket'); } }); test('an organization project overview reports no active request instead of unavailable', { timeout: 60000 }, async t => { const { base, diagnostics } = await consoleFor(t); const { b, wait } = await page(t, base, diagnostics); await b.navigate(`${base}#/orgs/alpha/chat`); await wait(`document.querySelector('[data-project-status="${P}"] .project-card-status-line')`); assert.match(await b.evaluate(`document.querySelector('[data-project-status="${P}"]').textContent`), /^No active requests ยท 1 conversation/); // The overview renders after its reads settle, so the metrics are final once they exist. await b.navigate(`${base}?v=2#/orgs/alpha/projects/${P}`); await wait(`document.querySelector('.project-metrics')`, 'project metrics'); const metrics = await b.evaluate(`[...document.querySelectorAll('.project-metrics a')].slice(0, 2).map(a => a.innerText.replace(/\\s+/g, ' ').trim())`); assert.deepEqual(metrics, ['Requests in progress 0 No active requests', 'Needs your input 0 Nothing waiting']); assert.doesNotMatch(await b.evaluate(`document.querySelector('main').innerText`), /Status unavailable|Request status unavailable|Request tasks unavailable/); }); test('organization role instances reflow inside the viewport at 390 pixels', { timeout: 60000 }, async t => { const { base, diagnostics } = await consoleFor(t); const { b, wait } = await page(t, base, diagnostics); await b.viewport(390, 844); await b.navigate(`${base}#/orgs/alpha/settings/business`); await wait(`document.querySelector('.s1-roles tbody tr')`, 'role instances'); const box = await b.evaluate(`(()=>{const table=document.querySelector('.s1-roles'),wrap=table.closest('.table-wrap'),r=wrap.getBoundingClientRect();return {right:r.right,tableRight:table.getBoundingClientRect().right,document:document.documentElement.scrollWidth,viewport:innerWidth,overflow:getComputedStyle(wrap).overflowX,client:wrap.clientWidth,scroll:wrap.scrollWidth}})()`); assert.ok(box.right <= box.viewport && box.document <= box.viewport, JSON.stringify(box)); assert.ok(box.tableRight <= box.viewport, JSON.stringify(box)); assert.ok(box.scroll <= box.client, JSON.stringify(box)); });