Add scoped native history and window agent controls
This commit is contained in:
1 parent
adf0940e5f
commit
61fcd8336f
27 files changed
+503
-25
No files matched your search
@@ -4,9 +4,11 @@ import {readFileSync} from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
import {DiagnosticsSchema} from '../packages/core/diagnostics.mjs';
|
||||
const html=readFileSync(new URL('../apps/server/public/index.html',import.meta.url),'utf8');
|
||||
test('every centrally offered agent scope has an accessible structured control',()=>{const source=readFileSync(new URL('../apps/server/public/admin.js',import.meta.url),'utf8'),scopes=vm.runInNewContext(source.match(/const agentScopes=(\[[^;]+\]);/)[1]);for(const scope of scopes)assert.ok(html.includes(`id="agent-scope-${scope}"`),scope);for(const scope of ['inspect','scroll','library:organize-plan','library:organize-apply'])assert.ok(scopes.includes(scope));});
|
||||
test('every centrally offered agent scope has an accessible structured control',()=>{const source=readFileSync(new URL('../apps/server/public/admin.js',import.meta.url),'utf8'),scopes=vm.runInNewContext(source.match(/const agentScopes=(\[[^;]+\]);/)[1]);for(const scope of scopes)assert.ok(html.includes(`id="agent-scope-${scope}"`),scope);for(const scope of ['inspect','scroll','library:organize-plan','library:organize-apply','history:list','history:go','window:state','window:control'])assert.ok(scopes.includes(scope));});
|
||||
test('diagnostic admin roundtrip retains bounded export settings and denies auditors',()=>{const fields=new Map(),get=id=>{if(!fields.has(id))fields.set(id,{value:'',checked:false,disabled:false,addEventListener(){}});return fields.get(id);};let p={diagnostics:DiagnosticsSchema.parse({enabled:true,mode:'debug',endpoint:'https://collector.example',maxQueueSize:123,flushIntervalMs:333,maxRecordAgeMs:4567,exportTimeoutMs:999})};const context=vm.createContext({$:get,fill(){},draft:()=>structuredClone(p),applyOperatorPermissions(){},independentApprovalRequired:true,policy:null,currentPrincipal:null,updateDraftStatus(){}});vm.runInContext(readFileSync(new URL('../apps/server/public/diagnostics-admin.js',import.meta.url),'utf8'),context);context.fill(p);assert.deepEqual(DiagnosticsSchema.parse(context.draft().diagnostics),p.diagnostics);context.applyOperatorPermissions({role:'auditor',permissions:['policy.read']});assert.equal(get('diagnostics-enabled').disabled,true);context.applyOperatorPermissions({role:'operator',permissions:['policy.prepare']});assert.equal(get('diagnostics-enabled').disabled,false);get('diagnostics-enabled').checked=false;assert.equal(DiagnosticsSchema.parse(context.draft().diagnostics).enabled,false);});
|
||||
test('SIEM control is administrator-only and writes the loaded revision',async()=>{const fields=new Map(),get=id=>{if(!fields.has(id))fields.set(id,{value:'',checked:false,disabled:false,hidden:false,textContent:'',addEventListener(type,fn){this.listener=fn;}});return fields.get(id);};const calls=[],context=vm.createContext({$:get,applyOperatorPermissions(){},load:async()=>{},task:fn=>fn,currentPrincipal:null,api:async(path,body,method)=>{calls.push({path,body,method});return {enabled:body?.enabled??true,revision:8,format:'ecs',pending:2,failures:0,configured:true};}});vm.runInContext(readFileSync(new URL('../apps/server/public/siem-admin.js',import.meta.url),'utf8'),context);context.currentPrincipal={role:'auditor',permissions:['policy.read']};context.applyOperatorPermissions(context.currentPrincipal);assert.equal(get('siem-controls').hidden,true);assert.equal(calls.length,0);await assert.rejects(get('siem-save').listener(),/Administrator/);context.currentPrincipal={role:'admin',permissions:['policy.read']};context.fillSIEMExport({enabled:true,revision:7,format:'cef'});context.applyOperatorPermissions(context.currentPrincipal);get('siem-enabled').checked=false;await get('siem-save').listener();assert.equal(calls[0].path,'/api/admin/siem');assert.equal(calls[0].method,'PUT');assert.equal(calls[0].body.enabled,false);assert.equal(calls[0].body.expectedRevision,7);});
|
||||
|
||||
import {FileTransfersSchema} from '../packages/core/file-transfer.mjs';
|
||||
test('download scan settings remain explicit and cannot alter upload policy',()=>{const fields=new Map(),get=id=>{if(!fields.has(id))fields.set(id,{checked:false,disabled:false});return fields.get(id);};const policy={fileTransfers:FileTransfersSchema.parse({upload:{blockUnknown:true},download:{}}),security:{yara:{enabled:true,rules:[],scanOfficeMacros:true}}};const context=vm.createContext({$:get,fill(){},draft:()=>structuredClone(policy),applyOperatorPermissions(){},independentApprovalRequired:true,policy:null,currentPrincipal:null});vm.runInContext(readFileSync(new URL('../apps/server/public/download-scan-admin.js',import.meta.url),'utf8'),context);context.fill(policy);assert.equal(get('download-document-dlp').checked,true);get('download-document-dlp').checked=false;get('yara-office-macros').checked=false;const updated=context.draft();assert.equal(FileTransfersSchema.parse(updated.fileTransfers).download.requireDocumentDlp,false);assert.deepEqual(updated.fileTransfers.upload,policy.fileTransfers.upload);assert.equal(updated.security.yara.scanOfficeMacros,false);context.applyOperatorPermissions({role:'auditor',permissions:['policy.read']});assert.equal(get('yara-office-macros').disabled,true);});
|
||||
|
||||
test('new history and window capability controls require explicit opt-in and explain limits',()=>{for(const scope of ['history:list','history:go','window:state','window:control']){const tag=html.match(new RegExp(`<input[^>]*id="agent-scope-${scope}"[^>]*>`))[0];assert.equal(/\bchecked\b/.test(tag),false);}assert.ok(html.includes('History read and history navigation are separate capabilities'));assert.ok(html.includes('never grants OS or administrator access'));const editor=readFileSync(new URL('../apps/server/public/agent-grants-admin.js',import.meta.url),'utf8');assert.ok(editor.includes('enabled:false'));assert.ok(editor.includes('scopes:[]'));assert.ok(editor.includes('check.disabled=!agentGrantsEditable'));});
|
||||
@@ -12,6 +12,16 @@ async function fixture(t, overrides = {}) {
|
||||
const request = (action,args={},headers={})=>requestAgent(api,{action,args},{headers});
|
||||
return {api, request,calls,events};
|
||||
}
|
||||
|
||||
test('new history and window actions use protected transport, strict arguments and projected outputs',async t=>{
|
||||
const snapshotId='00000000-0000-4000-8000-000000000001',entryId='00000000-0000-4000-8000-000000000002';let effects=0;
|
||||
const {request,events}=await fixture(t,{actions:{'history:list':async()=>({snapshotId,entries:[{entryId,title:'Public',url:'https://example.test/',active:false,index:0,pageState:'PRIVATE_NATIVE_STATE'}]}),'history:go':async()=>{effects++;return {completed:true,url:'PRIVATE_TARGET'};},'window:state':async()=>({width:1200,height:800,visible:true,focused:true,minimized:false,maximized:false,fullScreen:false,title:'PRIVATE_WINDOW_TITLE'}),'window:control':async()=>{effects++;return {completed:true,state:{title:'PRIVATE_STATE'}};}}});
|
||||
const list=await request('history:list',{sessionId:'session'});assert.equal(list.status,200);const value=await list.json();assert.equal(JSON.stringify(value).includes('PRIVATE'),false);assert.equal(value.result.entries[0].index,undefined);
|
||||
assert.equal((await request('history:go',{sessionId:'session',snapshotId,entryId})).status,200);
|
||||
assert.equal((await request('window:state')).status,200);assert.equal((await request('window:control',{command:'maximize'})).status,200);
|
||||
for(const [action,args]of [['history:list',{}],['history:go',{sessionId:'session',snapshotId,entryId,index:1}],['history:go',{sessionId:'session',snapshotId:'invalid',entryId}],['window:control',{command:'close'}],['window:state',{title:'PRIVATE'}]])assert.equal((await request(action,args)).status,400);
|
||||
assert.equal(effects,2);assert.ok(events.some(e=>e.action==='history:go'&&e.scope==='history:go'&&e.outcome==='completed'));assert.ok(events.some(e=>e.action==='window:control'&&e.scope==='window:control'&&e.outcome==='completed'));assert.equal(JSON.stringify(events).includes('PRIVATE'),false);
|
||||
});
|
||||
test('postquantum channel refuses plaintext, tampering and replays before adapter execution',async t=>{
|
||||
const {api,calls}=await fixture(t);
|
||||
const headers=(route,body='{}')=>({'Content-Type':'application/json',Authorization:authorization(api.token,route,body)});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const {createAgentAdapters}=createRequire(import.meta.url)('../apps/browser/agent-adapters.cjs');
|
||||
function fixture(){
|
||||
let url='https://work.example/current',generation=0,history={activeIndex:1,entries:[{url:'https://work.example/old',title:'Old'},{url,title:'Current'}]},calls=0;
|
||||
const policy={version:1,agents:{enabled:true,scopes:['history:list','history:go'],allowedHosts:['work.example'],allowSensitiveRead:true}};
|
||||
const webContents={getURL:()=>url},context=()=>({ready:true,policy,sessionId:'session',foreground:true,token:generation+1,webContents,destinationAllowed:()=>true});
|
||||
const callbacks={getContext:context,getSessions:()=>[],contextCurrent:token=>token===generation+1,navigate:async()=>{},ai:async()=>{},inspect:async()=>({decision:'allow'}),audit:async()=>{},getHistory:()=>history,goHistory:async(index,initial,{historyBinding})=>{assert.equal(JSON.stringify(history),historyBinding);calls++;url=history.entries[index].url;generation++;return {completed:true,context:context()};}};
|
||||
return {callbacks,policy,history,actions:()=>createAgentAdapters(callbacks),calls:()=>calls,invalidate:()=>generation++};
|
||||
}
|
||||
test('history exports bounded metadata and opaque identifiers, navigates exact snapshot once',async()=>{
|
||||
const f=fixture(),a=f.actions(),list=await a['history:list']({});assert.equal(list.entries.length,2);assert.equal(list.entries[0].index,undefined);assert.match(list.snapshotId,/^[a-f0-9-]{36}$/);
|
||||
assert.deepEqual(await a['history:go']({snapshotId:list.snapshotId,entryId:list.entries[0].entryId}),{completed:true});assert.equal(f.calls(),1);
|
||||
await assert.rejects(a['history:go']({snapshotId:list.snapshotId,entryId:list.entries[0].entryId}));assert.equal(f.calls(),1);
|
||||
});
|
||||
test('history masks credentials and DLP without sensitive override, discards native page state',async()=>{
|
||||
const f=fixture();f.history.entries[0]={url:'https://work.example/?access_token=private',title:'password=secret22',pageState:'private form'};f.callbacks.inspect=async()=>({decision:'block',ruleId:'corporate'});
|
||||
const list=await f.actions()['history:list']({});assert.equal(list.entries[0].title,'Protected page');assert.equal(list.entries[0].url,undefined);assert.equal(list.entries[1].url,undefined);assert.ok(!JSON.stringify(list).includes('private'));
|
||||
});
|
||||
test('history inspection errors, incomplete coverage and asynchronous replacement fail closed',async()=>{
|
||||
for(const verdict of [null,{decision:'allow',coverage:{complete:false}},{decision:'block',ruleId:'uninspectable'}]){const f=fixture();f.callbacks.inspect=async()=>verdict;await assert.rejects(f.actions()['history:list']({}),/Inspection/);}
|
||||
for(const mutate of [f=>f.history.entries[0].title='Changed',f=>f.invalidate(),f=>f.policy.agents.scopes=[]]){const f=fixture();f.callbacks.inspect=async()=>{mutate(f);return{decision:'allow'};};await assert.rejects(f.actions()['history:list']({}));}
|
||||
});
|
||||
test('history index replacement, authority revocation and foreign identifiers prevent effects',async()=>{
|
||||
for(const mutate of [f=>f.history.entries[0].url='https://work.example/replaced',f=>f.invalidate(),f=>f.policy.agents.scopes=['history:list']]){const f=fixture(),a=f.actions(),list=await a['history:list']({});mutate(f);await assert.rejects(a['history:go']({snapshotId:list.snapshotId,entryId:list.entries[0].entryId}));assert.equal(f.calls(),0);}
|
||||
const f=fixture(),a=f.actions(),list=await a['history:list']({});await assert.rejects(a['history:go']({snapshotId:list.snapshotId,entryId:'foreign'}));assert.equal(f.calls(),0);
|
||||
});
|
||||
test('history host restrictions exclude entries and cancellation prevents disclosure',async()=>{
|
||||
const f=fixture();f.history.entries[0].url='https://other.example/';assert.equal((await f.actions()['history:list']({})).entries.length,1);
|
||||
const g=fixture(),controller=new AbortController();g.callbacks.inspect=async()=>{controller.abort();return{decision:'allow'};};await assert.rejects(g.actions()['history:list']({}, {signal:controller.signal}));
|
||||
});
|
||||
test('history grants cannot cross agent identities or navigate masked credentials',async()=>{
|
||||
const f=fixture();let actor={agentId:'a',generation:1};const context=f.callbacks.getContext;f.callbacks.getContext=()=>({...context(),actor});
|
||||
const a=f.actions(),list=await a['history:list']({});actor={agentId:'b',generation:1};await assert.rejects(a['history:go']({snapshotId:list.snapshotId,entryId:list.entries[0].entryId}));assert.equal(f.calls(),0);
|
||||
const g=fixture();g.history.entries[0].url='https://work.example/?code=private';const b=g.actions(),masked=await b['history:list']({});await assert.rejects(b['history:go']({snapshotId:masked.snapshotId,entryId:masked.entries[0].entryId}));assert.equal(g.calls(),0);
|
||||
});
|
||||
test('encoded query keys and fragment credentials are masked and cannot be navigated',async()=>{
|
||||
for(const suffix of ['?%74oken=private','?%2574oken=private','#refresh_token=private','?sid=private','?client_secret=private']){
|
||||
const f=fixture();f.history.entries[0].url='https://work.example/'+suffix;const a=f.actions(),list=await a['history:list']({});assert.equal(list.entries[0].url,undefined);assert.equal(list.entries[0].title,'Protected page');await assert.rejects(a['history:go']({snapshotId:list.snapshotId,entryId:list.entries[0].entryId}));assert.equal(f.calls(),0);
|
||||
}
|
||||
});
|
||||
test('native page-state checksum stays private and detects same-title/URL replacement',async()=>{
|
||||
const f=fixture();f.history.nativeBinding='a'.repeat(64);const a=f.actions(),list=await a['history:list']({});assert.ok(!JSON.stringify(list).includes('a'.repeat(64)));f.history.nativeBinding='b'.repeat(64);await assert.rejects(a['history:go']({snapshotId:list.snapshotId,entryId:list.entries[0].entryId}));assert.equal(f.calls(),0);
|
||||
const g=fixture();g.history.nativeBinding='a'.repeat(64);g.callbacks.inspect=async()=>{g.history.nativeBinding='b'.repeat(64);return{decision:'allow'};};await assert.rejects(g.actions()['history:list']({}));
|
||||
});
|
||||
@@ -19,3 +19,14 @@ test('unexpected final URL and mainframe load failure reject; timeout stops',asy
|
||||
|
||||
test('missing positive native context proof denies history before any side effect',async()=>{const c=fixture();await assert.rejects(run(c,{current:async()=>undefined}),/context changed/);assert.equal(c.calls,0);});
|
||||
test('context guards distinguish captured state before dispatch from intentional navigation after',async()=>{const c=fixture(),phases=[];const p=run(c,{current:phase=>{phases.push(phase);return phase==='before'?c.calls===0:c.calls===1;}});await dispatched(c);c.emit('did-finish-load');assert.equal((await p).completed,true);assert.ok(phases.includes('before')&&phases.includes('after'));});
|
||||
|
||||
test('indexed history compares exact snapshot immediately before native dispatch and never retries',async()=>{
|
||||
const c=fixture(),snapshot=()=>({activeIndex:c.index,entries:[{url:'https://example.test/one',title:'One'},{url:'https://example.test/two',title:'Two'}]}),binding=JSON.stringify(snapshot());
|
||||
const p=run(c,{action:'index',index:0,getSnapshot:snapshot,historyBinding:binding});await dispatched(c);c.emit('did-finish-load');assert.equal((await p).url,'https://example.test/one');assert.equal(c.calls,1);
|
||||
const changed=fixture();let title='Two';const getSnapshot=()=>({activeIndex:changed.index,entries:[{url:'https://example.test/one',title:'One'},{url:'https://example.test/two',title}]});
|
||||
await assert.rejects(run(changed,{action:'index',index:0,getSnapshot,historyBinding:JSON.stringify(getSnapshot()),allowed:()=>{title='Replaced';return true;}}),/snapshot changed/);assert.equal(changed.calls,0);assert.equal(changed.eventNames().length,0);
|
||||
});
|
||||
test('indexed current-entry selection is a guarded no-op and raw indexes lack a snapshot authority',async()=>{
|
||||
const c=fixture(),snapshot=()=>({activeIndex:c.index,entries:[{url:'https://example.test/one',title:'One'},{url:'https://example.test/two',title:'Two'}]});assert.equal((await run(c,{action:'index',index:1,getSnapshot:snapshot,historyBinding:JSON.stringify(snapshot())})).completed,true);assert.equal(c.calls,0);
|
||||
for(const extra of [{index:-1},{index:0},{index:0,getSnapshot:snapshot},{index:0,getSnapshot:snapshot,historyBinding:'{}'}])await assert.rejects(run(c,{action:'index',...extra}));assert.equal(c.calls,0);
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import {createRequire} from 'node:module';
|
||||
const {readHistorySnapshot}=createRequire(import.meta.url)('../apps/browser/history-snapshot.cjs');
|
||||
function fixture(){const rows=[{url:'https://work.example/',title:'Work',pageState:'PRIVATE_FORM_VALUES',extra:'PRIVATE_NATIVE'}];return {rows,contents:{isDestroyed:()=>false,navigationHistory:{getAllEntries:()=>rows,getActiveIndex:()=>0}}};}
|
||||
test('native history strips form state and retains only a private exact-state checksum',()=>{
|
||||
const f=fixture(),first=readHistorySnapshot(f.contents);assert.deepEqual(first.entries,[{url:'https://work.example/',title:'Work'}]);assert.equal(JSON.stringify(first).includes('PRIVATE'),false);assert.match(first.nativeBinding,/^[a-f0-9]{64}$/);assert.equal(readHistorySnapshot(f.contents).nativeBinding,first.nativeBinding);
|
||||
f.rows[0].pageState='REPLACED_FORM_VALUES';assert.notEqual(readHistorySnapshot(f.contents).nativeBinding,first.nativeBinding);assert.deepEqual(readHistorySnapshot(f.contents).entries,first.entries);
|
||||
});
|
||||
test('unavailable or oversized native history cannot produce a comparison authority',()=>{
|
||||
for(const mutate of [f=>f.contents.isDestroyed=()=>true,f=>f.rows[0].pageState='x'.repeat(1048577),f=>f.rows[0].pageState={},f=>f.rows[0].title='x'.repeat(201),f=>f.contents.navigationHistory.getActiveIndex=()=>-1,f=>f.contents.navigationHistory.getAllEntries=()=>Array(101).fill(f.rows[0])]){const f=fixture();mutate(f);assert.throws(()=>readHistorySnapshot(f.contents));}
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const {createWindowControls}=createRequire(import.meta.url)('../apps/browser/window-controls.cjs');
|
||||
function fixture(options={}){
|
||||
let destroyed=false,current=true,window,context;
|
||||
const calls=[],state={visible:true,focused:false,minimized:false,maximized:false,fullScreen:false};
|
||||
const contents={isDestroyed:()=>false};
|
||||
window={webContents:contents,isDestroyed:()=>destroyed,getBounds:()=>({x:-500,y:90,width:1280,height:800,title:'secret'}),...Object.fromEntries(Object.entries(state).map(([key])=>['is'+key[0].toUpperCase()+key.slice(1),()=>state[key]])),minimize(){calls.push('minimize');state.minimized=true;},maximize(){calls.push('maximize');state.maximized=true;},restore(){calls.push('restore');state.minimized=false;state.maximized=false;},focus(){calls.push('focus');state.focused=true;}};
|
||||
context={ready:true,policy:{version:1,agents:{enabled:true,scopes:['window:state','window:control']}}};
|
||||
const controls=createWindowControls({getWindow:()=>window,getContext:()=>context,isCurrent:()=>current,...options});
|
||||
return {controls,calls,state,get window(){return window;},get context(){return context;},replace:()=>window={...window},invalidate:()=>current=false,destroy:()=>destroyed=true};
|
||||
}
|
||||
test('window state exports only bounded dimensions and booleans',async()=>{
|
||||
const f=fixture();assert.deepEqual(await f.controls.state(),{width:1280,height:800,visible:true,focused:false,minimized:false,maximized:false,fullScreen:false});
|
||||
f.window.getBounds=()=>({width:Infinity,height:1});await assert.rejects(f.controls.state(),/dimensions/);
|
||||
f.window.getBounds=()=>({width:1,height:1});f.window.isVisible=()=> 'secret';await assert.rejects(f.controls.state(),/state/);
|
||||
});
|
||||
test('the four allowed commands execute once and return fresh state',async()=>{
|
||||
const f=fixture();for(const command of ['minimize','maximize','restore','focus'])assert.equal((await f.controls.control({command})).completed,true);
|
||||
assert.deepEqual(f.calls,['minimize','maximize','restore','focus']);assert.equal((await f.controls.state()).focused,true);
|
||||
});
|
||||
test('state and control scopes are independent and global disable denies both',async()=>{
|
||||
const f=fixture();f.context.policy.agents.scopes=['window:state'];await f.controls.state();await assert.rejects(f.controls.control({command:'focus'}),/scope/);
|
||||
f.context.policy.agents.scopes=['window:control'];await assert.rejects(f.controls.state(),/scope/);await f.controls.control({command:'focus'});
|
||||
f.context.policy.agents.enabled=false;await assert.rejects(f.controls.control({command:'focus'}),/scope/);assert.deepEqual(f.calls,['focus']);
|
||||
});
|
||||
test('no close, resize, arbitrary OS command, extra input or prototype access',async()=>{
|
||||
const f=fixture();for(const args of [{command:'close'},{command:'setBounds'},{command:'__proto__'},{command:'focus',path:'secret'},null,Object.create({command:'focus'})])await assert.rejects(f.controls.control(args),/Invalid/);
|
||||
await assert.rejects(f.controls.state({command:'focus'}),/Invalid/);assert.deepEqual(f.calls,[]);
|
||||
});
|
||||
test('expired and malformed policy, stale identity and destroyed native window deny effects',async()=>{
|
||||
for(const mutate of [f=>f.context.policy.expiresAt='2000-01-01',f=>f.context.policy.expiresAt='invalid',f=>f.context.policy.agents.scopes='window:control',f=>f.invalidate(),f=>f.destroy(),f=>f.context.ready=false]){
|
||||
const f=fixture();mutate(f);await assert.rejects(f.controls.control({command:'focus'}));assert.deepEqual(f.calls,[]);
|
||||
}
|
||||
});
|
||||
test('asynchronous authorization cannot release a stale policy, native window or cancellation',async()=>{
|
||||
for(const mutation of ['policy','window','cancel','grant']){
|
||||
const controller=new AbortController();let f,counter=0;f=fixture({authorize:async()=>{if(++counter===1){if(mutation==='policy')f.context.policy={...f.context.policy};if(mutation==='window')f.replace();if(mutation==='cancel')controller.abort();if(mutation==='grant')f.invalidate();}return true;}});
|
||||
await assert.rejects(f.controls.control({command:'focus'},{signal:controller.signal}));assert.deepEqual(f.calls,[]);
|
||||
}
|
||||
});
|
||||
test('an effect is not repeated when it revokes policy or cancellation suppresses output',async()=>{
|
||||
for(const mutation of ['policy','cancel']){
|
||||
const f=fixture(),controller=new AbortController();f.window.focus=()=>{f.calls.push('focus');if(mutation==='policy')f.context.policy.agents.enabled=false;else controller.abort();};
|
||||
await assert.rejects(f.controls.control({command:'focus'},{signal:controller.signal}));assert.deepEqual(f.calls,['focus']);
|
||||
}
|
||||
});
|
||||
test('fresh grant checks apply before the effect and native state getters cannot export after revocation',async()=>{
|
||||
let checks=0;const f=fixture({authorize:async()=>++checks<2});await assert.rejects(f.controls.control({command:'focus'}),/authorization/);assert.deepEqual(f.calls,[]);
|
||||
const g=fixture();g.window.getBounds=()=>{g.invalidate();return {width:1,height:1};};await assert.rejects(g.controls.state(),/changed/);
|
||||
});
|
||||
test('operation tracing wraps execution and failed native effects are never retried',async()=>{
|
||||
const names=[],f=fixture({traceOperation:async(name,fn)=>{names.push(name);return fn();}});await f.controls.state();f.window.focus=()=>{f.calls.push('focus');throw Error('sensitive native failure');};await assert.rejects(f.controls.control({command:'focus'}),/^Error: Managed window operation failed$/);assert.deepEqual(names,['window:state','window:control']);assert.deepEqual(f.calls,['focus']);
|
||||
});
|
||||
Reference in new issue
Block a user