Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
240 lines
23 KiB
Diff
240 lines
23 KiB
Diff
diff --git a/packages/webui/src/public/bus.js b/packages/webui/src/public/bus.js
|
|
index 876dbad8a..42643689a 100644
|
|
--- a/packages/webui/src/public/bus.js
|
|
+++ b/packages/webui/src/public/bus.js
|
|
@@ -558,10 +558,14 @@
|
|
// REQ-WEB-1: drafts survive polling/refusals; only an explicit submit writes.
|
|
let settingsSnapshot=null, settingsInputs=null, settingsSaving=false, settingsReceipt='';
|
|
const inputKey=parts=>JSON.stringify(parts);
|
|
+ // Row 167 (#1664): one snapshot for the launch form, the assistant forms and the token limit
|
|
+ // reset. A read replaces it only while no form has a draft; every save and reset sets it from
|
|
+ // its answer, so the next submit sends the revision the last write returned.
|
|
+ const launchDrafts=()=>!!settingsInputs||Object.keys(assistantDrafts).length>0;
|
|
+ function takeSnapshot(value){if(!settingsSnapshot||!launchDrafts())settingsSnapshot=value;return settingsSnapshot;}
|
|
function launchForm(value){
|
|
if(!value||value.refused)return `<section class="panel"><h2>Agent limits</h2><p>${txt(value?.message||'Launch settings are not configured.')}</p></section>`;
|
|
- if(!settingsInputs)settingsSnapshot=value;
|
|
- const v=settingsSnapshot, modelTable=v.models||{}, limits=v.limits||{}, usage=v.usage||{};
|
|
+ const v=takeSnapshot(value), modelTable=v.models||{}, limits=v.limits||{}, usage=v.usage||{};
|
|
const val=(parts,fallback)=>settingsInputs&&Object.hasOwn(settingsInputs,inputKey(parts))?settingsInputs[inputKey(parts)]:fallback;
|
|
const disabled=!canSend||out()||settingsSaving?' disabled':'';
|
|
const field=(parts,label,count,current)=>`<label>${txt(label)} <input data-setting="${esc(inputKey(parts))}" type="number" min="0" step="1" value="${esc(val(parts,count??''))}"${disabled}> <span class="small muted">${txt(current??0)} running</span></label>`;
|
|
@@ -606,7 +610,7 @@
|
|
const path=e.target.dataset?.limitReset;if(path===undefined||!canSend||out()||settingsSaving)return;
|
|
const revision=settingsSnapshot?.revision;if(!/^[a-f0-9]{64}$/.test(revision??''))return;
|
|
settingsSaving=true;resetReceipt='';render(false);let reread=false;
|
|
- try{const response=await fetch('/api/settings',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({revision,reset:[JSON.parse(path)]}),cache:'no-store',redirect:'manual'});if(response.type==='opaqueredirect'){resetReceipt='Sign in again.';signOut();}else{const result=await response.json();if(response.ok&&/^[a-f0-9]{64}$/.test(result?.revision??'')){if(!settingsInputs)settingsSnapshot=result;const prior=last.get('api:settings');if(prior)last.set('api:settings',{...prior,launch:result});reread=true;resetReceipt='Limit reset.';}else resetReceipt=result?.sent==='unknown'?'Outcome unknown. Read current settings before trying again.':`${result?.message||'Reset refused.'}`;}}
|
|
+ try{const response=await fetch('/api/settings',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({revision,reset:[JSON.parse(path)]}),cache:'no-store',redirect:'manual'});if(response.type==='opaqueredirect'){resetReceipt='Sign in again.';signOut();}else{const result=await response.json();if(response.ok&&/^[a-f0-9]{64}$/.test(result?.revision??'')){settingsSnapshot=result;const prior=last.get('api:settings');if(prior)last.set('api:settings',{...prior,launch:result});reread=true;resetReceipt='Limit reset.';}else resetReceipt=result?.sent==='unknown'?'Outcome unknown. Read current settings before trying again.':`${result?.message||'Reset refused.'}`;}}
|
|
catch{resetReceipt='Outcome unknown. Read current settings before trying again.';}finally{settingsSaving=false;if(!out())render(reread);}
|
|
});
|
|
function tokenUsage(value) {
|
|
@@ -624,12 +628,12 @@
|
|
// Row 155 (#1649, Q169): each assistant instance's settings, its seat name and a stop. A save
|
|
// sends only the fields that changed, with the read's revision. Drafts survive polling and
|
|
// refusals as in the limits form, and nothing retries.
|
|
- let assistantSnapshot=null, assistantDrafts=Object.create(null), nameDrafts=Object.create(null), assistantBusy=false, assistantReceipts=Object.create(null);
|
|
+ let assistantDrafts=Object.create(null), nameDrafts=Object.create(null), assistantBusy=false, assistantReceipts=Object.create(null);
|
|
const asShown=(entry,field)=>field==='definition'?entry.definition??'':entry[field]==null?'':JSON.stringify(entry[field],null,2);
|
|
function assistantSection(value,names){
|
|
- if(!value||value.refused||!value.assistants||!Object.keys(value.assistants).length)return '';
|
|
- if(!Object.keys(assistantDrafts).length)assistantSnapshot=value;
|
|
- const v=assistantSnapshot, disabled=!canSend||out()||assistantBusy?' disabled':'';
|
|
+ if(!value||value.refused)return '';
|
|
+ const v=takeSnapshot(value);if(!v.assistants||!Object.keys(v.assistants).length)return '';
|
|
+ const disabled=!canSend||out()||assistantBusy?' disabled':'';
|
|
const named=names?Object.fromEntries(names.map(r=>[r.instance,r.name])):null;
|
|
const forms=Object.entries(v.assistants).map(([instance,entry])=>{
|
|
const d=assistantDrafts[instance]||{}, val=f=>Object.hasOwn(d,f)?d[f]:asShown(entry,f), k=esc(instance);
|
|
@@ -639,7 +643,7 @@
|
|
return `<section class="panel" aria-labelledby="assistant-title"><h2 id="assistant-title">Assistant</h2><p>The definition stays an assistant one. Holder and credentials stay hand edits. Stop ends the runs of your threads; the assistant starts again when you next write to it.</p>${forms}<p class="small muted">${canSend?'Changes are checked and logged with your identity.':'Changes require signed-in send mode.'}</p></section>`;
|
|
}
|
|
function assistantPayload(instance,form){
|
|
- const entry=assistantSnapshot.assistants[instance], patch={};
|
|
+ const entry=settingsSnapshot.assistants[instance], patch={};
|
|
for(const f of ['definition','vars','tracker']){
|
|
const text=form.querySelector(`[data-assistant-field="${f}"]`).value, what=f==='vars'?'Vars':'The tracker';
|
|
if(text===asShown(entry,f))continue;
|
|
@@ -650,7 +654,7 @@
|
|
patch[f]=parsed;
|
|
}
|
|
if(!Object.keys(patch).length)throw Error('Nothing changed.');
|
|
- return {revision:assistantSnapshot.revision,assistants:{[instance]:patch}};
|
|
+ return {revision:settingsSnapshot.revision,assistants:{[instance]:patch}};
|
|
}
|
|
const unknownAssistant='Outcome unknown. Read current settings before trying again. Draft kept.';
|
|
async function assistantPost(url,payload,instance,ok){
|
|
@@ -663,7 +667,7 @@
|
|
document.addEventListener('submit',async e=>{
|
|
const form=assistantForm(e.target);if(!form||e.target!==form)return;e.preventDefault();if(!canSend||out()||assistantBusy)return;const instance=form.dataset.instance;let payload;
|
|
try{payload=assistantPayload(instance,form);}catch(err){assistantReceipts[instance]=err.message;render(false);return;}
|
|
- await assistantPost('/api/settings',payload,instance,result=>{if(!/^[a-f0-9]{64}$/.test(result?.revision??''))return unknownAssistant;delete assistantDrafts[instance];assistantSnapshot=result;const prior=last.get('api:settings');if(prior)last.set('api:settings',{...prior,launch:result});return 'Assistant settings saved.';});
|
|
+ await assistantPost('/api/settings',payload,instance,result=>{if(!/^[a-f0-9]{64}$/.test(result?.revision??''))return unknownAssistant;delete assistantDrafts[instance];settingsSnapshot=result;const prior=last.get('api:settings');if(prior)last.set('api:settings',{...prior,launch:result});return 'Assistant settings saved.';});
|
|
});
|
|
document.addEventListener('click',async e=>{
|
|
const form=assistantForm(e.target);if(!form||!canSend||out()||assistantBusy)return;const instance=form.dataset.instance;
|
|
@@ -726,7 +730,7 @@
|
|
const s = await get.api('settings'), n = s.notifier || {};
|
|
let usage, names = null;
|
|
try { [usage] = await get('usage'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; }
|
|
- try { names = await get('names'); } catch { /* the section says the name could not be read */ }
|
|
+ try { names = await get('names'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; /* the section says the name could not be read */ }
|
|
let instances = null;
|
|
try { [instances] = await get('instances'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; }
|
|
let limitRows = [], limitsRefused = false;
|
|
diff --git a/packages/webui/tests/assistant.test.mjs b/packages/webui/tests/assistant.test.mjs
|
|
index f885562eb..0c3bd51e1 100644
|
|
--- a/packages/webui/tests/assistant.test.mjs
|
|
+++ b/packages/webui/tests/assistant.test.mjs
|
|
@@ -66,6 +66,21 @@ test('Q169 the name and stop routes take JSON within 4 KiB and only their own ke
|
|
assert.equal(s.calls.length, 0);
|
|
});
|
|
|
|
+// Row 167 (#1664, Dewey's F1): a JSON null body is refused by body() before the key check runs,
|
|
+// so neither route throws on it, calls the bus or stops answering.
|
|
+test('Row 167 a null body on the name and stop routes is 400 invalid-request with no write, and the next request is answered', async t => {
|
|
+ const s = await setup(t);
|
|
+ for (const [path, data, call] of [['/api/name', { instance: 'assistant', name: 'piper' }, 'name.set'], ['/api/assistant-stop', { instance: 'assistant' }, 'assistant.stop']]) {
|
|
+ const before = s.calls.length;
|
|
+ const r = await send(s, path, 'null');
|
|
+ assert.deepEqual([r.status, r.body.error, r.body.sent], [400, 'invalid-request', false], path);
|
|
+ assert.equal(s.calls.length, before, path);
|
|
+ assert.equal((await send(s, path, data)).status, 200, path);
|
|
+ assert.deepEqual(s.calls.at(-1), [call, data]);
|
|
+ }
|
|
+ assert.equal(s.calls.length, 2);
|
|
+});
|
|
+
|
|
test('Q169 a bus refusal says nothing changed; a lost or odd answer says the change may have been made', async t => {
|
|
const fail = code => async () => { throw new BusReadError(code); };
|
|
for (const [stop, status, error, sent] of [
|
|
diff --git a/packages/webui/tests/limits-browser.test.mjs b/packages/webui/tests/limits-browser.test.mjs
|
|
index c34152bba..1a92984fb 100644
|
|
--- a/packages/webui/tests/limits-browser.test.mjs
|
|
+++ b/packages/webui/tests/limits-browser.test.mjs
|
|
@@ -39,7 +39,7 @@ test('REQ-LIMIT-3 REQ-LIMIT-4 Settings shows token limits counted, not enforced
|
|
assert.equal(await b.evaluate('[...document.querySelectorAll("[data-limit-reset]")].every(e=>e.disabled)'),true);
|
|
assert.equal(await b.evaluate('window.injected===true'),false);
|
|
// Signed-in send mode: Reset posts the revision and that one path, and nothing else. A draft
|
|
- // typed before the reset stays, and so does the revision it was typed against.
|
|
+ // typed before the reset stays, and its save carries the revision the reset returned (row 167).
|
|
await b.navigate(`http://127.0.0.1:${port}/?send=1#/settings`);
|
|
await wait('document.querySelector("[data-limit-reset]")?.disabled===false');
|
|
await b.evaluate(`(()=>{const e=[...document.querySelectorAll('[data-setting]')].find(e=>e.dataset.setting===${JSON.stringify(JSON.stringify(['total']))});e.value='3';e.dispatchEvent(new Event('input',{bubbles:true}));return true;})()`);
|
|
@@ -51,7 +51,7 @@ test('REQ-LIMIT-3 REQ-LIMIT-4 Settings shows token limits counted, not enforced
|
|
await b.evaluate('document.querySelector("#launch-settings-form").requestSubmit();true');
|
|
await wait('posts.length===2');
|
|
const [,save]=await b.evaluate('posts');
|
|
- assert.equal(save.revision,'a'.repeat(64));
|
|
+ assert.equal(save.revision,'b'.repeat(64));
|
|
assert.equal(save.limits.total,3);
|
|
assert.deepEqual(save.limits.tokens,tokens);
|
|
// A limits answer the server refuses shows no rows and no count.
|
|
@@ -61,3 +61,25 @@ test('REQ-LIMIT-3 REQ-LIMIT-4 Settings shows token limits counted, not enforced
|
|
assert.equal(await b.evaluate('document.querySelectorAll("#token-limits-table").length'),0);
|
|
}finally{await b.close();await close(web);}
|
|
});
|
|
+// Row 167 (#1664): the reset and the Assistant section share one snapshot. With an assistant
|
|
+// draft open, a reset gives the assistant submit the revision the reset returned.
|
|
+test('Row 167 with an assistant draft open, a token limit reset gives the assistant submit its revision',{timeout:90000},async()=>{
|
|
+ const withAssistant={...launch,assistants:{assistant:{definition:'assistant',vars:{model:'claude-opus-5-5'},tracker:null}}};
|
|
+ const bus={messages:async()=>({messages:[],unread:0}),usage:async()=>{throw Error('no usage');},limits:async()=>({revision:launch.revision,limits:launch.limits,tokens:TOKENS})};
|
|
+ const web=await startServer({port:0,board:'http://127.0.0.1:9',bus,reads:{settings:()=>({business:'acme',release:'fixture',notifier:{},launch:withAssistant})}}),b=await browser();
|
|
+ try{
|
|
+ await b.call('Page.addScriptToEvaluateOnNewDocument',{source:`window.posts=[];window.fixture=${JSON.stringify(withAssistant)};const original=window.fetch;window.fetch=async(url,options={})=>{if(url==='/api/config')return new Response(JSON.stringify({send:true}));if(url==='/api/settings'&&options.method==='POST'){posts.push(JSON.parse(options.body));return new Response(JSON.stringify({...fixture,revision:'bc'[posts.length-1].repeat(64)}));}return original(url,options);};`});
|
|
+ const q=sel=>`document.querySelector(${JSON.stringify(sel)})`;
|
|
+ const wait=expr=>b.evaluate(`(async()=>{for(let n=0;n<200;n++){if(${expr})return true;await new Promise(r=>setTimeout(r,30));}throw Error('wait failed: '+${JSON.stringify(expr)});})()`);
|
|
+ await b.navigate(`http://127.0.0.1:${web.address().port}/#/settings`);
|
|
+ await wait(`${q('[data-limit-reset]')}?.disabled===false&&${q('.assistant-form button[type=submit]')}?.disabled===false`);
|
|
+ await b.evaluate(`(()=>{const e=${q('.assistant-form [data-assistant-field="tracker"]')};e.value='{"project":"P19"}';e.dispatchEvent(new Event('input',{bubbles:true}));return true;})()`);
|
|
+ await b.evaluate('document.querySelectorAll("[data-limit-reset]")[1].click();true');
|
|
+ await wait('document.querySelector("#token-limits-receipt")?.textContent.includes("Limit reset.")');
|
|
+ assert.deepEqual(await b.evaluate('posts'),[{revision:'a'.repeat(64),reset:[['tokens','keys','k1']]}]);
|
|
+ assert.equal(await b.evaluate(`${q('.assistant-form [data-assistant-field="tracker"]')}.value`),'{"project":"P19"}');
|
|
+ await b.evaluate(`${q('.assistant-form')}.requestSubmit();true`);
|
|
+ await wait(`${q('[data-assistant-receipt]')}?.textContent.includes("Assistant settings saved")`);
|
|
+ assert.deepEqual(await b.evaluate('posts[1]'),{revision:'b'.repeat(64),assistants:{assistant:{tracker:{project:'P19'}}}});
|
|
+ }finally{await b.close();await close(web);}
|
|
+});
|
|
diff --git a/packages/webui/tests/settings-form.test.mjs b/packages/webui/tests/settings-form.test.mjs
|
|
index faf18d07d..4778af293 100644
|
|
--- a/packages/webui/tests/settings-form.test.mjs
|
|
+++ b/packages/webui/tests/settings-form.test.mjs
|
|
@@ -50,3 +50,77 @@ test('Q169 Assistant section: gated, changed fields only, name and stop posts, r
|
|
await b.evaluate(`${q('[data-assistant-stop]')}.click();true`);await receipt('Stopped 2 runs');assert.deepEqual(await b.evaluate('posts[3]'),['/api/assistant-stop',{instance:'assistant'}]);assert.equal(await b.evaluate('posts.length'),4);
|
|
}finally{await b.close();await close(web);}
|
|
});
|
|
+// Row 167 (#1664): the names read fails like the usage read. A 403 on it gives the same refused
|
|
+// page as a 403 on usage; any other failure keeps the Assistant section and says so.
|
|
+test('Row 167 a refused names read gives the refused usage page; another names failure keeps the Assistant section',{timeout:90000},async()=>{
|
|
+ const withAssistant={...launch,assistants:{assistant:{definition:'assistant',vars:{model:'claude-sonnet-5-5'},tracker:null}}};
|
|
+ const bus={inbox:async()=>[],tasks:async()=>[],agents:async()=>[],messages:async()=>({messages:[],unread:0}),names:async()=>[{instance:'assistant',name:'piper',at:'2026-10-18T00:00:00.000Z'}]};
|
|
+ const web=await startServer({port:0,board:'http://127.0.0.1:9',bus,reads:{settings:()=>({business:'acme',release:'fixture',notifier:{binding:'fixture'},launch:withAssistant})}}),b=await browser();
|
|
+ try{
|
|
+ await b.call('Page.addScriptToEvaluateOnNewDocument',{source:`const query=new URL(location.href).searchParams,refuse=query.get('refuse'),fail=query.get('fail'),original=window.fetch;window.fetch=async(url,options={})=>{if(refuse&&url.startsWith('/api/bus/'+refuse))return new Response(JSON.stringify({error:'human-required',message:'fixture refusal'}),{status:403});if(fail&&url.startsWith('/api/bus/'+fail))return new Response(JSON.stringify({error:'bus-unreachable',message:'fixture failure'}),{status:503});return original(url,options);};`});
|
|
+ const port=web.address().port;
|
|
+ const wait=expr=>b.evaluate(`(async()=>{for(let n=0;n<200;n++){if(${expr})return true;await new Promise(r=>setTimeout(r,30));}throw Error('wait failed: '+${JSON.stringify(expr)});})()`);
|
|
+ const refused=async read=>{await b.navigate(`http://127.0.0.1:${port}/?refuse=${read}#/settings`);await wait('document.querySelector("#bus-view")?.textContent.includes("Bus refused the read")');return b.evaluate('document.querySelector("#bus-view").innerHTML');};
|
|
+ const usage=await refused('usage'), names=await refused('names');
|
|
+ assert.match(usage,/refused: <code>human-required<\/code>/);
|
|
+ assert.equal(names,usage);
|
|
+ assert.equal(names.includes('assistant-form')||names.includes('launch-settings-form'),false);
|
|
+ await b.navigate(`http://127.0.0.1:${port}/?fail=names#/settings`);await wait('!!document.querySelector(".assistant-form")');
|
|
+ assert.equal(await b.evaluate('document.querySelector(".assistant-form").textContent.includes("The current name could not be read.")'),true);
|
|
+ assert.equal(await b.evaluate('document.querySelector("[data-assistant-field=name]").value'),'');
|
|
+ assert.equal(await b.evaluate('!!document.querySelector("#launch-settings-form")'),true);
|
|
+ }finally{await b.close();await close(web);}
|
|
+});
|
|
+// Row 167 (#1664): one settings snapshot. Each POST answers the fixture with the next revision
|
|
+// (b, c, then d), and reads after it serve that answer, as the server would. A save in one form
|
|
+// gives the other form's open draft the revision it returned.
|
|
+const oneSnapshot=`window.posts=[];window.saved=null;const original=window.fetch;window.fetch=async(url,options={})=>{if(url==='/api/config')return new Response(JSON.stringify({send:true}));if(url==='/api/settings'&&options.method==='POST'){posts.push(JSON.parse(options.body));window.saved={...(window.saved||window.fixture),revision:'bcd'[posts.length-1].repeat(64)};return new Response(JSON.stringify(window.saved));}if(url==='/api/settings'&&window.saved){const read=await (await original(url,options)).json();return new Response(JSON.stringify({...read,launch:window.saved}));}return original(url,options);};`;
|
|
+async function snapshotPage(){
|
|
+ const withAssistant={...launch,assistants:{assistant:{definition:'assistant',vars:{model:'claude-sonnet-5-5'},tracker:null}}};
|
|
+ const bus={inbox:async()=>[],tasks:async()=>[],agents:async()=>[],messages:async()=>({messages:[],unread:0}),names:async()=>[{instance:'assistant',name:'piper',at:'2026-10-18T00:00:00.000Z'}]};
|
|
+ const web=await startServer({port:0,board:'http://127.0.0.1:9',bus,reads:{settings:()=>({business:'acme',release:'fixture',notifier:{binding:'fixture'},launch:withAssistant})}}),b=await browser();
|
|
+ await b.call('Page.addScriptToEvaluateOnNewDocument',{source:`window.fixture=${JSON.stringify(withAssistant)};${oneSnapshot}`});
|
|
+ const q=sel=>`document.querySelector(${JSON.stringify(sel)})`;
|
|
+ const wait=expr=>b.evaluate(`(async()=>{for(let n=0;n<200;n++){if(${expr})return true;await new Promise(r=>setTimeout(r,30));}throw Error('wait failed: '+${JSON.stringify(expr)});})()`);
|
|
+ await b.navigate(`http://127.0.0.1:${web.address().port}/#/settings`);
|
|
+ await wait(`${q('#launch-settings-form button[type=submit]')}?.disabled===false&&${q('.assistant-form button[type=submit]')}?.disabled===false`);
|
|
+ const total=()=>b.evaluate(`[...document.querySelectorAll('[data-setting]')].find(e=>e.dataset.setting===${JSON.stringify(JSON.stringify(['total']))}).value`);
|
|
+ return {web,b,q,wait,total,
|
|
+ setTotal:value=>b.evaluate(`(()=>{const e=[...document.querySelectorAll('[data-setting]')].find(e=>e.dataset.setting===${JSON.stringify(JSON.stringify(['total']))});e.value=${JSON.stringify(value)};e.dispatchEvent(new Event('input',{bubbles:true}));return true;})()`),
|
|
+ setTracker:value=>b.evaluate(`(()=>{const e=${q('.assistant-form [data-assistant-field="tracker"]')};e.value=${JSON.stringify(value)};e.dispatchEvent(new Event('input',{bubbles:true}));return true;})()`),
|
|
+ tracker:()=>b.evaluate(`${q('.assistant-form [data-assistant-field="tracker"]')}.value`),
|
|
+ submitLaunch:()=>b.evaluate(`${q('#launch-settings-form')}.requestSubmit();true`),
|
|
+ submitAssistant:()=>b.evaluate(`${q('.assistant-form')}.requestSubmit();true`),
|
|
+ };
|
|
+}
|
|
+test('Row 167 with a limits draft open, an assistant save gives the limits submit its revision and the typed limit stays',{timeout:90000},async()=>{
|
|
+ const p=await snapshotPage();
|
|
+ try{
|
|
+ await p.setTotal('7');await p.setTracker('{"project":"P19"}');await p.submitAssistant();
|
|
+ await p.wait(`${p.q('[data-assistant-receipt]')}?.textContent.includes("Assistant settings saved")`);
|
|
+ assert.equal((await p.b.evaluate('posts[0]')).revision,launch.revision);
|
|
+ assert.equal(await p.total(),'7');
|
|
+ await p.submitLaunch();await p.wait(`${p.q('#launch-settings-receipt')}?.textContent.includes("saved")`);
|
|
+ const second=await p.b.evaluate('posts[1]');
|
|
+ assert.equal(second.revision,'b'.repeat(64));
|
|
+ assert.equal(second.limits.total,7);
|
|
+ }finally{await p.b.close();await close(p.web);}
|
|
+});
|
|
+test('Row 167 with an assistant draft open, a launch save gives the assistant submit its revision, and reload reads the current one',{timeout:90000},async()=>{
|
|
+ const p=await snapshotPage();
|
|
+ try{
|
|
+ await p.setTracker('{"project":"P19"}');await p.setTotal('7');await p.submitLaunch();
|
|
+ await p.wait(`${p.q('#launch-settings-receipt')}?.textContent.includes("saved")`);
|
|
+ assert.equal((await p.b.evaluate('posts[0]')).revision,launch.revision);
|
|
+ assert.equal(await p.tracker(),'{"project":"P19"}');
|
|
+ await p.submitAssistant();await p.wait(`${p.q('[data-assistant-receipt]')}?.textContent.includes("Assistant settings saved")`);
|
|
+ assert.deepEqual(await p.b.evaluate('posts[1]'),{revision:'b'.repeat(64),assistants:{assistant:{tracker:{project:'P19'}}}});
|
|
+ // Someone else writes, so the file is at d. The reload button clears the snapshot even with an
|
|
+ // assistant draft open, and the draft's submit then carries d.
|
|
+ await p.setTracker('{"project":"P20"}');await p.b.evaluate(`window.saved={...window.saved,revision:'d'.repeat(64)};${p.q('#launch-settings-reload')}.click();true`);
|
|
+ await p.wait(`${p.q('#launch-settings-receipt')}?.textContent===''`);
|
|
+ assert.equal(await p.tracker(),'{"project":"P20"}');
|
|
+ await p.submitAssistant();await p.wait('posts.length===3');
|
|
+ assert.deepEqual(await p.b.evaluate('posts[2]'),{revision:'d'.repeat(64),assistants:{assistant:{tracker:{project:'P20'}}}});
|
|
+ }finally{await p.b.close();await close(p.web);}
|
|
+});
|