Files
stack/packages/webui/tests/org-refresh-browser.test.mjs
jason.woltje a559957085 feat(org): restore starts the bus; pages follow restart and quick setup (#1873)
P1-4: org restore starts the bus as mosaic bus start ID --dev-local-owner
--detach would, and its receipt names the bus. Where that start is refused
(deployed or stamped instance, agent run) restore refuses before changing
anything; --no-bus keeps the old restore. The Console Restore action
restores and starts the bus with one receipt, and offers Restart bus when
the start fails after a good restore.

P1-5: Quick Setup refreshes the sidebar organization list, and the
organization dashboard redraws on the config event Restart bus already
dispatches, so the bus line and System types follow without a reload.
2026-11-11 20:03:35 -06:00

95 lines
8.2 KiB
JavaScript

// E1 (P1-5): pages follow a state change without a reload. Restart bus redraws the organization
// dashboard's bus line and System types 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';
import { systemTypesFake } from './helpers/system-types-fake.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 and System types.
async function westOrgs(state) {
const st = await systemTypesFake({ enabled: ['coding'] });
const running = () => ({
busHealthy: true, requestRead: st.requestRead, systemTypesWrite: null,
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 System types 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.match(await b.evaluate('document.querySelector("[data-org-dashboard] [data-system-type-chips]").textContent'), /System types unavailable/);
// 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 the enabled type.
await wait('/bus running, read/.test(document.querySelector("[data-org-dashboard]")?.innerText || "") && !!document.querySelector("[data-org-dashboard] [data-system-type-chips] .ws-chip")');
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.doesNotMatch(await b.evaluate('document.querySelector("[data-org-dashboard] [data-system-type-chips]").textContent'), /unavailable/);
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);
});