import test from 'node:test'; import assert from 'node:assert/strict'; import { w15Fixture } from './w15-fixture.mjs'; test('Settings proxy navigation keeps the root document through Settings and Dashboard', {timeout:90000}, async t => { const {f,b,wait}=await w15Fixture(t,{scenario:{'/api/config':{devOwner:true,send:true},'/api/proxy/accounts':{accounts:[]},'/api/proxy/keys':{keys:[]}}}); await b.navigate(f.base+'/#/settings'); await wait('!!document.querySelector("#settings-nav")'); assert.equal(await b.evaluate('location.pathname'), '/'); await b.evaluate('[...document.querySelectorAll("#settings-nav a")].find(a=>a.textContent.includes("Model proxy")).click()'); await new Promise(r=>setTimeout(r,500)); assert.equal(await b.evaluate('location.pathname'), '/'); await wait('document.querySelector("#settings-page-title")?.textContent==="Model proxy" && !!document.querySelector("#proxy-accounts")'); await b.evaluate('[...document.querySelectorAll("#settings-nav a")].find(a=>a.textContent.includes("Appearance")).click()'); await wait('document.querySelector("#settings-page-title")?.textContent==="Appearance" && !!document.querySelector(".settings-appearance")'); assert.equal(await b.evaluate('location.pathname'), '/'); await b.evaluate('document.querySelector(".wordmark").click()'); await wait('location.hash==="#/" && !document.querySelector("#board-head").hidden'); assert.equal(await b.evaluate('location.pathname'), '/'); assert.equal(await b.evaluate('document.querySelector("#board-head h1").textContent'), 'Dashboard'); }); test('Settings opens browser appearance and its section menu', {timeout:90000}, async t => { const {f,b,wait}=await w15Fixture(t); await b.navigate(f.base+'/#/settings'); await wait('!!document.querySelector("#settings-nav")'); assert.equal(await b.evaluate('document.querySelector("#settings-page-title").textContent'), 'Appearance'); // The tiles are the installed theme modules (S4), so they follow the /api/themes answer: ten once it has arrived, Harbor alone before. await wait('document.querySelectorAll("input[name=palette]").length===10'); assert.equal(await b.evaluate('document.querySelectorAll("input[name=palette]").length'), 10); assert.equal(await b.evaluate('document.querySelector("#avatar-menu a").textContent'), 'Settings'); }); test('browser choices persist, disclosure works, and controls fit at phone width', {timeout:90000}, async t => { const {f,b,wait}=await w15Fixture(t); await b.viewport(390,900); await b.navigate(f.base+'/#/settings'); await wait('document.querySelectorAll(".palette-tile").length===10'); assert.equal(await b.evaluate('document.querySelector("#settings-disclosure").getAttribute("aria-expanded")'), 'false'); await b.evaluate('document.querySelector("#settings-disclosure").click()'); assert.equal(await b.evaluate('document.querySelector("#settings-disclosure").getAttribute("aria-expanded")'), 'true'); await b.key('Escape'); assert.equal(await b.evaluate('document.activeElement.id'), 'settings-disclosure'); await b.evaluate('document.querySelector("[name=palette][value=grove]").click();document.querySelector("[name=density][value=compact]").click()'); assert.equal(await b.evaluate('document.documentElement.dataset.palette'), 'grove'); assert.equal(await b.evaluate('document.documentElement.dataset.sidebarDensity'), 'compact'); assert.equal(await b.evaluate('document.documentElement.scrollWidth<=innerWidth'), true); assert.equal(await b.evaluate('[...document.querySelectorAll(".palette-content,.settings-segments span,#settings-disclosure")].every(e=>e.getBoundingClientRect().height>=44&&e.getBoundingClientRect().width>=44)'),true); await b.call('Page.reload'); await wait('document.querySelector("[name=palette][value=grove]")?.checked'); assert.equal(await b.evaluate('document.querySelector("[name=density][value=compact]").checked'),true); await b.evaluate('location.hash="#/tasks"'); await wait('location.hash==="#/tasks"'); assert.equal(await b.evaluate('!!document.querySelector("dialog[open]")'),false); }); test('server sections render and a routing draft can be discarded at phone width', {timeout:90000}, async t => { const revision='a'.repeat(64); const routes={revision,harnesses:['claude-code'],models:['opus'],levels:{'claude-code':['high','xhigh']},instances:{chief:{harness:'claude-code',model:'opus',thinking:'high'}}}; const launch={revision,models:{opus:{provider:'p',model:'m'}},limits:{},usage:{},assistants:{}}; const instances={revision,launch:true,instances:{chief:{definition:'chief',launcher:true,launched:false,kept:false,running:[]}},definitions:{chief:{accepted:true,services:[]}},noHire:[],retired:[]}; const {f,b,wait}=await w15Fixture(t,{scenario:{'/api/settings':{launch,notifier:{binding:null}},'/api/bus/routes':{rows:[routes]},'/api/bus/instances':{rows:[instances]},'/api/bus/names':{rows:[]},'/api/bus/limits':{rows:[]},'/api/bus/usage':{rows:[]}}}); await b.viewport(390,900); for(const [path,title] of [['limits','Launch limits'],['routing','Routing'],['agents','Agents'],['assistant','Assistant'],['tokens','Token use'],['model-proxy','Model proxy'],['about','About']]) { await b.navigate(f.base+'/#/settings/'+path); await wait(`document.querySelector('#settings-page-title')?.textContent===${JSON.stringify(title)}`); assert.equal(await b.evaluate('document.documentElement.scrollWidth<=innerWidth'),true,path); const undersized=await b.evaluate(`[...document.querySelectorAll('.settings-content button,.settings-content select,.settings-content input:not([type=checkbox]):not([type=radio]):not([type=hidden]),.settings-content summary')].filter(e=>e.getClientRects().length).filter(e=>{const r=e.getBoundingClientRect();return r.width<44||r.height<44}).map(e=>e.outerHTML.slice(0,150))`); assert.deepEqual(undersized,[],path+' control targets'); } await b.navigate(f.base+'/#/settings/routing'); await wait('!!document.querySelector("#routes-table select")'); await b.evaluate(`(()=>{const e=[...document.querySelectorAll('[data-route]')].find(e=>JSON.parse(e.dataset.route)[1]==='thinking');e.value='xhigh';e.dispatchEvent(new Event('change',{bubbles:true}));})()`); assert.equal(await b.evaluate('!!document.querySelector("#settings-nav .settings-dot")'),true); assert.equal(await b.evaluate('getComputedStyle(document.querySelector(".settings-actions")).position'),'fixed'); await b.evaluate('document.querySelector("#routes-reload").click()'); await wait('!!document.querySelector("#routes-table select") && !document.querySelector("#settings-nav .settings-dot")'); assert.equal(await b.evaluate(`[...document.querySelectorAll('[data-route]')].find(e=>JSON.parse(e.dataset.route)[1]==='thinking').value`),'high'); assert.deepEqual(await b.evaluate('window.__writes'),[]); }); test('agent More menu and confirmation dialogs support keyboard and pointer without posting on cancel', {timeout:90000}, async t => { const revision='a'.repeat(64), instances={revision,launch:true,instances:{pm:{definition:'coder',launched:true,kept:false,running:[],protected:false,endProtected:false}},definitions:{coder:{accepted:true,services:[]}},noHire:[],retired:[]}; const {f,b,wait}=await w15Fixture(t,{scenario:{'/api/settings':{notifier:{}},'/api/bus/instances':{rows:[instances]}}}); await b.viewport(1440,1000); await b.navigate(f.base+'/#/settings/agents'); await wait('!!document.querySelector(".agent-more")'); await b.evaluate('document.querySelector(".agent-more").focus()'); await b.key('ArrowDown'); assert.equal(await b.evaluate('document.querySelector(".agent-more").getAttribute("aria-expanded")'),'true'); assert.equal(await b.evaluate('document.activeElement.textContent'),'Edit'); assert.equal(await b.evaluate(`(()=>{const m=document.querySelector('.agent-menu'), last=m.querySelector('[data-agent-dialog=delete]'), r=last.getBoundingClientRect();return document.elementFromPoint(r.left+r.width/2,r.top+r.height/2)===last})()`),true,'the full menu is visible and clickable outside the table'); await b.key('ArrowDown'); assert.equal(await b.evaluate('document.activeElement.textContent'),'Terminate…'); await b.key('Enter'); assert.equal(await b.evaluate('document.querySelector("#agent-dialog").open'),true); assert.equal(await b.evaluate('document.activeElement.id'),'agent-dialog-reason'); await b.key('Escape'); assert.equal(await b.evaluate('document.activeElement.className'),'agent-more'); await b.evaluate('document.querySelector(".agent-more").click();document.querySelector("[data-agent-dialog=delete]").click()'); assert.equal(await b.evaluate('document.activeElement.id'),'agent-dialog-cancel'); assert.match(await b.evaluate('document.querySelector("#agent-dialog-copy").textContent'),/evidence.*workspace/); await b.evaluate('document.querySelector("#agent-dialog-cancel").click()'); assert.deepEqual(await b.evaluate('window.__writes'),[]); }); test('dev-owner send enables proxy controls after a delayed config read', {timeout:90000}, async t => { const {f,b,wait}=await w15Fixture(t,{scenario:{'/api/proxy/accounts':{accounts:[]},'/api/proxy/keys':{keys:[]}}}); await b.call('Page.addScriptToEvaluateOnNewDocument',{source:`(()=>{const original=fetch;window.fetch=async(u,o)=>{if(u==='/api/config'){await new Promise(r=>setTimeout(r,300));return new Response(JSON.stringify({devOwner:true,send:true,decide:true,edit:true,sso:true}));}return original(u,o)}})()`}); await b.navigate(f.base+'/#/settings/model-proxy'); await wait('document.querySelector("#proxy-login-start [data-write]")?.disabled===false'); assert.equal(await b.evaluate('!!document.querySelector(".settings-readonly")'),false); assert.equal(await b.evaluate('document.querySelector("#proxy-apikey [data-write]").disabled'),false); }); test('hiring switches lead names and launch settings inputs align in columns', {timeout:90000}, async t => { const revision='a'.repeat(64), instances={revision,launch:true,instances:{coder:{definition:'coder',launched:true}},definitions:{coder:{accepted:true,services:[]},reviewer:{accepted:true,services:[]}},noHire:[],retired:[]}; const launch={revision,models:{opus:{provider:'anthropic',model:'opus'},sonnet:{provider:'anthropic',model:'sonnet'},haiku:{provider:'anthropic',model:'haiku'}},limits:{providers:{anthropic:4},models:{anthropic:{opus:4,sonnet:4,haiku:20}}},usage:{}}; const {f,b,wait}=await w15Fixture(t,{scenario:{'/api/settings':{launch,notifier:{}},'/api/bus/instances':{rows:[instances]}}}); await b.viewport(1440,1000);await b.navigate(f.base+'/#/settings/agents');await wait('document.querySelectorAll(".hiring-grid [role=switch]").length===2'); const switchesLead=await b.evaluate('[...document.querySelectorAll(".hiring-grid > div")].every(row=>row.querySelector("[role=switch]").getBoundingClientRect().right<=row.querySelector(":scope > span").getBoundingClientRect().left)'); const formLayout=await b.evaluate(`(()=>{const f=document.querySelector('#instance-add-form'),labels=[...f.querySelectorAll('.filters>label')],boxes=labels.map(e=>e.getBoundingClientRect()),details=f.querySelector('details').getBoundingClientRect(),button=f.querySelector('button[type=submit]').getBoundingClientRect();return {separate:boxes[1].left>=boxes[0].right&&boxes[2].top>=boxes[0].bottom,gap:button.top-details.bottom,headingOutline:getComputedStyle(document.querySelector('#settings-page-title')).outlineStyle}})()`); assert.equal(formLayout.separate,true,JSON.stringify(formLayout));assert.ok(formLayout.gap>=12,JSON.stringify(formLayout));assert.equal(formLayout.headingOutline,'none'); await b.evaluate('location.hash="#/settings/limits"');await wait('document.querySelectorAll("#launch-settings-form input[type=number]").length>=5'); const positions=await b.evaluate(`(()=>{const f=document.querySelector('#launch-settings-form'), numbers=[...f.querySelectorAll('input[type=number]')], table=[...f.querySelectorAll('[data-setting]')].filter(e=>JSON.parse(e.dataset.setting)[0]==='table');const x=e=>Math.round(e.getBoundingClientRect().left);return {limits:numbers.map(x),providers:table.filter(e=>JSON.parse(e.dataset.setting)[2]==='provider').map(x),models:table.filter(e=>JSON.parse(e.dataset.setting)[2]==='model').map(x)};})()`); for(const column of Object.values(positions)) assert.equal(new Set(column).size,1,JSON.stringify(positions)); assert.equal(switchesLead,true); }); test('an unconfigured proxy is one empty state without forms or a false read-only note', {timeout:90000}, async t => { const {f,b,wait}=await w15Fixture(t,{scenario:{'/api/config':{send:true,devOwner:true},'GET /api/proxy/accounts':{status:503,body:{error:'not-configured'}},'GET /api/proxy/keys':{status:503,body:{error:'not-configured'}}}}); await b.navigate(f.base+'/#/settings/model-proxy');await wait('document.querySelector("#proxy-message")?.textContent.includes("not configured")'); assert.equal(await b.evaluate('document.querySelector("#proxy-message").classList.contains("empty")'),true); assert.equal(await b.evaluate('!!document.querySelector(".settings-readonly")'),false); assert.equal(await b.evaluate('[...document.querySelectorAll(".settings-model-proxy .panel:not(#proxy-dev-setup)")].every(e=>e.hidden)'),true); // F3: a dev Console adds one note naming the terminal setup. It holds no form and no input: the // management key is never typed into this page. assert.equal(await b.evaluate('document.querySelector("#proxy-dev-setup code")?.textContent'),'mosaic proxy fetch\nmosaic proxy init'); assert.equal(await b.evaluate('document.querySelectorAll("#proxy-dev-setup form,#proxy-dev-setup input,#proxy-dev-setup textarea").length'),0); }); test('an unconfigured proxy without the dev owner shows no setup note', {timeout:90000}, async t => { const {f,b,wait}=await w15Fixture(t,{scenario:{'/api/config':{send:true},'GET /api/proxy/accounts':{status:503,body:{error:'not-configured'}},'GET /api/proxy/keys':{status:503,body:{error:'not-configured'}}}}); await b.navigate(f.base+'/#/settings/model-proxy');await wait('document.querySelector("#proxy-message")?.textContent.includes("not configured")'); assert.equal(await b.evaluate('!!document.querySelector("#proxy-dev-setup")'),false); assert.equal(await b.evaluate('[...document.querySelectorAll(".settings-model-proxy .panel")].every(e=>e.hidden)'),true); });