import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { fixture } from './fixture.mjs'; import { browser } from './browser.mjs'; export const ids = Object.freeze({ project: '10000000-0000-4000-8000-000000000001', empty: '10000000-0000-4000-8000-000000000002', archived: '10000000-0000-4000-8000-000000000003', repository: 'fixture-repository', thread: '20000000-0000-4000-8000-000000000001', session: '30000000-0000-4000-8000-000000000001', observed: '2026-11-05T12:00:00.000Z' }); // All API responses are explicit, and all network destinations outside the fixture // origin are refused. Tests may override a response by assigning window.__scenario. export async function w15Fixture(t, { scenario = {}, slice = 'shell' } = {}) { const f = await fixture(); t.after(() => f.close()); const b = await browser({ machine: false }); t.after(() => b.close()); const errors = []; const networkErrors = []; b.on('Network.responseReceived', ({response}) => { if (response.status >= 400) networkErrors.push({url:response.url,status:response.status}); }); b.on('Network.loadingFailed', e => networkErrors.push({error:e.errorText})); await b.call('Network.enable'); b.on('Fetch.requestPaused', ({ requestId, request }) => { const url = new URL(request.url); const allowed = url.origin === f.base; void b.call(allowed ? 'Fetch.continueRequest' : 'Fetch.failRequest', allowed ? { requestId } : { requestId, errorReason: 'BlockedByClient' }).catch(e => errors.push(String(e))); }); await b.call('Fetch.enable', { patterns: [{ urlPattern: '*' }] }); await b.call('Page.addScriptToEvaluateOnNewDocument', { source: `(() => { const ids = ${JSON.stringify(ids)}; window.__writes=[]; window.__unexpected=[]; window.__errors=[]; window.__scenario=${JSON.stringify(scenario)}; addEventListener('error', e => __errors.push(e.message)); addEventListener('unhandledrejection', e => __errors.push(String(e.reason))); const data = (x,status=200) => new Response(JSON.stringify({at:ids.observed,...x}), {status,headers:{'content-type':'application/json'}}); const real = fetch.bind(window); window.fetch = async (input, init={}) => { const url=new URL(typeof input==='string' ? input : input.url, location.href), path=url.pathname+url.search, method=(init.method||'GET').toUpperCase(); if (url.origin!==location.origin) { __unexpected.push(method+' '+url.href); return new Response('refused',{status:403}); } if (!url.pathname.startsWith('/api/')) return real(input,init); if(method!=='GET') __writes.push({method,path,body:init.body||null}); const key = method+' '+path; if (Object.hasOwn(__scenario,key)) { const response=__scenario[key]; return data(response.body ?? {},response.status ?? 200); } if(method!=='GET') { __unexpected.push(key); return data({error:'fixture-write-refused'},403); } if (Object.hasOwn(__scenario,path)) return data(__scenario[path]); if(/^\\/api\\/library\\/status\\?project=[^&]+$/.test(path) || path==='/api/processing' || path==='/api/bus-status' || path==='/api/queue') return new Response(JSON.stringify({error:'fixture-unavailable'}),{status:503,headers:{'content-type':'application/json'}}); if(path==='/api/themes') return real(input,init); // S4: the installed theme modules, read-only, from the fixture's own server if(path==='/api/config') return data({send:true,edit:false,decide:false}); if(path==='/api/business') return data({projects:[ids.repository]}); if(path==='/api/chat/projects/archived') return data({rows:[]}); if(path==='/api/chat/projects') return data({rows:[{id:ids.project,name:'Active project',repo:ids.repository},{id:ids.empty,name:'Empty project',repo:null},{id:ids.archived,name:'Archived project',archived:true,repo:null}]}); if(path==='/api/chat/instances') return data({rows:[{instance:'assistant',name:'Assistant'}]}); if(/^\\/api\\/chat\\/threads\\/[^/]+\\/shares$/.test(path)) return data({rows:[]}); if(/^\\/api\\/chat\\/threads\\/[^/]+\\/sessions$/.test(path)) return data({rows:[{id:ids.session,name:'Default',default:true}]}); if(/^\\/api\\/chat\\/threads\\/[^/]+\\/messages(?:\\?.*)?$/.test(path)) return data({rows:[],after:null,hasMore:false}); if(path==='/api/chat/threads' || /^\\/api\\/chat\\/threads\\?(shared=true|project=)/.test(path)) return data({rows:[{id:ids.thread,project:ids.project,name:'Project conversation',instance:'assistant'}]}); if(/^\\/api\\/bus\\/(inbox|messages|agents|tasks|queue|trail)(\\?|$)/.test(path)) return data({rows:[],unread:0}); __unexpected.push(method+' '+path); return new Response(JSON.stringify({error:'fixture-unexpected-api'}),{status:403,headers:{'content-type':'application/json'}}); }; })();` }); const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(${expression})return true;await new Promise(r=>setTimeout(r,40));}throw new Error('Timed out: '+${JSON.stringify(expression)})})()`); async function capture(state, width, route) { if (!process.env.MOSAIC_UX_SHOTS) return; const dir = process.env.MOSAIC_UX_SHOTS; await mkdir(dir, { recursive: true }); await b.evaluate('document.fonts.ready'); const prefix = `${slice}-${state}-${width}`; await b.screenshot(join(dir, `${prefix}.png`)); const metrics = await b.call('Page.getLayoutMetrics'); const size = metrics.cssContentSize || metrics.contentSize; const { data } = await b.call('Page.captureScreenshot', { format:'png', captureBeyondViewport:true, clip:{x:0,y:0,width:Math.ceil(size.width),height:Math.ceil(size.height),scale:1} }); await writeFile(join(dir, `${prefix}-full.png`), Buffer.from(data,'base64')); return {state,route,width,height:1000,observation:ids.observed,viewport:`${prefix}.png`,full:`${prefix}-full.png`,errors:[...errors,...await b.evaluate('window.__errors')],unexpected:await b.evaluate('window.__unexpected'),networkErrors:[...networkErrors]}; } return { f, b, wait, capture, errors }; }