// TODO 21 验收:会话输入区的 Git 分支标签 + 上下文用量环。 // 真实隔离 YCode(真实 preload/IPC/渲染进程)+ 真实 OMP + 受控本地 OpenAI 兼容上游 + 真实 Git 仓库。 // // 原版依据(固定 REA 归档): // - 运行时几何/结构:ev_2569313a7a0aa901c5a3b8dc1d7fd190109b9006c915830109b62195f312376a // (WorkBuddy 5.8.3 会话输入区,视口 1336×902): // 左组 `.cr-add-menu`(431,32) → `._branchLabel_jdvlk_244`(471,82×32) → `.cr-permission-setting`(561) // 右组 `.cr-context-usage`(903,32×32) → `.cr-model-selector`(939) → 语音(1117) → 发送(1157) // 分支标签:radius 999 / padding 5px 8px / gap 8 / max-width 188 / 字体 14/400/22; // 图标按钮 16×16,默认分支字形,悬停换成 copy(opacity 互换);hover 背景 `--wb-bg-secondary`。 // 用量环:32×32 / radius 16;内部 16×16 SVG,track circle r=7 stroke-width 2、 // progress circle 带 `stroke-dashoffset .3s` 过渡;hover 背景 `--cr-context-usage-hover-bg,#0000000d`。 // 浮层:280 宽 / padding 16 / radius 16 / 标题「上下文用量」/ 关闭按钮 / 百分比 20/700/28 / // 「已使用」12/20 / 进度条 8 高 / 五类图例。 // - 回收样式表:`contexts-BkjwYSZT.css`(`._branchLabel_*`)、`src-CyXHA0IC.css`(`.cr-context-usage*`)。 // - 文案:已回收 i18n 表 `chatInput.contextUsage.*`(title 上下文用量 / used 已使用 / // tooltip "{percentage}% · {used} / {total} 上下文已使用" / needMessage / requesting / unavailable)。 // // YCode 侧数据真实性:分支名由主进程真实执行 git;用量只用上游真实 usage(经 OMP 归一化)+ // 模型自己的真实上下文窗口,拿不到就显示原版的「无法获取」文案,不填 0、不估算。 const fs = require('node:fs/promises') const path = require('node:path') const assert = require('node:assert/strict') const { execFileSync } = require('node:child_process') const QA_BRANCH = 'qa-branch' /** 受控上游返回的真实用量(provider prompt_tokens 含缓存,本例无缓存)。 */ const QA_PROMPT_TOKENS = 12800 const QA_COMPLETION_TOKENS = 40 const QA_CONTEXT_WINDOW = 64000 const QA_EXPECTED_PERCENT = 20 const RING_CIRCUMFERENCE = 2 * Math.PI * 7 if (!process.versions.electron) { ;(async () => { const directory = await fs.mkdtemp(path.resolve('.tmp/composer-context-')) const data = path.join(directory, 'data') const home = path.join(directory, 'home') const repo = path.join(directory, 'fixture-repo') await fs.mkdir(data, { recursive: true }) await fs.mkdir(home, { recursive: true }) await fs.mkdir(repo, { recursive: true }) // 真实 Git 仓库:分支名固定,供分支标签读取。 const git = (...args) => execFileSync('git', ['-C', repo, ...args], { stdio: 'pipe' }) git('init', '-b', QA_BRANCH) git('config', 'user.email', 'qa@example.com') git('config', 'user.name', 'QA') await fs.writeFile(path.join(repo, 'README.md'), '# qa\n') git('add', '.') git('-c', 'commit.gpgsign=false', 'commit', '-m', 'init') const branch = git('rev-parse', '--abbrev-ref', 'HEAD').toString().trim() assert.equal(branch, QA_BRANCH) const now = Date.now() const sessions = [{ id: 'context-info-session', title: 'Context Info', cwd: repo, modelId: 'local-usage', createdAt: now, updatedAt: now + 1, messages: [ { id: 'context-user', role: 'user', content: '上下文信息验收', createdAt: now, status: 'done', requestId: 'context-run' }, { id: 'context-assistant', role: 'assistant', content: '收到。', createdAt: now + 1, status: 'done', requestId: 'context-run', tools: [], parts: [] } ] }] await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({ sessions })) const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: home, YCODE_QA_REAL_OMP: process.env.YCODE_OMP_PATH || path.join(process.env.LOCALAPPDATA || '', 'Ygent', 'omp.exe'), YCODE_DISABLE_GPU: '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', bytes => { output += bytes; if (bytes.toString().includes('COMPOSER_CONTEXT_')) process.stdout.write(bytes) }) child.stderr.on('data', bytes => { output += bytes }) const timer = setTimeout(() => child.kill(), 300000) 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(-8000)) process.exitCode = code ? 1 : 0 }) })().catch(error => { console.error(error); process.exitCode = 1 }) } else { const { app, BrowserWindow, clipboard } = require('electron') const { createServer } = require('node:http') const { pathToFileURL } = require('node:url') const directory = process.env.YCODE_QA_DIRECTORY // GPU 关闭由主进程 bundle 依据 YCODE_DISABLE_GPU 处理;这里不能重复调用(Electron 会抛错)。 process.env.YCODE_OMP_PATH = process.env.YCODE_QA_REAL_OMP app.setPath('home', path.join(directory, 'home')) const checks = [], snapshots = {}, rendererErrors = [] let main, server, phase = 'boot' const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) async function read(source, win = main) { let timer try { return await Promise.race([win.webContents.executeJavaScript(source), new Promise((_, reject) => { timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 120))), 20000) })]) } finally { clearTimeout(timer) } } async function wait(source, label, win = main) { for (let index = 0; index < 400; index++) { const value = await read(source, win); if (value) return value; await pause(40) } throw Error('Timeout: ' + label) } async function step(label) { phase = label; await fs.writeFile(path.join(directory, 'phase.txt'), label) } function check(label, actual, expected) { const pass = typeof expected === 'function' ? expected(actual) : JSON.stringify(actual) === JSON.stringify(expected) checks.push({ label, pass, actual, expected: typeof expected === 'function' ? expected.toString() : expected }) } async function hover(selector, win = main) { const point = await read(`(()=>{const node=document.querySelector(${JSON.stringify(selector)});if(!node)return null;const box=node.getBoundingClientRect();return [Math.round(box.x+box.width/2),Math.round(box.y+box.height/2)]})()`, win) assert.ok(point, 'hover target: ' + selector) if (!win.webContents.debugger.isAttached()) win.webContents.debugger.attach('1.3') await win.webContents.debugger.sendCommand('Input.dispatchMouseEvent', { type: 'mouseMoved', x: point[0], y: point[1], button: 'none' }) await pause(260) } const branchSource = `(()=>{const left=document.querySelector('.cr-input-toolbar__left');const pill=left&&left.querySelector('._branchLabel_jdvlk_244'); if(!left||!pill)return null;const box=node=>{const r=node.getBoundingClientRect();return [r.x,r.y,r.width,r.height]}; const style=getComputedStyle(pill);const button=pill.querySelector('._branchLabelIconButton_jdvlk_279'); const def=pill.querySelector('._branchLabelIconDefault_jdvlk_267');const copy=pill.querySelector('._branchLabelIconCopy_jdvlk_270'); const text=pill.querySelector('._branchLabelText_jdvlk_316');const tstyle=getComputedStyle(text); const addMenu=left.querySelector('.cr-add-menu');const permission=left.querySelector('.cr-permission-setting'); const svgDefault=def.querySelector('svg');const svgCopy=copy.querySelector('svg'); return {pill:{box:box(pill),radius:style.borderRadius,padding:style.padding,gap:style.gap,maxWidth:style.maxWidth,height:style.height, font:style.fontSize+'/'+style.fontWeight+'/'+style.lineHeight,color:style.color,display:style.display, background:style.backgroundColor,transition:style.transitionProperty+' '+style.transitionDuration}, text:{value:text.textContent.trim(),overflow:tstyle.overflow,textOverflow:tstyle.textOverflow,whiteSpace:tstyle.whiteSpace,font:tstyle.fontSize+'/'+tstyle.fontWeight+'/'+tstyle.lineHeight}, button:button?{box:box(button),tag:button.tagName}:null, icons:{defaultOpacity:getComputedStyle(def).opacity,copyOpacity:getComputedStyle(copy).opacity, defaultSvg:svgDefault?{box:box(svgDefault),viewBox:svgDefault.getAttribute('viewBox'),className:svgDefault.getAttribute('class'),paths:svgDefault.querySelectorAll('path').length}:null, copySvg:svgCopy?{box:box(svgCopy),viewBox:svgCopy.getAttribute('viewBox'),className:svgCopy.getAttribute('class'),strokeWidth:svgCopy.getAttribute('stroke-width'),rects:svgCopy.querySelectorAll('rect').length,paths:svgCopy.querySelectorAll('path').length}:null}, order:{addMenu:addMenu?box(addMenu):null,permission:permission?box(permission):null}}})()` const ringSource = `(()=>{const right=document.querySelector('.cr-input-toolbar__right');const ring=right&&right.querySelector('.cr-context-usage'); if(!right||!ring)return null;const box=node=>{const r=node.getBoundingClientRect();return [r.x,r.y,r.width,r.height]}; const style=getComputedStyle(ring);const svg=ring.querySelector('svg.cr-context-usage__ring'); const track=ring.querySelector('circle:not(.cr-context-usage__ring-progress)');const progress=ring.querySelector('.cr-context-usage__ring-progress'); const wrapper=right.querySelector('.cr-context-usage-tooltip-trigger');const model=right.querySelector('.cr-model-selector'); return {ring:{box:box(ring),radius:style.borderRadius,width:style.width,height:style.height,transition:style.transitionProperty+' '+style.transitionDuration, display:style.display,role:ring.getAttribute('role'),background:style.backgroundColor}, wrapper:wrapper?box(wrapper):null, svg:svg?{box:box(svg),viewBox:svg.getAttribute('viewBox'),className:svg.getAttribute('class')}:null, track:track?{r:track.getAttribute('r'),strokeWidth:track.getAttribute('stroke-width'),fill:track.getAttribute('fill'),stroke:getComputedStyle(track).stroke}:null, progress:progress?{r:progress.getAttribute('r'),strokeWidth:progress.getAttribute('stroke-width'),strokeLinecap:progress.getAttribute('stroke-linecap'), transform:progress.getAttribute('transform'),dashArray:progress.getAttribute('stroke-dasharray'),dashOffset:Number(progress.getAttribute('stroke-dashoffset')), transition:getComputedStyle(progress).transitionProperty+' '+getComputedStyle(progress).transitionDuration,stroke:getComputedStyle(progress).stroke}:null, firstChild:right.firstElementChild?right.firstElementChild.className:null, modelLeft:model?box(model)[0]:null}})()` const popoverSource = `(()=>{const popover=document.querySelector('.cr-context-usage-popover');if(!popover)return null; const box=node=>{const r=node.getBoundingClientRect();return [r.x,r.y,r.width,r.height]};const style=getComputedStyle(popover); const probe=(property,token)=>{const el=document.createElement('div');el.style[property]='var('+token+')';popover.appendChild(el);const value=getComputedStyle(el)[property];el.remove();return value}; const title=popover.querySelector('.cr-context-usage-popover__title');const close=popover.querySelector('.cr-context-usage-popover__close'); const percent=popover.querySelector('.cr-context-usage-popover__percent');const usedValue=popover.querySelector('.cr-context-usage-popover__used-value'); const usedLabel=popover.querySelector('.cr-context-usage-popover__used-label');const bar=popover.querySelector('.cr-context-usage-popover__bar'); const rows=[...popover.querySelectorAll('.cr-context-usage-popover__row')].map(row=>({name:row.querySelector('.cr-context-usage-popover__row-name')?.textContent, value:row.querySelector('.cr-context-usage-popover__row-value')?.textContent,swatch:box(row.querySelector('.cr-context-usage-popover__swatch')).slice(2)})); return {box:box(popover),width:style.width,padding:style.padding,borderRadius:style.borderRadius,font:style.fontSize+'/'+style.lineHeight, background:style.backgroundColor,zIndex:style.zIndex, title:title?{text:title.textContent,font:getComputedStyle(title).fontSize+'/'+getComputedStyle(title).fontWeight+'/'+getComputedStyle(title).lineHeight}:null, close:close?box(close):null, percent:percent?{text:percent.textContent,font:getComputedStyle(percent).fontSize+'/'+getComputedStyle(percent).fontWeight+'/'+getComputedStyle(percent).lineHeight}:null, usedLabel:usedLabel?{text:usedLabel.textContent,color:getComputedStyle(usedLabel).color}:null, usedValue:usedValue?usedValue.textContent:null, bar:bar?{box:box(bar),height:getComputedStyle(bar).height,radius:getComputedStyle(bar).borderRadius,fillWidth:popover.querySelector('.cr-context-usage-popover__bar-fill')?.style.width}:null, rows,bodyText:popover.textContent,tokenTertiary:probe('color','--cr-text-tertiary')}})()` ;(async () => { // 受控上游:最后一次 chunk 带真实 usage(provider 语义,含总 prompt tokens)。 server = createServer(async (req, res) => { let raw = '' for await (const part of req) raw += part const body = JSON.parse(raw || '{}') if (!body.stream) { res.setHeader('content-type', 'application/json'); res.end(JSON.stringify({ choices: [{ message: { role: 'assistant', content: 'USAGE_REPLY' } }] })); return } res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' }) const chunk = payload => `data: ${JSON.stringify({ id: 'local-usage', object: 'chat.completion.chunk', created: 1, model: body.model, ...payload })}\n\n` res.write(chunk({ choices: [{ index: 0, delta: { role: 'assistant', content: 'USAGE_REPLY' }, finish_reason: null }] })) res.write(chunk({ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], usage: { prompt_tokens: QA_PROMPT_TOKENS, completion_tokens: QA_COMPLETION_TOKENS, total_tokens: QA_PROMPT_TOKENS + QA_COMPLETION_TOKENS } })) res.write('data: [DONE]\n\n') res.end() }) // 注意:导入主进程 bundle 之前不能 await 任何东西,否则 Electron 会先进入 ready, // bundle 顶层的 app.disableHardwareAcceleration() 就会抛错。这里全部用同步准备。 const sync = require('node:fs') server.listen(0, '127.0.0.1') sync.mkdirSync(process.env.YCODE_USER_DATA_DIR, { recursive: true }) app.setPath('userData', process.env.YCODE_USER_DATA_DIR) app.setPath('sessionData', process.env.YCODE_USER_DATA_DIR) await import(pathToFileURL(path.resolve('out/main/index.mjs')).href) await app.whenReady() await new Promise(resolve => server.listening ? resolve() : server.once('listening', resolve)) const baseUrl = `http://127.0.0.1:${server.address().port}/v1` for (let index = 0; index < 200; index++) { main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode') if (main && !main.webContents.isLoading()) break await pause(40) } assert.ok(main, 'main window') main.setSize(1336, 902) main.showInactive() main.webContents.on('console-message', (_event, level, message) => { if (level >= 2) rendererErrors.push(message) }) await wait('!!window.ycode && !!document.querySelector(".chat-pane")', 'main renderer') await read(`window.ycode.models.save(${JSON.stringify({ id: 'local-usage', name: 'Local Usage', providerId: 'custom', model: 'model-usage', baseUrl, apiKey: 'LOCAL_TEST_KEY', supportsToolCall: true, maxInputTokens: QA_CONTEXT_WINDOW, maxOutputTokens: 4096 })})`) await read('window.ycode.models.select("local-usage")') await step('conversation-with-git-workspace') await wait(`!!document.querySelector('.cr-input-toolbar__left')`, 'composer toolbar') await wait(`!!document.querySelector('._branchLabel_jdvlk_244')`, 'branch label') const branchInfo = await read(branchSource) snapshots.branch = branchInfo check('branch label shows the real repository branch', branchInfo.text.value, QA_BRANCH) // 原版证据里是 6 字符的 master(82 宽);宽度由分支名长度决定,这里断言固定契约 + 上限。 check('branch pill keeps the recovered geometry', [Math.round(branchInfo.pill.box[3]), branchInfo.pill.padding, branchInfo.pill.radius, branchInfo.pill.gap, branchInfo.pill.maxWidth], [32, '5px 8px', '999px', '8px', '188px']) check('branch pill width stays within the recovered cap', branchInfo.pill.box[2] <= 188 && branchInfo.pill.box[2] > 32, true) check('branch text keeps 14/400/22 with ellipsis', [branchInfo.text.font, branchInfo.text.overflow, branchInfo.text.textOverflow, branchInfo.text.whiteSpace], ['14px/400/22px', 'hidden', 'ellipsis', 'nowrap']) check('branch icon button is a 16x16 button', [branchInfo.button.tag, branchInfo.button.box[2], branchInfo.button.box[3]], ['BUTTON', 16, 16]) check('default glyph is the recovered 16x16 branch icon', [branchInfo.icons.defaultSvg.viewBox, branchInfo.icons.defaultSvg.className, branchInfo.icons.defaultSvg.paths, branchInfo.icons.defaultSvg.box[2]], ['0 0 16 16', 'wb-icon', 1, 16]) check('hover glyph is the recovered lucide copy icon', [branchInfo.icons.copySvg.viewBox, branchInfo.icons.copySvg.className, branchInfo.icons.copySvg.strokeWidth, branchInfo.icons.copySvg.rects, branchInfo.icons.copySvg.paths], ['0 0 24 24', 'lucide lucide-copy wb-icon', '1.5', 1, 1]) check('default glyph visible and copy glyph hidden before hover', [branchInfo.icons.defaultOpacity, branchInfo.icons.copyOpacity], ['1', '0']) check('branch label sits 8px after the add menu', Math.round(branchInfo.pill.box[0] - (branchInfo.order.addMenu[0] + branchInfo.order.addMenu[2])), 8) check('permission setting sits 8px after the branch label', Math.round(branchInfo.order.permission[0] - (branchInfo.pill.box[0] + branchInfo.pill.box[2])), 8) await step('branch-hover-and-copy') await hover('._branchLabel_jdvlk_244') snapshots.branchHover = await read(`(()=>{const pill=document.querySelector('._branchLabel_jdvlk_244'); const probe=(property,token)=>{const el=document.createElement('div');el.style[property]='var('+token+')';pill.appendChild(el);const value=getComputedStyle(el)[property];el.remove();return value}; return {background:getComputedStyle(pill).backgroundColor,hover:pill.matches(':hover'), defaultOpacity:getComputedStyle(pill.querySelector('._branchLabelIconDefault_jdvlk_267')).opacity, copyOpacity:getComputedStyle(pill.querySelector('._branchLabelIconCopy_jdvlk_270')).opacity, secondary:getComputedStyle(pill).getPropertyValue('--wb-bg-secondary').trim(), secondaryResolved:probe('backgroundColor','--wb-bg-secondary')}})()`) check('hovering swaps the glyph to copy', [snapshots.branchHover.defaultOpacity, snapshots.branchHover.copyOpacity], ['0', '1']) check('hovering paints the pill with the resolved --wb-bg-secondary', snapshots.branchHover.background, snapshots.branchHover.secondaryResolved) check('branch pill hover state is real', snapshots.branchHover.hover, true) clipboard.writeText('') await read(`(()=>{document.querySelector('._branchLabelIconButton_jdvlk_279').click();return true})()`) await pause(160) check('clicking the glyph copies the real branch name', clipboard.readText(), QA_BRANCH) await step('context-usage-ring') const ring = await read(ringSource) snapshots.ring = ring check('usage ring keeps the recovered 32x32 / radius 16 box', [Math.round(ring.ring.box[2]), Math.round(ring.ring.box[3]), ring.ring.radius, ring.ring.role], [32, 32, '16px', 'button']) check('usage ring is the first element of the right group', ring.firstChild.includes('cr-context-usage-tooltip-trigger'), true) check('usage ring sits 4px before the model selector', Math.round(ring.modelLeft - (ring.ring.box[0] + ring.ring.box[2])), 4) check('ring svg is the recovered 16x16 ring', [ring.svg.viewBox, ring.svg.className, ring.svg.box[2], ring.svg.box[3]], ['0 0 16 16', 'cr-context-usage__ring', 16, 16]) check('ring track matches the recovered circle', [ring.track.r, ring.track.strokeWidth, ring.track.fill], ['7', '2', 'none']) check('ring progress circle matches the recovered geometry', [ring.progress.r, ring.progress.strokeWidth, ring.progress.strokeLinecap, ring.progress.transform], ['7', '2', 'round', 'rotate(-90 8 8)']) check('ring progress keeps the recovered dashoffset transition', ring.progress.transition, 'stroke-dashoffset 0.3s') check('no usage yet: ring stays empty', Math.round(ring.progress.dashOffset), Math.round(RING_CIRCUMFERENCE)) await step('usage-popover-without-usage') await read(`(()=>{document.querySelector('.cr-context-usage').click();return true})()`) await wait('!!document.querySelector(".cr-context-usage-popover")', 'usage popover') const emptyPopover = await read(popoverSource) snapshots.emptyPopover = emptyPopover check('popover keeps the recovered 280-wide card', [Math.round(emptyPopover.box[2]), emptyPopover.padding, emptyPopover.borderRadius, emptyPopover.font], [280, '16px', '16px', '12px/18px']) check('popover keeps the recovered header', [emptyPopover.title.text, emptyPopover.title.font, Math.round(emptyPopover.close[2]), Math.round(emptyPopover.close[3])], ['上下文用量', '12px/400/20px', 16, 16]) check('no usage yet: popover says so instead of inventing numbers', emptyPopover.bodyText.includes('暂时无法获取用量明细,请稍后重试'), true) check('no usage yet: popover shows no percentage', emptyPopover.percent, null) await read(`(()=>{document.querySelector('.cr-context-usage-popover__close').click();return true})()`) await wait('!document.querySelector(".cr-context-usage-popover")', 'popover closed') check('close button dismisses the popover', await read('!document.querySelector(".cr-context-usage-popover")'), true) await step('real-turn-usage') await read(`(()=>{const input=[...document.querySelectorAll('.wb-composer__input')].find(node=>node.offsetParent!==null); if(!input)throw new Error('Missing composer input'); Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set.call(input,'用量验收'); input.dispatchEvent(new Event('input',{bubbles:true}));return true})()`) await read(`(()=>{document.querySelector('.cr-input-toolbar__send').click();return true})()`) await wait(`document.body.textContent.includes('USAGE_REPLY')`, 'controlled upstream reply') await wait(`(()=>{const progress=document.querySelector('.cr-context-usage__ring-progress');return progress&&Number(progress.getAttribute('stroke-dashoffset'))<${RING_CIRCUMFERENCE - 1}})()`, 'ring reflects real usage') await pause(400) const usedRing = await read(ringSource) snapshots.usedRing = usedRing const expectedOffset = RING_CIRCUMFERENCE * (1 - QA_EXPECTED_PERCENT / 100) check('ring dashoffset follows the real provider usage', Math.abs(usedRing.progress.dashOffset - expectedOffset) < 0.6, true) check('ring stroke uses the recovered progress colour', usedRing.progress.stroke, 'rgb(156, 163, 175)') check('ring track uses the recovered track colour', usedRing.track.stroke, 'rgba(128, 128, 128, 0.2)') await step('usage-tooltip') await hover('.cr-context-usage') const tooltip = await read(`(()=>{const node=document.querySelector('.cr-context-usage-tooltip');if(!node)return null; const probe=(property,token)=>{const el=document.createElement('div');el.style[property]='var('+token+')';node.appendChild(el);const value=getComputedStyle(el)[property];el.remove();return value}; const style=getComputedStyle(node);return {text:node.querySelector('.wb-tooltip__content')?.textContent,role:node.getAttribute('role'), background:style.backgroundColor,color:style.color,borderRadius:style.borderRadius,padding:style.padding,font:style.fontSize+'/'+style.lineHeight, arrow:!!node.querySelector('.wb-tooltip__arrow'),tokenBg:probe('backgroundColor','--wb-tooltip-bg'),tokenFg:probe('color','--wb-tooltip-text-color')}})()`) snapshots.tooltip = tooltip check('hover tooltip uses the recovered usage sentence', tooltip?.text, `${QA_EXPECTED_PERCENT}% · ${QA_PROMPT_TOKENS} / ${QA_CONTEXT_WINDOW} 上下文已使用`) check('tooltip keeps the recovered base surface', [tooltip.background, tooltip.color, tooltip.borderRadius, tooltip.padding, tooltip.font, tooltip.arrow], [tooltip.tokenBg || 'rgba(0, 0, 0, 0.75)', tooltip.tokenFg || 'rgb(255, 255, 255)', '8px', '4px 8px', '12px/20px', true]) await step('usage-popover-with-real-numbers') await read(`(()=>{document.querySelector('.cr-context-usage').click();return true})()`) await wait('!!document.querySelector(".cr-context-usage-popover")', 'popover with usage') await pause(200) const usedPopover = await read(popoverSource) snapshots.usedPopover = usedPopover check('popover shows the real percentage', usedPopover.percent.text, `${QA_EXPECTED_PERCENT}%`) check('popover percent keeps 20/700/28', usedPopover.percent.font, '20px/700/28px') check('popover shows the real used/total pair', usedPopover.usedValue, `${QA_PROMPT_TOKENS} / ${QA_CONTEXT_WINDOW}`) check('popover used label keeps the recovered copy', [usedPopover.usedLabel.text, usedPopover.usedLabel.color], ['已使用', usedPopover.tokenTertiary]) check('popover bar keeps the recovered 8px track', [Math.round(usedPopover.bar.box[3]), usedPopover.bar.height, usedPopover.bar.radius], [8, '8px', '3px']) check('popover bar fill follows the same percentage', usedPopover.bar.fillWidth, `${QA_EXPECTED_PERCENT}%`) check('popover legend keeps the five recovered categories', usedPopover.rows.map(row => row.name), ['系统提示词', '工具及子智能体', '对话消息', '连接器及MCP', '技能']) check('legend swatches keep the recovered 8px dots', usedPopover.rows.map(row => row.swatch), [[8, 8], [8, 8], [8, 8], [8, 8], [8, 8]]) check('legend values stay unknown instead of fabricated', usedPopover.rows.map(row => row.value), ['—', '—', '—', '—', '—']) await step('dark-theme-via-native-settings') // 真实路径:设置窗口切成深色,聊天区 `data-cr-theme` 随之变化,输入区 token 跟着切。 const lightTokens = await read(`(()=>{const popover=document.querySelector('.cr-context-usage-popover');const pill=document.querySelector('._branchLabel_jdvlk_244'); return {popoverBackground:getComputedStyle(popover).backgroundColor,bar:getComputedStyle(popover.querySelector('.cr-context-usage-popover__bar')).backgroundColor, pillColor:getComputedStyle(pill).color,scope:document.querySelector('.chat-pane')?.dataset.crTheme ?? null}})()`) await read(`window.ycode.app.openSettings('appearance')`) let settings for (let index = 0; index < 200; index++) { settings = BrowserWindow.getAllWindows().find(win => win !== main) if (settings && !settings.webContents.isLoading()) break await pause(25) } assert.ok(settings, 'settings window') await pause(600) snapshots.settingsWindow = await read(`({url:location.href,search:location.search,bodyClass:document.body.className,rootHtml:(document.querySelector('#root')?.innerHTML||'').slice(0,240)})`, settings) await wait('!!document.querySelector(".settings-window-root")', 'settings root', settings) await read(`(()=>{const tab=document.querySelector('[data-settings-tab="appearance"]');if(tab)tab.click();return true})()`, settings) await wait(`!!document.querySelector('.segmented')`, 'appearance panel', settings) await read(`(()=>{const node=[...document.querySelectorAll('.segmented__item')].find(item=>item.textContent.trim()==='深色');if(!node)throw new Error('missing dark option');node.click();return true})()`, settings) await wait(`document.documentElement.dataset.theme==='dark'`, 'main window dark theme') await wait(`document.querySelector('.chat-pane')?.dataset.crTheme==='dark'`, 'composer dark scope') await pause(250) const darkPopover = await read(`(()=>{const popover=document.querySelector('.cr-context-usage-popover');const pill=document.querySelector('._branchLabel_jdvlk_244'); const probe=(property,token)=>{const el=document.createElement('div');el.style[property]='var('+token+')';popover.appendChild(el);const value=getComputedStyle(el)[property];el.remove();return value}; return {popoverBackground:getComputedStyle(popover).backgroundColor,pillColor:getComputedStyle(pill).color, bar:getComputedStyle(popover.querySelector('.cr-context-usage-popover__bar')).backgroundColor, tokenElevated:probe('backgroundColor','--cr-bg-elevated'),tokenActive:probe('backgroundColor','--cr-bg-active'), tokenTextPrimary:probe('color','--wb-color-text-primary'),scope:document.querySelector('.chat-pane')?.dataset.crTheme ?? null}})()`) snapshots.dark = { lightTokens, darkPopover } check('native settings switches the composer to the dark scope', [lightTokens.scope, darkPopover.scope], ['light', 'dark']) check('dark popover uses the recovered dark elevated surface', darkPopover.popoverBackground, 'rgb(36, 38, 41)') check('dark popover bar uses the recovered dark active fill', darkPopover.bar, 'rgba(255, 255, 255, 0.1)') check('dark pill switches to the dark primary text colour', [darkPopover.pillColor, lightTokens.pillColor, darkPopover.pillColor === darkPopover.tokenTextPrimary], ['rgb(255, 255, 255)', 'rgb(0, 0, 0)', true]) await read(`(()=>{const node=[...document.querySelectorAll('.segmented__item')].find(item=>item.textContent.trim()==='浅色');if(!node)throw new Error('missing light option');node.click();return true})()`, settings) await wait(`document.documentElement.dataset.theme==='light'`, 'main window light theme') await wait(`document.querySelector('.chat-pane')?.dataset.crTheme==='light'`, 'composer light scope') await read(`document.querySelector('.settings-window-close').click()`, settings) for (let index = 0; index < 200 && !settings.isDestroyed(); index++) await pause(20) check('light theme restored through the same settings window', await read(`[document.documentElement.dataset.theme,document.querySelector('.chat-pane').dataset.crTheme]`), ['light', 'light']) check('no renderer error logs', rendererErrors, []) server.close() const report = { result: checks.every(item => item.pass) ? 'PASS' : 'FAIL', scope: 'Real isolated YCode App/preload/IPC/renderer + real OMP kernel + controlled local OpenAI-compatible upstream + real git repository; measured branch pill, usage ring, tooltip and popover in the live window after a real turn.', sourceEvidence: ['ev_2569313a7a0aa901c5a3b8dc1d7fd190109b9006c915830109b62195f312376a', 'contexts-BkjwYSZT.css', 'src-CyXHA0IC.css', 'chatInput.contextUsage.*'], snapshots, checks, rendererErrors } await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2)) console.log('COMPOSER_CONTEXT_' + report.result + ' ' + directory) for (const failure of checks.filter(item => !item.pass)) console.log('COMPOSER_CONTEXT_FAILURE ' + JSON.stringify(failure)) app.exit(report.result === 'PASS' ? 0 : 1) })().catch(async error => { if (server) server.close() await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({ phase, error: String(error), stack: error.stack, checks, snapshots, rendererErrors }, null, 2)) console.error(error); app.exit(1) }) }