// E1 (P1-5): pages follow a state change without a reload. Restart bus redraws the organization // dashboard's bus line and its settings at once, and Quick Setup puts the new organization in the // sidebar. Neither waits for the 10 s repaint: each check runs well inside that window. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { startServer } from '../src/serve.mjs'; import { createDevOwner } from '../src/dev-owner.mjs'; import { createOrgAdmin } from '../src/org-admin.mjs'; import { actionTrail } from '../src/action-trail.mjs'; import * as service from '../../organization/src/org-service.mjs'; import { browser } from './browser.mjs'; import { close } from './fixture.mjs'; const waitFor = (b, tries = 150) => expression => b.evaluate(`(async()=>{for(let i=0;i<${tries};i++){if(${expression})return;await new Promise(r=>setTimeout(r,30))}throw Error(${JSON.stringify(expression)})})()`); // One organization whose bus state the fake bus below owns. A stopped bus has no reads, as // organizations.mjs resolves it; a running one answers the dashboard's reads. async function westOrgs(state) { const running = () => ({ busHealthy: true, reads: { business: async () => ({ business: { id: 'west', human: 'jason', arbiters: [], projects: [], vars: {}, instances: [] } }), settings: async () => ({}), queue: async () => ({ rows: [] }), busStatus: async () => ({}) }, bus: { instances: async () => ({ business: 'west', revision: 'a'.repeat(64), launch: true, noHire: [], retired: [], definitions: {}, instances: {} }), messages: async () => ({ messages: [], unread: 0 }), agents: async () => [], 'thread.claims': async () => [], inbox: async () => [], tasks: async () => [] }, chat: { projects: async () => [], threads: async () => [] }, }); return { list: async () => [{ id: 'west', label: 'West', busHealthy: state.running }], resolve: async id => id !== 'west' ? null : state.running ? running() : { busHealthy: false, reads: null, bus: {}, chat: { projects: async () => [], threads: async () => [] } }, }; } test('E1 (P1-5b): Restart bus redraws the dashboard bus line and settings without a reload or the 10 s repaint', { timeout: 45000 }, async t => { const state = { running: false }; const starts = []; const bus = { busStatus: async (ctx, id) => ({ org: id, healthy: state.running, pid: state.running ? 4242 : null }), busStart: async (ctx, id, opts) => { starts.push([id, opts]); state.running = true; return { org: id, pid: 4242, socket: '/data/s', root: '/data/org-runtime/west' }; }, busRestart: async () => { throw Error('not reached: the bus is stopped'); }, }; const orgAdmin = createOrgAdmin({ service: { showOrg: async (ctx, id) => ({ id, revision: 1, doc: {} }) }, registry: { listOrgs: async () => [] }, bus, context: async () => ({ configDir: '/cfg', dataRoot: '/data', instance: 'test' }) }); const root = mkdtempSync(join(tmpdir(), 'e1-refresh-')); t.after(() => rmSync(root, { recursive: true, force: true })); const server = await startServer({ port: 0, sso: createDevOwner(), send: async () => ({}), organizations: await westOrgs(state), orgAdmin, trail: actionTrail(join(root, 'console')), log: () => {} }); t.after(() => close(server)); const b = await browser({ machine: false }); t.after(() => b.close()); const wait = waitFor(b); await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/west/organization`); await wait('document.querySelector("[data-org-dashboard] [data-bus-restart]") && /bus unavailable, read/.test(document.querySelector("[data-org-dashboard]").innerText)'); assert.equal(await b.evaluate('document.querySelector("[data-org-dashboard] [data-org-setting=\\"Identity\\"] .v").textContent'), 'Could not be read'); // The page's own repaint is 10 s after its last draw; mark the draw this test starts from. await b.evaluate('window.__drawn = Date.now(); window.__first = document.querySelector("[data-org-dashboard]")'); await b.evaluate('document.querySelector("[data-org-dashboard] [data-bus-restart]").click()'); await wait('/It answers/.test(document.querySelector("[data-org-dashboard] [data-bus-restart-receipt]")?.textContent || "")'); // No reload: the page keeps its globals, and the dashboard shows the running bus and its organization file. await wait('/bus running, read/.test(document.querySelector("[data-org-dashboard]")?.innerText || "")'); assert.ok(await b.evaluate('Date.now() - window.__drawn') < 8000, 'the dashboard redrew before the 10 s repaint'); assert.equal(await b.evaluate('typeof window.__first'), 'object', 'the page was not reloaded'); assert.match(await b.evaluate('document.querySelector("[data-org-dashboard] [data-org-setting=\\"Identity\\"] .v").textContent'), /^West · west · /); assert.match(await b.evaluate('document.querySelector("[data-org-dashboard] [data-bus-restart-receipt]").textContent'), /^Bus started at .*It answers\./); assert.deepEqual(starts.map(([id]) => id), ['west']); }); test('E1 (P1-5a): Quick Setup from inside another organization lists the new one in the sidebar without a reload', { timeout: 60000 }, async t => { const registry = await import('../../organization/src/org-registry.mjs'); const dir = mkdtempSync(join(tmpdir(), 'e1-quick-')); const configDir = join(dir, 'config'), dataRoot = join(dir, 'data'); mkdirSync(configDir, { mode: 0o700 }); mkdirSync(dataRoot, { mode: 0o700 }); let started = false; const bus = { busStatus: async () => ({ healthy: false }), busStart: async () => { started = true; return { healthy: true }; } }; const admin = createOrgAdmin({ service, registry, bus, context: async () => ({ configDir, dataRoot, instance: 'test' }) }); const projects = []; const shell = { send: async () => ({}), busHealthy: true, chat: { projects: async () => projects, threads: async () => [], createProject: async args => { const row = { id: '20000000-0000-4000-8000-000000000002', ...args }; projects.push(row); return row; } } }; const organizations = { list: async () => [{ id: 'east', label: 'East', busHealthy: true }, ...(started ? [{ id: 'alpha', label: 'alpha', busHealthy: true }] : [])], resolve: async id => (id === 'east' || (id === 'alpha' && started)) ? shell : null, }; const server = await startServer({ port: 0, sso: createDevOwner(), organizations, orgAdmin: admin, send: async () => ({}), board: 'http://127.0.0.1:9', log: () => {} }); t.after(() => close(server)); const b = await browser({ machine: false }); t.after(() => b.close()); t.after(() => rmSync(dir, { recursive: true, force: true })); const wait = waitFor(b, 300); await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/east/settings/organizations`); await wait('document.querySelector("[data-workspace-org=east]") && document.querySelector("#org-new-form [data-write]") && !document.querySelector("#org-new-form [data-write]").disabled'); assert.equal(await b.evaluate('document.querySelector("[data-workspace-org=alpha]")'), null); await b.evaluate('window.__first = true'); await b.evaluate(`document.querySelector('#org-new-id').value='alpha';document.querySelector('#org-new-form [data-write]').click()`); await wait('document.querySelector("#org-quick-open") && !document.querySelector("#org-paths").hidden'); await b.evaluate(`document.querySelector('#org-quick-open').click();document.querySelector('#org-risk-accept').click();document.querySelector('#org-quick-run').click()`); await wait(`document.querySelector('[data-quick-step="5"] .org-pill')?.textContent === 'Done'`); await b.evaluate('window.__done = Date.now()'); await wait('!!document.querySelector("[data-workspace-org=alpha]")'); assert.ok(await b.evaluate('Date.now() - window.__done') < 8000, 'the sidebar followed the setup, not a later poll'); assert.equal(await b.evaluate('window.__first'), true, 'the page was not reloaded'); assert.equal(started, true); });