Merge branch 'org-quick-project' into ui-org-r2
This commit is contained in:
commit
6ca7f3b656
3 files changed
+45
-16
No files matched your search
@@ -308,6 +308,7 @@
|
||||
{ title: 'Initialize organization checkout', verb: `mosaic org queue init ${id} --local-only --accept-local-loss-risk` },
|
||||
{ title: `Initialize project repository: ${project}`, verb: `mosaic org project repo init ${id} ${project} ${remote ? `--remote ${remote}` : '--local-only --accept-local-loss-risk'}` },
|
||||
{ title: 'Start the bus', verb: `mosaic bus start ${id} --dev-local-owner --detach` },
|
||||
{ title: `Create project: ${project}`, verb: `mosaic project create ${project} --repo ${project} --business ${id}` },
|
||||
];
|
||||
const list = $('org-quick-steps');
|
||||
list.innerHTML = steps.map((s, i) => `<li class="panel org-step" data-quick-step="${i}" tabindex="-1"><div class="org-step-head"><h4>${txt(s.title)}</h4><span class="org-pill">Not started</span></div><p class="org-verb">Same as <code>${txt(s.verb)}</code></p><p class="org-field-err" role="alert"></p></li>`).join('');
|
||||
@@ -323,6 +324,7 @@
|
||||
[`/api/orgs/${enc(id)}/setup/queue`, local, 'queue'],
|
||||
[`/api/orgs/${enc(id)}/setup/project/${enc(project)}`, remote ? { mode: 'remote', remote } : local, 'project'],
|
||||
[`/api/orgs/${enc(id)}/setup/bus`, {}, 'bus'],
|
||||
[`/api/orgs/${enc(id)}/chat/projects`, { name: project, repo: project }, 'chat-project'],
|
||||
];
|
||||
for (current = 0; current < requests.length; current++) {
|
||||
const card = list.children[current]; card.querySelector('.org-pill').textContent = 'In progress';
|
||||
@@ -330,7 +332,7 @@
|
||||
const result = await api('POST', url, body);
|
||||
if (result.status !== 200 && result.status !== 201) throw Error(result.body.message || result.body.errors?.map(e => e.message).join(' ') || `Refused (${result.body.error || result.status}).`);
|
||||
// A success status alone is not proof: an unreadable or unexpected reply leaves the outcome unknown, so stop here.
|
||||
if (result.body.outcome !== 'done' || result.body.step !== step) throw Error('The reply could not be read, so this step\'s outcome is unknown.');
|
||||
if (step === 'chat-project' ? !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(result.body.id || '') : result.body.outcome !== 'done' || result.body.step !== step) throw Error('The reply could not be read, so this step\'s outcome is unknown.');
|
||||
card.querySelector('.org-pill').textContent = 'Done'; card.classList.add('org-step-done');
|
||||
if (result.body.verb) card.querySelector('code').textContent = result.body.verb;
|
||||
}
|
||||
@@ -338,7 +340,10 @@
|
||||
await refreshList();
|
||||
} catch (e) {
|
||||
const card = list.children[Math.min(current, steps.length - 1)];
|
||||
card.querySelector('.org-pill').textContent = 'Stopped'; card.querySelector('.org-field-err').textContent = `${e.message} Open the organization to inspect and continue; do not repeat a step whose outcome is unknown.`;
|
||||
card.querySelector('.org-pill').textContent = 'Stopped'; card.querySelector('.org-field-err').textContent = current === 4
|
||||
? `${e.message} Open the organization and check Projects before creating the project; its outcome may be unknown.`
|
||||
: `${e.message} Open the organization to inspect and continue; do not repeat a step whose outcome is unknown.`;
|
||||
if (current === 4) { editorDirty = false; $('org-new-id').value = ''; $('org-quick-open-org').hidden = false; }
|
||||
card.classList.add('org-step-warn'); card.focus(); status('Setup stopped.');
|
||||
await refreshList();
|
||||
}
|
||||
|
||||
@@ -23,16 +23,24 @@ test('setup paths and preset controls are visible', { timeout: 45000 }, async t
|
||||
for (const selector of ['#org-project', '#org-project-remote', '#org-risk-accept', '#org-quick-run']) assert.equal(await shown(selector), true, selector);
|
||||
});
|
||||
|
||||
for (const shouldFail of [true, false]) test(`quick setup creates real repositories and ${shouldFail ? 'stops at a bus refusal' : 'completes after bus start'}`, { timeout: 45000 }, async t => {
|
||||
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('../../business/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 (shouldFail) throw Object.assign(Error('bus refused'), { code: 'org-invalid' }); return { healthy: true }; } };
|
||||
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 server = await startServer({ port: 0, sso: createDevOwner(), orgAdmin: admin, send: async () => ({}), board: 'http://127.0.0.1:9', log: () => {} }); t.after(() => close(server));
|
||||
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).
|
||||
@@ -42,11 +50,12 @@ for (const shouldFail of [true, false]) test(`quick setup creates real repositor
|
||||
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 finalState = shouldFail ? 'Stopped' : 'Done';
|
||||
await wait(`document.querySelector('[data-quick-step="3"] .org-pill')?.textContent === '${finalState}'`);
|
||||
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("[data-quick-step] .org-pill")].map(e=>e.textContent)'), ['Done', 'Done', 'Done', finalState]);
|
||||
if (shouldFail) {
|
||||
const finalStep = scenario === 'bus-refused' ? 3 : 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]);
|
||||
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');
|
||||
}
|
||||
@@ -54,6 +63,24 @@ for (const shouldFail of [true, false]) test(`quick setup creates real repositor
|
||||
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 !== '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 => {
|
||||
|
||||
@@ -32,17 +32,14 @@ test('Quick Setup starts the real fixture and a browser message gets its canned
|
||||
await wait('document.querySelector("#org-quick") && !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);
|
||||
await click('#org-risk-accept'); await click('#org-quick-run');
|
||||
await wait(`document.querySelector('[data-quick-step="3"] .org-pill')?.textContent === 'Done' || [...document.querySelectorAll('[data-quick-step] .org-pill')].some(e=>e.textContent==='Stopped')`, 90000);
|
||||
await wait(`document.querySelector('[data-quick-step="4"] .org-pill')?.textContent === 'Done' || [...document.querySelectorAll('[data-quick-step] .org-pill')].some(e=>e.textContent==='Stopped')`, 90000);
|
||||
console.log('setup reached a terminal step');
|
||||
const states = await b.evaluate('[...document.querySelectorAll("[data-quick-step] .org-pill")].map(e=>e.textContent)');
|
||||
assert.deepEqual(states, ['Done', 'Done', 'Done', 'Done'], await b.evaluate('document.querySelector("#org-quick-steps").innerText'));
|
||||
assert.deepEqual(states, ['Done', 'Done', 'Done', 'Done', 'Done'], await b.evaluate('document.querySelector("#org-quick-steps").innerText'));
|
||||
assert.equal(await shown('#org-quick-open-org'), true, 'the finished organization can be opened');
|
||||
await click('#org-quick-open-org'); console.log('opening organization');
|
||||
await wait(`window.MosaicOrg?.id === ${JSON.stringify(id)} && !!document.querySelector('#chat-repo option[value=work]')`, 30000);
|
||||
await wait('!!document.querySelector("#chat-repo option[value=work]")');
|
||||
console.log('repository offered');
|
||||
await click('.project-create summary'); await input('#chat-project-name', 'Work'); await input('#chat-repo', 'work');
|
||||
await click('#chat-project-form button[type=submit]');
|
||||
await wait(`window.MosaicOrg?.id === ${JSON.stringify(id)} && document.querySelector('.project-card')?.textContent.includes('Repository: work')`, 30000);
|
||||
await click('.project-card');
|
||||
await wait('location.hash.match(/\\/projects\\/[0-9a-f-]{36}/i)');
|
||||
console.log('project created');
|
||||
const projectRoute = await b.evaluate('location.hash');
|
||||
|
||||
Reference in new issue
Block a user