import test from 'node:test'; import assert from 'node:assert/strict'; import {createRequire} from 'node:module'; const require=createRequire(import.meta.url); const {buildInteractionEvidence,validateInteractionEvidence}=require('../apps/browser/evidence-capture.cjs'); const {collectionAllowed,stripPendingEvidence,deliverAuditRecord,validatedEvidence}=require('../apps/browser/evidence-delivery.cjs'); const sessionId='00000000-0000-4000-8000-000000000001'; test('human-observer accepts observed click/submit only and uses its own audit source',async()=>{ const policy={expiresAt:new Date(Date.now()+60000).toISOString(),evidence:{enabled:true,captureInteractions:true}},context={ready:true,deviceId:'device-a',policy}; for(const action of ['click','submit']){ const payload=buildInteractionEvidence({action,sessionId,url:'https://example.com/path?secret=1',sensitiveField:false,outcome:'observed',captureSource:'human-observer'},policy); assert.equal(payload.pageUrl,'https://example.com/');assert.equal(payload.outcome,'observed'); for(const extra of [{outcome:'completed'},{action:'copy'},{bytes:1},{captureSource:'agent-adapter'}])assert.equal(validateInteractionEvidence({...payload,...extra}),false); const record={kind:'browser.interaction',eventId:sessionId,protectedEvidence:{deviceId:'device-a',type:'interaction',payload}},routes=[]; assert.equal((await deliverAuditRecord({record,getContext:()=>context,send:async route=>{routes.push(route);return {id:sessionId,sourceEventId:1};}})).evidenceDelivered,true); assert.deepEqual(routes,['/api/device/events','/api/device/evidence']);record.kind='agent.action';await assert.rejects(deliverAuditRecord({record,getContext:()=>context,send:async()=>({})}),/source/); } }); function fixture(){const context={ready:true,deviceId:'device-a',policy:{expiresAt:new Date(Date.now()+60000).toISOString(),evidence:{enabled:true,captureDownloads:true,captureInteractions:true}}};const payload=buildInteractionEvidence({action:'copy',sessionId,url:'https://example.com/private/password=secret?q=secret#secret',sensitiveField:false,outcome:'completed',captureSource:'guarded-clipboard',bytes:12},context.policy);const record={eventId:sessionId,kind:'clipboard.copy',protectedEvidence:{deviceId:'device-a',type:'interaction',payload}};return {context,payload,record};} test('automatic interaction capture requires observed completion and excludes path and sensitive data',()=>{const f=fixture();assert.equal(f.payload.pageUrl,'https://example.com/');assert.equal(validateInteractionEvidence(f.payload),true);for(const extra of [{content:'secret'},{contentSha256:'a'.repeat(64)},{target:{tag:'INPUT',label:'secret'}},{sensitiveField:true},{outcome:'authorized'},{captureSource:'page'},{bytes:16777217}])assert.equal(validateInteractionEvidence({...f.payload,...extra}),false);assert.equal(buildInteractionEvidence({action:'copy',sessionId,url:'https://example.com',sensitiveField:false,outcome:'authorized',captureSource:'guarded-clipboard'},f.context.policy),null);}); test('type binding rejects cross-type payloads without attempting evidence send',async()=>{const f=fixture();assert.throws(()=>validatedEvidence(f.payload,'download'),/Invalid/);f.record.protectedEvidence.type='download';const calls=[];await assert.rejects(deliverAuditRecord({record:f.record,getContext:()=>f.context,send:async route=>{calls.push(route);return {};}}),/Invalid/);assert.deepEqual(calls,['/api/device/events']);}); test('partial disable discards interaction tuples while preserving pending legacy downloads',()=>{const f=fixture();f.context.policy.evidence.captureInteractions=false;assert.equal(collectionAllowed({...f.context,type:'interaction'}),false);assert.equal(collectionAllowed({...f.context,type:'download'}),true);const download={eventId:'download',protectedEvidence:{deviceId:'device-a',payload:{}}};const result=stripPendingEvidence([f.record,download],f.context);assert.equal('protectedEvidence' in result[0],false);assert.equal(result[1],download);assert.ok(f.record.protectedEvidence);}); test('revocation during ACK never marks evidence delivered and replay preserves original tuple',async()=>{const f=fixture(),snapshot=structuredClone(f.record),bodies=[];const send=async(route,body)=>{bodies.push(body);if(route.endsWith('/evidence')){f.context.policy.evidence.captureInteractions=false;return {id:sessionId,sourceEventId:1};}return {};};assert.deepEqual(await deliverAuditRecord({record:f.record,getContext:()=>f.context,send}),{eventDelivered:true,evidenceDiscarded:true});assert.deepEqual(f.record,snapshot);assert.equal(bodies[1].type,'interaction');assert.equal('protectedEvidence' in bodies[0],false);}); test('native hook target tags are canonicalized without accepting labels or arbitrary DOM data',()=>{const f=fixture(),base={action:'click',sessionId,url:'https://example.com/page',sensitiveField:false,outcome:'completed',captureSource:'agent-adapter'};for(const tag of ['a','button','input','textarea','select','div','span','p','body','unknown']){const value=buildInteractionEvidence({...base,target:{tag,role:'menuitem',x:12,y:34}},f.context.policy);assert.equal(value.target.tag,tag.toUpperCase());assert.equal(value.target.role,'menuitem');assert.deepEqual([value.target.x,value.target.y],[12,34]);}for(const target of [{tag:'button',label:'secret'},{tag:'a',selector:'#secret'},{tag:'body',text:'secret'},{tag:'script'},{tag:'button',x:Infinity},{tag:'button',y:-1}])assert.equal(buildInteractionEvidence({...base,target},f.context.policy),null);assert.equal(buildInteractionEvidence({...base,target:{tag:'select',role:'option'}},f.context.policy).target.role,'option');});