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.
118 lines
11 KiB
JavaScript
118 lines
11 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { startServer } from '../src/serve.mjs';
|
|
import { createDevOwner } from '../src/dev-owner.mjs';
|
|
import { createOrgAdmin } from '../src/org-admin.mjs';
|
|
import * as service from '../../organization/src/org-service.mjs';
|
|
import { browser } from './browser.mjs';
|
|
import { close } from './fixture.mjs';
|
|
|
|
test('setup paths and preset controls are visible', { timeout: 45000 }, async t => {
|
|
const admin = createOrgAdmin({ service, registry: { listOrgs: async () => [] }, context: async () => ({ configDir: '/cfg', dataRoot: '/data', instance: 'test' }) });
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), 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());
|
|
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30))}throw Error(${JSON.stringify(expression)})})()`);
|
|
const shown = selector => b.evaluate(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});return !!e && getComputedStyle(e).display !== 'none' && e.getClientRects().length > 0})()`);
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/settings/organizations`);
|
|
await wait('document.querySelector("#org-new-form [data-write]") && !document.querySelector("#org-new-form [data-write]").disabled');
|
|
await b.evaluate(`document.querySelector('#org-new-id').value='alpha';document.querySelector('#org-new-form [data-write]').click()`);
|
|
await wait('document.querySelector("#org-paths") && !document.querySelector("#org-paths").hidden');
|
|
for (const selector of ['#org-quick-open', '#org-advanced-open', '#org-preset', '#org-thinking']) assert.equal(await shown(selector), true, selector);
|
|
await b.evaluate(`document.querySelector('#org-quick-open').click()`);
|
|
await wait('!document.querySelector("#org-quick").hidden');
|
|
for (const selector of ['#org-project', '#org-project-remote', '#org-risk-accept', '#org-quick-run']) assert.equal(await shown(selector), true, selector);
|
|
});
|
|
|
|
for (const scenario of ['bus-refused', 'project-refused', 'project-unknown', 'success']) test(`quick setup creates real repositories: ${scenario}`, { timeout: 45000 }, async t => {
|
|
const { mkdtempSync, mkdirSync, rmSync, existsSync } = await import('node:fs');
|
|
const { tmpdir } = await import('node:os'); const { join } = await import('node:path');
|
|
const registry = await import('../../organization/src/org-registry.mjs');
|
|
const dir = mkdtempSync(join(tmpdir(), 'quick-'));
|
|
const configDir = join(dir, 'config'), dataRoot = join(dir, 'data'); mkdirSync(configDir, { mode: 0o700 }); mkdirSync(dataRoot, { mode: 0o700 });
|
|
let starts = 0;
|
|
const bus = { busStatus: async () => ({ healthy: false }), busStart: async () => { starts++; if (scenario === 'bus-refused') throw Object.assign(Error('bus refused'), { code: 'org-invalid' }); return { healthy: true }; } };
|
|
const admin = createOrgAdmin({ service, registry, bus, context: async () => ({ configDir, dataRoot, instance: 'test' }) });
|
|
const projects = [];
|
|
const organizations = {
|
|
list: async () => starts ? [{ id: 'alpha', label: 'alpha', busHealthy: true }] : [],
|
|
resolve: async id => id === 'alpha' ? { send: async () => ({}), busHealthy: true, chat: {
|
|
projects: async () => projects, threads: async () => [],
|
|
createProject: async args => { assert.deepEqual(args, { name: 'work', repo: 'work' }); const row = { id: '20000000-0000-4000-8000-000000000002', ...args }; projects.push(row); return row; },
|
|
} } : 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());
|
|
// Remove the directory last: hooks run in the order they are registered, and a removal that
|
|
// fails while the server still writes there would skip the server and browser closes (P19 #865).
|
|
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<300;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30))}throw Error(${JSON.stringify(expression)})})()`);
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/settings/organizations`);
|
|
await wait('document.querySelector("#org-new-form [data-write]") && !document.querySelector("#org-new-form [data-write]").disabled');
|
|
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');
|
|
if (scenario === 'project-refused' || scenario === 'project-unknown') await b.evaluate(`window.__projectPosts=0;const real=window.fetch;window.fetch=async(url,init={})=>{if(String(url)==='/api/orgs/alpha/chat/projects' && init.method==='POST'){window.__projectPosts++;${scenario === 'project-refused' ? "return new Response(JSON.stringify({error:'not-configured',message:'Project refused',sent:false}),{status:503,headers:{'content-type':'application/json'}});" : "await real(url,init);return new Response(JSON.stringify({error:'outcome-unknown',message:'Outcome unknown',sent:'unknown'}),{status:503,headers:{'content-type':'application/json'}});"}}return real(url,init)}`);
|
|
await b.evaluate(`document.querySelector('#org-quick-open').click();document.querySelector('#org-risk-accept').click();document.querySelector('#org-quick-run').click()`);
|
|
const finalStep = scenario === 'bus-refused' ? 3 : scenario === 'success' ? 5 : 4, finalState = scenario === 'success' ? 'Done' : 'Stopped';
|
|
await wait(`document.querySelector('[data-quick-step="${finalStep}"] .org-pill')?.textContent === '${finalState}'`);
|
|
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("[data-quick-step] .org-pill")].map(e=>e.textContent)'), ['Done', 'Done', 'Done', scenario === 'bus-refused' ? 'Stopped' : 'Done', scenario === 'bus-refused' ? 'Not started' : finalState, scenario === 'success' ? 'Done' : 'Not started']);
|
|
if (scenario === 'bus-refused') {
|
|
assert.match(await b.evaluate(`document.querySelector('[data-quick-step="3"]').textContent`), /bus refused/);
|
|
assert.equal(await b.evaluate('document.activeElement.dataset.quickStep'), '3');
|
|
}
|
|
assert.equal(await b.evaluate(`document.querySelector('[data-quick-step="3"] code').textContent`), 'mosaic bus start alpha --dev-local-owner --detach');
|
|
assert.equal(starts, 1);
|
|
assert.ok(existsSync(join(dataRoot, 'org-runtime/alpha/checkout/.git')));
|
|
assert.ok(existsSync(join(dataRoot, 'org-runtime/alpha/projects/work/.git')));
|
|
if (scenario === 'success') {
|
|
assert.equal(await b.evaluate(`document.querySelector('[data-quick-step="5"] code').textContent`), 'mosaic project init alpha work');
|
|
assert.ok(existsSync(join(dataRoot, 'org-runtime/alpha/projects/alpha/work/spec/north-star.md')));
|
|
const readback = await b.evaluate(`fetch('/api/orgs/alpha/setup/records/work').then(r=>r.json())`);
|
|
assert.equal(readback.state, 'done'); assert.equal(readback.verb, 'mosaic project init alpha work');
|
|
}
|
|
if (scenario !== 'bus-refused') {
|
|
assert.equal(await b.evaluate(`document.querySelector('[data-quick-step="4"] code').textContent`), 'mosaic project create work --repo work --business alpha');
|
|
if (scenario !== 'success') {
|
|
assert.match(await b.evaluate(`document.querySelector('[data-quick-step="4"]').textContent`), scenario === 'project-refused' ? /Project refused/ : /Outcome unknown/i);
|
|
assert.equal(await b.evaluate('document.activeElement.dataset.quickStep'), '4');
|
|
await b.evaluate(`document.querySelector('#org-quick-run').click()`);
|
|
assert.equal(await b.evaluate('window.__projectPosts'), 1, 'a stopped setup does not repeat the write');
|
|
}
|
|
await wait('!document.querySelector("#org-quick-open-org").hidden');
|
|
await b.evaluate(`document.querySelector('#org-quick-open-org').click()`);
|
|
await wait(`window.MosaicOrg?.id === 'alpha' && !!document.querySelector('.chat-projects')`);
|
|
if (scenario === 'project-refused') assert.match(await b.evaluate(`document.querySelector('.chat-projects').textContent`), /No projects yet/);
|
|
else {
|
|
await wait(`document.querySelector('.chat-projects')?.textContent.includes('Repository: work')`);
|
|
assert.equal(await b.evaluate(`document.querySelectorAll('.chat-projects .project-card').length`), 1);
|
|
assert.doesNotMatch(await b.evaluate(`document.querySelector('.chat-projects').textContent`), /No projects yet/);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('an unreadable success reply stops quick setup and a new draft gets fresh controls', { timeout: 45000 }, async t => {
|
|
const admin = createOrgAdmin({ service, registry: { listOrgs: async () => [] }, context: async () => ({ configDir: '/cfg', dataRoot: '/data', instance: 'test' }) });
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), 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());
|
|
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<300;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30))}throw Error(${JSON.stringify(expression)})})()`);
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/settings/organizations`);
|
|
await wait('document.querySelector("#org-new-form [data-write]") && !document.querySelector("#org-new-form [data-write]").disabled');
|
|
// The create reply is a truncated 201; every POST is recorded so the test can prove nothing follows it.
|
|
await b.evaluate(`window.__posts=[];const real=window.fetch;window.fetch=(url,init={})=>{if((init.method||'GET')==='POST'){window.__posts.push(String(url));if(String(url)==='/api/orgs')return Promise.resolve(new Response('{"outcome":',{status:201,headers:{'content-type':'application/json'}}));}return real(url,init)}`);
|
|
const draft = async id => {
|
|
await b.evaluate(`document.querySelector('#org-new-id').value=${JSON.stringify(id)};document.querySelector('#org-new-form [data-write]').click()`);
|
|
await wait(`document.querySelector("#org-quick-open") && !document.querySelector("#org-paths").hidden && !document.querySelector("#org-quick-open").disabled`);
|
|
await b.evaluate(`document.querySelector('#org-quick-open').click()`);
|
|
await wait('!document.querySelector("#org-quick").hidden');
|
|
};
|
|
const controls = () => b.evaluate('["#org-preset","#org-thinking","#org-quick-run"].map(s=>document.querySelector(s).disabled)');
|
|
await draft('alpha');
|
|
await b.evaluate(`document.querySelector('#org-risk-accept').click();document.querySelector('#org-quick-run').click()`);
|
|
await wait(`document.querySelector('[data-quick-step="0"] .org-pill')?.textContent === 'Stopped'`);
|
|
assert.match(await b.evaluate(`document.querySelector('[data-quick-step="0"]').textContent`), /outcome is unknown/);
|
|
assert.deepEqual(await b.evaluate('window.__posts'), ['/api/orgs']);
|
|
assert.deepEqual(await controls(), [true, true, true]);
|
|
await draft('beta');
|
|
assert.deepEqual(await controls(), [false, false, false]);
|
|
assert.equal(await b.evaluate(`document.querySelector('#org-risk-accept').checked`), false);
|
|
});
|