Files
stack/packages/webui/tests/org-list-refresh-browser.test.mjs
jason.woltje a8d9eecf65 organization: move packages/business to packages/organization (rename r2 stage B1)
git mv packages/business -> packages/organization (@mosaic/organization). The
old package stays as one-line re-export shims of each moved module, with
src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs
forwarding to the moved executable. BusinessError is OrganizationConfigError
(one constructor, name kept BusinessError); OrganizationError stays distinct.
The bus adapter gains organization-context.mjs (busOrganization), with
business.mjs (busBusiness) as the same function.

Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh
COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure
walker (organization-imports.mjs, both roots required) move to the new path.
v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage
A contract tests stay where they were and pass as before.
2026-11-09 11:47:48 -06:00

57 lines
4.0 KiB
JavaScript

// On a selected org's page, the header shows why the org list failed or which org is broken,
// and Retry re-reads the list and clears the banner once it recovers (polish review F3, P19 #868).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as fs from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createOrg, showOrg, templateRender } from '../../organization/src/org-service.mjs';
import { listOrgs, resolveOrg } from '../../organization/src/org-registry.mjs';
import { organizationConsole } from '../src/organizations.mjs';
import { startServer } from '../src/serve.mjs';
import { createDevOwner } from '../src/dev-owner.mjs';
import { browser } from './browser.mjs';
const principal = { provider: 'local-dev', iss: 'local-dev', sub: 'local-dev-owner' };
async function setup(t) {
const root = await fs.mkdtemp(join(tmpdir(), 'orgref-'));
const ctx = { configDir: join(root, 'config'), dataRoot: join(root, 'data'), instance: 'test', principal };
await fs.mkdir(ctx.configDir, { mode: 0o700 }); await fs.mkdir(ctx.dataRoot, { mode: 0o700 });
for (const id of ['alpha', 'beta']) await createOrg(ctx, id, await templateRender(id));
let listRequests = 0;
const organizations = organizationConsole({ ctx, listOrgs: async (...args) => { listRequests++; return listOrgs(...args); }, showOrg, resolveOrg, orgPaths: () => ({}), busStatus: async () => ({ healthy: true }), options: ({ id }) => ({ reads: { business: async () => ({ business: { id, projects: [] } }) }, bus: {} }) });
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, log: () => {} });
const b = await browser({ machine: false });
t.after(async () => { await b.close(); await new Promise(r => { server.close(r); server.closeIdleConnections(); }); await fs.rm(root, { recursive: true, force: true }); });
return { ctx, b, base: `http://127.0.0.1:${server.address().port}`, requests: () => listRequests };
}
const wait = (b, expr) => b.evaluate(`(async()=>{for(let i=0;i<200;i++){if(${expr})return;await new Promise(r=>setTimeout(r,25))}throw Error(${JSON.stringify(expr)})})()`);
const bannerText = "document.getElementById('org-list-banner')?.textContent || ''";
test('a registry error shows on a selected org page and Retry recovers', async t => {
const f = await setup(t), file = join(f.ctx.configDir, 'org-runtime.json');
const original = await fs.readFile(file, 'utf8'); await fs.writeFile(file, '{');
await f.b.navigate(`${f.base}/#/orgs/alpha/`);
await wait(f.b, `(${bannerText}).includes('Organization registry could not be read') && document.querySelector('#held [data-retry]')`);
const before = f.requests(); await fs.writeFile(file, original);
await f.b.evaluate("document.querySelector('#held [data-retry]').click()");
await wait(f.b, "!!document.querySelector('#current-organization') && !document.getElementById('org-list-banner')");
assert.ok(f.requests() > before, 'Retry re-read the organization list');
assert.equal(await f.b.evaluate("document.querySelectorAll('#current-organization').length"), 1);
});
test('a broken org banner is replaced, not stacked, and clears after repair', async t => {
const f = await setup(t), file = join(f.ctx.configDir, 'org-runtime.json');
const original = await fs.readFile(file, 'utf8'), reg = JSON.parse(original);
reg.orgs.beta.digest = '0'.repeat(64); await fs.writeFile(file, JSON.stringify(reg));
await f.b.navigate(`${f.base}/#/orgs/alpha/`);
await wait(f.b, `(${bannerText}).includes('beta: invalid') && document.querySelector('#held [data-retry]')`);
await f.b.evaluate('window.MosaicRefreshOrganizations()');
await wait(f.b, `(${bannerText}).includes('beta: invalid')`);
assert.equal(await f.b.evaluate("document.querySelectorAll('#org-list-banner').length"), 1);
await fs.writeFile(file, original);
await f.b.evaluate("document.querySelector('#held [data-retry]').click()");
await wait(f.b, "!document.getElementById('org-list-banner')");
assert.equal(await f.b.evaluate("document.querySelectorAll('#current-organization').length"), 1);
});