Keep assistant image previews stable through live delivery and reload

This commit is contained in:
2026-10-20 14:19:23 -07:00
parent 489c347976
commit 9098fee32a
2 changed files with 29 additions and 6 deletions
+28 -5
View File
@@ -11,13 +11,15 @@ const browser=await chromium.launch({executablePath:process.env.CHROME_EXECUTABL
...(process.env.BROWSER_NO_SANDBOX === '1' ? {args:['--no-sandbox']} : {})});
const context=await browser.newContext({viewport:{width:1200,height:900},permissions:['clipboard-write']});
await context.addInitScript(()=>localStorage.setItem('sybil_admin_token','browser-fixture-token'));
const page=await context.newPage(), errors=[], uploads=[], sends=[], downloads=[];
const page=await context.newPage(), errors=[], uploads=[], sends=[], downloads=[], trackingRequests=[];
page.on('pageerror',e=>errors.push(e.stack || e.message));
const cap={version:1,maxCount:8,maxTotalBytes:33554432,maxImageBytes:6291456,maxFileBytes:8388608,imageMimeTypes:['image/png','image/jpeg','image/gif','image/webp'],quotaBytes:1073741824,usedBytes:0,remainingBytes:1073741824,maxConcurrentUploads:2,maxConcurrentDecodes:1};
const idle={state:'idle',isThinking:false,queued:0,backgroundWorkers:0,tools:[],queuedMessages:[]};
let failedUpload=true, failedSend=true, live, cursor=0;const history=[];
let failedUpload=true, failedSend=true, live, cursor=0, inputAttachments=true;const history=[];
const image=Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a2ioAAAAASUVORK5CYII=','base64');
const attachment=(name='notes.txt')=>({id:`att_12345678-1234-4234-8234-${name==='picture.png'?'123456789abd':'123456789abc'}`,filename:name,mimeType:name==='picture.png'?'image/png':'text/plain',kind:name==='picture.png'?'image':'text',sizeBytes:name==='picture.png'?image.length:4,sha256:createHash('sha256').update(name==='picture.png'?image:Buffer.from('data')).digest('hex'),workspacePath:`/data/workspace/uploads/test/${name}`});
const assistantImage={...attachment('picture.png'),id:'att_12345678-1234-4234-8234-123456789abe',filename:'assistant.png'};delete assistantImage.workspacePath;
await page.route('https://never-fetch.invalid/**',async route=>{trackingRequests.push(route.request().url());await route.abort();});
await page.route('**/api/**',async route=>{
const req=route.request(),url=new URL(req.url()),p=url.pathname,m=req.method();let value;
if(p.startsWith('/api/v1/agent'))assert.equal(req.headers().authorization,'Bearer browser-fixture-token');
@@ -26,7 +28,7 @@ await page.route('**/api/**',async route=>{
if(failedUpload){failedUpload=false;await route.fulfill({status:507,json:{message:'Storage quota reached',code:'attachment_quota_exceeded'}});return;}
await route.fulfill({status:201,json:{attachment:attachment(body.includes('picture.png')?'picture.png':'notes.txt')}});return;
}
if(p.endsWith('/content')){downloads.push(req.url());await route.fulfill({contentType:p.includes('789abd')?'image/png':'text/plain',body:p.includes('789abd')?image:'data'});return;}
if(p.endsWith('/content')){downloads.push(req.url());const isImage=p.includes('789abd')||p.includes('789abe');await route.fulfill({contentType:isImage?'image/png':'text/plain',body:isImage?image:'data'});return;}
if(p.endsWith('/agent/messages')&&m==='POST'){
const body=req.postDataJSON();sends.push(body);
if(failedSend){failedSend=false;await route.fulfill({status:503,json:{message:'Temporarily unavailable'}});return;}
@@ -37,7 +39,7 @@ await page.route('**/api/**',async route=>{
}
if(p.endsWith('/agent/messages'))value={conversationId:'1',messages:history,cursor:String(cursor),beforeCursor:null,hasMore:false};
else if(p.endsWith('/read-state'))value={cursor:String(cursor),updatedAt:null};
else if(p.endsWith('/agent'))value={id:'1',title:'Sybil Agent',activity:idle,capabilities:{attachments:cap},protocol:'pi-room.v1',websocket:'/v1/agent/ws'};
else if(p.endsWith('/agent'))value={id:'1',title:'Sybil Agent',activity:idle,capabilities:{...(inputAttachments?{attachments:cap}:{}),assistantImages:{version:1}},protocol:'pi-room.v1',websocket:'/v1/agent/ws'};
else if(p.includes('session'))value={mode:'token',authenticated:true};
else if(p.endsWith('/tools')||p.endsWith('/chat-tools'))value={tools:[]};
else if(p.endsWith('/models'))value={models:[],providers:{}};
@@ -75,7 +77,28 @@ try{
await page.getByText('Uploaded',{exact:true}).waitFor();await page.getByRole('button',{name:'Remove notes.txt'}).click();
await page.locator('.agent-room__composer-area').evaluate(el=>{const data=new DataTransfer();data.items.add(new File(['data'],'notes.txt',{type:'text/plain'}));el.dispatchEvent(new DragEvent('drop',{dataTransfer:data,bubbles:true,cancelable:true}));});
await page.getByText('Uploaded',{exact:true}).waitFor();
await page.getByRole('button',{name:'Remove notes.txt'}).click();
// Assistant image-only messages use authenticated bytes automatically, with no source path.
const assistant={id:'outbound:900',role:'assistant',createdAt:new Date().toISOString(),content:'',metadata:{kind:'message',status:'complete',entryId:++cursor,agentAttachments:[assistantImage]}};history.push(assistant);
live.send(JSON.stringify({type:'messages',conversationId:'1',messages:[assistant],cursor:String(cursor),beforeCursor:String(cursor),hasMore:false}));
const assistantBubble=page.locator('[data-agent-message-id="outbound:900"]'),inlineImage=assistantBubble.locator('.agent-attachment__preview');
await inlineImage.waitFor();await inlineImage.evaluate(img=>img.decode());
assert.ok((await inlineImage.getAttribute('src')).startsWith('blob:'));
assert.equal(await assistantBubble.getByRole('button',{name:'Preview',exact:true}).count(),0);
assert.equal(await assistantBubble.locator('.agent-attachment__path').count(),0);
const tracking={id:'outbound:901',role:'assistant',createdAt:new Date().toISOString(),content:'Remote images must not load.\n\n![tracking](https://never-fetch.invalid/markdown.png) <img src="https://never-fetch.invalid/html.png">',metadata:{kind:'message',status:'complete',entryId:++cursor}};history.push(tracking);
live.send(JSON.stringify({type:'messages',conversationId:'1',messages:[tracking],cursor:String(cursor),beforeCursor:String(cursor),hasMore:false}));
await page.getByText('Remote images must not load.',{exact:true}).waitFor();
assert.equal(await page.locator('[data-agent-message-id="outbound:901"] .md-content img').count(),0);
// An output-only capability must preserve history without enabling input controls.
inputAttachments=false;await page.reload({waitUntil:'networkidle'});await page.locator('[data-testid="agent-sidebar-item"]').click();
await inlineImage.waitFor();await inlineImage.evaluate(img=>img.decode());
assert.equal(await assistantBubble.count(),1);assert.equal(await page.locator('.agent-attachment__preview').count(),1);
assert.ok((await inlineImage.getAttribute('src')).startsWith('blob:'));
assert.equal(await page.getByRole('button',{name:'Attach files',exact:true}).count(),0);
assert.equal(await page.getByLabel('Choose attachments').count(),0);
assert.deepEqual(trackingRequests,[],'Neither live nor historical Markdown/HTML image markup fetches remote bytes');
await page.setViewportSize({width:390,height:844});if(process.env.ATTACHMENT_SCREENSHOT)await page.screenshot({path:process.env.ATTACHMENT_SCREENSHOT});
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>window.innerWidth),false);
assert.deepEqual(errors,[]);console.log('PASS browser picker/retry/quota/remove, file-only send/reload, authenticated preview/download, copy, paste/drop, mobile layout');
assert.deepEqual(errors,[]);console.log('PASS browser picker/retry/quota/remove, file-only send/reload, authenticated preview/download, copy, paste/drop, assistant image-only live/history, output-only capability, blocked Markdown/HTML images, mobile layout');
}finally{await context.close();await browser.close();}