Files
stack/packages/cli/live/ui-browser.test.mjs
jason.woltje 5df42478b6 fix(cli): answer native dialogs in the tester browser (#797)
A click on the test Console's Resume button opened window.confirm and the
browser daemon hung: the mouse release, screenshots and the frame tree all
block until the dialog is answered, and ui-browser had no way to answer it.

A click that opens a dialog now returns {dialog:{type,message}}. The new
command `dialog accept|dismiss` answers it and records the type, message
and answer in the receipt. While a dialog is open other commands except
stop refuse (journaled), and no screenshot or URL check is attempted.
A real-Chromium confirm test covers accept, dismiss and the refusals.
2026-11-06 00:13:16 -06:00

280 lines
22 KiB
JavaScript

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:[email protected]: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('<h1>offline artifact</h1>');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,`<!doctype html><title>fixture</title><h1>Browser fixture</h1><input aria-label="Request"><button onclick="document.querySelector('output').textContent=document.querySelector('input').value">Send</button><output></output><a href="/download">Download reviewed HTML</a>`);
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'),'<h1>offline artifact</h1>');
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,`<!doctype html><h1>Dialog fixture</h1><button onclick="document.querySelector('output').textContent=confirm('Resume processing?')?'accepted':'dismissed'">Resume</button><output>idle</output>`);
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,'<!doctype html><h1>safe</h1><a href="/redirect">Escape</a>');
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,`<!doctype html><h1>safe</h1><button onclick="window.ws=new WebSocket('ws://127.0.0.1:17442/nope');window.wss=new WebSocket('wss://127.0.0.1:17442/nope')">WebSocket</button>`);
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('<h1>failure cleanup</h1>'));
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,`<!doctype html><h1>ICE boundary</h1><button onclick="go()">Send ICE</button><output>idle</output><script>
const pcs=[];async function go(){for(const server of ${JSON.stringify(targets)}){const pc=new RTCPeerConnection({iceServers:[server]});pcs.push(pc);pc.createDataChannel('probe');await pc.setLocalDescription(await pc.createOffer());}document.querySelector('output').textContent='9 ICE agents started';}</script>`);
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,'<!doctype html><h1>D-Bus boundary</h1>');
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,'<!doctype html><h1>Native mount boundary</h1>');
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));
});
});