import test,{after} from 'node:test'; import assert from 'node:assert/strict'; import { createServer } from 'node:http'; import { createServer as tcpServer,createConnection } from 'node:net'; import { createSocket } from 'node:dgram'; import { readFileSync, readdirSync, existsSync,writeFileSync,lstatSync,readlinkSync } from 'node:fs'; import { join } from 'node:path'; import { spawn } from 'node:child_process'; import { start,callBrowser,testURL } from './ui-browser.mjs'; import { prepare,serve } from './isolated-identity.mjs'; import { fixture,closeServer,closeChild,ownedProcesses } from './test-support.mjs'; const pause=ms=>new Promise(r=>setTimeout(r,ms)); test('browser URL guard refuses live, remote, credentials, non-test ports and origin changes',()=>{ for(const u of ['http://127.0.0.1:7332','https://mosaic.woltje.com','http://10.1.1.53:17440','http://localhost:17440','http://u:p@127.0.0.1:17440','https://127.0.0.1:17440','http://127.0.0.1:17443'])assert.throws(()=>testURL(u)); assert.equal(testURL('/chat','http://127.0.0.1:17440').pathname,'/chat'); assert.throws(()=>testURL('https://127.0.0.1:17443','http://127.0.0.1:17440')); }); async function setup(t,html) { const f=fixture(t),identity=join(f.root,'id'); prepare({instance:'ui797',config:f.config,owner:'ui797-owner',runtime:identity,tlsPort:17443}); const hits=[]; const server=createServer((req,res)=>{ hits.push(req.url); if(req.url==='/download'){res.writeHead(200,{'content-disposition':'attachment; filename="product.html"','content-type':'text/html'});res.end('

offline artifact

');return;} if(req.url==='/redirect'){res.writeHead(302,{location:'http://127.0.0.1:7332/live'});res.end();return;} res.setHeader('content-type','text/html');res.end(html); }); await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(17440,'127.0.0.1',resolve);});f.after(()=>closeServer(server)); const evidence=join(f.root,'browser'); f.after(async()=>{if(existsSync(join(evidence,'browser.sock')))await callBrowser(evidence,'stop');}); return {...f,identity,evidence,hits}; } test('real Chromium keeps a session across CLI processes; mouse/keyboard refs, screenshots and actual downloads',async t=>{ const f=await setup(t,`fixture

Browser fixture

Download reviewed HTML`); await start({evidence:f.evidence,origin:'http://127.0.0.1:17440',ca:join(f.identity,'ca.pem')}); const snapshot=await callBrowser(f.evidence,'snapshot');assert.match(snapshot.text,/Browser fixture/); const input=snapshot.elements.find(e=>e.name==='Request');assert.ok(input); const again=await callBrowser(f.evidence,'snapshot');assert.equal(again.elements.find(e=>e.name==='Request').ref,input.ref); await callBrowser(f.evidence,'type',[input.ref,'discard this']); await callBrowser(f.evidence,'press',['Ctrl+A']); await callBrowser(f.evidence,'press',['Backspace']); await callBrowser(f.evidence,'type',[input.ref,'Pomodoro from keyboard']); await callBrowser(f.evidence,'click',['--text','Send']); await callBrowser(f.evidence,'wait',['--text','Pomodoro from keyboard','--timeout','2']); const cli=spawn(process.execPath,['packages/cli/live/ui-browser.mjs','snapshot','--evidence',f.evidence],{stdio:['ignore','pipe','pipe']});let out='',err='';cli.stdout.on('data',c=>out+=c);cli.stderr.on('data',c=>err+=c); const code=await new Promise(r=>cli.once('exit',r));assert.equal(code,0,err);assert.match(out,/Pomodoro from keyboard/); await callBrowser(f.evidence,'screenshot',['fixture']); assert.ok(readFileSync(join(f.evidence,'fixture.png')).length>1000); await callBrowser(f.evidence,'click',['--text','Download reviewed HTML']); await pause(300); const downloads=await callBrowser(f.evidence,'downloads');assert.equal(downloads.length,1);assert.equal(downloads[0].sha256.length,64);assert.equal(readFileSync(downloads[0].file,'utf8'),'

offline artifact

'); await assert.rejects(callBrowser(f.evidence,'goto',['http://127.0.0.1:7332']),/only loopback/); await assert.rejects(callBrowser(f.evidence,'click',['bad-ref']),/stale ref/); await assert.rejects(callBrowser(f.evidence,'wait',['--text','missing','--timeout','0.2']),/timed out/); await callBrowser(f.evidence,'press',['Tab']);await callBrowser(f.evidence,'stop'); const receipts=readFileSync(join(f.evidence,'actions.jsonl'),'utf8').trim().split('\n').map(JSON.parse); assert.equal(receipts.length,18);assert.ok(receipts.some(r=>r.ok===false)); for(const r of receipts){assert.ok(existsSync(join(f.evidence,r.before)));assert.ok(existsSync(join(f.evidence,r.after)));} assert.equal(receipts.map(r=>r.sequence).join(','),Array.from({length:18},(_,i)=>i+1).join(',')); assert.ok(!JSON.stringify(receipts).includes('Pomodoro from keyboard')); assert.ok(readdirSync(join(f.evidence,'profile')).length>0); }); test('a click that opens a native confirm returns it; dialog accept|dismiss answers it and other commands refuse meanwhile',async t=>{ const f=await setup(t,`

Dialog fixture

idle`); await start({evidence:f.evidence,origin:'http://127.0.0.1:17440',ca:join(f.identity,'ca.pem')}); await assert.rejects(callBrowser(f.evidence,'dialog',['accept']),/no dialog is open/); const opened=await callBrowser(f.evidence,'click',['--text','Resume']); assert.deepEqual(opened,{dialog:{type:'confirm',message:'Resume processing?'}}); await assert.rejects(callBrowser(f.evidence,'snapshot'),/confirm dialog is open/); await assert.rejects(callBrowser(f.evidence,'dialog',['maybe']),/dialog accept\|dismiss/); const accepted=await callBrowser(f.evidence,'dialog',['accept']);assert.match(accepted.text,/accepted/); await callBrowser(f.evidence,'click',['--text','Resume']); const dismissed=await callBrowser(f.evidence,'dialog',['dismiss']);assert.match(dismissed.text,/dismissed/); await callBrowser(f.evidence,'stop'); const receipts=readFileSync(join(f.evidence,'actions.jsonl'),'utf8').trim().split('\n').map(JSON.parse); const rows=receipts.slice(-8).map(r=>[r.command,r.ok,r.dialog?.type??null,r.dialog?.answer??null,!!r.before,!!r.after]); assert.deepEqual(rows,[ ['dialog',false,null,null,true,true], ['click',true,'confirm',null,true,false], ['snapshot',false,null,null,false,false], ['dialog',false,null,null,false,false], ['dialog',true,'confirm','accept',false,true], ['click',true,'confirm',null,true,false], ['dialog',true,'confirm','dismiss',false,true], ['stop',true,null,null,true,true], ]); assert.equal(receipts.find(r=>r.dialog?.answer==='accept').dialog.message,'Resume processing?'); for(const r of receipts)for(const file of [r.before,r.after])if(file)assert.ok(existsSync(join(f.evidence,file))); }); test('redirects to the live port are blocked before a request reaches it',async t=>{ const f=await setup(t,'

safe

Escape'); await start({evidence:f.evidence,origin:'http://127.0.0.1:17440',ca:join(f.identity,'ca.pem')}); await assert.rejects(callBrowser(f.evidence,'click',['--text','Escape']),/blocked|only loopback/); assert.ok(f.hits.includes('/redirect')); assert.ok(readFileSync(join(f.evidence,'actions.jsonl'),'utf8').includes('false')); }); test('private CA browser performs the actual authorization redirect into production Console SSO',async t=>{ const {writeFileSync}=await import('node:fs'); const f=fixture(t),identity=join(f.root,'id'),evidence=join(f.root,'browser'); const output=prepare({instance:'ui797',config:f.config,owner:'ui797-owner',runtime:identity,tlsPort:17443}); const config=JSON.parse(readFileSync(f.config));config.console=output.console;writeFileSync(f.config,JSON.stringify(config)); const idp=await serve(identity);f.after(()=>closeServer(idp)); const child=spawn(process.execPath,['packages/webui/src/cli.mjs','serve','--port','17440','--board','http://127.0.0.1:17439','--sso'],{env:{...process.env,MOSAIC_CONFIG:f.config,NODE_EXTRA_CA_CERTS:join(identity,'ca.pem')},stdio:['ignore','pipe','pipe']});let text='';child.stdout.on('data',c=>text+=c);child.stderr.on('data',c=>text+=c); f.after(()=>closeChild(child)); f.after(async()=>{if(existsSync(join(evidence,'browser.sock')))await callBrowser(evidence,'stop');}); await new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error(text)),15000);child.stdout.on('data',()=>{if(text.includes('http://127.0.0.1:17440')){clearTimeout(timer);resolve();}});child.once('exit',()=>{clearTimeout(timer);reject(new Error(text));});}); const initial=await start({evidence,origin:'https://127.0.0.1:17443',ca:join(identity,'ca.pem')}); // The sole provider signs in immediately through redirects during start. const signedIn=await callBrowser(evidence,'wait',['--text','Sign out','--timeout','10']); assert.ok(signedIn.elements.some(e=>e.name==='Sign out')); assert.ok(!text.includes('sso refused')); assert.ok(!readFileSync(join(evidence,'actions.jsonl'),'utf8').includes('code=')); }); test('a different private CA is refused by Chromium with TLS verification enabled',async t=>{ const f=fixture(t),g=fixture(t),id=join(f.root,'id'),other=join(g.root,'id'),evidence=join(f.root,'browser'); for(const [config,runtime] of [[f.config,id],[g.config,other]])prepare({instance:'ui797',config,owner:'ui797-owner',runtime,tlsPort:17443}); const server=await serve(other);f.after(()=>closeServer(server)); f.after(async()=>{if(existsSync(join(evidence,'browser.sock')))await callBrowser(evidence,'stop');}); await assert.rejects(start({evidence,origin:'https://127.0.0.1:17443',ca:join(id,'ca.pem')}),/navigation failed/); assert.ok(readFileSync(join(evidence,'actions.jsonl'),'utf8').includes('false')); }); for(const origin of ['http://127.0.0.1:17440','https://127.0.0.1:17443'])test(`Chromium ${origin} blocks WebSocket handshakes before the forbidden listener sees any TCP connection`,async t=>{ let connections=0,upgrades=0; const outside=createServer((_req,res)=>res.end('forbidden')); outside.on('connection',()=>connections++); outside.on('upgrade',(_req,socket)=>{upgrades++;socket.destroy();}); await new Promise((resolve,reject)=>{outside.once('error',reject);outside.listen(17442,'127.0.0.1',resolve);}); t.after(()=>closeServer(outside)); const f=await setup(t,`

safe

`); if(origin.startsWith('https:')) {const server=await serve(f.identity);f.after(()=>closeServer(server));} await start({evidence:f.evidence,origin,ca:join(f.identity,'ca.pem')}); await assert.rejects(callBrowser(f.evidence,'click',['--text','WebSocket']),/blocked/); await pause(300); assert.equal(connections,0);assert.equal(upgrades,0); await callBrowser(f.evidence,'stop'); }); test('a failing whole test file shuts down its browser and servers before removing fixture roots',async t=>{ const f=fixture(t),file=join(f.root,'failure.test.mjs'); const url=name=>JSON.stringify(new URL(name,import.meta.url).href); writeFileSync(file,`import test from 'node:test'; import {createServer} from 'node:http'; import {join} from 'node:path'; import {fixture,closeServer} from ${url('./test-support.mjs')}; import {prepare} from ${url('./isolated-identity.mjs')}; import {start,callBrowser} from ${url('./ui-browser.mjs')}; test('intentional failure after browser startup',async t=>{ const f=fixture(t),identity=join(f.root,'id'),evidence=join(f.root,'browser'); console.log('FIXTURE_ROOT='+f.root); prepare({instance:'ui797',config:f.config,owner:'ui797-owner',runtime:identity,tlsPort:17443}); const server=createServer((req,res)=>res.end('

failure cleanup

')); await new Promise(r=>server.listen(17440,'127.0.0.1',r));f.after(()=>closeServer(server)); f.after(()=>callBrowser(evidence,'stop')); await start({evidence,origin:'http://127.0.0.1:17440',ca:join(identity,'ca.pem')}); throw new Error('expected-cleanup-failure'); });`); const child=spawn(process.execPath,['--test','--test-reporter=tap',file],{env:{...process.env,NODE_TEST_CONTEXT:undefined},stdio:['ignore','pipe','pipe']}); f.after(()=>closeChild(child)); let output='';child.stdout.on('data',c=>output+=c);child.stderr.on('data',c=>output+=c); const code=await new Promise(resolve=>child.once('close',resolve)); assert.equal(code,1,output);assert.match(output,/expected-cleanup-failure/); const root=output.match(/FIXTURE_ROOT=(\S+)/)?.[1];assert.ok(root,output); assert.equal(existsSync(root),false,output);assert.deepEqual(ownedProcesses(root),[]); }); for(const origin of ['http://127.0.0.1:17440','https://127.0.0.1:17443'])test(`Chromium ${origin} emits no STUN/TURN UDP or TCP to host loopback listeners`,async t=>{ const targets=[],counters=[]; // Each protocol/address has its own independent host listener, outside bwrap. for(const host of ['127.0.0.1','::1','127.0.0.2']) { const address=host.includes(':')?`[${host}]`:host; for(const scheme of ['stun','turn']) { const udp=createSocket(host.includes(':')?'udp6':'udp4'),count={host,scheme,datagrams:0};counters.push(count); udp.on('message',()=>count.datagrams++); await new Promise((resolve,reject)=>{udp.once('error',reject);udp.bind(0,host,resolve);}); count.port=udp.address().port; t.after(()=>new Promise(resolve=>udp.close(resolve))); targets.push({urls:`${scheme}:${address}:${udp.address().port}${scheme==='turn'?'?transport=udp':''}`,username:'test',credential:'test'}); } const tcp=tcpServer(socket=>{count.connections++;socket.destroy();}),count={host,connections:0};counters.push(count); await new Promise((resolve,reject)=>{tcp.once('error',reject);tcp.listen(host==='127.0.0.1'?17442:0,host,resolve);}); count.port=tcp.address().port; t.after(()=>new Promise(resolve=>tcp.close(resolve))); targets.push({urls:`turn:${address}:${tcp.address().port}?transport=tcp`,username:'test',credential:'test'}); } const f=await setup(t,`

ICE boundary

idle`); if(origin.startsWith('https:')){const id=await serve(f.identity);f.after(()=>closeServer(id));} await start({evidence:f.evidence,origin,ca:join(f.identity,'ca.pem')}); assert.equal(lstatSync(join(f.evidence,'browser.sock')).mode&0o777,0o600); assert.equal(lstatSync(join(f.evidence,'proxy.sock')).mode&0o777,0o600); const processes=ownedProcesses(f.root).filter(p=>p.args.includes('--user-data-dir='+f.evidence+'/profile')); assert.ok(processes.length); for(const p of processes.filter(p=>!p.args.includes('--unshare-net')))assert.notEqual(readlinkSync(`/proc/${p.pid}/ns/net`),readlinkSync('/proc/self/ns/net')); await callBrowser(f.evidence,'click',['--text','Send ICE']); await callBrowser(f.evidence,'wait',['--text','9 ICE agents started','--timeout','5']); await pause(2500); await callBrowser(f.evidence,'stop'); t.diagnostic(JSON.stringify({origin,listeners:counters})); for(const count of counters)assert.equal(count.datagrams??count.connections,0,JSON.stringify(count)); }); for(const origin of ['http://127.0.0.1:17440','https://127.0.0.1:17443'])test(`actual Chromium ${origin} cannot reach parent pathname D-Bus`,async t=>{ const f=await setup(t,'

D-Bus boundary

'); if(origin.startsWith('https:')){const id=await serve(f.identity);f.after(()=>closeServer(id));} const path=join(f.root,'dbus.sock');let connections=0,auth=0; const bus=tcpServer(peer=>{connections++;peer.on('error',()=>{});peer.on('data',data=>{if(data.includes(Buffer.from('AUTH EXTERNAL')))auth++;peer.destroy();});peer.setTimeout(100,()=>peer.destroy());}); await new Promise((r,j)=>{bus.once('error',j);bus.listen(path,r);});f.after(()=>new Promise(r=>bus.close(r))); // Prove the fixture is reachable from the host before asserting zero traffic. await new Promise((r,j)=>{const p=createConnection(path);p.once('error',j);p.once('connect',()=>p.end('AUTH EXTERNAL positive control'));p.on('data',()=>{});p.once('close',r);}); assert.equal(connections,1);assert.equal(auth,1);connections=0;auth=0; const saved=process.env.DBUS_SESSION_BUS_ADDRESS; process.env.DBUS_SESSION_BUS_ADDRESS='unix:path='+path; try {await start({evidence:f.evidence,origin,ca:join(f.identity,'ca.pem')});await pause(500);await callBrowser(f.evidence,'stop');} finally {if(saved===undefined)delete process.env.DBUS_SESSION_BUS_ADDRESS;else process.env.DBUS_SESSION_BUS_ADDRESS=saved;} t.diagnostic(JSON.stringify({origin,connections,auth}));assert.equal(connections,0);assert.equal(auth,0); }); test('CHROMIUM diagnostic sees only browser mounts and explicit environment; host sockets and a 0600 file stay hidden',async t=>{ const f=await setup(t,'

Native mount boundary

'); const path=join(f.root,'host-agent.sock'),abstract='\0ui797-native-'+process.pid; let pathnameHits=0,abstractHits=0; for(const [address,count] of [[path,()=>pathnameHits++],[abstract,()=>abstractHits++]]) { const server=tcpServer(peer=>{count();peer.on('error',()=>{});peer.on('data',()=>peer.destroy());}); await new Promise((r,j)=>{server.once('error',j);server.listen(address,r);});f.after(()=>new Promise(r=>server.close(r))); await new Promise((r,j)=>{const p=createConnection(address);p.once('error',j);p.once('connect',()=>p.end('positive'));p.on('data',()=>{});p.once('close',r);}); } assert.equal(pathnameHits,1);assert.equal(abstractHits,1);pathnameHits=abstractHits=0; let tcpHits=0,udpHits=0; const tcp=tcpServer(peer=>{tcpHits++;peer.on('error',()=>{});peer.on('data',()=>peer.destroy());}); await new Promise((r,j)=>{tcp.once('error',j);tcp.listen(0,'127.0.0.1',r);});f.after(()=>new Promise(r=>tcp.close(r))); await new Promise((r,j)=>{const p=createConnection({host:'127.0.0.1',port:tcp.address().port});p.once('error',j);p.once('connect',()=>p.end('positive'));p.on('data',()=>{});p.once('close',r);}); const udp=createSocket('udp4');udp.on('message',()=>udpHits++); await new Promise(r=>udp.bind(0,'127.0.0.1',r));f.after(()=>new Promise(r=>udp.close(r))); await new Promise(r=>udp.send('positive',udp.address().port,'127.0.0.1',r));await pause(50); assert.equal(tcpHits,1);assert.equal(udpHits,1);tcpHits=udpHits=0; const secret=join(f.root,'synthetic-credential');writeFileSync(secret,'synthetic fixture only',{mode:0o600}); assert.equal(readFileSync(secret,'utf8'),'synthetic fixture only'); const output=join(f.evidence,'native.json'),wrapper=join(f.root,'chromium-diagnostic.mjs'); // The supported override is a single read-only executable. It needs no host // script directory or inherited variables, then starts the actual Chromium. writeFileSync(wrapper,`#!${process.execPath} import {createConnection} from 'node:net'; import {createSocket} from 'node:dgram'; import {readFileSync,writeFileSync,existsSync,readlinkSync,closeSync} from 'node:fs'; import {spawn} from 'node:child_process'; const hostNet=${JSON.stringify(readlinkSync('/proc/self/ns/net'))}; if(readlinkSync('/proc/self/ns/net')===hostNet)throw Error('network namespace missing'); const attempts=[]; for(const path of ${JSON.stringify([path,abstract,{host:'127.0.0.1',port:tcp.address().port}])})attempts.push(await new Promise(resolve=>{ const p=createConnection(path);p.setTimeout(300,()=>p.destroy());p.on('connect',()=>{p.end('unexpected');resolve('connected');});p.on('error',e=>resolve(e.code));p.on('close',()=>resolve('closed')); })); const udp=createSocket('udp4');await new Promise(r=>udp.send('negative',${udp.address().port},'127.0.0.1',()=>{udp.close();r();})); let file;try{readFileSync(${JSON.stringify(secret)});file='readable';}catch(e){file=e.code;} writeFileSync(${JSON.stringify(output)},JSON.stringify({attempts,file,environment:process.env,hostRoot:existsSync(${JSON.stringify(f.config)}),hostRuntime:existsSync('/run/dbus/system_bus_socket'),privateHome:process.env.HOME==='/home/browser',privateRuntime:process.env.XDG_RUNTIME_DIR==='/run/browser',nss:existsSync(process.env.HOME+'/.pki/nssdb/cert9.db')})); const child=spawn('/usr/bin/chromium',process.argv.slice(2),{stdio:['ignore','ignore','ignore',3,4]});closeSync(3);closeSync(4); child.once('exit',(code)=>{process.exitCode=code;}); `,{mode:0o700}); const names=['CHROMIUM','DBUS_SESSION_BUS_ADDRESS','SSH_AUTH_SOCK','XDG_RUNTIME_DIR','UI797_SYNTHETIC_SECRET']; const saved=Object.fromEntries(names.map(n=>[n,process.env[n]])); Object.assign(process.env,{CHROMIUM:wrapper,DBUS_SESSION_BUS_ADDRESS:'unix:path='+path,SSH_AUTH_SOCK:path,XDG_RUNTIME_DIR:f.root,UI797_SYNTHETIC_SECRET:'synthetic env canary'}); try {await start({evidence:f.evidence,origin:'http://127.0.0.1:17440',ca:join(f.identity,'ca.pem')});await callBrowser(f.evidence,'stop');} finally {for(const n of names)if(saved[n]===undefined)delete process.env[n];else process.env[n]=saved[n];} const result=JSON.parse(readFileSync(output));t.diagnostic(JSON.stringify(result)); assert.equal(result.file,'ENOENT');assert.equal(result.hostRoot,false);assert.equal(result.hostRuntime,false); assert.equal(result.privateHome,true);assert.equal(result.privateRuntime,true);assert.equal(result.nss,true); assert.ok(result.attempts.every(r=>r!=='connected'));assert.equal(pathnameHits,0);assert.equal(abstractHits,0);assert.equal(tcpHits,0);assert.equal(udpHits,0); assert.equal(result.environment.PWD,'/home/browser'); assert.deepEqual(Object.keys(result.environment).sort(),['HOME','LANG','PATH','PWD','TMPDIR','XDG_CACHE_HOME','XDG_CONFIG_HOME','XDG_DATA_HOME','XDG_RUNTIME_DIR'].sort()); }); after(async()=>{ const {createConnection}=await import('node:net'); for(const port of [17440,17442,17443]) await new Promise((resolve,reject)=>{ const socket=createConnection({host:'127.0.0.1',port}); socket.once('connect',()=>{socket.destroy();reject(new Error(`test listener survived on ${port}`));}); socket.once('error',e=>e.code==='ECONNREFUSED'?resolve():reject(e)); }); });