Stop quick setup on an unreadable reply and reset controls for a new draft

A 200 or 201 with a body that does not name the expected step as done now
stops at that step as an unknown outcome, with no further writes. Starting a
new draft re-enables the preset, thinking and run controls and clears the
risk checkbox. Browser regression covers both (Sol review P1, P2).
This commit is contained in:
jason.woltje committed 2026-11-07 22:03:45 -06:00
1 parent c09570d696
commit 653b3e2e75
2 files changed
+38 -5

No files matched your search

+11 -5
View File
@@ -226,6 +226,8 @@
if (r.status !== 200) { banner(r.body.message || `Presets could not be read (${r.body.error || r.status}).`); return; }
presets = r.body.presets;
st.id = id; st.mode = 'create'; editorDirty = true;
// A new draft starts with fresh controls; an earlier run locked them only for its own draft.
$('org-preset').disabled = false; $('org-thinking').disabled = false; $('org-quick-run').disabled = !canWrite; $('org-risk-accept').checked = false;
$('org-preset').innerHTML = presets.map(p => `<option value="${txt(p.name)}"${p.available.ok ? '' : ' disabled'}>${txt(p.label)}${p.available.ok ? '' : ` — ${txt(p.available.reason)}`}</option>`).join('');
$('org-preset').value = presets.find(p => p.available.ok)?.name || '';
updatePreset();
@@ -274,17 +276,21 @@
try {
const template = await selectedTemplate();
if (template.status !== 200) throw Error(template.body.message || 'Could not load the template.');
if (!template.body.doc || typeof template.body.doc !== 'object') throw Error('The template reply could not be read.');
const doc = template.body.doc; doc.projects = { [project]: {} };
const requests = [
['/api/orgs', { id, doc }],
[`/api/orgs/${enc(id)}/setup/queue`, local],
[`/api/orgs/${enc(id)}/setup/project/${enc(project)}`, remote ? { mode: 'remote', remote } : local],
[`/api/orgs/${enc(id)}/setup/bus`, {}],
['/api/orgs', { id, doc }, 'create'],
[`/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'],
];
for (current = 0; current < requests.length; current++) {
const card = list.children[current]; card.querySelector('.org-pill').textContent = 'In progress';
const result = await api('POST', ...requests[current]);
const [url, body, step] = requests[current];
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.');
card.querySelector('.org-pill').textContent = 'Done'; card.classList.add('org-step-done');
if (result.body.verb) card.querySelector('code').textContent = result.body.verb;
}
@@ -52,3 +52,30 @@ for (const shouldFail of [true, false]) test(`quick setup creates real repositor
assert.ok(existsSync(join(dataRoot, 'org-runtime/alpha/checkout/.git')));
assert.ok(existsSync(join(dataRoot, 'org-runtime/alpha/projects/work/.git')));
});
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);
});