// Isolated real YCode/preload/attachment staging. Only model replies and selected staging failures are controlled. const fs = require('node:fs/promises'), path = require('node:path'), assert = require('node:assert/strict') const table = '| 项目 | 说明 |\n| --- | --- |\n| alpha | 第一行 |\n| beta | 第二行 |' if (!process.versions.electron) { ;(async () => { const directory = await fs.mkdtemp(path.resolve('.tmp/reference-content-')) for (const name of ['data','home','workspace']) await fs.mkdir(path.join(directory,name)) const cwd = path.join(directory,'workspace'), now = Date.now() const quote = { id:'history-text-0:table:table-0',sourceId:'history-text-0:table:table-0',messageId:'history',kind:'table',markdown:table,preview:table } const sessions = [ { id:'reference-live',title:'Reference Live',cwd,createdAt:now,updatedAt:now,messages:[{id:'source',role:'assistant',content:'表格:\n\n'+table,createdAt:now-1000,status:'done'}] }, { id:'reference-history',title:'Reference History',cwd,createdAt:now-4000,updatedAt:now-4000,messages:[{id:'history-user',role:'user',content:'这条来自历史记录',quotes:[quote],createdAt:now-3000,status:'done'}] } ] await fs.writeFile(path.join(cwd,'项目说明.md'),'# QA reference\n') await fs.mkdir(path.join(cwd,'参考文件夹')) await fs.writeFile(path.join(directory,'data','sessions.json'),JSON.stringify({sessions})) const env={...process.env,YCODE_REFERENCE_QA:directory,YCODE_USER_DATA_DIR:path.join(directory,'data'), YCODE_DISABLE_GPU:'1',YCODE_QA_OFFSCREEN:'1'} delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL const child=require('node:child_process').spawn(require('electron'),['--no-sandbox','--disable-gpu-sandbox',__filename],{env,windowsHide:true,stdio:['ignore','pipe','pipe']}) let output='';child.stdout.on('data',b=>{output+=b;process.stdout.write(b)});child.stderr.on('data',b=>{output+=b}) const timer=setTimeout(()=>child.kill(),90000) child.once('exit',async code=>{clearTimeout(timer);await fs.writeFile(path.join(directory,'process.log'),output);console.log(JSON.stringify({directory,exitCode:code}));if(code)console.error(output.slice(-3000));process.exitCode=code?1:0}) })().catch(error=>{console.error(error);process.exitCode=1}) } else { const {app,BrowserWindow,ipcMain}=require('electron') const directory=process.env.YCODE_REFERENCE_QA assert.ok(directory&&path.relative(path.resolve('.tmp'),directory).startsWith('reference-content-')) app.disableHardwareAcceleration();app.setPath('userData',path.join(directory,'data'));app.setPath('sessionData',path.join(directory,'data'));app.setPath('home',path.join(directory,'home')) let win,phase='boot',releaseSlow const checks=[],snapshots={},rendererErrors=[],payloads=[],staged=[] const pause=ms=>new Promise(r=>setTimeout(r,ms)), read=code=>win.webContents.executeJavaScript(code) async function wait(code,label=code){for(let n=0;n<200;n++){const v=await read(code);if(v)return v;await pause(30)}throw Error('Timeout '+label)} function check(label,actual,expected=true){const pass=typeof expected==='function'?expected(actual):JSON.stringify(actual)===JSON.stringify(expected);checks.push({label,actual,expected:typeof expected==='function'?String(expected):expected,pass});if(!pass)console.log('REFERENCE_CHECK_FAIL '+JSON.stringify(checks.at(-1)))} async function step(name){phase=name;await fs.writeFile(path.join(directory,'phase.txt'),name)} async function shot(name,full=false){await read(`document.fonts.ready.then(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))))`);await pause(220);const rect=full?undefined:await read(`(()=>{const r=document.querySelector('.wb-composer').getBoundingClientRect();return{x:Math.floor(r.x),y:Math.floor(r.y),width:Math.ceil(r.width),height:Math.ceil(r.height)}})()`);await fs.writeFile(path.join(directory,name+'.png'),(await win.webContents.capturePage(rect)).toPNG())} async function key(keyCode,modifiers=[]){win.webContents.sendInputEvent({type:'keyDown',keyCode,modifiers});win.webContents.sendInputEvent({type:'keyUp',keyCode,modifiers});await pause(40)} async function pasteFiles(files){await read(`(async()=>{const dt=new DataTransfer();for(const f of ${JSON.stringify(files)}){let b=f.text||'fixture';if(f.image){const c=document.createElement('canvas');c.width=64;c.height=48;const x=c.getContext('2d');x.fillStyle='#73b5e8';x.fillRect(0,0,64,48);x.fillStyle='#17242f';x.fillText('QA',20,28);b=await new Promise(r=>c.toBlob(r,'image/png'))}dt.items.add(new File([b],f.name,{type:f.image?'image/png':'text/markdown'}))}document.querySelector('.wb-composer__input').dispatchEvent(new ClipboardEvent('paste',{clipboardData:dt,bubbles:true}));return true})()`)} async function pasteResource(name){const uri=require('node:url').pathToFileURL(path.join(directory,'workspace',name)).href;await read(`(()=>{const dt=new DataTransfer();dt.setData('application/vnd.code.uri-list',${JSON.stringify(uri)});document.querySelector('.wb-composer__input').dispatchEvent(new ClipboardEvent('paste',{clipboardData:dt,bubbles:true}));return true})()`)} const tags=`[...document.querySelectorAll('.wb-composer .cr-reference-tag')]` const quoteSelector='.wb-composer [data-reference-kind="table"]' const measure=`(()=>{const c=document.querySelector('.wb-composer .cr-input-container').getBoundingClientRect();return ${tags}.map(e=>{const r=e.getBoundingClientRect(),s=getComputedStyle(e),i=e.querySelector('.cr-reference-tag__icon'),b=e.querySelector('button');return{label:e.querySelector('.cr-reference-tag__text').textContent,key:e.dataset.referenceKey,kind:e.dataset.referenceKind,height:r.height,width:r.width,x:r.x,y:r.y,inside:r.x>=c.x&&r.y>=c.y&&r.right<=c.right&&r.bottom<=c.bottom,radius:s.borderRadius,padding:s.padding,gap:s.gap,font:s.fontSize,weight:s.fontWeight,lineHeight:s.lineHeight,background:s.backgroundColor,icon:i?[i.clientWidth,i.clientHeight]:null,remove:b?getComputedStyle(b).visibility:null}})})()` ;(async()=>{ await import(require('node:url').pathToFileURL(path.resolve('out/main/index.mjs')).href);await app.whenReady() const realStage=ipcMain._invokeHandlers.get('attachments:stage') ipcMain.removeHandler('attachments:stage');ipcMain.handle('attachments:stage',async(event,payload)=>{ if(payload.name==='failed.md')throw Error('受控附件处理失败') if(payload.name==='slow.md')await new Promise(resolve=>{releaseSlow=resolve}) const result=await realStage(event,payload);staged.push(result);return result }) ipcMain.removeHandler('chat:inspect-edit');ipcMain.handle('chat:inspect-edit',()=>({editable:true})) ipcMain.removeHandler('chat:send');ipcMain.handle('chat:send',async(_event,payload)=>{ payloads.push(payload) const userMessage={id:'sent-user-'+payloads.length,role:'user',content:payload.text,attachments:payload.attachments,quotes:payload.quotes,resources:payload.resources,userContent:payload.userContent,createdAt:Date.now(),status:'done'} setTimeout(()=>{try{win.webContents.send('chat:event',{sessionId:payload.sessionId,runId:'reference-'+payloads.length,type:'done',content:'收到。',tools:[]})}catch{}},60) return{runId:'reference-'+payloads.length,assistantMessageId:'reference-answer',userMessage,messages:[userMessage,{id:'reference-answer',role:'assistant',content:'收到。',createdAt:Date.now(),status:'done'}]} }) win=BrowserWindow.getAllWindows()[0];win.setSize(1336,902);win.webContents.debugger.attach('1.3') win.webContents.on('console-message',(_event,level,message)=>{if(level>=3)rendererErrors.push(message)}) await wait(`!!document.querySelector('[data-session-id="reference-live"]')`) await read(`document.querySelector('[data-session-id="reference-live"]').click()`) await wait(`!!document.querySelector('.cr-message-quote-table-action')`) await step('table-menu-and-draft') const menu=await read(`(()=>{const m=document.querySelector('.cr-message-quote-table-menu'),b=m.querySelector('button'),s=getComputedStyle(m),bs=getComputedStyle(b);return{padding:s.padding,radius:s.borderRadius,top:s.top,right:s.right,label:b.textContent,font:bs.fontSize,lineHeight:bs.lineHeight,weight:bs.fontWeight,icon:m.querySelector('svg').getBoundingClientRect().width}})()`) check('source add label',menu.label,'添加到对话');check('source menu geometry',[menu.padding,menu.radius,menu.top,menu.right],['4px','8px','8px','8px']);check('source menu typography',[menu.font,menu.lineHeight,menu.weight,menu.icon],['14px','22px','500',16]) await read(`document.querySelector('.cr-message-quote-table-action').focus()`);await pause(200) check('focus reveals source menu',await read(`getComputedStyle(document.querySelector('.cr-message-quote-table-menu')).opacity`),'1') check('source identity stays bound to the table',await read(`/source-text-\\d+:table:table-\\d+$/.test(document.querySelector('.cr-message-quote-table-menu').dataset.messageQuoteSourceId)`)) await read(`document.querySelector('.cr-message-quote-table-action').click()`);await wait(`!!document.querySelector('${quoteSelector}')`) snapshots.table=await read(measure) check('table label is compact',snapshots.table.map(t=>t.label),['会话表格']) check('tag matches recovered source geometry',[snapshots.table[0].height,snapshots.table[0].radius,snapshots.table[0].padding,snapshots.table[0].font,snapshots.table[0].lineHeight,snapshots.table[0].icon],[23,'100px','1px 8px','13px','21px',[14,14]]) check('quote is inside rounded input',snapshots.table[0].inside) check('added source hides the menu and highlights the table',await read(`!!document.querySelector('.cr-message-quote-table-menu--added')&&getComputedStyle(document.querySelector('.cr-table-block__inner')).borderTopColor==='rgb(64, 209, 179)'`)) check('old external summary rail is absent',await read(`!document.querySelector('.cr-input-quote-card')&&!document.querySelector('.cr-message-quote-rail')`)) check('close starts hidden',snapshots.table[0].remove,'hidden') await shot('table-draft-light') const hover=snapshots.table[0];win.webContents.sendInputEvent({type:'mouseMove',x:Math.round(hover.x+hover.width/2),y:Math.round(hover.y+10)});await pause(120) check('hover swaps icon for close',await read(`getComputedStyle(document.querySelector('${quoteSelector} button')).visibility==='visible'&&getComputedStyle(document.querySelector('${quoteSelector} .cr-reference-tag__icon')).visibility==='hidden'`)) await read(`document.querySelector('${quoteSelector} button').click()`);await wait(`!document.querySelector('${quoteSelector}')`) check('removing restores add button',await read(`!!document.querySelector('.cr-message-quote-table-action')`)) await read(`document.querySelector('.cr-message-quote-table-action').click()`);await wait(`document.activeElement?.classList.contains('cr-reference-editor')`) await key('End',['control']);await key('Backspace') await wait(`!document.querySelector('${quoteSelector}')`,'keyboard deletes inline reference') check('keyboard removal restores the plain editor and focus',await read(`document.activeElement===document.querySelector('.wb-composer textarea')`)) await read(`document.querySelector('.cr-message-quote-table-action').click()`);await wait(`!!document.querySelector('${quoteSelector}')`) check('a removed source can be referenced again',await read(`${tags}.filter(t=>t.dataset.referenceKind==='table').length`),1) await read(`document.querySelector('${quoteSelector} button').click()`);await wait(`!!document.querySelector('.wb-composer textarea')`) await step('inline-caret') await read(`document.querySelector('.wb-composer textarea').focus()`) await win.webContents.debugger.sendCommand('Input.insertText',{text:'前半后半'}) await read(`(()=>{const t=document.querySelector('.wb-composer textarea');t.setSelectionRange(2,2);t.dispatchEvent(new Event('select',{bubbles:true}));document.dispatchEvent(new Event('selectionchange'));return true})()`);await pause(90) await read(`document.querySelector('.cr-message-quote-table-action').click()`);await wait(`!!document.querySelector('${quoteSelector}')`) await wait(`document.activeElement?.classList.contains('cr-reference-editor')`,'rich editor receives focus') await pause(60) await win.webContents.debugger.sendCommand('Input.insertText',{text:'中间'});await pause(100) snapshots.inline=await read(`document.querySelector('.cr-reference-editor').innerHTML`) check('inline editor preserves surrounding draft',snapshots.inline.includes('前半')&&snapshots.inline.includes('后半')) await step('files-image-and-resources') await pasteFiles([{name:'项目说明.md'}]);await wait(`${tags}.filter(t=>t.dataset.referenceKey.startsWith('attachment:')).length===1&&!document.querySelector('.wb-composer [data-attachment-status=processing]')`) await pasteFiles([{name:'界面截图.png',image:true}]);await wait(`${tags}.some(t=>t.dataset.referenceKind==='image')&&!document.querySelector('.wb-composer [data-attachment-status=processing]')`) await pasteResource('项目说明.md');await pasteResource('参考文件夹');await wait(`${tags}.some(t=>t.dataset.referenceKind==='folder')`) snapshots.mixed=await read(measure) check('all five reference types stay in input',snapshots.mixed.length===5&&snapshots.mixed.every(t=>t.inside)) check('uniform source metrics',snapshots.mixed.every(t=>t.height===23&&t.font==='13px'&&t.radius==='100px'&&t.icon[0]===14)) check('resource labels omit full paths',snapshots.mixed.filter(t=>t.key.startsWith('resource:')).every(t=>!t.label.includes('Users'))) await shot('mixed-light') await read(`(()=>{const e=document.querySelector('.wb-composer__input'),r=e.getBoundingClientRect(),dt=new DataTransfer();dt.items.add(new File(['dropped body'],'dropped.md',{type:'text/markdown'}));e.dispatchEvent(new DragEvent('drop',{dataTransfer:dt,bubbles:true,clientX:r.x+10,clientY:r.y+10}));return true})()`) await wait(`${tags}.some(t=>t.textContent==='dropped.md')&&!document.querySelector('.wb-composer [data-attachment-status=processing]')`) check('dragged attachment enters the same inline editor',await read(`${tags}.find(t=>t.textContent==='dropped.md').closest('.cr-input-container')!==null`)) await read(`${tags}.find(t=>t.textContent==='dropped.md').querySelector('button').click()`) await wait(`${tags}.length===5`) const image=snapshots.mixed.find(t=>t.kind==='image');win.webContents.sendInputEvent({type:'mouseMove',x:Math.round(image.x+image.width/2),y:Math.round(image.y+10)}) await wait(`!!document.querySelector('.cr-reference-image-tooltip')`,'image hover preview');check('hover image preview loads',await read(`document.querySelector('.cr-reference-image-tooltip').naturalWidth>0`));await shot('image-preview',true) win.webContents.sendInputEvent({type:'mouseMove',x:20,y:80});await pause(250) await step('processing-failure-and-cancellation') await pasteFiles([{name:'failed.md'}]);await wait(`!!document.querySelector('.wb-composer [data-attachment-status=failed]')`) check('failed tag uses source class',await read(`document.querySelector('.wb-composer [data-attachment-status=failed]').classList.contains('_failed_1krka_60')`)) check('failed attachment cannot be submitted',await read(`document.querySelector('.wb-composer__send').disabled`));await shot('attachment-failed') await read(`document.querySelector('.wb-composer [data-attachment-status=failed] button').click()`) await pasteFiles([{name:'slow.md'}]);await wait(`!!document.querySelector('.wb-composer [data-attachment-status=processing]')`) check('processing tag has spinner and source opacity',await read(`getComputedStyle(document.querySelector('.wb-composer [data-attachment-status=processing]')).opacity==='0.7'&&!!document.querySelector('.wb-composer [data-attachment-status=processing] svg')`));await shot('attachment-processing') await read(`document.querySelector('.wb-composer [data-attachment-status=processing] button').click()`);releaseSlow();await wait(`!document.querySelector('.wb-composer__send').disabled`) const slow=staged.find(t=>t.name==='slow.md');check('late completion of removed attachment is discarded',!!slow&&await fs.access(slow.path).then(()=>false,()=>true)) check('cancelled and failed files are absent',await read(`${tags}.every(t=>!t.textContent.includes('failed')&&!t.textContent.includes('slow'))`)) await step('dark-and-wrap') await read(`localStorage.setItem('ycode:theme','dark');window.dispatchEvent(new StorageEvent('storage',{key:'ycode:theme',newValue:'dark'}))`);await wait(`document.documentElement.dataset.theme==='dark'`);await shot('mixed-dark') win.setSize(920,902);await pause(100) await pasteFiles([{name:'a-very-long-reference-file-name-for-ellipsis-and-wrapping-'.repeat(3)+'.md'},{name:'second-file-with-a-long-name-for-wrap.md'}]) await wait(`${tags}.length===7&&!document.querySelector('.wb-composer [data-attachment-status=processing]')`) snapshots.narrow=await read(measure);check('long labels wrap without escaping the rounded input',snapshots.narrow.every(t=>t.inside));await shot('mixed-narrow-dark') await step('send-and-edit') await read(`document.querySelector('.wb-composer__send').click()`);await wait(`!!document.querySelector('.cr-frame--right [data-reference-kind=table]')`) check('plaintext survives caret edits',payloads[0].text,'前半中间后半') check('full table snapshot remains in IPC',payloads[0].quotes[0].markdown.includes('| alpha |')) check('IPC keeps reference order between text',payloads[0].userContent.filter(b=>b.type==='text').map(b=>b.text).join(''),'前半中间后半') const tableIndex=payloads[0].userContent.findIndex(b=>b.type==='quote');check('table was inserted between text blocks',tableIndex>0&&payloads[0].userContent.slice(0,tableIndex).filter(b=>b.type==='text').map(b=>b.text).join('')==='前半') check('no transient attachment leaks to payload',payloads[0].attachments.every(a=>!a.id.startsWith('pending-')&&!['slow.md','failed.md'].includes(a.name))) check('sent references live inside the user bubble',await read(`document.querySelector('.cr-self-bubble').querySelectorAll('.cr-reference-tag').length===7&&!document.querySelector('.msg__attachment')`)) check('read-only tags have no remove button',await read(`[...document.querySelectorAll('.cr-self-bubble .cr-reference-tag')].every(t=>!t.querySelector('button')&&t.classList.contains('_readOnly_1krka_57'))`));await shot('sent-dark',true) await wait(`!!document.querySelector('.wb-composer__send')`) win.setSize(1336,902);await pause(100) await read(`[...document.querySelectorAll('.cr-self-bubble [data-reference-key^="attachment:"]')].find(e=>e.textContent==='项目说明.md').click()`) await wait(`document.querySelector('.markdown-viewer')?.textContent.includes('fixture')`,'registered attachment preview') snapshots.filePreview=await read(`(()=>{const e=document.querySelector('.markdown-viewer'),p=e.querySelector('p'),r=e.getBoundingClientRect(),ancestors=[];for(let n=e;n;n=n.parentElement){const s=getComputedStyle(n);ancestors.push({cls:n.className,background:s.backgroundColor,color:s.color,wbBg:s.getPropertyValue('--wb-bg-primary'),wbColorBg:s.getPropertyValue('--wb-color-bg-primary'),cbPanel:s.getPropertyValue('--cb-panel-bg-primary'),crBg:s.getPropertyValue('--cr-bg-primary')})}return{text:e.textContent,color:getComputedStyle(p||e).color,background:getComputedStyle(e).backgroundColor,rect:[r.x,r.y,r.width,r.height],ancestors}})()`) check('staged file opens the actual right-side preview',snapshots.filePreview.text.includes('fixture'));await shot('file-preview',true) check('dark attachment text has a dark source-theme surface',snapshots.filePreview.ancestors.find(n=>n.cls==='detail-main__body').background,'rgb(31, 31, 31)') await read(`localStorage.setItem('ycode:theme','light');window.dispatchEvent(new StorageEvent('storage',{key:'ycode:theme',newValue:'light'}))`);await wait(`document.documentElement.dataset.theme==='light'`) check('light attachment preview uses dark text on the light surface',await read(`getComputedStyle(document.querySelector('.markdown-viewer p')).color==='rgba(0, 0, 0, 0.9)'&&getComputedStyle(document.querySelector('.detail-main__body')).backgroundColor==='rgb(255, 255, 255)'`));await shot('file-preview-light',true) await read(`localStorage.setItem('ycode:theme','dark');window.dispatchEvent(new StorageEvent('storage',{key:'ycode:theme',newValue:'dark'}))`);await wait(`document.documentElement.dataset.theme==='dark'`) await read(`document.querySelector('[aria-label="收起面板"]').click()`) await read(`document.querySelector('.cr-self-bubble [data-reference-kind=image]').click()`) await wait(`!!document.querySelector('.wb-image-preview__image')`) check('sent image opens the actual image viewer',await read(`document.querySelector('.wb-image-preview__image').naturalWidth>0`)) await read(`document.querySelector('.wb-image-preview__close').click()`) await wait(`!!document.querySelector('.wb-composer__send')`) await read(`document.querySelector('.cr-frame--right [data-action=edit],.cr-frame--right button[aria-label="编辑"]').click()`) await wait(`!!document.querySelector('.cr-user-message-editor')`) await wait(`!!document.querySelector('.cr-user-message-editor [data-reference-kind=folder]')`) check('editing keeps folder type in the actual session workspace',await read(`document.querySelector('.cr-user-message-editor [data-reference-kind=folder]').textContent`),'参考文件夹') check('edit retains all inline references',await read(`document.querySelectorAll('.cr-user-message-editor .cr-reference-tag').length===7`));await shot('edit-dark',true) await read(`document.querySelector('.cr-user-message-editor [data-reference-kind=table] button').click()`) await wait(`!document.querySelector('.cr-user-message-editor [data-reference-kind=table]')`) await read(`document.querySelector('.cr-user-message-editor__cancel').click()`) check('cancelling edit preserves original reference',await read(`!!document.querySelector('.cr-self-bubble [data-reference-kind=table]')`)) await read(`document.querySelector('.cr-frame--right [data-action=edit]').click()`);await wait(`!!document.querySelector('.cr-user-message-editor')`) await read(`document.querySelector('.cr-user-message-editor [data-reference-kind=table] button').click()`) await wait(`!document.querySelector('.cr-user-message-editor [data-reference-kind=table]')`) await read(`document.querySelector('.cr-user-message-editor__send').click()`) await wait(`!!document.querySelector('.wb-composer__send') && !document.querySelector('.cr-user-message-editor')`) check('submitting edit removes only the selected reference',payloads[1].quotes.length===0&&payloads[1].attachments.length===4&&payloads[1].userContent.every(b=>b.type!=='quote')) await step('legacy-history') await read(`document.querySelector('[data-session-id="reference-history"]').click()`);await wait(`document.querySelector('.cr-frame--right [data-reference-kind=table]')?.textContent==='会话表格'`) check('legacy Markdown summary maps to compact table label',await read(`document.querySelector('.cr-self-bubble').textContent`),'会话表格这条来自历史记录') check('no renderer errors',rendererErrors,[]) const report={result:checks.every(c=>c.pass)?'PASS':'FAIL',scope:'Real isolated built YCode App, preload, real file/image staging; model reply and selected attachment failures controlled. Original live whole-state pixel parity is not established.',sourceEvidence:['ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb','ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'],checks,snapshots,payloads,rendererErrors} await fs.writeFile(path.join(directory,'report.json'),JSON.stringify(report,null,2));console.log('REFERENCE_CONTENT_'+report.result+' '+directory+' checks='+checks.length);app.exit(report.result==='PASS'?0:1) })().catch(async error=>{await fs.writeFile(path.join(directory,'failure.json'),JSON.stringify({phase,error:String(error),stack:error.stack,checks,snapshots,payloads,rendererErrors},null,2));if(win&&!win.isDestroyed())await shot('failure',true).catch(()=>{});console.error(error);app.exit(1)}) }