Files
stack/packages/webui/tests/autonomy-browser.test.mjs

55 lines
5.8 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {mkdirSync} from 'node:fs';
import {join} from 'node:path';
import {tmpdir} from 'node:os';
import {startServer} from '../src/serve.mjs';
import {createDevOwner} from '../src/dev-owner.mjs';
import {browser} from './browser.mjs';
import {close} from './fixture.mjs';
const shots=join(tmpdir(),'autonomy-shots');
const waitFor=(b,expression)=>b.evaluate(`(async()=>{for(let i=0;i<300;i++){if(${expression})return true;await new Promise(r=>setTimeout(r,30))}throw Error('autonomy view unavailable '+document.body.innerText)})()`);
for(const [width,height]of [[1440,900],[390,844]])for(const theme of ['light','dark'])test(`AU owner settings ${width}x${height} ${theme}`,{timeout:60000},async t=>{
let writes=0,confirms=0,declines=0,reverses=0,setting={merge:'out of the loop'},revision='a'.repeat(64);
const call=async(verb,args)=>{
if(verb==='autonomy.get.console')return {revision,autonomy:setting,levels:{'cross-role':'out of the loop',merge:setting.merge,policy:'out of the loop',authority:'in the loop'}};
if(verb==='autonomy.set.console'){assert.equal(args.revision,revision);writes++;setting=args.autonomy;revision='b'.repeat(64);return {revision,transaction:'10000000-0000-4000-8000-000000000001'};}
if(verb==='decision.confirm.console'){if(args.confirm)confirms++;else{declines++;assert.equal(args.reason,'Revise the plan');}return {op:args.confirm?'confirmed':'declined'};}
if(verb==='decision.reverse.console'){reverses++;return {reversed:true};}
if(verb==='system-types.console')return {installed:[],enabled:[],project:null};
throw Object.assign(new Error('not-configured'),{code:'not-configured'});
};
const ids=['10000000-0000-4000-8000-000000000001','10000000-0000-4000-8000-000000000002','10000000-0000-4000-8000-000000000003'];
const rows=ids.map((id,i)=>({id,business:'acme',at:'2026-11-11T12:00:00.000Z',class:'gated',action:'git.merge.protected',route_to:'cto',raised_by_role:'coder',raised_by_run:'coder-run',question:'Land the reviewed change?',options:[{key:'yes',text:'Land'},{key:'no',text:'Revise'}],recommendation:'yes',blocking:false,autonomy:i===2?{notice:true,reverseUntil:'2099-01-01T00:00:00Z'}:{pending:true}}));
const trail=async id=>[{...rows.find(r=>r.id===id),table:'decisions'},{table:'events',kind:'action.allowed',body:{operation:'decision.raise',decision:id,approvalChoice:'yes',target:'vikunja:1/2'}}];
const server=await startServer({port:0,sso:createDevOwner(),send:async()=>({}),organizations:{list:async()=>[{id:'acme',label:'Acme',busHealthy:true}],resolve:async()=>({busHealthy:true,decisionWrite:async()=>({}),requestRead:call,requestWrite:call,send:async()=>({}),reads:{business:async()=>({business:{id:'acme',projects:[],instances:[]}}),settings:async()=>({}),queue:async()=>({rows:[]})},bus:{instances:async()=>({instances:{}}),agents:async()=>[],'thread.claims':async()=>[],tasks:async()=>[],inbox:async()=>rows,trail,messages:async()=>({messages:[]})},chat:{projects:async()=>[],threads:async()=>[]}})},log:()=>{}});
t.after(()=>close(server));const b=await browser({machine:false});t.after(()=>b.close());await b.viewport(width,height);
await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/acme/organization`);
await waitFor(b,`document.querySelector('[data-autonomy-form]')`);
await b.evaluate(`document.documentElement.dataset.mode=${JSON.stringify(theme)};document.querySelector('[data-autonomy-form]').scrollIntoView();true`);
assert.equal(await b.evaluate(`document.querySelector('[data-autonomy-form] select[name=merge]').value`),'out of the loop');
assert.equal(await b.evaluate(`document.documentElement.scrollWidth<=innerWidth`),true);
await b.evaluate(`document.querySelector('[data-autonomy-form] select[name=merge]').value='in the loop';document.querySelector('[data-autonomy-form]').requestSubmit();true`);
await waitFor(b,`/Saved/.test(document.querySelector('[data-autonomy-status]').textContent)`);assert.equal(writes,1);assert.equal(setting.merge,'in the loop');
mkdirSync(shots,{recursive:true});await b.evaluate(`document.querySelector('[data-autonomy]').scrollIntoView();true`);await b.screenshot(join(shots,`autonomy-${width}-${theme}.png`));
const detail=async(id,selector)=>{
await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/acme/inbox/${id}`);
await waitFor(b,`document.querySelector(${JSON.stringify(selector)})`);
await b.evaluate(`document.documentElement.dataset.mode=${JSON.stringify(theme)};document.querySelector('[data-autonomy-decision]').scrollIntoView();true`);
assert.equal(await b.evaluate(`document.documentElement.scrollWidth<=innerWidth`),true);
assert.equal(await b.evaluate(`document.querySelector('[data-autonomy-decision]').dataset.autonomyDecision`),id);
};
await detail(ids[0],'[data-autonomy-confirm=yes]');
await b.evaluate(`document.querySelector('[data-autonomy-confirm=yes]').click();true`);
await waitFor(b,`/Confirmation recorded/.test(document.querySelector('[data-autonomy-status]').textContent)`);assert.equal(confirms,1);
await b.screenshot(join(shots,`confirm-${width}-${theme}.png`));
await detail(ids[1],'[data-autonomy-confirm=no]');
await b.evaluate(`document.querySelector('[data-autonomy-reason]').value='Revise the plan';document.querySelector('[data-autonomy-confirm=no]').click();true`);
await waitFor(b,`/Returned to arbiter/.test(document.querySelector('[data-autonomy-status]').textContent)`);assert.equal(declines,1);
await b.screenshot(join(shots,`decline-${width}-${theme}.png`));
await detail(ids[2],'[data-autonomy-reverse]');
await b.evaluate(`document.querySelector('[data-autonomy-reverse]').click();true`);
await waitFor(b,`/Reversal recorded/.test(document.querySelector('[data-autonomy-status]').textContent)`);assert.equal(reverses,1);
await b.screenshot(join(shots,`reverse-${width}-${theme}.png`));
});