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.
57 lines
4.0 KiB
JavaScript
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);
|
|
});
|