Files

18 lines
3.8 KiB
JavaScript
Raw Permalink Normal View History

import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const {createLibraryOrganization}=createRequire(import.meta.url)('../apps/browser/library-organization.cjs');
function fixture(){
const snapshot={bookmarks:[{id:'b1'}],tabs:[{id:'t1'}]},plan={bookmarks:[{id:'b1',folder:'Work'}],tabs:[{id:'t1',group:'Research'}]};
let current=true,auto=false,commits=0,applied=0;
const f={capture:async()=>snapshot,assertCurrent:()=>{if(!current)throw Error('stale');},inspect:async()=>{},propose:async()=>({proposalId:'p1',plan}),commit:async()=>{commits++;return {revision:2};},applyTabs:()=>{applied++;},audit:async()=>{},autoAllowed:()=>auto};
return {f,plan,stale:()=>current=false,auto:()=>auto=true,counts:()=>({commits,applied})};
}
test('native organization requires review and pins authoritative plan',async()=>{const x=fixture(),o=createLibraryOrganization(x.f);const result=await o.plan();result.plan.tabs[0].group='Hostile';await assert.rejects(o.apply({proposalId:'p1'}),/Review/);await o.apply({proposalId:'p1',confirm:true});assert.deepEqual(x.counts(),{commits:1,applied:1});await assert.rejects(o.apply({proposalId:'p1',confirm:true}),/expired/);});
test('native stale context blocks before central commit',async()=>{const x=fixture(),o=createLibraryOrganization(x.f);await o.plan();x.stale();await assert.rejects(o.apply({proposalId:'p1',confirm:true}),/stale/);assert.deepEqual(x.counts(),{commits:0,applied:0});});
test('native postcommit context change explicitly leaves tabs untouched',async()=>{const x=fixture();x.f.commit=async()=>{x.stale();return {revision:2};};const o=createLibraryOrganization(x.f);await o.plan();await assert.rejects(o.apply({proposalId:'p1',confirm:true}),/Bookmarks were organized centrally; tabs were unchanged/);assert.equal(x.counts().applied,0);});
test('model cannot introduce URL, unknown ID or arbitrary properties',async()=>{const x=fixture();x.f.propose=async()=>({proposalId:'p1',plan:{bookmarks:[{id:'b1',folder:'Work',url:'https://evil.test'}],tabs:[{id:'t1',group:'R'}]}});await assert.rejects(createLibraryOrganization(x.f).plan());x.f.propose=async()=>({proposalId:'p1',plan:{bookmarks:[{id:'unknown',folder:'Work'}],tabs:[{id:'t1',group:'R'}]}});await assert.rejects(createLibraryOrganization(x.f).plan());});
test('automatic apply requires central local-only permission',async()=>{const x=fixture(),o=createLibraryOrganization(x.f);await o.plan();await assert.rejects(o.apply({proposalId:'p1',autoApply:true}),/Review/);x.auto();await o.apply({proposalId:'p1',autoApply:true});assert.equal(x.counts().applied,1);});
test('pending organization serializes library operations and releases reservation on failure',async()=>{const x=fixture();let release;x.f.propose=()=>new Promise(resolve=>release=resolve);const o=createLibraryOrganization(x.f),pending=o.plan();await new Promise(resolve=>setImmediate(resolve));assert.equal(o.isBusy(),true);await assert.rejects(o.plan(),/already running/);release({proposalId:'p1',plan:x.plan});await pending;assert.equal(o.isBusy(),false);});
test('aborted organization cannot commit or apply native groups',async()=>{const x=fixture(),o=createLibraryOrganization(x.f),controller=new AbortController();await o.plan();controller.abort();await assert.rejects(o.apply({proposalId:'p1',confirm:true},{signal:controller.signal}),/cancelled/);assert.deepEqual(x.counts(),{commits:0,applied:0});});
test('proposal binding cannot be replaced by an unrelated snapshot',async()=>{const x=fixture();x.f.capture=async()=>({bookmarks:[{id:'b1'}],tabs:[{id:'t1'}],snapshotHash:'a'.repeat(64),policyVersion:1,bookmarkRevision:1});x.f.propose=async()=>({proposalId:'p1',plan:x.plan,binding:{snapshotHash:'b'.repeat(64),policyVersion:1,bookmarkRevision:1}});await assert.rejects(createLibraryOrganization(x.f).plan(),/binding/);});