import test from 'node:test'; import assert from 'node:assert/strict'; import {createServer} from 'node:http'; import {mkdtempSync,rmSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join} from 'node:path'; import {openStore} from '../apps/server/store.mjs'; import {createApp} from '../apps/server/app.mjs'; import {librarySnapshotHash,validateLibraryPlan,LibraryPolicySchema} from '../packages/core/library.mjs'; import {verifyBookmarkSnapshot} from '../packages/core/bookmarks.mjs'; const bookmark={id:'personal',title:'Public project',url:'https://example.com/project',folder:''}; const tab={id:'tab-1',title:'Public documentation',url:'https://docs.example.com/path?session=private#secret'}; const plan={bookmarks:[{id:'personal',folder:'Projects'}],tabs:[{id:'tab-1',group:'Research'}]}; async function fixture(t){ const dir=mkdtempSync(join(tmpdir(),'library-test-')),store=openStore(dir),adminToken='library-fixture-admin';let output=JSON.stringify(plan),calls=0,prompt='',gate=null; const provider=createServer(async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);prompt=JSON.parse(Buffer.concat(chunks).toString()).messages[0].content;calls++;if(gate)await gate;res.setHeader('content-type','application/json');res.end(JSON.stringify({choices:[{message:{content:output}}]}));}); await new Promise(r=>provider.listen(0,'127.0.0.1',r)); const config={adminToken,publicUrl:'http://127.0.0.1',secure:false,aiBaseUrl:`http://127.0.0.1:${provider.address().port}`,aiModel:'fixture',aiEndpointId:'default'}; const server=createApp(store,config).listen(0,'127.0.0.1');await new Promise(r=>server.on('listening',r)); t.after(async()=>{await Promise.all([new Promise(r=>server.close(r)),new Promise(r=>provider.close(r))]);store.db.close();rmSync(dir,{recursive:true,force:true});}); async function api(route,body,token=adminToken){const r=await fetch(`http://127.0.0.1:${server.address().port}${route}`,{method:body?'POST':'GET',headers:{authorization:`Bearer ${token}`,...(body?{'content-type':'application/json'}:{})},body:body?JSON.stringify(body):undefined});return {status:r.status,body:await r.json()};} const code=(await api('/api/admin/enrollments',{user:'fixture-user'})).body.code,device=(await api('/api/device/enroll',{code,name:'Fixture',platform:'win32'},'')).body; const p=store.get('policy');p.ai={...p.ai,enabled:true,trustMode:'local',model:'fixture'};p.library={aiSuggestionsEnabled:true,aiAutoApplyEnabled:false};store.put('policy',p); await api('/api/device/bookmarks',{action:'add',expectedRevision:0,policyVersion:1,bookmark},device.token); const request={policyVersion:1,bookmarkRevision:1,tabs:[tab],snapshotHash:librarySnapshotHash([tab])}; return {store,device,api,request,propose:()=>api('/api/device/library/propose',request,device.token),setOutput:v=>{output=v;},setGate:v=>{gate=v;},calls:()=>calls,prompt:()=>prompt}; } test('AI library defaults disabled and strict organization permits only existing item IDs and labels',()=>{ assert.deepEqual(LibraryPolicySchema.parse({}),{aiSuggestionsEnabled:false,aiAutoApplyEnabled:false});assert.deepEqual(validateLibraryPlan(plan,{bookmarks:[bookmark],tabs:[tab]}),plan); for(const v of [{...plan,bookmarks:[{...plan.bookmarks[0],url:'https://evil.test'}]},{...plan,tabs:[{id:'other',group:'Research'}]},{...plan,bookmarks:[]},{...plan,tabs:[{id:tab.id,group:'a'.repeat(41)}]},{...plan,bookmarks:[{id:bookmark.id,folder:'bad\nlabel'}]}])assert.throws(()=>validateLibraryPlan(v,{bookmarks:[bookmark],tabs:[tab]})); }); test('approved gateway proposes metadata-only AI plan and atomically applies signed personal bookmarks',async t=>{ const f=await fixture(t),result=await f.propose();assert.equal(result.status,200);assert.ok(f.prompt().includes('docs.example.com'));assert.ok(!f.prompt().includes('/path'));assert.ok(!f.prompt().includes('session=private'));assert.ok(!f.prompt().includes('/project')); const applied=await f.api('/api/device/library/apply',{proposalId:result.body.proposalId,policyVersion:1,expectedRevision:1,snapshotHash:f.request.snapshotHash},f.device.token);assert.equal(applied.status,200);assert.equal(applied.body.revision,2);assert.equal(applied.body.items[0].folder,'Projects');assert.deepEqual(applied.body.tabs,plan.tabs); assert.equal(verifyBookmarkSnapshot(applied.body.bookmarks,f.device.publicKey,{deviceId:f.device.id,policyVersion:1,minimumRevision:2}).items[0].folder,'Projects'); assert.equal((await f.api('/api/device/library/apply',{proposalId:result.body.proposalId,policyVersion:1,expectedRevision:2,snapshotHash:f.request.snapshotHash},f.device.token)).status,404); const events=f.store.db.prepare("SELECT metadata FROM events WHERE kind LIKE 'library.%'").all();assert.equal(events.length,2);assert.ok(events.every(e=>!e.metadata.includes(bookmark.title)&&!e.metadata.includes(bookmark.url)&&!e.metadata.includes('Projects'))); }); test('central disable, hidden library, expired policy, malformed snapshot and disabled automatic application fail closed',async t=>{ const f=await fixture(t),p=f.store.get('policy');p.library.aiSuggestionsEnabled=false;f.store.put('policy',p);assert.equal((await f.propose()).status,403);assert.equal(f.calls(),0); p.library.aiSuggestionsEnabled=true;p.browser.settings.hiddenSections=['library'];f.store.put('policy',p);assert.equal((await f.propose()).status,403);p.browser.settings.hiddenSections=[];f.store.put('policy',p); assert.equal((await f.api('/api/device/library/propose',{...f.request,snapshotHash:'0'.repeat(64)},f.device.token)).status,409);const r=await f.propose();assert.equal(r.status,200); assert.equal((await f.api('/api/device/library/apply',{proposalId:r.body.proposalId,policyVersion:1,expectedRevision:1,snapshotHash:f.request.snapshotHash,mode:'automatic'},f.device.token)).status,403); p.expiresAt=new Date(Date.now()-1000).toISOString();f.store.put('policy',p);assert.equal((await f.propose()).status,403); }); test('malicious model output and protected metadata rejected before proposal or transmission',async t=>{ const f=await fixture(t);for(const bad of [{...plan,bookmarks:[{id:'managed',folder:'Work'}]},{...plan,delete:['personal']},{...plan,tabs:[{...plan.tabs[0],url:'https://evil.test'}]}]){f.setOutput(JSON.stringify(bad));assert.notEqual((await f.propose()).status,200);assert.equal(f.store.personalBookmarks(f.device.id).revision,1);} const before=f.calls(),p=f.store.get('policy');p.dlp.rules.push({id:'confidential-title',label:'Protected title',kind:'literal',value:bookmark.title,action:'block'});f.store.put('policy',p);assert.equal((await f.propose()).status,403);assert.equal(f.calls(),before); }); test('stale bookmark revision, policy generation and managed collisions cannot apply or partially change library',async t=>{ const f=await fixture(t),r=await f.propose();assert.equal(r.status,200);const apply={proposalId:r.body.proposalId,policyVersion:1,expectedRevision:1,snapshotHash:f.request.snapshotHash}; assert.equal((await f.api('/api/device/library/apply',{...apply,snapshotHash:'0'.repeat(64)},f.device.token)).status,409); f.store.db.prepare('UPDATE bookmarks SET revision=2 WHERE device_id=?').run(f.device.id);assert.equal((await f.api('/api/device/library/apply',apply,f.device.token)).status,409);assert.equal(f.store.personalBookmarks(f.device.id).items[0].folder,''); f.store.db.prepare('UPDATE bookmarks SET revision=1 WHERE device_id=?').run(f.device.id);const p=f.store.get('policy');p.version=2;f.store.put('policy',p);assert.equal((await f.api('/api/device/library/apply',{...apply,policyVersion:2},f.device.token)).status,409); p.version=1;p.browser.bookmarks.managed=[bookmark];f.store.put('policy',p);assert.equal((await f.api('/api/device/library/apply',apply,f.device.token)).status,403);assert.equal(f.store.personalBookmarks(f.device.id).revision,1); }); test('automatic organization is separately enabled and existing proposals cannot survive a remote disable',async t=>{ const f=await fixture(t),p=f.store.get('policy');p.library.aiAutoApplyEnabled=true;f.store.put('policy',p);const r=await f.propose();assert.equal(r.status,200); const body={proposalId:r.body.proposalId,policyVersion:1,expectedRevision:1,snapshotHash:f.request.snapshotHash,mode:'automatic'}; p.library.aiSuggestionsEnabled=false;f.store.put('policy',p);assert.equal((await f.api('/api/device/library/apply',body,f.device.token)).status,403);assert.equal(f.store.personalBookmarks(f.device.id).revision,1); p.library.aiSuggestionsEnabled=true;f.store.put('policy',p);const applied=await f.api('/api/device/library/apply',body,f.device.token);assert.equal(applied.status,200);assert.equal(applied.body.revision,2); }); test('model output DLP and pending proposal bounds are enforced without storing rejected model data',async t=>{ const f=await fixture(t),p=f.store.get('policy');p.dlp.rules.push({id:'protected-output',label:'Protected',kind:'literal',value:'Projects',action:'block'});f.store.put('policy',p);assert.equal((await f.propose()).status,403);assert.equal(f.store.personalBookmarks(f.device.id).revision,1); p.dlp.rules=[];f.store.put('policy',p);for(let n=0;n<3;n++)assert.equal((await f.propose()).status,200);const before=f.calls();assert.equal((await f.propose()).status,409);assert.equal(f.calls(),before); }); test('concurrent proposal requests are bounded and policy changes during provider execution invalidate results',async t=>{ const f=await fixture(t);let release;f.setGate(new Promise(r=>{release=r;}));const pending=f.propose(); for(let n=0;n<100&&f.calls()===0;n++)await new Promise(r=>setTimeout(r,10));assert.equal(f.calls(),1); assert.equal((await f.propose()).status,409);assert.equal(f.calls(),1); const p=f.store.get('policy');p.version++;f.store.put('policy',p);release();assert.equal((await pending).status,409);assert.equal(f.store.personalBookmarks(f.device.id).revision,1); });