Files
safetynet/tests/agent-visual-transport.test.mjs
T

17 lines
4.7 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const require=createRequire(import.meta.url),{PNG}=require('pngjs');
const {startAgentControl}=require('../apps/browser/agent-control.cjs');
const {requestAgent}=require('../apps/browser/agent-transport-client.cjs');
const {createAgentVisualExport}=require('../apps/browser/agent-visual-export.cjs');
const image=()=>{const p=new PNG({width:16,height:16});p.data.fill(255);const bytes=PNG.sync.write(p);return {mime:'image/png',width:16,height:16,data:bytes.toString('base64')};};
async function api(t,options={}){const events=[],result=image(),context=options.context||{ready:true,foreground:true,interactive:true,policy:{expiresAt:new Date(Date.now()+60000).toISOString(),agents:{enabled:true,scopes:['screenshot']}},token:{},sessionId:'fixture',actor:{agentId:'fixture',generation:1},webContents:{getURL:()=> 'https://example.com',isDestroyed:()=>false},destinationAllowed:()=>true};const screenshot=createAgentVisualExport({getContext:()=>context,contextCurrent:()=>true,probeSensitive:async()=>({safe:true,revision:'fixed',width:16,height:16}),capture:async()=>Buffer.from(result.data,'base64'),inspectLocal:async()=>{if(options.delayInspection)await new Promise(r=>setTimeout(r,options.delayInspection));return {decision:'allow',contentType:'image',coverage:{complete:true}};},inspectEnterprise:async()=>({decision:'allow',coverage:{complete:true}}),audit:async()=>{}});const service=await startAgentControl({authorize:async({scope})=>({allowed:true,role:'agent',scopes:[scope]}),actions:{screenshot},audit:async event=>events.push(event),...options});t.after(()=>service.close());const request=async(action='screenshot',args={})=>{const response=await requestAgent(service,{action,args});return {status:response.status,...await response.json()};};return {events,result,request,service,screenshot,context};}
test('inspected PNG projection traverses authenticated PQ response and excludes extra arguments',async t=>{const f=await api(t),response=await f.request();assert.equal(response.status,200);assert.deepEqual(response.result,f.result);assert.ok(f.events.every(event=>event.scope==='browser:screenshot'));assert.equal(JSON.stringify(f.events).includes(f.result.data),false);for(const args of [{rect:{}},{desktop:true},{format:'jpeg'}])assert.equal((await f.request('screenshot',args)).status,400);});
test('forged PNG metadata and revocation before encryption return no image',async t=>{const bad=await api(t,{actions:{screenshot:async()=>({...image(),width:17})}});const result=await bad.request();assert.notEqual(result.status,200);assert.equal(result.result,undefined);let allowed=true;const revoked=await api(t,{authorize:async({scope})=>({allowed,role:'agent',scopes:[scope]}),audit:async event=>{if(event.outcome==='completed')allowed=false;}});const denied=await revoked.request();assert.equal(denied.status,403);assert.equal(denied.result,undefined);});
test('long inspection deadline is action-specific and ordinary operations retain their deadline',async t=>{const f=await api(t,{timeoutMs:20,actionTimeouts:{screenshot:1000},delayInspection:60});const read=await api(t,{timeoutMs:20,actions:{read:async()=>{await new Promise(r=>setTimeout(r,60));return {text:'public'};}}});assert.equal((await f.request()).status,200);assert.equal((await read.request('read')).status,504);await assert.rejects(startAgentControl({authorize:()=>{},actions:{},audit:()=>{},actionTimeouts:{unknown:100}}));});
test('original inspected context is required after completed audit even if grants and host still allow',async t=>{for(const change of ['policy','foreground','actor']){let f;f=await api(t,{audit:async event=>{if(event.outcome==='completed'){if(change==='policy')f.context.policy={...f.context.policy,dlp:{rules:[{id:'new-block'}]}};if(change==='foreground')f.context.foreground=false;if(change==='actor')f.context.actor={agentId:'other',generation:1};}}});const response=await f.request();assert.equal(response.status,403,change);assert.equal(response.result,undefined);}});
test('synchronous release diagnostics cannot skip or repeat the final native authority check',async t=>{for(const mode of ['before','after','twice','missing']){let f;const traceRelease=(_name,fn)=>{if(mode==='before')throw Error('Diagnostic unavailable');if(mode==='missing')return;fn();if(mode==='after')throw Error('Diagnostic unavailable');if(mode==='twice')fn();};f=await api(t,{traceRelease,audit:async event=>{if(event.outcome==='completed')f.context.foreground=false;}});const denied=await f.request();assert.equal(denied.status,403,mode);assert.equal(denied.result,undefined);const allowed=await api(t,{traceRelease});assert.equal((await allowed.request()).status,200,mode);}});