const test=require('node:test'); const assert=require('node:assert/strict'); const fs=require('node:fs'); const vm=require('node:vm'); const path=require('node:path'); const context=vm.createContext({}); vm.runInContext(fs.readFileSync(path.join(__dirname,'../../static/js/drafts.js'),'utf8'),context); function storage(){ const data=new Map(); return {data,get length(){return data.size;},key:index=>Array.from(data.keys())[index] ?? null, getItem:key=>data.get(key) ?? null,setItem:(key,value)=>data.set(key,value),removeItem:key=>data.delete(key)}; } function drafts(scope='1'.repeat(64),local=storage(),tab=storage()){ return context.ChatDrafts.create({scope,storage:()=>local,tabStorage:()=>tab}); } test('conversation drafts and active tab restore independently of another visitor or tab',()=>{ const local=storage(),tab=storage(),a=drafts(undefined,local,tab); a.write(9,'A');a.write(10,'B');a.write(null,'NEW');a.active(9); const restored=drafts(undefined,local,tab); assert.equal(restored.read(9),'A');assert.equal(restored.read(10),'B');assert.equal(restored.read(null),'NEW'); assert.equal(restored.active(),'9'); const otherTab=drafts(undefined,local);otherTab.write(10,'B updated'); assert.equal(otherTab.active(),'new');assert.equal(restored.read(10),'B updated');assert.equal(restored.read(9),'A'); otherTab.remove(10);assert.equal(restored.read(10),''); assert.equal(drafts('2'.repeat(64),local,tab).read(9),''); }); test('malformed records and invalid identifiers cannot become restored drafts',()=>{ const local=storage(),a=drafts(undefined,local),key='baiyue-draft-v1:'+'1'.repeat(64)+':9'; for(const value of ['null','{"version":2,"text":"old"}','{"version":1,"text":45}']){ local.setItem(key,value);assert.equal(a.read(9),''); } assert.equal(a.write('../9','invalid'),false);assert.equal(a.read('../9'),''); }); test('removing and clearing a visitor never clears another visitor or unrelated preferences',()=>{ const local=storage(),a=drafts(undefined,local),b=drafts('2'.repeat(64),local); local.setItem('theme','dark');a.write(9,'A');a.write(10,'B');b.write(9,'PRIVATE'); a.remove(9);assert.equal(a.read(9),'');assert.equal(a.read(10),'B'); a.clear();assert.equal(a.read(10),'');assert.equal(b.read(9),'PRIVATE');assert.equal(local.getItem('theme'),'dark'); }); test('blocked storage and quota errors retain the latest in-memory draft without a shared bucket',()=>{ const broken={getItem(){throw Error();},setItem(){throw Error();},removeItem(){throw Error();}}; const a=drafts(undefined,broken,broken); assert.equal(a.write(9,'latest'),false);assert.equal(a.read(9),'latest');a.active(9);assert.equal(a.active(),'9'); const local=storage(),b=drafts(undefined,local);b.write(9,'old'); local.setItem=()=>{throw Error('quota');};assert.equal(b.write(9,'new'),false);assert.equal(b.read(9),'new'); const anonymous=drafts(null,local);anonymous.write(9,'memory only');assert.equal(anonymous.read(9),'memory only'); assert.equal(drafts(null,local).read(9),''); });