Files
stack/packages/webui/tests/hire-console-browser.test.mjs

51 lines
5.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {fixture} from './fixture.mjs';
import {mkdir} from 'node:fs/promises';
import {join} from 'node:path';
import {browser} from './browser.mjs';
for(const width of [1440,390])test(`A4 Console hire start and narrowed confirmation at ${width}px`,{timeout:120000},async t=>{
const f=await fixture(),b=await browser({machine:false});t.after(async()=>{await b.close();await f.close();});
await b.call('Page.addScriptToEvaluateOnNewDocument',{source:`(()=>{
const real=fetch;window.__posts=[];
const hire={id:'d1',kind:'hire',op:'hire',level:'org',name:'scout',route:{harness:'pi',model:'fixture-model'},flagged:[{field:'tools',value:'bash',reasons:['above-chief']}],credentialLines:[{service:'gitea',scope:'read:repository'}],expiresAt:'2099-01-01T00:00:00.000Z',digest:'a'.repeat(64),role:{tools:['read','bash'],network:'none',authority:{withinRole:[],crossRole:[]},credentials:[{service:'gitea',scopes:['read:repository']}],skills:[]},options:[{key:'yes',text:'Approve'},{key:'no',text:'Deny'}]};
const d={id:'d1',at:'2026-11-11T00:00:00.000Z',raised_by_role:'chief',raised_by_run:'chief-run',class:'gated',action:'role.hire',route_to:'human',question:'Hire scout?',options:hire.options,blocking:false,console:hire};
const json=v=>new Response(JSON.stringify(v),{headers:{'content-type':'application/json'}});
window.fetch=async(url,init={})=>{
const p=new URL(url,location.href).pathname;
if(p==='/api/config')return json({sso:true,send:true,decide:true});
if(p==='/api/business')return json({business:{id:'demo',projects:[]}});
if(p==='/api/bus/inbox')return json({rows:[d]});
if(p==='/api/bus/trail')return json({rows:[{...d,table:'decisions'}]});
if(p==='/api/bus/agents'||p==='/api/bus/messages'||p==='/api/chat/projects'||p==='/api/chat/threads')return json({rows:[]});
if(p.startsWith('/api/decisions/')){
const body=JSON.parse(init.body);window.__posts.push({p,body});
if(p.endsWith('/templates'))return json({templates:[{name:'coder',sha256:'b'.repeat(64)}]});
if(p.endsWith('/start'))return json({id:'started',op:'resolved',choice:'yes'});
if(p.endsWith('/confirmation'))return json({id:'d1',kind:'hire',action:'answer',question:'Hire scout?',choice:body.choice,text:'Approve',flagged:[],credentialLines:[],expiresAt:hire.expiresAt,confirmation:'c'.repeat(64)});
return json({id:'d1',op:'resolved',choice:body.choice});
}
if(p==='/api/processing'||p==='/api/bus-status')return json({error:'unavailable'});
return real(url,init);
};
})()`});
const shots=async(state,selector)=>{if(!process.env.MOSAIC_UX_SHOTS)return;await mkdir(process.env.MOSAIC_UX_SHOTS,{recursive:true});for(const mode of ['light','dark']){await b.evaluate(`document.documentElement.dataset.mode=${JSON.stringify(mode)};document.querySelector(${JSON.stringify(selector)}).scrollIntoView({block:'start'})`);await b.screenshot(join(process.env.MOSAIC_UX_SHOTS,`hire-${state}-${width}-${mode}.png`));}};
const wait=expr=>b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(${expr})return;await new Promise(r=>setTimeout(r,50));}throw Error('Timed out: '+${JSON.stringify(expr)})})()`);
await b.viewport(width,width===390?844:900);await b.navigate(f.base+'/#/inbox');await wait('document.querySelector("#hire-start-form")');
await b.evaluate(`document.querySelector('[data-hire-field="business"]').value='demo';document.querySelector('[data-hire-field="business"]').dispatchEvent(new Event('input',{bubbles:true}));document.querySelector('[data-hire-load]').click()`);
await wait('document.querySelector("[data-hire-field=template] option[value=coder]")');
await shots('start','#hire-start');
await b.evaluate(`for(const [k,v] of Object.entries({name:'scout',template:'coder',harness:'pi',model:'fixture-model',reason:'Need a coder'})){const e=document.querySelector('[data-hire-field="'+k+'"]');e.value=v;e.dispatchEvent(new Event('input',{bubbles:true}));}document.querySelector('#hire-start-form').requestSubmit()`);
await wait('document.querySelector("#hire-start-status")?.textContent.includes("Recorded")');
let posts=await b.evaluate('window.__posts');assert.equal(posts.filter(x=>x.p.endsWith('/start')).length,1);assert.equal(posts.find(x=>x.p.endsWith('/start')).body.hire.role.sha256,'b'.repeat(64));
await b.navigate(f.base+'/#/inbox/d1');await wait('document.querySelector("#hire-adjust")');
assert.doesNotMatch(await b.evaluate("document.querySelector('#b-o').parentElement.textContent"),/mosaic decide/);
await b.evaluate("document.querySelector('#hire-adjust').open=true");await shots('adjust','#hire-adjust');
await b.evaluate(`document.querySelector('[data-hire-list="tools"][data-index="1"]').checked=false;document.querySelector('[data-hire-credential]').checked=false;document.querySelector('[data-decide="option-0"]').click()`);
await wait('document.querySelector("[data-decide=confirm]")');posts=await b.evaluate('window.__posts');const confirm=posts.find(x=>x.p.endsWith('/confirmation'));
assert.deepEqual(confirm.body.adjust.role.tools,['read']);assert.deepEqual(confirm.body.adjust.credentials,[]);
assert.equal(await b.evaluate('document.documentElement.scrollWidth<=innerWidth'),true);await shots('confirm','#decide-section');
await b.evaluate('document.querySelector("[data-decide=confirm]").click()');await wait('document.querySelector("#decide-status")?.textContent.startsWith("Recorded")');
posts=await b.evaluate('window.__posts');assert.deepEqual(Object.keys(posts.find(x=>x.p.endsWith('/resolve')).body).sort(),['choice','confirmation']);
});