Jason's 2026-11-12 ruling: the only deployed system is the dev environment on dragon-lin, so the system-type plug-in kind goes with no retired kind, fence, conversion or new refusal code. - plugins: system-type leaves KINDS; system-types.mjs and the five modules are deleted. An org file naming the kind refuses unknown-kind. - organization: the plugins.system-type check, systemTypesFence and the system-types.console right are removed. - bus: system-types.console, project.system-types[.set], the system-type half of the tester check, the runtime reads and system-type-not-enabled are removed. Step 3v-3w drops project_system_types, its index, its four guards and its rows; SCHEMA_VERSION is 3w (32 tables, 126 triggers, 5 views). - cli: mosaic org system-types and mosaic project system-types[-set] are removed; stack-release COUNTED and the budget supervisor's closed list move to 3w. - channel: the project.system-types verbs are removed. - webui: the System types page, route, dashboard row, chips and project selection are removed. - docs: REQ-SYS-1/2 now describe starter packs; README system-type text and the requirements titles follow.
93 lines
7.9 KiB
JavaScript
93 lines
7.9 KiB
JavaScript
// 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);
|
|
});
|