import test from 'node:test'; import assert from 'node:assert/strict'; import vm from 'node:vm'; import {createRequire} from 'node:module'; import {inspect as inspectPolicy} from '../packages/core/policy.mjs'; import {AgentsSchema} from '../packages/core/security.mjs'; const {createAgentAdapters}=createRequire(import.meta.url)('../apps/browser/agent-adapters.cjs'); function fixture(overrides={}) { let url='https://work.example/',version=1,script,generation=0; const context={ready:true,policy:{version:1,dlp:{rules:[{id:'corporate',kind:'literal'}]},agents:{enabled:true,allowSensitiveRead:false,allowedHosts:[],scopes:['sessions','read','click','type','navigate','ai']}},destinationAllowed:()=>true,webContents:{getURL:()=>url,executeJavaScript:async code=>{script=code;return {text:'Public material',completed:true};}}}; const id='71911cdc-a390-4e73-a604-d98d05fd52a5'; const capture=sessionId=>sessionId!==undefined && sessionId!==id?null:{...context,sessionId:id,foreground:true,policyVersion:version,token:{generation,version,url}}; const callbacks={getContext:capture,getSessions:()=>[{id,title:'Managed session',foreground:true}],contextCurrent:token=>token.generation===generation&&token.version===version&&token.url===url,navigate:async target=>{url=target;generation++;return capture(id);},ai:async()=>({text:'Helpful answer',token:'secret'}),inspect:async()=>({decision:'allow'}),audit:async()=>{},...overrides}; return {actions:createAgentAdapters(callbacks),context,id,setVersion:n=>version=n,setUrl:value=>{url=value;generation++;},invalidate:()=>generation++,getScript:()=>script}; } test('managed session metadata and outputs never include credentials',async()=>{ const {actions,id}=fixture(); assert.deepEqual(await actions.sessions(),[{id,title:'Managed session',foreground:true}]); assert.deepEqual(await actions.read({}),{text:'Public material'}); assert.deepEqual(await actions.ai({prompt:'Hello'}),{text:'Helpful answer'}); await assert.rejects(actions.read({sessionId:'other'}),/unavailable|Unknown/); }); test('policy changes, navigation races and cancellation suppress read results',async()=>{ for(const mutation of ['policy','url','abort']) { const controller=new AbortController();const f=fixture(); f.context.webContents.executeJavaScript=async()=>{if(mutation==='policy')f.setVersion(2);if(mutation==='url')f.setUrl('https://evil.example/');if(mutation==='abort')controller.abort();return{text:'safe'};}; await assert.rejects(f.actions.read({}, {signal:controller.signal})); } }); test('inspection blocks sensitive reads; explicit read approval never overrides secret detection',async()=>{ const f=fixture({inspect:async()=>({decision:'block',ruleId:'corporate'})}); await assert.rejects(f.actions.read({}),/Company policy/); f.context.policy.agents.allowSensitiveRead=true; assert.equal((await f.actions.read({})).text,'Public material'); f.context.webContents.executeJavaScript=async()=>({text:'password=hunter22'}); await assert.rejects(f.actions.read({}),/Sensitive/); }); test('fixed script excludes form content, hidden text and attributes',async()=>{ const f=fixture();await f.actions.read({}); const element=(excluded=false,hidden=false)=>({closest:selector=>selector.includes('input,textarea')&&excluded?{}:null,getAttribute:()=>null,parentElement:null,getClientRects:()=>hidden?[]:[{}]}); const nodes=[{parentElement:element(),textContent:'Visible company text'},{parentElement:element(true),textContent:'Form secret'},{parentElement:element(false,true),textContent:'Hidden secret'}]; let index=0; const result=vm.runInNewContext(f.getScript(),{location:{href:'https://work.example/'},document:{body:{},createTreeWalker:()=>({nextNode:()=>nodes[index++]})},NodeFilter:{SHOW_TEXT:4},getComputedStyle:()=>({display:'block',visibility:'visible',opacity:'1'})}); assert.equal(result.text,'Visible company text'); }); test('typing script rejects password targets and treats text as data',async()=>{ const f=fixture();await f.actions.type({selector:'#prompt',text:'";process.exit();//'}); const node={type:'password',tagName:'INPUT',parentElement:null,getAttribute:()=>null,closest:()=>null,getClientRects:()=>[{}]}; assert.throws(()=>vm.runInNewContext(f.getScript(),{location:{href:'https://work.example/'},document:{body:{},querySelector:()=>node},getComputedStyle:()=>({display:'block',visibility:'visible',opacity:'1'})}),/sensitive/); await assert.rejects(f.actions.type({selector:'input:has(*)',text:'x'}),/Invalid selector/); }); test('navigate permits managed destination change but refuses non-web schemes',async()=>{ const f=fixture();assert.deepEqual(await f.actions.navigate({url:'https://allowed.example/'}),{completed:true}); await assert.rejects(f.actions.navigate({url:'file:///secret'}),/Destination/); }); test('encoded built-in secrets and scanner failures never permit sensitive-read override',async()=>{ for (const verdict of [{decision:'block',ruleId:'api-secret'},{decision:'block',ruleId:'uninspectable'},{decision:'block',ruleId:'size-limit'},{decision:'block',ruleId:'corporate',engine:'yara'},{decision:'block',ruleId:'corporate',malware:{decision:'block'}},{decision:'block',ruleId:'corporate',coverage:{complete:false}},{decision:'block',ruleId:'unknown'}]) { const f=fixture({inspect:async()=>verdict});f.context.policy.agents.allowSensitiveRead=true; f.context.webContents.executeJavaScript=async()=>({text:'sk%2D'+'a'.repeat(25)}); await assert.rejects(f.actions.read({}),/Company policy/); } }); test('real core percent-decoded credential verdict is preserved',async()=>{ const policy={version:1,dlp:{maxBytes:10000,rules:[]},security:{content:{blockedPhrases:[]}}}; const f=fixture({inspect:async text=>inspectPolicy(Buffer.from(text),policy)}); f.context.policy.agents.allowSensitiveRead=true; const text='sk%2D'+'a'.repeat(25);f.context.webContents.executeJavaScript=async()=>({text}); assert.equal(inspectPolicy(Buffer.from(text),policy).ruleId,'api-secret'); await assert.rejects(f.actions.read({}),/Company policy/); }); test('scopes and agent hosts rechecked before and after asynchronous operations',async()=>{ const f=fixture();f.context.policy.agents.allowedHosts=['work.example']; f.setUrl('https://other.example/');await assert.rejects(f.actions.read({}),/Agent destination/); f.setUrl('https://work.example/');f.context.policy.agents.scopes=['sessions']; await assert.rejects(f.actions.read({}),/Agent scope/); f.context.policy.agents.scopes=['read']; f.context.webContents.executeJavaScript=async()=>{f.context.policy.agents.scopes=[];return{text:'safe'};}; await assert.rejects(f.actions.read({}),/Agent scope/); }); test('fixed DOM script rejects document replacement before any click side effect',async()=>{ const f=fixture();await f.actions.click({selector:'#submit'});let selected=false; assert.throws(()=>vm.runInNewContext(f.getScript(),{location:{href:'https://other.example/'},document:{querySelector:()=>{selected=true;}}}),/Document changed/); assert.equal(selected,false); }); test('same-URL reload, close and switch-away-and-back invalidate captured reads and AI',async()=>{ for(const action of ['read','ai']) { for(const race of ['reload','close','switch']) { const f=fixture(); const mutate=()=>{f.invalidate();if(race==='close')f.context.webContents.isDestroyed=()=>true;}; if(action==='read')f.context.webContents.executeJavaScript=async()=>{mutate();return{text:'Public material'};}; else { const g=fixture({ai:async()=>{g.invalidate();if(race==='close')g.context.webContents.isDestroyed=()=>true;return{text:'Public material'};}}); await assert.rejects(g.actions.ai({prompt:'Hello'}),/changed|unavailable/);continue; } await assert.rejects(f.actions.read({}),/changed|unavailable/); } } }); test('navigation cannot complete against another native identity or changed policy',async()=>{ for(const kind of ['identity','policy']) { const f=fixture({navigate:async target=>{ f.setUrl(target); return {...f.context,sessionId:f.id,token:{},policyVersion:kind==='policy'?2:1,webContents:kind==='identity'?{}:f.context.webContents}; }}); await assert.rejects(f.actions.navigate({url:'https://work.example/next'}),/changed/); } }); test('inspection itself cannot outlive the captured tab generation',async()=>{ const f=fixture({inspect:async()=>{f.invalidate();return{decision:'allow'};}}); await assert.rejects(f.actions.read({sessionId:f.id}),/changed/); }); test('session inventory and navigation work on the start page',async()=>{ const f=fixture();f.setUrl('about:blank'); assert.equal((await f.actions.sessions())[0].id,f.id); assert.deepEqual(await f.actions.navigate({url:'https://work.example/'}),{completed:true}); f.context.policy.agents.scopes=[]; await assert.rejects(f.actions.sessions(),/unavailable/); }); test('an omitted ID captures foreground once instead of following later foreground changes',async()=>{ const f=fixture();const calls=[];let foreground=f.id; const actions=createAgentAdapters({ getContext:id=>{calls.push(id);return {ready:true,sessionId:id??foreground,foreground:true,token:{},policyVersion:1,policy:f.context.policy,webContents:f.context.webContents,destinationAllowed:()=>true};}, getSessions:()=>[],contextCurrent:()=>true,navigate:async()=>{},ai:async()=>{},inspect:async()=>({decision:'allow'}),audit:async()=>{} }); f.context.webContents.executeJavaScript=async()=>{foreground='8dc41a3d-c46e-4c35-ae4a-98cfcb3a3553';return{text:'Public material'};}; await actions.read({}); assert.deepEqual(calls,[undefined,f.id,f.id]); }); test('background interactions fail before page code runs',async()=>{ const f=fixture();let executed=false; const actions=createAgentAdapters({getContext:()=>({...f.context,sessionId:f.id,token:{},foreground:false}),getSessions:()=>[],contextCurrent:()=>true,navigate:async()=>{},ai:async()=>{},inspect:async()=>({decision:'allow'}),audit:async()=>{}}); f.context.webContents.executeJavaScript=async()=>{executed=true;return{completed:true};}; await assert.rejects(actions.click({selector:'#submit'}),/unavailable/); await assert.rejects(actions.type({selector:'#prompt',text:'test'}),/unavailable/); assert.equal(executed,false); }); test('app controls require explicit new scopes and preserve typed trusted callback arguments',async()=>{ let called=0; const f=fixture({appControls:{'settings:update':async args=>{called++;assert.deepEqual(args,{appearance:{mode:'dark'}});return{visible:true,appearance:args.appearance};}}}); await assert.rejects(f.actions['settings:update']({appearance:{mode:'dark'}}),/scope/); assert.equal(called,0);f.context.policy.agents.scopes.push('settings:update');f.setUrl('about:blank'); assert.equal((await f.actions['settings:update']({appearance:{mode:'dark'}})).appearance.mode,'dark'); assert.equal(called,1); }); test('app result is withheld after policy change and bookmark metadata must pass DLP',async()=>{ const f=fixture({appControls:{'settings:get':async()=>{f.setVersion(2);return{visible:true};}}}); f.context.policy.agents.scopes.push('settings:get');await assert.rejects(f.actions['settings:get']({}),/changed/); const g=fixture({appControls:{'bookmarks:list':async()=>({managed:[{title:'password=hunter22',url:'https://example.test/',folder:''}],personal:{items:[]}})}}); g.context.policy.agents.scopes.push('bookmarks:list');await assert.rejects(g.actions['bookmarks:list']({}),/Sensitive/); const h=fixture({inspect:async()=>({decision:'block'}),appControls:{'bookmarks:list':async()=>({managed:[{title:'Confidential program',url:'https://example.test/',folder:''}],personal:{items:[]}})}}); h.context.policy.agents.scopes.push('bookmarks:list');await assert.rejects(h.actions['bookmarks:list']({}),/Company policy/); }); test('agent tab creation cannot bypass approved destination hosts',async()=>{ let called=false;const f=fixture({appControls:{'tabs:create':async()=>{called=true;return{completed:true};}}}); f.context.policy.agents.scopes.push('tabs:create');f.context.policy.agents.allowedHosts=['work.example']; await assert.rejects(f.actions['tabs:create']({url:'https://other.example/'}),/destination/); assert.equal(called,false); }); test('whole-application policy capabilities are opt-in and unknown scopes are rejected',()=>{ assert.deepEqual(AgentsSchema.parse({enabled:true}).scopes,['sessions']); const scopes=['application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group']; assert.deepEqual(AgentsSchema.parse({enabled:true,scopes}).scopes,scopes); assert.throws(()=>AgentsSchema.parse({enabled:true,scopes:['policy:override']})); }); test('structured agent inspection classifies field values and rejects policy races',async()=>{ const f=fixture();f.context.policy.agents.scopes.push('inspect','scroll'); f.context.webContents.executeJavaScript=async()=>({controls:[{selector:'#search',role:'input',label:'Search',value:'Public query',text:''}]}); assert.equal((await f.actions.inspect({})).controls[0].value,'Public query'); f.context.webContents.executeJavaScript=async()=>({controls:[{value:'password=hunter22'}]}); await assert.rejects(f.actions.inspect({}),/Sensitive/); f.context.webContents.executeJavaScript=async()=>{f.setVersion(2);return{controls:[]};}; await assert.rejects(f.actions.inspect({}),/changed/); }); test('agent scroll is bounded and restricted to an interactive foreground session',async()=>{ const f=fixture();f.context.policy.agents.scopes.push('scroll'); assert.equal((await f.actions.scroll({x:0,y:200})).completed,true); await assert.rejects(f.actions.scroll({x:0,y:-1}),/Invalid scroll/); }); test('structured DOM inspection excludes credentials and hidden inputs',async()=>{ const f=fixture();f.context.policy.agents.scopes.push('inspect'); f.context.webContents.executeJavaScript=async code=>{ const element=(id,type,value,hidden=false)=>({id,type,value,tagName:'INPUT',parentElement:null,innerText:'',getAttribute:a=>a==='id'?id:null,closest:()=>null,getClientRects:()=>hidden?[]:[{}]}); const elements=[element('search','text','Public value'),element('password','password','PRIVATE_PASSWORD_SENTINEL'),element('token','text','PRIVATE_TOKEN_SENTINEL'),element('hidden','text','HIDDEN_SENTINEL',true)]; return vm.runInNewContext(code,{location:{href:'https://work.example/'},document:{body:{},querySelectorAll:s=>s.startsWith('#')?elements.filter(e=>'#'+e.id===s):elements},getComputedStyle:()=>({display:'block',visibility:'visible',opacity:'1'})}); }; const result=await f.actions.inspect({});assert.equal(result.controls.length,1);assert.equal(result.controls[0].selector,'#search');assert.equal(result.controls[0].value,'Public value'); }); function formFixture(node) { const f=fixture(); f.context.policy.agents.scopes.push('select','check'); let clicks=0; class Select {} Object.defineProperty(Select.prototype,'value',{set(value){this.value=value;}}); for(const option of node.options||[]) {option.getAttribute??=()=>null;option.parentElement??=node;} node.getAttribute ??= ()=>null; node.closest ??= ()=>null; node.getClientRects ??= ()=>[{}]; node.dispatchEvent ??= ()=>{}; node.click ??= ()=>{clicks++;node.checked=!node.checked;}; f.context.webContents.executeJavaScript=async code=>vm.runInNewContext(code,{location:{href:'https://work.example/'},document:{body:{},querySelector:()=>node,querySelectorAll:()=>[node]},getComputedStyle:e=>e.style??{display:'block',visibility:'visible',opacity:'1'},HTMLSelectElement:Select,Event:class {constructor(type){this.type=type;}}}); return {...f,clicks:()=>clicks}; } test('fixed select chooses a unique enabled existing option and checks hostile handlers',async()=>{ const node={tagName:'SELECT',value:'a',options:[{value:'a'},{value:'b'}]}; const f=formFixture(node);assert.equal((await f.actions.select({selector:'#choice',value:'b'})).completed,true);assert.equal(node.value,'b'); node.options.push({value:'b',getAttribute:()=>null,parentElement:node});await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/ambiguous/);node.options.pop(); node.options[1].disabled=true;await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/unavailable/);node.options[1].disabled=false; node.options[1].parentElement={disabled:true};await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/unavailable/);node.options[1].parentElement=node; node.options[1].getAttribute=a=>a==='data-sensitive'?'':null;await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/unavailable/);node.options[1].getAttribute=()=>null; node.options[1].parentElement={hidden:true,getAttribute:()=>null,parentElement:node};await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/unavailable/);node.options[1].parentElement=node; await assert.rejects(f.actions.select({selector:'#choice',value:'missing'}),/unavailable/); node.dispatchEvent=()=>{node.value='a';};await assert.rejects(f.actions.select({selector:'#choice',value:'b'}),/changed/); }); test('checkbox check is idempotent, rejects radio and verifies hostile toggles',async()=>{ const node={tagName:'INPUT',type:'checkbox',checked:false};const f=formFixture(node); await f.actions.check({selector:'#choice',checked:true});assert.equal(f.clicks(),1); await f.actions.check({selector:'#choice',checked:true});assert.equal(f.clicks(),1); await f.actions.check({selector:'#choice',checked:false});assert.equal(f.clicks(),2); node.type='radio';await assert.rejects(f.actions.check({selector:'#choice',checked:true}),/cannot be checked/);node.type='checkbox'; node.click=()=>{};await assert.rejects(f.actions.check({selector:'#choice',checked:true}),/changed/); await assert.rejects(f.actions.check({selector:'#choice',checked:'true'}),/Invalid checked/); }); test('select and check reject hidden, credential and disabled-ancestor targets',async()=>{ for(const mode of ['select','check'])for(const kind of ['hidden','password','disabled','ancestor','sensitive-empty','sensitive-body']) { const node={tagName:mode==='select'?'SELECT':'INPUT',type:mode==='check'?'checkbox':'select-one',checked:false,options:[{value:'b'}]};const f=formFixture(node); if(kind==='sensitive-empty')node.getAttribute=a=>a==='data-sensitive'?'':null; if(kind==='sensitive-body')node.parentElement={parentElement:null,getAttribute:a=>a==='data-sensitive'?'':null}; if(kind==='hidden')node.getClientRects=()=>[]; if(kind==='password')node.getAttribute=a=>a==='name'?'password':null; if(kind==='disabled')node.closest=()=>({}); if(kind==='ancestor')node.parentElement={parentElement:null,getAttribute:()=>null,style:{display:'none',visibility:'visible',opacity:'1'}}; await assert.rejects(f.actions[mode]({selector:'#choice',value:'b',checked:true}),/unavailable|sensitive/); } }); test('select and check retain native generation and scope checks around execution',async()=>{ for(const mode of ['select','check']) { const f=fixture();f.context.policy.agents.scopes.push(mode);f.context.webContents.executeJavaScript=async()=>{f.invalidate();return {completed:true};}; await assert.rejects(f.actions[mode]({selector:'#choice',value:'b',checked:true}),/changed/); const g=fixture();await assert.rejects(g.actions[mode]({selector:'#choice',value:'b',checked:true}),/scope/); } }); test('structured inspection returns bounded safe dropdown choices and checkbox state',async()=>{ const f=fixture();f.context.policy.agents.scopes.push('inspect'); const base={parentElement:null,getAttribute:()=>null,closest:()=>null,getClientRects:()=>[{}]}; const select={...base,id:'choice',tagName:'SELECT',value:'one',options:[]}; select.options=[{...base,parentElement:select,value:'one',label:'One'},{...base,parentElement:select,value:'locked',label:'Locked',disabled:true},{...base,parentElement:select,value:'secret',label:'SECRET_SENTINEL',getAttribute:a=>a==='data-sensitive'?'':null}]; const checkbox={...base,id:'accept',tagName:'INPUT',type:'checkbox',checked:true,value:'on'};const elements=[select,checkbox]; f.context.webContents.executeJavaScript=async code=>vm.runInNewContext(code,{location:{href:'https://work.example/'},document:{body:{},querySelectorAll:s=>s.startsWith('#')?elements.filter(e=>'#'+e.id===s):elements},getComputedStyle:()=>({display:'block',visibility:'visible',opacity:'1'})}); const result=await f.actions.inspect({});assert.equal(result.controls[0].options.length,2);assert.equal(result.controls[0].options[1].disabled,true);assert.equal(result.controls[1].checked,true);assert.equal(JSON.stringify(result).includes('SECRET_SENTINEL'),false); }); test('native history refreshes intentional navigation while binding the original tab',async()=>{ for(const action of ['back','forward','reload']) { let f,calls=0; f=fixture({history:async(mode,initial,{signal})=>{ calls++;assert.equal(mode,action);assert.equal(initial.sessionId,f.id);assert.equal(signal,undefined); f.setUrl('https://work.example/history'); return {completed:true,context:{...f.context,sessionId:f.id,foreground:true,policyVersion:1,token:{generation:1,version:1,url:'https://work.example/history'}}}; }}); f.context.policy.agents.scopes.push(action); assert.deepEqual(await f.actions[action]({}),{completed:true});assert.equal(calls,1); } }); test('history actions require explicit scope, native callback and interactive foreground',async()=>{ for(const action of ['back','forward','reload']) { const f=fixture();await assert.rejects(f.actions[action]({}),/scope/); f.context.policy.agents.scopes.push(action);await assert.rejects(f.actions[action]({}),/unavailable/); let called=false; const callbacks={getContext:()=>({...f.context,sessionId:f.id,foreground:false,token:{}}),getSessions:()=>[],contextCurrent:()=>true,navigate:async()=>{},history:async()=>{called=true;},ai:async()=>{},inspect:async()=>({decision:'allow'}),audit:async()=>{}}; await assert.rejects(createAgentAdapters(callbacks)[action]({}),/unavailable/);assert.equal(called,false); callbacks.getContext=()=>({...f.context,sessionId:f.id,foreground:true,interactive:false,token:{}}); await assert.rejects(createAgentAdapters(callbacks)[action]({}),/unavailable/);assert.equal(called,false); } }); test('history suppresses cancellation, policy races, switched tabs and blocked final destinations',async()=>{ for(const kind of ['abort','policy','identity','session','token','blocked','host','scheme','incomplete','scope']) { let f;const controller=new AbortController(); f=fixture({history:async()=>{ f.setUrl(kind==='scheme'?'file:///private':'https://work.example/history'); if(kind==='abort')controller.abort();if(kind==='policy')f.setVersion(2); if(kind==='blocked')f.context.destinationAllowed=()=>false; if(kind==='host')f.context.policy.agents.allowedHosts=['other.example']; if(kind==='scope')f.context.policy.agents.scopes=[]; return {completed:kind!=='incomplete',context:{...f.context,sessionId:kind==='session'?'other':f.id,foreground:true,webContents:kind==='identity'?{}:f.context.webContents,policyVersion:kind==='policy'?2:1,token:{generation:kind==='token'?0:1,version:1,url:'https://work.example/history'}}}; }}); f.context.policy.agents.scopes.push('back'); await assert.rejects(f.actions.back({}, {signal:controller.signal}),/changed|unavailable|blocked|scope/); } }); test('history can leave a start page but reload never invokes native callback there',async()=>{ let called=0;const f=fixture({history:async()=>{called++;throw Error('Native target unavailable');}}); f.context.policy.agents.scopes.push('back','forward','reload');f.setUrl('about:blank'); await assert.rejects(f.actions.back({}),/Native target/);await assert.rejects(f.actions.forward({}),/Native target/); await assert.rejects(f.actions.reload({}),/destination blocked/);assert.equal(called,2); }); test('completed click forensic callback excludes selectors and text, and context changes suppress it',async()=>{ const events=[];const f=fixture({onInteraction:async value=>events.push(value)}); f.context.webContents.executeJavaScript=async()=>({completed:true,target:{tag:'button',role:'button',x:12,y:24,text:'SECRET',selector:'#private'}}); assert.deepEqual(await f.actions.click({selector:'#public'}),{completed:true}); assert.deepEqual(events,[{action:'click',sessionId:f.id,pageUrl:'https://work.example/',outcome:'completed',sensitiveField:false,target:{tag:'button',role:'button',x:12,y:24}}]); f.context.webContents.executeJavaScript=async()=>{f.invalidate();return{completed:true,target:{tag:'button'}};}; await assert.rejects(f.actions.click({selector:'#public'}));assert.equal(events.length,1); }); test('completed click callback failure is surfaced without repeating the committed click',async()=>{ let calls=0;const f=fixture({onInteraction:async()=>{throw Error('Evidence unavailable');}}); f.context.webContents.executeJavaScript=async()=>{calls++;return{completed:true};}; await assert.rejects(f.actions.click({selector:'#public'}),/Evidence unavailable/);assert.equal(calls,1); }); test('session inventory classifies full titles and redacts corporate, semantic and credential content',async()=>{ for(const kind of ['corporate','semantic','credential','boundary']){ let calls=0,seen;const title=kind==='credential'?'password=SyntheticCredential':kind==='boundary'?'x'.repeat(150)+' password=SyntheticCredential':'Synthetic confidential title'; const f=fixture({getSessions:()=>[{id:'inventory',title,foreground:false}],inspect:async text=>{calls++;seen=text;return {decision:'block',ruleId:kind==='semantic'?'semantic-confidential':'corporate'};}}); f.context.policy.agents.allowSensitiveRead=true; assert.deepEqual(await f.actions.sessions(),[{id:'inventory',title:'Protected page',foreground:false}]); assert.equal(calls,['credential','boundary'].includes(kind)?0:1);if(calls)assert.equal(seen,title); } const title='x'.repeat(170),f=fixture({getSessions:()=>[{id:'inventory',title,foreground:true}],inspect:async text=>{assert.equal(text.length,170);return {decision:'allow'};}}); f.setUrl('about:blank');assert.equal((await f.actions.sessions())[0].title.length,160); }); test('session inventory refuses unavailable inspection, changed policy, native context and inventory',async()=>{ for(const kind of ['missing','outage','incomplete','throw','abort','version','policy','token','title']){ const controller=new AbortController();let title='Safe title',f; f=fixture({getSessions:()=>[{id:'inventory',title,foreground:true}],inspect:async()=>{ if(kind==='missing')return undefined;if(kind==='outage')return {decision:'block',ruleId:'inspector-unavailable'}; if(kind==='incomplete')return {decision:'block',ruleId:'corporate',coverage:{complete:false}};if(kind==='throw')throw Error('Scanner failed'); if(kind==='abort')controller.abort();if(kind==='version')f.setVersion(2);if(kind==='policy')f.context.policy={...f.context.policy};if(kind==='token')f.invalidate();if(kind==='title')title='Changed title'; return {decision:'allow'}; }}); await assert.rejects(f.actions.sessions({}, {signal:controller.signal}),/unavailable|changed|Scanner failed/); } }); test('session titles use real core decoding and classify corporate suffix beyond wire truncation',async()=>{ for(const title of ['sk-SyntheticFixture123456789012345','x'.repeat(160)+' confidential']){ const f=fixture({getSessions:()=>[{id:'inventory',title,foreground:true}],inspect:async text=>inspectPolicy(Buffer.from(text),{dlp:{maxBytes:10000,rules:[{id:'corporate',kind:'literal',value:'confidential'}]}})}); assert.equal((await f.actions.sessions())[0].title,'Protected page'); } const title='sk%2DSyntheticFixture123456789012345',f=fixture({getSessions:()=>[{id:'inventory',title,foreground:true}],inspect:async text=>inspectPolicy(Buffer.from(text),{dlp:{maxBytes:10000,rules:[]}})}); assert.equal((await f.actions.sessions())[0].title,'Protected page'); }); test('inventory uses one bounded aggregate scan and masks all titles when any classified title blocks',async()=>{ for(const decision of ['allow','block']){ let calls=0;const rows=[{id:'public',title:'Public page',foreground:true},{id:'corporate',title:'Registered corporate page',foreground:false},{id:'credential',title:'password=SyntheticCredential',foreground:false}]; const f=fixture({getSessions:()=>rows,inspect:async text=>{calls++;assert.equal(text,'Public page\nRegistered corporate page');return {decision,ruleId:decision==='block'?'corporate':null};}}); const result=await f.actions.sessions();assert.equal(calls,1);assert.deepEqual(result.map(row=>row.id),rows.map(row=>row.id)); assert.deepEqual(result.map(row=>row.title),decision==='allow'?['Public page','Registered corporate page','Protected page']:['Protected page','Protected page','Protected page']); } for(const rows of [Array.from({length:1001},()=>({id:'inventory',title:'Safe'})),[{id:'inventory',title:'x'.repeat(201)}]]){ let calls=0;const f=fixture({getSessions:()=>rows,inspect:async()=>{calls++;return {decision:'allow'};}});await assert.rejects(f.actions.sessions(),/Invalid managed/);assert.equal(calls,0); } });