// SP1 (#1873, single pane slice 1): #/dashboard is the system landing route, an organization page is a // drill-in route inside the same shell (no reload, the sidebar and top bar stay), and an organization // whose bus is down says so with its reason. Every bus is a fake: acme and beta answer, gamma's bus is // stopped. The real GET /api/orgs, /api/workspace and /api/agents/levels serve the page. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { fileURLToPath } from 'node:url'; import { startServer } from '../src/serve.mjs'; import { createDevOwner } from '../src/dev-owner.mjs'; import { browser } from './browser.mjs'; import { close } from './fixture.mjs'; const waitFor = b => expression => b.evaluate(`(async()=>{for(let i=0;i<200;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30))}throw Error(${JSON.stringify(expression)})})()`); const at = minutesAgo => new Date(Date.now() - minutesAgo * 60000).toISOString(); async function fixture({ empty = false, bootless = false } = {}) { const { checkRootAgents } = await import('../../organization/src/index.mjs'); const rolesDir = fileURLToPath(new URL('../../../roles', import.meta.url)); const root = { state: 'ok', ...checkRootAgents({ agentsVersion: 1, agents: {} }, { rolesDir }) }; const LIST = [ { id: 'acme', label: 'Acme Studio', theme: 'cobalt', busHealthy: true }, { id: 'beta', label: 'Beta Works', theme: 'grove', busHealthy: true }, { id: 'gamma', label: 'Gamma Home', theme: 'rosewood', busHealthy: false, busStopped: true }, ]; const ROLES = { acme: { chief: { definition: 'chief-of-staff' }, coder: { definition: 'coder' }, reviewer: { definition: 'reviewer' } }, beta: { chief: { definition: 'chief-of-staff' } }, gamma: { chief: { definition: 'chief-of-staff' }, researcher: { definition: 'research' } }, }; const DATA = { acme: { projects: [{ id: 'p-site', repo: 'site', name: 'Website', archived: false, at: at(30) }, { id: 'p-zeta', repo: 'zeta', name: 'Zeta API', archived: false, at: at(300) }], agents: [{ role: 'coder', holder_run: 'run-1', harness: 'claude', state: 'working', at: at(40), working: { since: at(40) } }], inbox: [{ id: 'dec-acme-1', question: 'Ship the hero?', class: 'gated', blocking: true, raised_by_role: 'coder', at: at(10), options: [{ key: 'yes', text: 'Ship it' }] }], }, beta: { projects: [{ id: 'p-shop', repo: 'shop', name: 'Shop', archived: false, at: at(120) }], agents: [], inbox: [] }, }; // Launch settings differ per organization so a value carried from one to the other shows. const LAUNCH = id => ({ business: id, revision: (id === 'acme' ? 'a' : 'b').repeat(64), models: { opus: { provider: 'anthropic', model: 'claude-opus-5-5' } }, limits: { total: id === 'acme' ? 4 : 7 }, instances: {}, usage: { total: 0, providers: {}, models: {} } }); const value = (id, name) => Promise.resolve(DATA[id]?.[name] ?? (['usage', 'limits'].includes(name) ? {} : [])); const opts = id => ({ bus: { inbox: () => value(id, 'inbox'), agents: () => value(id, 'agents'), 'thread.claims': () => value(id, 'claims'), tasks: () => value(id, 'tasks'), usage: () => value(id, 'usage'), limits: () => value(id, 'limits'), decisions: () => value(id, 'decisions') }, chat: { threads: () => value(id, 'threads'), projects: () => value(id, 'projects') }, }); const member = org => ({ id: org.id, doc: { appearance: { theme: org.theme }, roles: ROLES[org.id], projects: {} }, status: async () => (org.busHealthy ? { healthy: true, pid: 4242, boot: null } : bootless ? { healthy: false, boot: null } : { healthy: false, boot: { ok: false, at: at(90), lastGood: null, reason: 'stopped by the owner' } }), options: () => opts(org.id), }); const instances = id => Object.fromEntries(Object.entries(ROLES[id]).map(([n, r]) => [n, { definition: r.definition, vars: {}, launched: true, launcher: false, protected: false, kept: false, running: n === 'coder' ? ['run-1'] : [], alive: [] }])); const organizations = { list: async () => (empty ? [] : LIST), members: async () => (empty ? [] : LIST.map(member)), resolve: async id => { const org = LIST.find(o => o.id === id); if (!org) return null; if (!org.busHealthy) return { busHealthy: false, reads: null, bus: {}, chat: { projects: async () => [], threads: async () => [] } }; return { busHealthy: true, reads: { business: async () => ({ business: { id, human: 'jason', projects: DATA[id].projects.map(p => p.repo), appearance: { theme: org.theme } } }), settings: async () => ({ business: id, release: 'fixture', notifier: {}, launch: LAUNCH(id) }), queue: async () => ({ rows: [] }), busStatus: async () => ({}) }, bus: { messages: async () => ({ messages: [], unread: 0 }), agents: async () => DATA[id].agents, 'thread.claims': async () => [], inbox: async () => DATA[id].inbox.map(d => ({ ...d, business: id, raised_by_run: 'run', routed_to: 'human' })), tasks: async () => [], instances: async () => ({ business: id, revision: 'a'.repeat(64), launch: true, noHire: [], retired: [], definitions: {}, instances: instances(id) }) }, chat: { projects: async () => DATA[id].projects, threads: async () => [] }, }; }, }; return { organizations, agentLevels: { loadRoot: () => root, rolesDir, ctx: { configDir: '/cfg', dataRoot: '/data', instance: 'test' } } }; } async function setup(t, { viewport = [1440, 900], path = '/', empty = false, init = null, bootless = false } = {}) { const { organizations, agentLevels } = await fixture({ empty, bootless }); const server = await startServer({ port: 0, sso: createDevOwner(), organizations, agentLevels, log: () => {} }); t.after(() => close(server)); const b = await browser({ machine: false }); t.after(() => b.close()); await b.viewport(...viewport); // `init` runs before the page's own scripts, under the display.js fetch wrapper, so it sees the scoped URL. if (init) await b.call('Page.addScriptToEvaluateOnNewDocument', { source: init }); const base = `http://127.0.0.1:${server.address().port}`; await b.navigate(`${base}${path}`); return { b, base, wait: waitFor(b), text: sel => b.evaluate(`document.querySelector(${JSON.stringify(sel)})?.innerText ?? null`) }; } const rowText = org => `document.querySelector("[data-ws-orgrow=${org}]")?.innerText.replace(/\\s+/g, " ").trim() ?? ""`; test('the bare address lands on #/dashboard without a reload and lists every organization with only the figures read', async t => { const { b, wait, text } = await setup(t); await wait('location.hash === "#/dashboard" && document.querySelectorAll("[data-ws-orgrow]").length === 3'); // One page load: the landing route is set in place, never by location.reload(). assert.equal(await b.evaluate('performance.getEntriesByType("navigation").length'), 1); assert.equal(await b.evaluate('performance.getEntriesByType("navigation")[0].type'), 'navigate'); assert.equal(await text('#bus-view h1'), 'Dashboard'); assert.equal(await b.evaluate('document.querySelector("#sections a[aria-current=page]")?.getAttribute("href")'), '#/dashboard'); assert.deepEqual(await b.evaluate('[...document.querySelectorAll("[data-ws-orgrow]")].map(r => r.dataset.wsOrgrow)'), ['acme', 'beta', 'gamma']); // Agents from the levels read, projects and needs-you from the workspace read. await wait(`${rowText('acme')}.includes("3 agents")`); const acme = await b.evaluate(rowText('acme')); assert.match(acme, /Acme Studio/); assert.match(acme, /bus running/); assert.match(acme, /2 projects/); assert.match(acme, /1 needs you/); assert.match(await b.evaluate(rowText('beta')), /1 agent\b/); // gamma: down with its reason; its agent count comes from its file, the bus figures are not read. const gamma = await b.evaluate(rowText('gamma')); assert.match(gamma, /bus stopped/); assert.match(gamma, /stopped by the owner/); assert.match(gamma, /2 agents/); assert.doesNotMatch(gamma, /\d+ projects?|\d+ needs you/); assert.equal(await b.evaluate('document.querySelector("[data-ws-orgrow=beta] a")?.getAttribute("href")'), '#/orgs/beta/organization'); // A system route marks no organization in the sidebar and shows no breadcrumb. assert.equal(await b.evaluate('document.querySelectorAll("#workspace-nav .sel, #workspace-nav [aria-current]").length'), 0); assert.equal(await b.evaluate('document.querySelector(".ws-crumb")'), null); // The top bar names no organization. assert.equal(await b.evaluate('document.getElementById("current-organization")'), null); }); test('an organization opens inside the same shell with a breadcrumb, and its pages keep reading that organization', async t => { const { b, wait, text } = await setup(t); await wait('document.querySelectorAll("[data-ws-orgrow]").length === 3'); await b.evaluate('window.__marker = 1; window.__bar = document.querySelector(".cmdbar"); window.__side = document.getElementById("workspace-nav")'); await b.evaluate('document.querySelector("[data-ws-orgrow=acme] a").click()'); await wait('location.hash === "#/orgs/acme/organization" && document.querySelector("[data-org-dashboard] h1")?.textContent === "Acme Studio"'); assert.equal(await b.evaluate('window.__marker'), 1); assert.equal(await b.evaluate('window.__bar === document.querySelector(".cmdbar") && window.__side === document.getElementById("workspace-nav")'), true); assert.equal(await b.evaluate('performance.getEntriesByType("navigation").length'), 1); assert.equal(await b.evaluate('document.querySelector(".ws-crumb")?.innerText.replace(/\\s+/g, " ").trim()'), 'Dashboard › Acme Studio'); assert.equal(await b.evaluate('document.querySelector(".ws-crumb a")?.getAttribute("href")'), '#/dashboard'); assert.equal(await b.evaluate('document.getElementById("current-organization")'), null); // The sidebar marks the open organization. await wait('document.querySelector("#workspace-nav a[aria-current=page]")?.getAttribute("href") === "#/orgs/acme/organization"'); // The sidebar opens another organization in place; its page reads that organization's routes. await b.evaluate('performance.clearResourceTimings(); document.querySelector("#workspace-nav a[href=\'#/orgs/beta/organization\']").click()'); await wait('location.hash === "#/orgs/beta/organization" && document.querySelector("[data-org-dashboard] h1")?.textContent === "Beta Works"'); assert.equal(await b.evaluate('window.__marker'), 1); // #1873 SP5: #/agents is the system Agents page, not beta's; it keeps the address and opens no organization. await b.evaluate('location.hash = "#/agents"'); await wait('location.hash === "#/agents" && document.querySelector("[data-ws-agents]:not([data-ws-scope])")'); assert.equal(await b.evaluate('window.MosaicOrg.id'), 'beta'); // A page inside beta scopes its reads to beta. await b.evaluate('location.hash = "#/orgs/beta/agents"'); await wait('location.hash === "#/orgs/beta/agents" && document.querySelector("[data-org-agents]")'); assert.equal(await b.evaluate('window.MosaicOrg.id'), 'beta'); assert.equal(await b.evaluate('window.__marker'), 1); // The organization's control board is named as such after the organization, not as a second Dashboard. await b.evaluate('location.hash = "#/orgs/beta/board"'); await wait('/^Dashboard\\/.+\\/Control board$/.test(document.getElementById("breadcrumbs")?.textContent ?? "")'); assert.equal(await b.evaluate('document.querySelector("#breadcrumbs [data-crumb-org]")?.getAttribute("href")'), '#/orgs/beta/organization'); // The breadcrumb goes back to the system dashboard, still without a reload. await b.evaluate('location.hash = "#/orgs/beta/organization"'); await wait('document.querySelector(".ws-crumb a")'); await b.evaluate('document.querySelector(".ws-crumb a").click()'); await wait('location.hash === "#/dashboard" && document.querySelectorAll("[data-ws-orgrow]").length === 3'); assert.equal(await b.evaluate('window.__marker'), 1); assert.equal(await b.evaluate('performance.getEntriesByType("navigation").length'), 1); assert.equal(await text('#bus-view h1'), 'Dashboard'); }); test('an old #/orgs//... link still opens that page', async t => { const { b, wait } = await setup(t, { path: '/#/orgs/beta/agents' }); await wait('document.querySelector("[data-org-agents]")'); assert.equal(await b.evaluate('location.hash'), '#/orgs/beta/agents'); assert.equal(await b.evaluate('window.MosaicOrg.id'), 'beta'); assert.equal(await b.evaluate('performance.getEntriesByType("navigation").length'), 1); }); test('a stopped organization says so on its page and on its agents, with the reason', async t => { const { b, wait } = await setup(t, { path: '/#/orgs/gamma/organization' }); await wait('document.querySelector("[data-org-dashboard] [data-org-down]")'); const down = await b.evaluate('document.querySelector("[data-org-down]").innerText.replace(/\\s+/g, " ")'); assert.match(down, /The bus is not running/); assert.match(down, /stopped by the owner/); await b.evaluate('location.hash = "#/orgs/gamma/agents"'); await wait('document.querySelector("[data-org-agents] [data-org=gamma] [data-agents-why]")'); assert.equal(await b.evaluate('document.querySelector("[data-org=gamma] [data-agents-why]").innerText.trim()'), 'Agents not read: the bus is stopped'); const agents = await b.evaluate('document.querySelector("[data-org-agents]").innerText.replace(/\\s+/g, " ")'); assert.doesNotMatch(agents, /Agents unavailable/); }); test('at 390 px the dashboard lists the organizations without a horizontal scroll', async t => { const { b, wait } = await setup(t, { viewport: [390, 844] }); await wait('document.querySelectorAll("[data-ws-orgrow]").length === 3'); assert.equal(await b.evaluate('document.documentElement.scrollWidth <= window.innerWidth'), true); }); // Review round finding 1: with no organization yet the page keeps its first-run addresses, so the // wordmark (href "#/") opens the first-run board, not a #/dashboard the first-run page has no view for. test('first run: the wordmark opens the first-run board, not a Not found page', async t => { const { b, wait } = await setup(t, { path: '/#/settings/appearance', empty: true }); await wait('document.querySelector("#bus-view h1")'); await b.evaluate('document.querySelector(".wordmark").click()'); // The board route empties the page view; give the router and render time to settle before checking. await wait('location.hash !== "#/settings/appearance"'); await new Promise(r => setTimeout(r, 800)); assert.equal(await b.evaluate('location.hash'), '#/'); assert.equal(await b.evaluate('!!document.querySelector("#bus-view .nf")'), false); assert.equal(await b.evaluate('window.MosaicOrg'), undefined); }); test('a switch stops following an agent request: no poll reaches the next organization and its page shows no receipt', async t => { const ID = '11111111-2222-4333-8444-555555555555'; const json = 'new Response(JSON.stringify(x), { status: 200, headers: { "content-type": "application/json" } })'; const init = `window.__reads = []; window.__posts = []; const original = window.fetch; window.fetch = async (url, options = {}) => { const s = String(url); let x; if (/^\\/api\\/orgs\\/[^/]+\\/config$/.test(s)) { const r = await original(url, options), body = await r.json(); x = { ...body, send: true }; return ${json}; } if (options.method === 'POST' && /\\/agent\\/launch$/.test(s)) { __posts.push(s); x = { request: '${ID}', at: 'x' }; return ${json}; } if (/\\/agent\\/request\\?/.test(s)) { __reads.push(s); x = { request: '${ID}', action: 'launch', instance: 'reviewer', state: 'open', done: null, cancelled: null }; return ${json}; } return original(url, options); };`; const { b, wait } = await setup(t, { path: '/#/orgs/acme/settings/agents', init }); const launch = '#agents-section [data-agent-action=launch][data-agent-name=reviewer]'; await wait(`document.querySelector(${JSON.stringify(launch)}) && !document.querySelector(${JSON.stringify(launch)}).disabled`); await b.evaluate(`document.querySelector(${JSON.stringify(launch)}).click()`); await wait('window.__reads.length >= 1'); assert.deepEqual(await b.evaluate('window.__posts'), ['/api/orgs/acme/agent/launch']); assert.match(await b.evaluate('window.__reads[0]'), /^\/api\/orgs\/acme\/agent\/request\?id=/); await b.evaluate('location.hash = "#/orgs/beta/settings/agents"'); await wait('location.hash === "#/orgs/beta/settings/agents" && document.querySelector("#agents-section [data-agent-name=chief]") && !document.querySelector("#agents-section [data-agent-name=reviewer]")'); const seen = await b.evaluate('window.__reads.length'); // Two poll periods pass on beta's page. await new Promise(r => setTimeout(r, 4500)); const reads = await b.evaluate('window.__reads'); assert.equal(reads.filter(u => u.includes('/orgs/beta/')).length, 0, reads.join(' ')); assert.equal(reads.length, seen, 'the poll stopped at the switch'); assert.equal(await b.evaluate('document.querySelector("#agents-receipt")?.textContent.trim() ?? ""'), ''); assert.equal(await b.evaluate('document.querySelector("#agents-section [data-agent-action=launch][data-agent-name=chief]")?.disabled'), false); }); test('a save still in flight at a switch: its answer is not drawn on the next organization and its Save is not held', async t => { const json = 'new Response(JSON.stringify(x), { status: 200, headers: { "content-type": "application/json" } })'; // acme's settings POST is held until the test releases it, then answers with acme's saved values. const init = `window.__posts = []; const original = window.fetch; window.fetch = async (url, options = {}) => { const s = String(url); let x; if (/^\\/api\\/orgs\\/[^/]+\\/config$/.test(s)) { const r = await original(url, options), body = await r.json(); x = { ...body, send: true }; return ${json}; } if (options.method === 'POST' && s === '/api/orgs/acme/settings') { __posts.push(JSON.parse(options.body)); await new Promise(r => { window.__release = r; }); x = { business: 'acme', revision: 'c'.repeat(64), models: { opus: { provider: 'anthropic', model: 'claude-opus-5-5' } }, limits: { total: 5 }, instances: {}, usage: { total: 0, providers: {}, models: {} } }; return ${json}; } return original(url, options); };`; const { b, wait } = await setup(t, { path: '/#/orgs/acme/settings/limits', init }); const total = '#launch-settings-form input[data-setting=\'["total"]\']'; const submit = '#launch-settings-form button[type=submit]'; await wait(`document.querySelector(${JSON.stringify(total)})?.value === "4" && !document.querySelector(${JSON.stringify(submit)}).disabled`); await b.evaluate(`(() => { const i = document.querySelector(${JSON.stringify(total)}); i.value = "5"; i.dispatchEvent(new Event("input", { bubbles: true })); document.querySelector(${JSON.stringify(submit)}).click(); })()`); await wait('typeof window.__release === "function"'); assert.equal(await b.evaluate('window.__posts[0]?.revision'), 'a'.repeat(64)); await b.evaluate('location.hash = "#/orgs/beta/settings/limits"'); await wait(`location.hash === "#/orgs/beta/settings/limits" && document.querySelector(${JSON.stringify(total)})?.value === "7"`); // beta's Save is its own: not held by acme's write. assert.equal(await b.evaluate(`document.querySelector(${JSON.stringify(submit)}).disabled`), false); await b.evaluate('window.__release()'); await new Promise(r => setTimeout(r, 700)); assert.equal(await b.evaluate('location.hash'), '#/orgs/beta/settings/limits'); assert.equal(await b.evaluate(`document.querySelector(${JSON.stringify(total)})?.value`), '7'); assert.equal(await b.evaluate('document.getElementById("launch-settings-receipt")?.textContent.trim() ?? ""'), ''); assert.equal(await b.evaluate(`document.querySelector(${JSON.stringify(submit)}).disabled`), false); }); test('a down banner does not repeat its heading as the reason', async t => { // No boot record: the server's reason is its fallback, "the bus is not running", the heading's own words. const { b, wait } = await setup(t, { path: '/#/orgs/gamma/organization', bootless: true }); await wait('document.querySelector("[data-org-dashboard] [data-org-down]")'); const down = await b.evaluate('document.querySelector("[data-org-down]").innerText.replace(/\\s+/g, " ")'); assert.match(down, /The bus is not running\./); assert.equal((down.match(/the bus is not running/gi) || []).length, 1, down); assert.doesNotMatch(down, /Reason:|No reason was read/); }); test('a restart started on one organization finishes on that organization after a switch', { timeout: 60000 }, async t => { const END = '11111111-2222-4333-8444-5555555555e1', LAUNCHED = '11111111-2222-4333-8444-5555555555a1'; const json = 'new Response(JSON.stringify(x), { status: 200, headers: { "content-type": "application/json" } })'; // Every agent request read and write is recorded, whichever organization it names; the bus answers // are stubs (terminate, closed requests, one launch). Only acme's instances read is stubbed and // recorded (the session gone): beta's own page reads beta's instances, which is not the chain. const init = `window.__chain = []; const original = window.fetch; window.fetch = async (url, options = {}) => { const s = String(url); let x; if (options.method === 'POST' && /\\/agent\\/terminate$/.test(s)) { __chain.push(s); x = { request: '${END}' }; return ${json}; } if (options.method === 'POST' && /\\/agent\\/launch$/.test(s)) { __chain.push(s); x = { request: '${LAUNCHED}' }; return ${json}; } if (/\\/agent\\/request\\?/.test(s)) { __chain.push(s.split('?')[0]); const id = new URL(s, location.href).searchParams.get('id'); x = { request: id, instance: 'coder', action: id === '${END}' ? 'end' : 'launch', state: 'closed', done: { run: id === '${END}' ? null : 'run-9' }, cancelled: null }; return ${json}; } if (s === '/api/orgs/acme/bus/instances' && window.__restarting) { __chain.push(s); x = { rows: [{ instances: { coder: { running: [] } } }] }; return ${json}; } return original(url, options); };`; const { b, wait } = await setup(t, { path: '/#/orgs/acme/organization', init }); await wait('window.MosaicAgents && document.querySelector("[data-org-dashboard] h1")'); // The button the Routing receipt offers (agents.js restartMarkup), placed on acme's page. await b.evaluate('window.__restarting = true; document.querySelector("[data-org-dashboard]").insertAdjacentHTML("beforeend", window.MosaicAgents.restartMarkup("coder")); document.querySelector("[data-agent-restart=coder]").click()'); await wait('window.__chain.length >= 1'); await b.evaluate('location.hash = "#/orgs/beta/organization"'); await wait('location.hash === "#/orgs/beta/organization"'); // Done when the chain has launched and read the launch request back, or as soon as any of it names beta. await wait('window.__chain.some(u => u.includes("/orgs/beta/")) || (window.__chain.some(u => /\\/agent\\/launch$/.test(u)) && window.__chain.filter(u => /\\/agent\\/request$/.test(u)).length >= 2)', 30000); const chain = await b.evaluate('window.__chain'); assert.deepEqual([...new Set(chain)], ['/api/orgs/acme/agent/terminate', '/api/orgs/acme/agent/request', '/api/orgs/acme/bus/instances', '/api/orgs/acme/agent/launch'], chain.join(' ')); // beta's page draws nothing of acme's chain. assert.equal(await b.evaluate('!!document.querySelector("[data-agent-restart-status=coder]")'), false); }); test('a file save still in flight at a switch: its outcome is not announced on the next organization', async t => { const json = 'new Response(JSON.stringify(x), { status: 200, headers: { "content-type": "application/json" } })'; // acme's file reads are stubbed; its save POST is held until the test releases it, then answers with a commit. const init = `window.__posts = []; const original = window.fetch; window.fetch = async (url, options = {}) => { const s = String(url); let x; if (/^\\/api\\/orgs\\/[^/]+\\/config$/.test(s)) { const r = await original(url, options), body = await r.json(); x = { ...body, send: true, edit: true }; return ${json}; } if (s.startsWith('/api/orgs/acme/project-file?')) { x = { path: 'docs/a.md', commit: 'a'.repeat(40), content: '# A\\n', at: new Date().toISOString() }; return ${json}; } if (options.method === 'POST' && s === '/api/orgs/acme/project-file') { __posts.push(JSON.parse(options.body)); await new Promise(r => { window.__release = r; }); x = { path: 'docs/a.md', commit: 'c'.repeat(40) }; return ${json}; } return original(url, options); };`; const { b, wait } = await setup(t, { path: `/#/orgs/acme/docs/site/${encodeURIComponent('docs/a.md')}`, init }); await wait('document.querySelector("#doc-form")'); if (!await b.evaluate('!!document.querySelector("#doc-text")')) await b.evaluate('document.querySelector("#doc-mode-source").click(), true'); await wait('document.querySelector("#doc-text")?.value === "# A\\n"'); await b.evaluate('(() => { const a = document.querySelector("#doc-text"); a.value = "# A\\n\\nmore\\n"; a.dispatchEvent(new Event("input", { bubbles: true })); document.querySelector("#doc-form").requestSubmit(); })()'); await wait('typeof window.__release === "function"'); assert.equal(await b.evaluate('window.__posts[0]?.base'), 'a'.repeat(40)); await b.evaluate('location.hash = "#/orgs/beta/organization"'); await wait('location.hash === "#/orgs/beta/organization" && document.querySelector("[data-org-dashboard] h1")?.textContent === "Beta Works"'); await b.evaluate('window.__release()'); await new Promise(r => setTimeout(r, 700)); assert.equal(await b.evaluate('location.hash'), '#/orgs/beta/organization'); assert.doesNotMatch(await b.evaluate('document.getElementById("announce")?.textContent ?? ""'), /Saved as commit|Draft kept/); }); // SP2 item 1: an organization's own bare address is its page, not the system dashboard. test('a bare #/orgs/ opens that organization\'s page', async t => { const { b, wait } = await setup(t, { path: '/#/orgs/beta' }); await wait('location.hash === "#/orgs/beta/organization" && document.querySelector("[data-org-dashboard] h1")?.textContent === "Beta Works"'); assert.equal(await b.evaluate('window.MosaicOrg.id'), 'beta'); // In place too, from another organization's page. await b.evaluate('location.hash = "#/orgs/acme"'); await wait('location.hash === "#/orgs/acme/organization" && document.querySelector("[data-org-dashboard] h1")?.textContent === "Acme Studio"'); assert.equal(await b.evaluate('performance.getEntriesByType("navigation").length'), 1); // The old per-organization dashboard address is still the system dashboard. await b.evaluate('location.hash = "#/orgs/acme/dashboard"'); await wait('location.hash === "#/dashboard" && document.querySelectorAll("[data-ws-orgrow]").length === 3'); }); // SP2 item 2: the system dashboard shows the system theme, never the organization opened last. test('the dashboard keeps the system theme after an organization with its own theme was opened', async t => { const { b, base, wait } = await setup(t, { path: '/#/orgs/acme/organization' }); await wait('document.querySelector("[data-org-dashboard] h1")?.textContent === "Acme Studio" && document.documentElement.dataset.palette === "cobalt"'); await b.evaluate('location.hash = "#/dashboard"'); await wait('document.querySelectorAll("[data-ws-orgrow]").length === 3'); await wait('document.documentElement.dataset.palette === "harbor"'); assert.equal(await b.evaluate('window.MosaicAppearance.state().organization'), null); assert.equal(await b.evaluate('document.getElementById("org-cue").hidden'), true); // A fresh load of the bare address, with acme stored as the last organization shown, starts on the system theme. await b.navigate(`${base}/`); await wait('location.hash === "#/dashboard" && document.querySelectorAll("[data-ws-orgrow]").length === 3'); await new Promise(r => setTimeout(r, 500)); assert.equal(await b.evaluate('document.documentElement.dataset.palette'), 'harbor'); // A choice made on the dashboard is the system's own: acme keeps its theme, and the dashboard keeps the choice. await b.evaluate('window.MosaicAppearance.set({ mode: "light" })'); assert.equal(await b.evaluate('document.documentElement.dataset.mode'), 'light'); await b.evaluate('location.hash = "#/orgs/acme/organization"'); await wait('document.documentElement.dataset.palette === "cobalt"'); assert.equal(await b.evaluate('document.documentElement.dataset.mode'), 'dark'); await b.evaluate('location.hash = "#/dashboard"'); await wait('document.documentElement.dataset.palette === "harbor" && document.documentElement.dataset.mode === "light"'); }); // SP2 item 3: a stopped organization's page shows one bus banner, with the restart action and what can't be read. test('a stopped organization\'s page shows one bus banner, with the restart action', async t => { const json = 'new Response(JSON.stringify(x), { status: 200, headers: { "content-type": "application/json" } })'; const init = `const original = window.fetch; window.fetch = async (url, options = {}) => { const s = String(url); let x; if (/^\\/api\\/(orgs\\/[^/]+\\/)?config$/.test(s)) { const r = await original(url, options), body = await r.json(); x = { ...body, orgSetup: true }; return ${json}; } return original(url, options); };`; const { b, wait } = await setup(t, { path: '/#/orgs/gamma/organization', init }); await wait('document.querySelector("[data-org-dashboard] [data-org-down] [data-bus-restart]")'); // Both shell painters (display.js on the list read, app.js on its own read) have had their turn. await b.evaluate('window.MosaicOrg.list'); await new Promise(r => setTimeout(r, 800)); const banners = 'Array.from(document.querySelectorAll(".banner")).filter(e => !e.hidden && e.getClientRects().length && /bus/i.test(e.textContent)).map(e => e.innerText.replace(/\\s+/g, " ").trim())'; const shown = await b.evaluate(banners); assert.equal(shown.length, 1, shown.join(' | ')); assert.match(shown[0], /The bus is not running\. Nothing about Gamma Home's agents or packs can be read until it starts\./); assert.match(shown[0], /Restart the bus/); assert.equal(await b.evaluate('document.getElementById("bus-down").hidden'), true); // The organization's other pages keep the shell's banner, with its restart. await b.evaluate('location.hash = "#/orgs/gamma/agents"'); await wait('!document.getElementById("bus-down").hidden && /Bus unavailable/.test(document.getElementById("bus-down").textContent)'); // And back on the organization page it gives way again. await b.evaluate('location.hash = "#/orgs/gamma/organization"'); await wait('document.querySelector("[data-org-dashboard] [data-org-down]") && document.getElementById("bus-down").hidden'); }); // SP2 item 4: the dashboard row for a stopped organization says it once. test('a stopped organization\'s dashboard row does not repeat its state as the reason', async t => { const { b, wait } = await setup(t, { bootless: true }); await wait('document.querySelectorAll("[data-ws-orgrow]").length === 3'); await wait(`${rowText('gamma')}.includes("bus stopped")`); const gamma = await b.evaluate(rowText('gamma')); assert.doesNotMatch(gamma, /the bus is not running/i, gamma); assert.equal(await b.evaluate('document.querySelector("[data-ws-orgrow=gamma] .why")'), null); }); // SP2 item 6: no status-badge glyph ("?") is drawn before the live dot in the dashboard's Active agents. test('the dashboard\'s active agents draw no stray glyph before the status dot', async t => { const { b, wait } = await setup(t); await wait('document.querySelector("[data-ws-sessions] tbody tr")'); const cells = await b.evaluate('Array.from(document.querySelectorAll("[data-ws-sessions] tbody td")).map(td => getComputedStyle(td, "::before").content)'); assert.ok(cells.length >= 2, String(cells)); for (const c of cells) assert.ok(c === 'none' || c === 'normal', `generated content ${c}`); }); // SP2 item 7 (the test SP1 lacks): a read still in flight when the organization switches keeps nothing. // acme's settings read is held across a switch to beta and then answers with a figure only it has (99); // beta's own read then fails, so beta's page draws from what was kept. Nothing of acme's may be there. test('a read still in flight at a switch: its answer is not kept for the next organization', async t => { const json = 'new Response(JSON.stringify(x), { status: 200, headers: { "content-type": "application/json" } })'; const init = `window.__acme = []; window.__beta = []; const original = window.fetch; window.fetch = async (url, options = {}) => { const s = String(url); let x; if (s === '/api/orgs/acme/settings' && !options.method && window.__holdAcme) { await new Promise(r => window.__acme.push(r)); const r = await original(url, options), body = await r.json(); x = { ...body, launch: { ...body.launch, limits: { total: 99 } } }; return ${json}; } if (s === '/api/orgs/beta/settings' && !options.method && window.__holdBeta) { await new Promise(r => window.__beta.push(r)); return new Response(JSON.stringify({ error: 'unavailable', message: 'beta settings read failed' }), { status: 500, headers: { 'content-type': 'application/json' } }); } return original(url, options); };`; const { b, wait } = await setup(t, { path: '/#/orgs/acme/settings/limits', init }); const total = '#launch-settings-form input[data-setting=\'["total"]\']'; await wait(`document.querySelector(${JSON.stringify(total)})?.value === "4"`); // A fresh read of acme's settings starts and is held. await b.evaluate('window.__holdAcme = true; location.hash = "#/orgs/acme/settings/tokens"'); await wait('window.__acme.length >= 1'); await b.evaluate('window.__holdBeta = true; location.hash = "#/orgs/beta/settings/limits"'); await wait('location.hash === "#/orgs/beta/settings/limits" && window.__beta.length >= 1'); // acme answers after the switch; then beta's read fails. await b.evaluate('window.__acme.forEach(r => r())'); await new Promise(r => setTimeout(r, 400)); await b.evaluate('window.__beta.forEach(r => r())'); await wait('document.querySelector("#bus-view .banner")'); await new Promise(r => setTimeout(r, 400)); assert.equal(await b.evaluate('location.hash'), '#/orgs/beta/settings/limits'); assert.equal(await b.evaluate(`document.querySelector(${JSON.stringify(total)})?.value ?? null`), null, 'acme\'s held answer was drawn on beta'); assert.equal(await b.evaluate('!!document.querySelector("#bus-view .s1-stale")'), false); assert.match(await b.evaluate('document.getElementById("bus-view").innerText'), /beta settings read failed/); });