// 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/board`); 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/board`); 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); });