2026-10-13 10:33:31 +02:00
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' ]}));
});
2026-10-13 16:58:15 +02:00
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 );
});