import test from 'node:test'; import assert from 'node:assert/strict'; import vm from 'node:vm'; import {createRequire} from 'node:module'; const {humanInteractionScript}=createRequire(import.meta.url)('../apps/browser/human-interaction.cjs'); const token='a'.repeat(32),url='https://intranet.example/path?private=1'; function fixture(){ let time=1000;const listeners=new Map();const win={addEventListener:(type,fn)=>listeners.set(type,fn),removeEventListener:(type,fn)=>{if(listeners.get(type)===fn)listeners.delete(type);}};win.top=win; const context=vm.createContext({window:win,location:{href:url},innerWidth:1000,innerHeight:800,navigator:{userActivation:{isActive:true}},Date:{now:()=>time}}); const run=mode=>vm.runInContext(humanInteractionScript(mode,{url,token}),context); const target=(tag='BUTTON',attrs={})=>({nodeType:1,tagName:tag,isConnected:true,parentElement:null,type:'button',hasAttribute:key=>Object.hasOwn(attrs,key),getAttribute:key=>attrs[key]||null,closest:()=>null}); const emit=(type,element=target(),extra={})=>listeners.get(type)?.({type,target:element,isTrusted:true,clientX:12.2,clientY:31.8,...extra}); return {run,target,emit,listeners,context,advance:ms=>{time+=ms;}}; } test('trusted observation exports structural metadata only, rejects synthetic events',()=>{ const f=fixture();assert.equal(f.run('install').installed,true); f.emit('click',f.target('BUTTON',{'aria-label':'Confidential deal','data-value':'sensitive'}),{isTrusted:false}); assert.equal(f.run('drain').events.length,0); f.emit('click',f.target('BUTTON',{'aria-label':'ordinary button','id':'id-private','data-value':'sensitive'})); const result=JSON.parse(JSON.stringify(f.run('drain'))); assert.deepEqual(result.events,[{action:'click',outcome:'observed',sensitiveField:false,target:{tag:'BUTTON',x:12,y:32}}]); assert.equal(JSON.stringify(result).includes('id-private'),false); }); test('password, credential ancestors and sensitive forms never contribute metadata',()=>{ const f=fixture();f.run('install');const password=f.target('INPUT');password.type='password'; f.emit('click',password);const child=f.target();child.parentElement=f.target('DIV',{'data-sensitive':''});f.emit('click',child); const form=f.target('FORM');form.elements=[password];f.emit('submit',form);const button=f.target();button.form=form;f.emit('click',button); assert.equal(f.run('drain').events.length,0); }); test('submit is observed, not evidence of transmission, and carries no fields',()=>{ const f=fixture();f.run('install');const form=f.target('FORM');form.elements=[f.target('INPUT')];form.elements[0].value='company confidential'; f.emit('keydown',form.elements[0],{key:'Enter'}); f.emit('submit',form,{defaultPrevented:true}); assert.deepEqual(JSON.parse(JSON.stringify(f.run('drain').events)),[{action:'submit',outcome:'observed',sensitiveField:false,target:{tag:'OTHER'}}]); }); test('queue and rate are bounded, lease expiry and navigation prevent new capture',()=>{ const f=fixture();f.run('install');for(let i=0;i<1000;i++)f.emit('click'); let result=f.run('drain');assert.equal(result.events.length,16);assert.equal(result.dropped,984); for(let i=0;i<100;i++)f.emit('click');assert.equal(f.run('drain').events.length,4); f.advance(1600);f.emit('click');assert.equal(f.run('drain').events.length,0); f.context.location.href='https://other.example/';f.emit('click');assert.equal(f.run('drain'),null); }); test('replacement and dispose remove listeners and pending metadata',()=>{ const f=fixture();f.run('install');f.emit('click');f.run('install');assert.equal(f.run('drain').events.length,0); f.run('dispose');assert.equal(f.listeners.size,0);assert.equal(f.run('drain'),null); assert.throws(()=>humanInteractionScript('install',{url,token:'untrusted'})); }); const {createHumanInteractionObserver}=createRequire(import.meta.url)('../apps/browser/human-interaction.cjs'); function controllerFixture(){ const dom=fixture();const contents={isDestroyed:()=>false};let context={ready:true,enabled:true,foreground:true,sessionId:'c391515c-cd19-4009-b042-e04d723d9f11',url,token:'nav-1',policyVersion:1,webContents:contents};const seen=[]; const observer=createHumanInteractionObserver({getContext:()=>context,isCurrent:s=>context.ready&&context.enabled&&s.token===context.token&&s.policyVersion===context.policyVersion&&s.foreground===context.foreground,executeScript:async(_contents,code)=>vm.runInContext(code,dom.context),onObserved:async event=>seen.push(event)}); return {dom,observer,seen,set:update=>{context={...context,...update};}}; } test('controller binds current navigation and policy and drops queued stale metadata',async()=>{ const f=controllerFixture();try{ await f.observer.poll();f.dom.emit('click');await f.observer.poll();assert.equal(f.seen.length,1); f.dom.emit('click');f.set({token:'nav-2'});await f.observer.poll();await f.observer.poll();assert.equal(f.seen.length,1); f.dom.emit('click');f.set({enabled:false});await f.observer.poll();assert.equal(f.dom.listeners.size,0);assert.equal(f.seen.length,1); }finally{f.observer.dispose();} }); test('refresh and dispose invalidate in-flight batches without replay',async()=>{ let resolve;const context={ready:true,enabled:true,foreground:true,sessionId:'c391515c-cd19-4009-b042-e04d723d9f11',url,token:'nav-1',policyVersion:1,webContents:{isDestroyed:()=>false}};const seen=[];let calls=0; const observer=createHumanInteractionObserver({getContext:()=>context,isCurrent:()=>true,executeScript:async()=>{calls++;if(calls===1)return {installed:true};return new Promise(r=>{resolve=r;});},onObserved:async value=>seen.push(value)}); try{await observer.poll();const pending=observer.poll();observer.refresh();resolve({events:[{action:'click',outcome:'observed',sensitiveField:false,target:{tag:'BUTTON'}}],dropped:0});await pending;assert.equal(seen.length,0);}finally{observer.dispose();} }); test('submit without native user activation is excluded',()=>{ const f=fixture();f.run('install');f.context.navigator.userActivation.isActive=false;const form=f.target('FORM');form.elements=[];f.emit('submit',form);assert.equal(f.run('drain').events.length,0); }); test('late DOM drain remains serialized and expires without invoking audit',async()=>{ const context={ready:true,enabled:true,foreground:true,sessionId:'c391515c-cd19-4009-b042-e04d723d9f11',url,token:'nav-1',policyVersion:1,webContents:{isDestroyed:()=>false}};let resolve,calls=0;const seen=[]; const observer=createHumanInteractionObserver({deadline:100,getContext:()=>context,isCurrent:()=>true,executeScript:async()=>{calls++;if(calls===1)return {installed:true};if(calls===2)return new Promise(r=>{resolve=r;});return null;},onObserved:async event=>seen.push(event)}); try{await observer.poll();const pending=observer.poll();await new Promise(r=>setTimeout(r,120));await observer.poll();assert.equal(calls,2);resolve({events:[{action:'click',outcome:'observed',sensitiveField:false,target:{tag:'BUTTON'}}],dropped:0});await pending;assert.equal(seen.length,0);}finally{observer.dispose();} }); test('destroyed renderer disposal errors never escape lifecycle callback',async()=>{ const context={ready:true,enabled:true,foreground:true,sessionId:'c391515c-cd19-4009-b042-e04d723d9f11',url,token:'nav-1',policyVersion:1,webContents:{isDestroyed:()=>false}};let installed=false; const observer=createHumanInteractionObserver({getContext:()=>context,isCurrent:()=>true,executeScript:()=>{if(!installed){installed=true;return {installed:true};}throw Error('Destroyed renderer');},onObserved:()=>{}}); await observer.poll();assert.doesNotThrow(()=>observer.dispose());await new Promise(r=>setImmediate(r)); }); test('trusted programmatic submit without recent click or Enter is excluded',()=>{ const f=fixture();f.run('install');const form=f.target('FORM');form.elements=[];f.emit('submit',form);assert.equal(f.run('drain').events.length,0); f.emit('keydown',form,{key:'Enter'});f.advance(1100);f.emit('submit',form);assert.equal(f.run('drain').events.length,0); });