Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499import test, {beforeEach, afterEach} from 'node:test';import assert from 'node:assert/strict';import {register} from 'node:module';register(new URL('./phone-loader.mjs', import.meta.url));const {createSession} = await import('../phone/session.mjs');const networkFetch = globalThis.fetch;beforeEach(() => { globalThis.fetch = async () => { throw new Error('Unexpected network request in unit test'); }; });afterEach(() => { globalThis.fetch = networkFetch; });// Editing/history tests start after account onboarding; access tests below use// createSession directly and exercise actual account verification.function signedInSession(options = {}) { const session = createSession({accountFetch:async url => Response.json(url.includes('/userinfo') ? {sub:'test-user'} : {handle:'test'}), ...options}); Object.assign(session.state, {token:'test-token', handle:'test', accountVerified:true, autoPublish:false}); return session;}
test('native MCP permission preference round-trips and expired approvals dismiss', async () => { const events=[];let allowed=true; const session=signedInSession({emit:e=>events.push(e),hostRPC:async(method,params)=>{ if(method==='approvalPolicy')allowed=params.mcpAutoAllow; return {providers:[],mcpAutoAllow:allowed}; }}); await session.refreshProviders(); assert.equal(events.findLast(e=>e.type==='providers').supportsApprovalPolicy,true); await session.setMcpAutoAllow(false); assert.equal(events.findLast(e=>e.type==='providers').mcpAutoAllow,false); await assert.rejects(session.respondToApproval('expired','decline'),/no longer available/); assert.deepEqual(events.at(-1),{type:'approval',approval:null});});
test('unsent drafts survive relaunch and thread switches without leaking into another thread', async () => { const values = new Map(); const storage = {get:k=>values.get(k),set:(k,v)=>values.set(k,v)}; const first = signedInSession({storage}); await first.open(); const id = first.state.id; first.setDraft('Make a green circle', id); await first.newSession(); const next = first.state.id; first.setDraft('A different idea', next); assert.throws(()=>first.setDraft('stale input',id), /another thread/); await first.resumeSession(id); assert.equal(first.state.composer, 'Make a green circle'); const events = []; const reopened = signedInSession({storage,emit:e=>events.push(e)}); await reopened.open(); assert.equal(reopened.state.composer, 'Make a green circle'); assert.equal(events.find(e=>e.type==='thread').composer, 'Make a green circle'); await reopened.resumeSession(next); assert.equal(reopened.state.composer, 'A different idea');});
test('phone publication verifies uploaded source without desktop revision storage', async () => { const source='export function paint({wipe}) {wipe("orange")}'; const originalFetch=globalThis.fetch; const calls=[]; globalThis.fetch=async (url,options={})=>{ calls.push(url); if(url.includes('/presigned-upload-url/'))return Response.json({uploadURL:'https://upload.test/piece'}); if(options.method==='PUT')return new Response(''); return new Response(source); }; try { const values=new Map([['session',JSON.stringify({slug:'phone-test',source})]]); const events=[]; const session=signedInSession({storage:{get:k=>values.get(k),set:(k,v)=>values.set(k,v)},emit:e=>events.push(e)}); await session.open(); session.state.token='test-token';session.state.handle='test'; await session.publish(); assert.equal(session.state.published,true); assert.equal(calls.length,3); assert.equal(events.some(e=>e.type==='bad'),false); assert(events.some(e=>e.type==='preview')); }finally{globalThis.fetch=originalFetch;}});
test('phone threads preserve source, transcript and engine context without account credentials', async () => { const source = 'export function paint({wipe}) {wipe("orange")}'; const values = new Map([['session', JSON.stringify({slug:'legacy',source,published:true,token:'fake-token',handle:'test'})]]); const storage = {get:key=>values.get(key),set:(key,value)=>values.set(key,value)}; const originalFetch = globalThis.fetch; globalThis.fetch = async url => new Response(JSON.stringify(url.includes('/userinfo') ? {sub:'test-user'} : {handle:'test'})); try { const first = createSession({storage}); await first.restore(); await first.open(); const legacyID = first.state.id; assert.equal(first.history().length, 1); assert.equal(first.state.published, true); first.state.transcript.push({type:'you',text:'make orange'}); first.state.engine = {threadId:'engine-1',messages:[{role:'user',content:'make orange'}],turns:1}; first.saveCurrent(); await first.newSession('piece'); assert.equal(first.history().length, 2); assert.notEqual(first.state.id, legacyID); await first.resumeSession(legacyID); assert.equal(first.state.slug, 'legacy'); assert.equal(first.state.engine.messages[0].content, 'make orange'); assert.equal(first.state.transcript[0].text, 'make orange'); assert.equal(JSON.parse(values.get('session')).source, source); assert.ok(!values.get('threads').includes('fake-token')); await assert.rejects(()=>first.newSession('picture'), /not supported/); const relaunched = createSession({storage}); await relaunched.restore(); await relaunched.open(); assert.equal(relaunched.state.id, legacyID); assert.equal(relaunched.state.transcript[0].text, 'make orange'); assert.equal(relaunched.history().length, 2); assert.equal(relaunched.state.published, true); } finally { globalThis.fetch = originalFetch; }});
test('expired account tokens are cleared while saved drafts remain', async () => { const values = new Map([['session',JSON.stringify({token:'expired',slug:'keep',source:'export function paint(){}'})]]); const originalFetch = globalThis.fetch; globalThis.fetch = async () => new Response('',{status:401}); try { const session = createSession({storage:{get:key=>values.get(key),set:(key,value)=>values.set(key,value)}}); assert.equal(await session.restore(), false); assert.equal(session.state.token, ''); await session.open(); assert.equal(session.state.slug, 'keep'); assert.equal(JSON.parse(values.get('session')).token, ''); assert.equal(session.history().length, 1); } finally { globalThis.fetch = originalFetch; }});
test('braincell model stays automatic across old threads and model commands', async () => { const {DEFAULT_AC_MODEL} = await import('../src/ac-server.mjs'); const values = new Map(); const storage = {get:key=>values.get(key),set:(key,value)=>values.set(key,value)}; const session = signedInSession({storage}); await session.open(); assert.equal(session.state.model, DEFAULT_AC_MODEL); await assert.rejects(session.ask('/model opus'), /managed automatically/); assert.throws(()=>session.setModel('unknown'), /managed automatically/); // A pre-update thread may still have a manually chosen premium model. const stored = JSON.parse(values.get('threads')); stored.items[0].model = 'anthropic/claude-opus-5'; if (stored.items[0].engine) stored.items[0].engine.model = stored.items[0].model; values.set('threads', JSON.stringify(stored)); const restored = signedInSession({storage}); await restored.open(); assert.equal(restored.state.model, DEFAULT_AC_MODEL);});
test('source validation and rollback preserve versioned bytes without generation',async()=>{ const values=new Map(),session=signedInSession({storage:{get:k=>values.get(k),set:(k,v)=>values.set(k,v)}});await session.open(); session.setAutoPublish(false);const original=session.state.revisions[0].source;assert.equal(session.state.revisions[0].version,0); await session.editSource('export function paint({wipe}) { wipe("blue") }'); assert.equal(session.state.revisions.length,2);assert.equal(session.state.revisions.at(-1).version,1); await assert.rejects(session.editSource('export function paint(){let let = 1}'),/Invalid JavaScript/); assert.equal(session.state.revisions.length,2);assert.equal(session.state.revisions.at(-1).version,1); await session.restoreRevision(0); assert.equal(session.state.revisions.at(-1).source,original); assert.equal(session.state.revisions.at(-1).reason,'restored v0'); const reopened=signedInSession({storage:{get:k=>values.get(k),set:(k,v)=>values.set(k,v)}});await reopened.open(); assert.equal(reopened.state.revisions.length,3);assert.equal(reopened.state.autoPublish,false);});
test('publication never marks newer draft or unverified bytes public',async()=>{ const originalFetch=globalThis.fetch,events=[];let finishUpload,uploaded; const session=signedInSession({emit:e=>events.push(e)});await session.open();session.setAutoPublish(false); session.state.token='test';session.state.handle='test'; const first='export function paint(){return "first"}',second='export function paint(){return "second"}'; await session.editSource(first); globalThis.fetch=async(url,options={})=>{ if(url.includes('/presigned-upload-url/'))return Response.json({uploadURL:'https://upload.test/piece'}); if(options.method==='PUT'){uploaded=options.body;await new Promise(resolve=>finishUpload=resolve);return new Response('');} return new Response(uploaded); }; try{ const pending=session.publish();while(!finishUpload)await new Promise(resolve=>setTimeout(resolve,1)); await session.editSource(second);finishUpload();await pending; assert.equal(session.state.published,false);assert.equal(session.state.publication.source,first); assert.equal(events.some(e=>e.type==='preview'),false); globalThis.fetch=async(url,options={})=>url.includes('/presigned-upload-url/')?Response.json({uploadURL:'https://upload.test/piece'}):new Response(options.method==='PUT'?'':'different source'); await session.publish();assert.equal(session.state.published,false); assert.equal(events.some(e=>e.type==='notice' && e.text.includes('could not be verified')),true); }finally{globalThis.fetch=originalFetch;}});
test('portable notebooks roundtrip source/history without credentials, host identity or publication',async()=>{ const session=signedInSession();await session.open();session.setAutoPublish(false); await session.editSource('export function paint(){return 42}');session.setDraft('unsent'); session.state.token='never-export';session.state.handle='private-owner'; session.state.engine={threadId:'private-provider-state'};session.state.transcript=[{type:'you',text:'draw a circle'},{type:'bridge',method:'item/agentMessage/delta',params:{delta:'Done'}}]; const json=session.exportNotebook();assert.ok(!json.includes('never-export'));assert.ok(!json.includes('private-owner'));assert.ok(!json.includes('private-provider-state')); const previous=session.state.id;await session.importNotebook(json); assert.notEqual(session.state.id,previous);assert.equal(session.state.autoPublish,false);assert.equal(session.state.published,false); assert.equal(session.state.composer,'unsent');assert.equal(session.state.revisions.at(-1).source,'export function paint(){return 42}'); const bad=JSON.parse(json);bad.source='export function {'; await assert.rejects(session.importNotebook(JSON.stringify(bad)),/Invalid JavaScript/); assert.equal(session.history().length,2);});
test('native versions match Electron: v0, changed bytes increment, same bytes do not, restore appends',async()=>{ const session=signedInSession();await session.open();session.setAutoPublish(false); assert.equal(session.state.revisions.at(-1).version,0); const original=session.state.revisions[0].source; const changed='export function paint(){ return "updated"; }'; await session.editSource(changed);assert.equal(session.state.revisions.at(-1).version,1); await session.editSource(changed);assert.equal(session.state.revisions.at(-1).version,1); await session.restoreRevision(0);assert.equal(session.state.revisions.at(-1).version,2); assert.equal(session.state.revisions.at(-1).source,original); assert.equal(session.state.revisions.at(-1).summary,'Restored version 0.');});
test('piece revision starts at zero, counts changed source, and survives thread reload', async () => { const values=new Map(); const events=[]; const session=signedInSession({storage:{get:k=>values.get(k),set:(k,v)=>values.set(k,v)},emit:e=>events.push(e)}); const originalFetch=globalThis.fetch; globalThis.fetch=async()=>new Response('fixture guide'); try { await session.begin();await session.open(); const id=session.state.id; assert.equal(session.state.version,0); const vfs=await import('../phone/shim/fs.mjs'); vfs.writeFileSync(session.state.file,'export function paint() {}'); assert.equal(session.state.version,1); vfs.writeFileSync(session.state.file,'export function paint() {}'); assert.equal(session.state.version,1); assert.equal(events.filter(e=>e.type==='source').at(-1).version,1); await session.newSession();assert.equal(session.state.version,0); await session.resumeSession(id);assert.equal(session.state.version,1); assert.equal(events.filter(e=>e.type==='piece').at(-1).version,1); } finally {globalThis.fetch=originalFetch;}});
test('connected Claude and Codex require an AC account while keeping inference on the selected provider', async () => { const originalFetch = globalThis.fetch; const network = []; const previousGuides = globalThis.__aeselGuides; globalThis.__aeselGuides = Object.fromEntries(['pieces.md','screen.md','hand.md','kidlisp.md','api.json'].map(name=>['/easel/context/'+name,'fixture guide'])); globalThis.fetch = async url => { network.push(url); throw new Error('Unexpected AC request'); }; try { for (const provider of ['claude', 'codex']) { const events = [], calls = []; let available = true; const source = 'export function paint({wipe}) { wipe("purple") }'; const hostRPC = async (method, params) => { calls.push({method, params}); if (method === 'capabilities') return {providers: [{id:provider, available, models:[{id:'',title:'CLI default'}]}]}; if (method === 'configure') return {sequence:0}; if (method === 'turn') return {id:params.operationID,status:'running'}; if (method === 'events') return { oldest:1, events:[{sequence:1,operation:params.operationID,type:'source',source}], operation:{id:params.operationID,status:'completed'}, approvals:[] }; throw new Error(`Unexpected RPC: ${method}`); }; const session = signedInSession({hostRPC,emit:e=>events.push(e)}); await session.begin(); await session.open(); await session.refreshProviders(); session.setProvider(provider); session.setAutoPublish(false); await session.ask('Make purple'); assert.equal(calls.filter(c=>c.method==='turn').length, 1); assert.equal(calls.find(c=>c.method==='configure').params.provider, provider); assert.equal(session.state.revisions.at(-1).source, source); assert(!session.state.published); assert.equal(session.state.hostOperation, null); assert.equal(events.some(e=>e.type==='bad'), false); available = false; await session.refreshProviders(); await session.ask('Do not lose this request'); assert.equal(calls.filter(c=>c.method==='turn').length, 1); assert.equal(session.state.provider, provider); assert(events.some(e=>e.type==='bad' && e.text.includes('not connected'))); session.setProvider('ac'); session.signOut(); await session.ask('AC still needs login'); assert(events.some(e=>e.type==='bad' && e.text.includes('Sign in to Aesthetic Computer'))); } assert(!network.some(url=>url.includes('easel-inference'))); } finally { globalThis.fetch = originalFetch; globalThis.__aeselGuides = previousGuides; }});
test('upload recovery is ephemeral, preserves the draft, and never retries authorization',async()=>{ const originalFetch=globalThis.fetch; try { for(const mode of ['recover','offline','auth']) { const events=[];let grants=0,putSource; const session=signedInSession({emit:e=>events.push(e),retryOptions:{sleep:async()=>{}}}); await session.open();session.setAutoPublish(false); const source='export function paint(){return "saved"}';await session.editSource(source); session.state.token='test';session.state.handle='test'; globalThis.fetch=async(url,options={})=>{ if(url.includes('/presigned-upload-url/')) { grants++;assert(!Object.keys(options.headers).some(key=>key.toLowerCase()==='user-agent')); if(mode==='auth')return Response.json({error:'expired'},{status:401}); if(mode==='offline'||grants===1)throw new TypeError('Load failed'); return Response.json({uploadURL:'https://upload.test/piece'}); } if(options.method==='PUT'){putSource=options.body;return new Response('');} return new Response(putSource); }; await session.publish(); assert.equal(grants,mode==='auth'?1:mode==='offline'?3:2); assert.equal(session.state.published,mode==='recover'); assert.equal(session.state.revisions.at(-1).source,source); assert(!session.state.transcript.some(e=>e.type==='notice'||e.type==='bad')); const notice=events.filter(e=>e.type==='notice').at(-1); if(mode==='recover')assert.equal(notice.text,''); else {assert.equal(notice.action,mode==='auth'?'signIn':'publish');assert(!notice.text.includes('Load failed'));} } } finally {globalThis.fetch=originalFetch;}});
test('saved notebook opens while account lookup is pending; logout wins a late response',async()=>{ const originalFetch=globalThis.fetch;let complete; const values=new Map([['session',JSON.stringify({token:'test-token',handle:'test',slug:'keep',source:'export function paint(){}'})]]); globalThis.fetch=()=>new Promise(resolve=>complete=resolve); try { const session=createSession({storage:{get:k=>values.get(k),set:(k,v)=>values.set(k,v)}}); const restoring=session.restore();await session.open(); assert.equal(session.state.slug,'keep');assert(session.state.id); session.signOut(); globalThis.fetch=async()=>Response.json({handle:'test'}); complete(Response.json({sub:'user'})); assert.equal(await restoring,false);assert.equal(session.state.token,''); assert.equal(JSON.parse(values.get('session')).token,''); }finally{globalThis.fetch=originalFetch;}});
test('transient account failures preserve saved sign-in and stay out of the notebook',async()=>{ const originalFetch=globalThis.fetch,events=[]; const values=new Map([['session',JSON.stringify({token:'test-token',handle:'test',slug:'keep',source:'export function paint(){}'})]]); globalThis.fetch=async()=>{throw new TypeError('Load failed');}; try{ const session=createSession({storage:{get:k=>values.get(k),set:(k,v)=>values.set(k,v)},emit:e=>events.push(e)}); await session.open();assert.equal(await session.restore(),false); assert.equal(session.state.accountVerified,false); assert.equal(session.state.token,'test-token');assert.equal(JSON.parse(values.get('session')).token,'test-token'); assert(events.some(e=>e.type==='accountRequired'));assert(!session.state.transcript.some(e=>e.type==='bad'||e.type==='notice')); }finally{globalThis.fetch=originalFetch;}});
test('generation progress remains live while saved reply deltas stay compact',async()=>{ const originalFetch=globalThis.fetch,values=new Map(),events=[]; globalThis.fetch=async url=>{ if(!String(url).includes('easel-inference'))return Response.json({}); return new Response([ {type:'content_block_start',index:0,content_block:{type:'text',text:''}}, {type:'content_block_delta',index:0,delta:{type:'text_delta',text:'Hello '}}, {type:'content_block_delta',index:0,delta:{type:'text_delta',text:'bell'}}, {type:'content_block_stop',index:0}, {type:'message_delta',delta:{stop_reason:'end_turn'}}, ].map(event=>`data: ${JSON.stringify(event)}\n`).join('')); }; try { const session=signedInSession({storage:{get:k=>values.get(k),set:(k,v)=>values.set(k,v)},emit:e=>events.push(e)}); await session.open();session.state.token='test';session.state.handle='test';session.setAutoPublish(false); await session.ask('hello'); assert(events.some(e=>e.method==='turn/progress')); const stored=JSON.parse(values.get('threads')).items[0].events; assert(!stored.some(e=>e.method==='turn/progress'||e.method==='item/modelCode/delta')); const deltas=stored.filter(e=>e.method==='item/agentMessage/delta'); assert.equal(deltas.length,1);assert.equal(deltas[0].params.delta,'Hello bell'); }finally{globalThis.fetch=originalFetch;}});
test('history selection is read-only and preserves the current source, transcript, engine and unsent draft', async()=>{ const values=new Map(),events=[]; const storage={get:k=>values.get(k),set:(k,v)=>values.set(k,v)}; const session=signedInSession({storage,emit:e=>events.push(e)});await session.open();session.setAutoPublish(false); session.state.transcript.push({type:'you',text:'first'}); await session.editSource('export function paint(){return 1}'); session.state.transcript.push({type:'you',text:'second'}); await session.editSource('export function paint(){return 2}'); session.setDraft('unsent');session.state.engine={messages:[{role:'user',content:'keep context'}]};session.saveCurrent(); const before=JSON.parse(values.get('threads')).items[0]; session.selectRevision(1); const selected=events.findLast(e=>e.type==='revisionSelection'); assert.equal(selected.version,1);assert.equal(selected.events.length,1);assert.equal(selected.events[0].text,'first'); assert.equal(session.state.version,2);assert.equal(session.state.composer,'unsent'); assert.throws(()=>session.setDraft('no'),/Return to v2/); await assert.rejects(session.ask('no'),/Return to v2/); await assert.rejects(session.editSource('export function paint(){}'),/Return to v2/); await assert.rejects(session.restoreRevision(0),/Return to v2/); await assert.rejects(session.publish(),/Return to v2/); const after=JSON.parse(values.get('threads')).items[0]; for(const key of ['source','events','engine','composer','revisions'])assert.deepEqual(after[key],before[key],key); const reopened=signedInSession({storage});await reopened.open();assert.equal(reopened.state.selectedRevision,1); reopened.selectRevision(2);reopened.setDraft('can edit again');assert.equal(reopened.state.selectedRevision,null); assert.equal(reopened.state.composer,'can edit again'); assert.throws(()=>reopened.selectRevision(1,'another-thread'),/another thread/); reopened.state.busy=true;assert.throws(()=>reopened.selectRevision(1),/Wait/);});
test('legacy generated versions use completed writing turns, never later conversation', async()=>{ const {migrateRevisionCheckpoints}=await import('../phone/revision-history.mjs'); const events=[{type:'you',text:'one'},{type:'bridge',method:'item/completed',params:{item:{type:'fileChange',status:'written'}}}, {type:'bridge',method:'item/agentMessage/delta',params:{delta:'Done'}},{type:'bridge',method:'turn/completed'}, {type:'you',text:'later chat'},{type:'bridge',method:'turn/completed'}, {type:'you',text:'two'},{type:'bridge',method:'item/completed',params:{item:{type:'fileChange',status:'written'}}}, {type:'bridge',method:'turn/completed'}]; const revisions=[{version:0,reason:'opened'},{version:1,reason:'generated'},{version:2,reason:'generated'}]; assert.deepEqual(migrateRevisionCheckpoints(revisions,events).map(r=>r.transcriptEnd),[0,4,9]); assert.equal(migrateRevisionCheckpoints([{version:4,reason:'imported'}],events)[0].transcriptEnd,undefined);});
test('every native provider blocks signed-out, handleless, expired and offline accounts',async()=>{ for(const provider of ['ac','claude','codex'])for(const mode of ['signed-out','no-handle','expired','offline']){ const calls=[],events=[]; const session=createSession({emit:e=>events.push(e),hostRPC:async(method)=>{calls.push(method);return {};},accountFetch:async url=>{ if(mode==='offline')throw Error('offline'); if(mode==='expired')return new Response('',{status:401}); return url.includes('/userinfo')?Response.json({sub:'user'}):new Response('',{status:404}); }}); await session.open();session.state.provider=provider; if(mode!=='signed-out'){session.state.token='token';session.state.handle='stale-cached-handle';} await session.ask('Do not run'); assert(!calls.includes('configure')&&!calls.includes('turn')); assert(!session.state.accountVerified); assert(!events.some(e=>e.type==='you')); assert(events.some(e=>e.type==='accountRequired')); assert.throws(()=>session.setDraft('blocked'),/Sign in/); assert.throws(()=>session.newSession(),/Sign in/); await assert.rejects(session.resumeTurn()); }});
test('a new blank is public before its first prompt, with one upload per blank',async()=>{ const originalFetch=globalThis.fetch,events=[],uploads=[]; let uploaded=''; globalThis.fetch=async(url,options={})=>{ if(url.includes('/presigned-upload-url/'))return Response.json({uploadURL:'https://upload.test/blank'}); if(options.method==='PUT'){uploaded=options.body;uploads.push(uploaded);return new Response('');} if(url.includes('/api/easel-credits'))return Response.json({}); return new Response(uploaded); }; try { const session=signedInSession({emit:e=>events.push(e)}); session.state.autoPublish=true; await session.open(); assert.equal(session.state.published,true); assert.equal(uploads.length,1); assert(events.some(e=>e.type==='publication'&&e.url.includes('@test/'))); assert(!events.some(e=>e.type==='you')); await session.open();assert.equal(uploads.length,1); await session.newSession();assert.equal(uploads.length,2); assert.equal(session.state.published,true); }finally{globalThis.fetch=originalFetch;}});
test('a blank opened before account restoration publishes after verification',async()=>{ const originalFetch=globalThis.fetch,values=new Map([['session',JSON.stringify({token:'token',handle:'test'})]]); let source='',uploads=0; globalThis.fetch=async(url,options={})=>{ if(url.includes('/userinfo'))return Response.json({sub:'user'}); if(url.includes('/handle?'))return Response.json({handle:'test'}); if(url.includes('/presigned-upload-url/'))return Response.json({uploadURL:'https://upload.test/blank'}); if(options.method==='PUT'){source=options.body;uploads++;return new Response('');} if(url.includes('/api/easel-credits'))return Response.json({}); return new Response(source); }; try{ const session=createSession({storage:{get:k=>values.get(k),set:(k,v)=>values.set(k,v)}}); await session.open();assert.equal(uploads,0); await session.restore();assert.equal(uploads,1);assert(session.state.published); }finally{globalThis.fetch=originalFetch;}});
test('artifact context reaches the model without replacing the visible user prompt',async()=>{ const events=[];const session=signedInSession({emit:e=>events.push(e)}); await session.open();let input='';session.state.server={startTurn:async text=>{input=text;}}; await session.ask('Make it a rainbow','Edit the isolated shirt-symbol descriptor.'); assert.equal(input,'Edit the isolated shirt-symbol descriptor.\n\nUser request: Make it a rainbow'); assert.equal(events.findLast(e=>e.type==='you').text,'Make it a rainbow');});
test('malformed Fia circle commands are rejected before the model can save success',async()=>{ const {validatePieceSource}=await import('../phone/shim/revisions.mjs'); const source=draw=>'/* ac-artifact '+JSON.stringify({version:1,kind:'shirt-symbol',shape:'drawing',color:'#fac83c',draw})+' */\nexport function paint() {}'; await assert.rejects(validatePieceSource(source([[0,0,9,1,2,1]]),'piece.mjs'),/Circle exactly/); await assert.doesNotReject(validatePieceSource(source([[0,0,9,1,1],[1,-8,-6,-8,6,2,1]]),'piece.mjs'));});
test('renewal retries account verification only and publishes once with the fresh token',async()=>{ const renewals=[],requests=[];let armed=false; const session=signedInSession({renewToken:async({force})=>{if(!armed)return "test-token";renewals.push(force);return force?'fresh':'expired';}, accountFetch:async(url,init)=>{ if(url.includes('/userinfo')&&init.headers.Authorization==='Bearer expired')return new Response('',{status:401}); return Response.json(url.includes('/userinfo')?{sub:'test-user'}:{handle:'test'}); }}); await session.open();armed=true; globalThis.fetch=async(url,init)=>{requests.push({url,init});throw new Error('stop upload after credential check');}; await session.publish(); assert.deepEqual(renewals,[false,true]);assert.equal(session.state.token,'fresh'); assert.equal(requests.length,1,'no automatic replay of public work'); assert.ok(JSON.stringify(requests[0].init).includes('fresh'));});
test('logout wins delayed token renewal without losing the notebook',async()=>{ let finish,armed=false;const refreshed=new Promise(r=>finish=r); const session=signedInSession({renewToken:()=>armed?refreshed:Promise.resolve("test-token")});await session.open();armed=true; const source=session.state.file,id=session.state.id; const pending=session.publish();session.signOut();finish('late-token');await pending; assert.equal(session.state.token,'');assert.equal(session.state.id,id);assert.equal(session.state.file,source);});