Files
safetynet/tests/session-restore.test.mjs
T

17 lines
6.5 KiB
JavaScript

import test from 'node:test';import assert from 'node:assert/strict';import {createRequire} from 'node:module';const require=createRequire(import.meta.url),{restoreNativeSession}=require('../apps/browser/session-restore.cjs');
function fixture(count=1){let active='old0',activation=1,revision='r1',authority=true,creates=0;const records=Array.from({length:count},(_,i)=>({id:'old'+i,contents:{isDestroyed:()=>false}})),removed=[],stages=[];const registry={all:()=>records,active:()=>records.find(r=>r.id===active),context:()=>({activation}),get:id=>{const row=records.find(r=>r.id===id);if(!row)throw Error('Unknown tab');return row;},remove:id=>{removed.push(id);records.splice(records.findIndex(r=>r.id===id),1);if(active===id)active=records[0]?.id;}};const options={activeIndex:0,assertCurrent:()=>{if(!authority)throw Error('Authority revoked');},revision:'r1',owner:{deviceId:'device',ownerId:'owner'}},deps={registry,store:{list:()=>({revision})},activate:id=>{if(active!==id){active=id;activation++;}},traceOperation:async(name,fn)=>{stages.push(name);return fn();},create:async(_input,controls)=>{creates++;controls.assertCurrent();const native={id:'new'+creates,contents:{isDestroyed:()=>false}};records.push(native);controls.onCreated(native);controls.assertCurrent();if(active!==native.id){active=native.id;activation++;}controls.onActivated(native);controls.assertCurrent();return native.id;}};return {records,removed,stages,options,deps,setRevision:r=>revision=r,revoke:()=>authority=false,creates:()=>creates,active:()=>active};}
const tabs=[{url:'https://example.com/docs',title:'Docs',group:'Research'},{url:'https://example.com/other',title:'Other'}];
test('restore creates owned native tabs once and selects saved active index',async()=>{const f=fixture();f.options.activeIndex=1;const r=await restoreNativeSession(tabs,f.options,f.deps);assert.deepEqual(r.sessionIds,['new1','new2']);assert.equal(r.activeSessionId,'new2');assert.equal(f.creates(),2);assert.deepEqual(f.removed,[]);assert.deepEqual(f.stages,['preflight','native','native']);});
test('capacity and changed stored revision fail before any create',async()=>{for(const f of [fixture(32),fixture()]){if(f.records.length===1)f.setRevision('r2');await assert.rejects(restoreNativeSession(tabs,f.options,f.deps));assert.equal(f.creates(),0);assert.deepEqual(f.removed,[]);}});
test('authority revoked during creation rolls back only created tabs without retry',async()=>{const f=fixture(),create=f.deps.create;f.deps.create=async(input,c)=>{const id=await create(input,c);f.revoke();return id;};await assert.rejects(restoreNativeSession(tabs,f.options,f.deps),/Authority revoked/);assert.equal(f.creates(),1);assert.deepEqual(f.removed,['new1']);assert.equal(f.active(),'old0');assert.deepEqual(f.records.map(r=>r.id),['old0']);assert.ok(f.stages.includes('rollback'));});
test('internally removed failed creation is skipped during exact-owned rollback',async()=>{const f=fixture(),create=f.deps.create;f.deps.create=async(input,c)=>{const id=await create(input,c);f.deps.registry.remove(id);throw Error('Native creation failed');};await assert.rejects(restoreNativeSession(tabs,f.options,f.deps),/Native creation failed/);assert.deepEqual(f.removed,['new1']);assert.equal(f.creates(),1);assert.equal(f.active(),'old0');});
test('external foreign tab change triggers failure without closing foreign tabs',async()=>{const f=fixture(),create=f.deps.create;f.deps.create=async(input,c)=>{const id=await create(input,c);f.records.push({id:'foreign',contents:{isDestroyed:()=>false}});return id;};await assert.rejects(restoreNativeSession(tabs,f.options,f.deps));assert.deepEqual(f.removed,['new1']);assert.deepEqual(f.records.map(r=>r.id),['old0','foreign']);});
test('identity replacement prevents cleanup from removing replacement tab',async()=>{const f=fixture(),create=f.deps.create;f.deps.create=async(input,c)=>{const id=await create(input,c);f.records.find(r=>r.id===id).contents={isDestroyed:()=>false};throw Error('Replaced');};await assert.rejects(restoreNativeSession(tabs,f.options,f.deps),/Replaced/);assert.deepEqual(f.removed,[]);assert.equal(f.creates(),1);});
test('storage revision changed during create rolls back created tabs',async()=>{const f=fixture(),create=f.deps.create;f.deps.create=async(input,c)=>{const id=await create(input,c);f.setRevision('r2');return id;};await assert.rejects(restoreNativeSession(tabs,f.options,f.deps));assert.deepEqual(f.removed,['new1']);assert.equal(f.creates(),1);});
test('rollback attempts every exact-owned tab once when one close fails',async()=>{const f=fixture(),create=f.deps.create,remove=f.deps.registry.remove;f.deps.create=async(input,c)=>{const id=await create(input,c);if(f.creates()===2)f.revoke();return id;};const attempts=[];f.deps.registry.remove=id=>{attempts.push(id);if(id==='new2')throw Error('Close failed');remove(id);};await assert.rejects(restoreNativeSession(tabs,f.options,f.deps),e=>e.message==='Managed session rollback incomplete'&&e.cause.message==='Authority revoked');assert.deepEqual(attempts,['new2','new1']);assert.equal(f.active(),'old0');assert.equal(f.creates(),2);assert.deepEqual(f.records.map(r=>r.id),['old0','new2']);});
test('create callback cannot claim original tab ownership for rollback',async()=>{const f=fixture();f.deps.create=async(_input,c)=>{c.onCreated(f.records[0]);return 'old0';};await assert.rejects(restoreNativeSession(tabs,f.options,f.deps),/ownership mismatch/);assert.deepEqual(f.removed,[]);assert.deepEqual(f.records.map(r=>r.id),['old0']);});
test('switching away and back during creation invalidates activation epoch',async()=>{const f=fixture(2),create=f.deps.create;f.deps.create=async(input,c)=>{const id=await create(input,c);f.deps.activate('old1');f.deps.activate(id);return id;};await assert.rejects(restoreNativeSession(tabs,f.options,f.deps),/context changed/);assert.equal(f.creates(),1);assert.deepEqual(f.removed,['new1']);assert.equal(f.active(),'old0');});
test('native activation hook rejects unexpected extra activation epochs',async()=>{const f=fixture(2),create=f.deps.create;f.deps.create=async(input,c)=>create(input,{...c,onActivated:native=>{f.deps.activate('old1');f.deps.activate(native.id);c.onActivated(native);}});await assert.rejects(restoreNativeSession(tabs,f.options,f.deps),/activation changed/);assert.deepEqual(f.removed,['new1']);});
test('registry without native activation context is rejected before release',async()=>{const f=fixture();delete f.deps.registry.context;await assert.rejects(restoreNativeSession(tabs,f.options,f.deps));assert.equal(f.creates(),0);});