/** Isolated Electron fixture for the recommended-skill page. * The host is fully controlled: no network, no real skill storage, no user data. */ import fs from 'node:fs' import path from 'node:path' import assert from 'node:assert/strict' import { createRequire } from 'node:module' import { spawn } from 'node:child_process' const require = createRequire(import.meta.url) const electronModule = require('electron') const electron = typeof electronModule === 'object' && electronModule.app ? electronModule : null const repository = path.resolve('.'), base = path.join(repository, '.tmp') if (!electron) { fs.mkdirSync(base, { recursive: true }) const output = fs.mkdtempSync(path.join(base, 'recommended-skills-ui-')) const env = { ...process.env } delete env.ELECTRON_RUN_AS_NODE delete env.ELECTRON_RENDERER_URL const child = spawn(electronModule, [path.resolve('scripts/test-recommended-skills-ui.mjs'), output], { stdio: 'inherit', windowsHide: true, env }) const timer = setTimeout(() => child.kill(), 120000) process.exitCode = await new Promise(resolve => child.on('exit', code => { clearTimeout(timer); resolve(code ?? 1) })) } else { void (async () => { const { app, BrowserWindow, ipcMain } = electron const output = path.resolve(process.argv.at(-1)), relative = path.relative(base, output) assert.ok(relative.startsWith('recommended-skills-ui-') && !relative.includes('..') && !path.isAbsolute(relative)) const userData = path.join(output, 'data') fs.mkdirSync(userData, { recursive: true }) app.setPath('userData', userData) const checks = [], errors = [], calls = [], screenshots = [], actionStyles = [] const recommendation = { id: 'archify', name: 'Archify', description: '生成可交互的架构图、流程图和一页式说明', author: 'tt-a1i', license: 'MIT', repository: 'tt-a1i/archify', version: '3.2.0', commit: '25cf37080fc8b1dbe758a4efc3935b55f3327083', packagePath: 'archify.zip', sha256: '70e210265c59f24dcbdf2a502ff671104b5454e7ee7d3554aecd86bad32384f5', expectedBytes: 5271959, containsScripts: true } const localSkill = { id: 'user:local', name: 'Archify', description: '本地导入的同名技能', version: '1.0.0', path: 'C:/local/archify', scope: 'user', enabled: true, userInvocable: true, disableModelInvocation: false, source: 'imported', userModified: true } const installedRecommendation = { id: 'user:rec', name: 'Archify', description: recommendation.description, version: '3.2.0', path: 'C:/managed/archify', scope: 'user', enabled: true, userInvocable: true, disableModelInvocation: false, source: 'imported', userModified: false, origin: { recommendationId: 'archify', repository: recommendation.repository, commit: recommendation.commit, packagePath: recommendation.packagePath, archiveSha256: recommendation.sha256, version: '3.2.0', installedAt: 1 } } let installed = [], progress = null, failNext = false, cancelRequested = false, delay = 0, recommendationOverride = null, recommendationFails = false let clock = 0 const listeners = new Set(), changedListeners = new Set() const emit = state => { // The host stamps a monotonic time; a strict comparison rejects a stale frame. const stamped = { ...state, updatedAt: ++clock } progress = stamped for (const listener of listeners) listener(stamped) for (const win of BrowserWindow.getAllWindows()) if (!win.isDestroyed()) win.webContents.send('skills:install-progress', stamped) } const changed = () => { for (const listener of changedListeners) listener() for (const win of BrowserWindow.getAllWindows()) if (!win.isDestroyed()) win.webContents.send('skills:changed') } ipcMain.handle('skills-qa', async (_event, method, args) => { calls.push([method, ...args]) if (method === 'recommendations') { if (recommendationFails) throw Error('推荐目录服务不可用') return recommendationOverride ?? [recommendation] } if (method === 'list') return installed if (method === 'setEnabled') { installed = installed.map(skill => skill.id === args[0] ? { ...skill, enabled: args[1] } : skill); changed(); return } if (method === 'installState') return progress // The host only accepts the cancel request; the terminal frame still comes from the install flow. if (method === 'cancelInstall') { cancelRequested = true; return { cancelled: true } } if (method === 'installRecommended') { const [id, options] = args if (id !== recommendation.id) throw Error('未知推荐技能') if (failNext) { failNext = false emit({ operationId: options.operationId, recommendationId: id, phase: 'failed', receivedBytes: 0, error: '下载失败:网络错误', updatedAt: Date.now() }) throw Error('下载失败:网络错误') } emit({ operationId: options.operationId, recommendationId: id, phase: 'downloading', receivedBytes: 0, totalBytes: recommendation.expectedBytes, updatedAt: Date.now() }) await new Promise(resolve => setTimeout(resolve, delay)) if (cancelRequested) { cancelRequested = false; emit({ operationId: options.operationId, recommendationId: id, phase: 'cancelled', receivedBytes: 0, updatedAt: Date.now() }); throw Object.assign(Error('推荐技能安装已取消'), { name: 'AbortError' }) } emit({ operationId: options.operationId, recommendationId: id, phase: 'validating', receivedBytes: recommendation.expectedBytes, totalBytes: recommendation.expectedBytes, updatedAt: Date.now() }) await new Promise(resolve => setTimeout(resolve, delay)) if (cancelRequested) { cancelRequested = false; emit({ operationId: options.operationId, recommendationId: id, phase: 'cancelled', receivedBytes: recommendation.expectedBytes, updatedAt: Date.now() }); throw Object.assign(Error('推荐技能安装已取消'), { name: 'AbortError' }) } emit({ operationId: options.operationId, recommendationId: id, phase: 'installing', receivedBytes: recommendation.expectedBytes, totalBytes: recommendation.expectedBytes, updatedAt: Date.now() }) await new Promise(resolve => setTimeout(resolve, delay)) installed = [installedRecommendation] emit({ operationId: options.operationId, recommendationId: id, phase: 'installed', receivedBytes: recommendation.expectedBytes, totalBytes: recommendation.expectedBytes, updatedAt: Date.now() }) changed() return installedRecommendation } if (method === 'qaSetInstalled') { installed = args[0]; changed(); return installed } if (method === 'qaFailNext') { failNext = true; return true } if (method === 'qaDelay') { delay = args[0]; return true } if (method === 'qaSetRecommendations') { recommendationOverride = args[0]; return true } if (method === 'qaFailRecommendations') { recommendationFails = args[0] === true; return true } // Simulate an operation owned by another window. if (method === 'qaEmitForeign') { const [phase, operationId] = args emit({ operationId, recommendationId: recommendation.id, phase, receivedBytes: 0, totalBytes: recommendation.expectedBytes, updatedAt: Date.now() }) return true } // Replay a frame with an older timestamp to prove the terminal state never regresses. if (method === 'qaEmitStale') { const [phase, operationId, updatedAt] = args const stamped = { operationId, recommendationId: recommendation.id, phase, receivedBytes: 0, totalBytes: recommendation.expectedBytes, updatedAt } progress = stamped for (const listener of listeners) listener(stamped) for (const win of BrowserWindow.getAllWindows()) if (!win.isDestroyed()) win.webContents.send('skills:install-progress', stamped) return true } if (method === 'qaSubscribe') { listeners.add(args[0]); return true } if (method === 'qaSubscribeChanged') { changedListeners.add(args[0]); return true } throw Error('Unknown QA method ' + method) }) fs.writeFileSync(path.join(output, 'preload.cjs'), `const{contextBridge,ipcRenderer}=require('electron'); const invoke=(method,...args)=>ipcRenderer.invoke('skills-qa',method,args); contextBridge.exposeInMainWorld('ycode',{skills:{ list:(cwd)=>invoke('list',cwd), recommendations:()=>invoke('recommendations'), installRecommended:(id,options)=>invoke('installRecommended',id,options), cancelInstall:(operationId)=>invoke('cancelInstall',operationId), installState:()=>invoke('installState'), onInstallProgress:(listener)=>{const wrapped=(_event,state)=>listener(state);ipcRenderer.on('skills:install-progress',wrapped);return()=>ipcRenderer.removeListener('skills:install-progress',wrapped)}, onChanged:(listener)=>{const wrapped=()=>listener();ipcRenderer.on('skills:changed',wrapped);return()=>ipcRenderer.removeListener('skills:changed',wrapped)}, read:async()=>({skill:{},content:'',files:[]}), setEnabled:(...args)=>invoke('setEnabled',...args), remove:async()=>{}, export:async()=>null, importPath:async()=>null ,qaSetInstalled:(v)=>invoke('qaSetInstalled',v), qaFailNext:()=>invoke('qaFailNext'), qaDelay:(v)=>invoke('qaDelay',v), qaSetRecommendations:(v)=>invoke('qaSetRecommendations',v), qaFailRecommendations:(v)=>invoke('qaFailRecommendations',v), qaEmitForeign:(phase,operationId)=>invoke('qaEmitForeign',phase,operationId), qaEmitStale:(phase,operationId,updatedAt)=>invoke('qaEmitStale',phase,operationId,updatedAt) },resources:{openExternal:async()=>undefined}});`) const location = file => JSON.stringify(path.join(repository, file).replaceAll('\\', '/')) fs.writeFileSync(path.join(output, 'fixture.tsx'), `import React from 'react'; import{createRoot}from'react-dom/client'; import Center from ${location('src/renderer/src/capabilities/CapabilityCenter')}; import${location('src/renderer/src/styles/tokens.css')}; import${location('src/renderer/src/styles/app.css')}; window.__qa={uses:[],created:0,find:0}; createRoot(document.getElementById('root')).render(
{}} onUseSkill={skill=>window.__qa.uses.push(skill)} onCreateSkill={()=>window.__qa.created++} onFindSkill={()=>window.__qa.find++}/>
);`) await require('esbuild').build({ entryPoints: [path.join(output, 'fixture.tsx')], bundle: true, platform: 'browser', format: 'esm', jsx: 'automatic', outfile: path.join(output, 'fixture.js'), loader: { '.svg': 'file', '.png': 'file', '.jpg': 'file', '.woff': 'file', '.woff2': 'file', '.ttf': 'file' }, logLevel: 'warning' }) fs.writeFileSync(path.join(output, 'index.html'), '
') const win = new BrowserWindow({ width: 1052, height: 901, useContentSize: true, frame: false, show: false, webPreferences: { preload: path.join(output, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, backgroundThrottling: false } }) win.webContents.on('console-message', (_event, level, message) => { if (level === 3) errors.push(message) }) const read = code => win.webContents.executeJavaScript(code) const check = (label, actual, expected) => { const pass = typeof expected === 'function' ? expected(actual) : JSON.stringify(actual) === JSON.stringify(expected) assert.ok(pass, `${label}: ${JSON.stringify(actual)}`) checks.push(label) } const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) const wait = async code => { for (let index = 0; index < 200; index++) { if (await read(code)) return; await pause(50) } throw Error('Timeout ' + code) } const click = async selector => { await read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e)throw Error('Missing '+${JSON.stringify(selector)});e.click();return true})()`) await pause(140) } const capture = async name => { win.webContents.invalidate() await pause(120) // A hidden window can capture the previous frame; warm the compositor first. await win.webContents.capturePage() await pause(200) const image = await win.webContents.capturePage() fs.writeFileSync(path.join(output, name + '.png'), image.toPNG()) screenshots.push(name + '.png') } const verifyCurrentActionStyles = async theme => { const selector = '[data-recommendation-id=archify] .sm-use-btn' // Selector :hover can be false despite a trusted mouse entry and applied CSS // (B02 Windows evidence). Keep it diagnostic, never as the success predicate. const inspect = (point = null) => read(`(()=>{ const b=document.querySelector(${JSON.stringify(selector)}),p=${JSON.stringify(point)}; const hit=p?document.elementFromPoint(p.x,p.y):null; const describe=e=>e?{tag:e.tagName,id:e.id,className:e.getAttribute('class')}:null; if(!b)return{missing:true,hitTarget:describe(hit)}; const s=getComputedStyle(b),r=b.getBoundingClientRect(); return{width:s.width,height:s.height,background:s.backgroundColor,color:s.color,outline:s.outline, focused:document.activeElement===b,hover:b.matches(':hover'),pointerEvents:s.pointerEvents, rect:{x:r.x,y:r.y,width:r.width,height:r.height},hitTarget:describe(hit), hitWithinAction:!!hit&&(hit===b||b.contains(hit)),pointerEntry:window.__qaHoverProbe?.entry??null}; })()`) win.webContents.sendInputEvent({type:'mouseMove',x:1,y:1}) await read(`document.activeElement?.blur()`); await pause(200) const normal = await inspect() check(theme + ' use action default background is transparent', normal.background, value => value === 'rgba(0, 0, 0, 0)' || value === 'transparent') const point = await read(`(()=>{const r=document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect();return{x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)}})()`) // Arm only after moving off the action, so prior themes/entries cannot pass. await read(`(()=>{ const b=document.querySelector(${JSON.stringify(selector)}); const probe=window.__qaHoverProbe={entry:null}; const listener=event=>{probe.entry={isTrusted:event.isTrusted,pointerType:event.pointerType, targetIsAction:event.target===b,x:event.clientX,y:event.clientY}}; b.addEventListener('pointerenter',listener); probe.cleanup=()=>b.removeEventListener('pointerenter',listener); })()`) let hover try { win.webContents.sendInputEvent({type:'mouseMove',...point}) const deadline = Date.now() + 10000 while (true) { hover = await inspect(point) if (hover.pointerEntry?.isTrusted === true && hover.pointerEntry.pointerType === 'mouse' && hover.pointerEntry.targetIsAction && hover.hitWithinAction && hover.background !== normal.background) break if (Date.now() >= deadline) { throw Error(theme + ' native hover timeout: ' + JSON.stringify({selector,point,normal,last: hover})) } await pause(50) } } finally { await read('window.__qaHoverProbe?.cleanup(); delete window.__qaHoverProbe') } check(theme + ' trusted mouse pointer enters use action', hover.pointerEntry.isTrusted && hover.pointerEntry.pointerType === 'mouse' && hover.pointerEntry.targetIsAction, true) check(theme + ' pointer hit target is the action or its descendant', hover.hitWithinAction, true) check(theme + ' hover has a distinct background', hover.background !== normal.background, true) await capture('installed-use-' + theme + '-hover') win.webContents.sendInputEvent({type:'mouseMove',x:1,y:1}) win.webContents.sendInputEvent({type:'keyDown',keyCode:'Tab'});win.webContents.sendInputEvent({type:'keyUp',keyCode:'Tab'}) await read(`document.querySelector(${JSON.stringify(selector)}).focus()`);await pause(200) const focus = await inspect() check(theme + ' use action remains focusable with stable 24px slot', focus.focused && focus.width === '24px' && focus.height === '24px', true) await capture('installed-use-' + theme + '-focus') actionStyles.push({theme,normal,hover,focus}) await read(`document.activeElement?.blur()`) } try { await win.loadFile(path.join(output, 'index.html')) await wait('!!document.querySelector(".yc-skills-page")') await wait('!!document.querySelector("[data-recommendation-id=archify]")') check('the recommendation card renders from the host catalog', await read('document.querySelector("[data-recommendation-id=archify] .sm-card-name")?.textContent'), 'Archify') check('the card exposes its install state for the host test', await read('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState'), 'none') check('the card shows the author supplied description', await read('document.querySelector("[data-recommendation-id=archify] .sm-card-desc")?.textContent'), recommendation.description) check('no maintenance placeholder is shown', await read('!!document.querySelector("[data-capability-maintenance]")'), false) check('the remote catalog tabs still report no connection', await read('!!document.querySelector(".yc-skill-market-unavailable")'), false) await capture('recommendation-card') // First install asks for an explicit confirmation. await click('[aria-label="安装 Archify"]') await wait('!!document.querySelector(".yc-skill-detail-shell")') check('the confirm dialog names the source repository', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("tt-a1i/archify")'), true) check('the confirm dialog states that no install script runs', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("不会执行仓库中的任何安装命令")'), true) check('the confirm dialog warns that the skill contains scripts', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("包含脚本") || document.querySelector(".yc-skill-detail-shell").textContent.includes("脚本")'), true) await capture('install-confirm') await read('(()=>{const first=document.querySelector("[role=dialog] .sm-skill-detail-close");first.focus();first.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",shiftKey:true,bubbles:true,cancelable:true}))})()') check('reverse Tab reaches the collapsible source information', await read('document.activeElement.tagName'), 'SUMMARY') await read('document.activeElement.dispatchEvent(new KeyboardEvent("keydown",{key:"Tab",bubbles:true,cancelable:true}))') check('Tab wraps inside the modal rather than entering the sidebar', await read('document.activeElement.getAttribute("aria-label")'), '关闭技能详情') await read('window.__detailBeforeInstall=document.querySelector("[role=dialog]")') await click('[aria-label="确认安装推荐技能"]') await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="installed"') check('the install progress is reported by the host', await read('document.querySelector(".yc-skill-inline-install").dataset.installPhase'), 'installed') check('the host received the recommendation id', await read(`JSON.stringify(${JSON.stringify(calls)}.filter(c=>c[0]==='installRecommended').map(c=>c[1]))`), '["archify"]') check('the host received a fresh operation id', await read(`(${JSON.stringify(calls)}.filter(c=>c[0]==='installRecommended').every(c=>typeof c[2]?.operationId==='string'&&c[2].operationId.length>8))`), true) check('a first install is not an overwrite', await read(`(${JSON.stringify(calls)}.filter(c=>c[0]==='installRecommended').every(c=>c[2]?.overwrite!==true))`), true) await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="current"') check('the card reports the recommendation as installed', await read('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState'), 'current') check('the installed skill has no add button', await read('!!document.querySelector("[data-recommendation-id=archify] .sm-add-btn")'), false) check('the installed skill uses the recovered installed card', await read('!!document.querySelector("[data-recommendation-id=archify] .sm-card--installed")'), true) check('the installed action uses the original action slot', await read('!!document.querySelector("[data-recommendation-id=archify] .sm-status .sm-use-btn")'), true) check('no duplicate under-card use action remains', await read('!!document.querySelector(".yc-skill-recommendation-meta button")'), false) check('the duplicate footer is absent', await read('!!document.querySelector(".yc-skill-recommendation-footer")'), false) check('the detail remains the same DOM instance throughout installation', await read('window.__detailBeforeInstall===document.querySelector("[role=dialog]")'), true) check('the detail offers use after installation', await read('document.querySelector("[role=dialog] .sm-skill-detail-action-row")?.textContent.includes("去使用")'), true) check('the success notification also offers use', await read('document.querySelector(".yc-skill-install-success")?.textContent.includes("去使用")'), true) check('there is no permanent list footer progress panel', await read('!!document.querySelector(".yc-skills-scroll > .yc-skill-inline-install")'), false) await capture('installed-detail') await click('[role=dialog] .sm-skill-detail-close') const installedActionStyle = await read(`(()=>{const b=document.querySelector('[data-recommendation-id=archify] .sm-use-btn');const s=getComputedStyle(b);const svg=b.querySelector('svg');return {width:s.width,height:s.height,svgWidth:getComputedStyle(svg).width,svgHeight:getComputedStyle(svg).height}})()`) check('installed action uses recovered 24px slot and 16px icon', installedActionStyle, {width:'24px',height:'24px',svgWidth:'16px',svgHeight:'16px'}) await capture('installed-recommendation-light') await verifyCurrentActionStyles('light') // The installed list stays reachable and the skill can be used. await click('[aria-label="已安装技能"]') await wait('document.querySelectorAll(".sm-card").length===1') check('the installed skill appears in the installed list', await read('document.querySelector(".sm-card-name")?.textContent'), 'Archify') await click('[aria-label="返回专家中心"]') await wait('!!document.querySelector("[data-recommendation-id=archify]")') // A local skill with the same name requires an explicit overwrite. await read(`window.ycode.skills.qaSetInstalled(${JSON.stringify([localSkill])})`) await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="conflict"') await click('[aria-label="替换 Archify"]') await wait('!!document.querySelector(".yc-skill-detail-shell")') check('a same-name local skill requires an explicit replacement', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("替换")'), true) check('the replacement notice mentions the local modification', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("本地内容已被修改")'), true) await click('[aria-label="确认安装推荐技能"]') await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="current"') check('an explicit replacement is sent to the host as an overwrite', await read(`(${JSON.stringify(calls)}.filter(c=>c[0]==='installRecommended').at(-1)[2]?.overwrite)===true`), true) await click('[role=dialog] .sm-skill-detail-close') // A failing download reports the real host error and allows a retry. await read(`window.ycode.skills.qaSetInstalled(${JSON.stringify([])})`) await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="none"') await read(`window.ycode.skills.qaFailNext()`) await click('[aria-label="安装 Archify"]') await click('[aria-label="确认安装推荐技能"]') await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="failed"') check('a failed install shows the host error', await read('document.querySelector(".yc-skill-inline-install").textContent.includes("网络错误")'), true) check('a failed install can be retried from the same card', await read('document.querySelector("[data-recommendation-id=archify] .sm-add-btn")?.disabled'), false) await click('[role=dialog] .sm-skill-detail-close') // Cancellation is allowed before the atomic commit phase. await read(`window.ycode.skills.qaDelay(400)`) await click('[aria-label="安装 Archify"]') await click('[aria-label="确认安装推荐技能"]') await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="downloading"') check('the download phase can be cancelled', await read('[...document.querySelectorAll("[role=dialog] button")].some(e=>e.textContent==="取消安装"&&!e.disabled)'), true) await read('document.querySelector("[role=dialog]").querySelectorAll("button").forEach(e=>{if(e.textContent==="取消安装")e.click()})') await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="cancelled"') check('the cancellation is sent to the host', await read(`${JSON.stringify(calls)}.some(c=>c[0]==='cancelInstall')`), true) await click('[role=dialog] .sm-skill-detail-close') await read(`window.ycode.skills.qaDelay(0)`) // Closing the detail never aborts an accepted install; reopening resumes it. await read('window.ycode.skills.qaDelay(250)') await click('[aria-label="安装 Archify"]'); await click('[aria-label="确认安装推荐技能"]') await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="downloading"') const continuingId = await read('document.querySelector(".yc-skill-inline-install").dataset.operationId') await click('[role=dialog] .sm-skill-detail-close') check('closing a running detail restores the background', await read('document.getElementById("root").inert'), false) await click('[data-recommendation-id=archify] .sm-card') check('reopening detail keeps the same install operation', await read('document.querySelector(".yc-skill-inline-install").dataset.operationId'), continuingId) await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="installing"') check('the commit phase disables cancellation in place', await read('[...document.querySelectorAll("[role=dialog] button")].find(e=>e.textContent==="取消安装")?.disabled'), true) await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="installed"') await click('[role=dialog] .sm-skill-detail-close') await click('.yc-skill-install-success [aria-label="使用推荐技能 Archify"]') check('the success notification invokes the same use callback', await read('window.__qa.uses.length'), 1) await read(`window.ycode.skills.qaSetInstalled(${JSON.stringify([{ ...installedRecommendation, enabled: false }])})`) await wait('document.querySelector("[data-recommendation-id=archify] .sm-use-btn")?.title.includes("启用")') await click('[data-recommendation-id=archify] .sm-use-btn') await wait('window.__qa.uses.length===2') check('disabled recommendation is enabled before use', await read('window.__qa.uses.at(-1).enabled'), true) await read('window.ycode.skills.qaDelay(0)'); await read('window.ycode.skills.qaSetInstalled([])') await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="none"') // A failing catalog is reported as unavailable, not as a maintenance page. // The page loads its catalog once, so the failure is proven on a fresh mount. await read(`window.ycode.skills.qaFailRecommendations(true)`) await read(`location.reload()`) await wait('!!document.querySelector(".yc-skill-recommendation-unavailable")') check('an empty catalog is reported as unavailable, not as a maintenance page', await read('!!document.querySelector("[data-capability-maintenance]")'), false) check('the unavailable state keeps top installed navigation', await read('!!document.querySelector("[aria-label=已安装技能]")'), true) check('the unavailable state has no duplicate installed entry', await read('[...document.querySelectorAll(".yc-skill-recommendation-unavailable button")].some(e=>e.textContent.includes("查看已安装技能"))'), false) await read(`window.ycode.skills.qaFailRecommendations(false)`) await click('.yc-skill-recommendation-unavailable button') await wait('!!document.querySelector("[data-recommendation-id=archify]")') // An install owned by another window blocks a new install here. const foreignOperation = '11111111-2222-4333-8444-555555555555' await read(`window.ycode.skills.qaEmitForeign('downloading', ${JSON.stringify(foreignOperation)})`) await wait('!!document.querySelector("[data-foreign-install=true]")') check('an install from another window is reported', await read('document.querySelector("[data-foreign-install=true]").textContent.includes("其他窗口正在安装推荐技能")'), true) check('a new install is blocked while another window is installing', await read('document.querySelector("[data-recommendation-id=archify] .sm-add-btn")?.disabled'), true) check('the blocked state is explained on the card', await read('!!document.querySelector(".yc-skill-recommendation-busy")'), true) await capture('foreign-install') await read(`window.ycode.skills.qaEmitForeign('installed', ${JSON.stringify(foreignOperation)})`) await wait('!document.querySelector("[data-foreign-install=true]")') check('the foreign block clears when that operation finishes', await read('document.querySelector("[data-recommendation-id=archify] .sm-add-btn")?.disabled'), false) // A stale frame must not move a settled operation back into a running phase. await read(`window.ycode.skills.qaSetInstalled(${JSON.stringify([])})`) await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="none"') await read(`window.ycode.skills.qaDelay(0)`) await click('[aria-label="安装 Archify"]') await click('[aria-label="确认安装推荐技能"]') await wait('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase==="installed"') const settledOperation = await read(`(${JSON.stringify(calls)}.filter(c=>c[0]==='installRecommended').at(-1)[2]?.operationId)`) check('the settled install recorded its operation id', typeof settledOperation === 'string' && settledOperation.length > 8, true) await read(`window.ycode.skills.qaEmitStale('downloading', ${JSON.stringify(settledOperation)}, 1)`) await pause(250) check('a stale frame does not regress the settled install', await read('document.querySelector(".yc-skill-inline-install")?.dataset.installPhase'), 'installed') await click('[role=dialog] .sm-skill-detail-close') // Dark and narrow captures of the real page. // This fixture mounts only the capability center, so the theme attribute is set directly. // The dark token switch is proven here only; the production app harness covers the light flow. await read(`(()=>{localStorage.setItem('ycode:theme','dark');document.documentElement.dataset.theme='dark';return true})()`) await wait('document.documentElement.dataset.theme==="dark"') await wait('!!document.querySelector(".yc-capability-center .expert-center-dark")') check('the dark theme reaches the real page', await read('!!document.querySelector(".yc-capability-center .expert-center-dark")'), true) // The card transitions its background, so the dark surface only settles after the animation. await wait(`(()=>{const card=document.querySelector('[data-recommendation-id=archify] .sm-card');if(!card)return false;const m=/rgba?\\((\\d+)/.exec(getComputedStyle(card).backgroundColor);return !!m&&Number(m[1])<90})()`) const darkColors = await read(`(()=>{const card=document.querySelector('[data-recommendation-id=archify] .sm-card');const page=document.querySelector('.yc-capability-center .expert-center-page');return{card:card?getComputedStyle(card).backgroundColor:null,page:page?getComputedStyle(page).backgroundColor:null}})()`) check('the dark card uses a dark surface', darkColors.card, value => typeof value === 'string' && /rgba?\((\d+), (\d+), (\d+)/.test(value) && Number(value.match(/rgba?\((\d+)/)[1]) < 90) const overflow = await read(`(()=>{const wide=[...document.querySelectorAll('*')].filter(e=>e.getBoundingClientRect().right>innerWidth+1).map(e=>e.className||e.tagName).slice(0,4);return{scrollWidth:document.documentElement.scrollWidth,innerWidth,wide}})()`) check('the dark layout keeps the recommendation card inside the viewport', await read(`(()=>{const c=document.querySelector('[data-recommendation-id=archify] .sm-card');if(!c)return false;const r=c.getBoundingClientRect();return r.left>=0&&r.right<=innerWidth+1})()`), true) check('the dark layout does not overflow the document', overflow.scrollWidth <= overflow.innerWidth + 1, true) await capture('recommendation-dark') await verifyCurrentActionStyles('dark') win.setSize(720, 900) await pause(400) check('the recommendation card stays inside the narrow viewport', await read('(()=>{const c=document.querySelector("[data-recommendation-id=archify] .sm-card");if(!c)return false;const r=c.getBoundingClientRect();return r.left>=0&&r.right<=innerWidth+1})()'), true) const narrowOverflow = await read(`(()=>{const wide=[...document.querySelectorAll('*')].filter(e=>e.getBoundingClientRect().right>innerWidth+1).map(e=>String(e.className||e.tagName)).slice(0,5);return{scrollWidth:document.documentElement.scrollWidth,innerWidth,wide}})()`) check('the narrow recommendation card causes no document overflow', narrowOverflow.scrollWidth <= narrowOverflow.innerWidth + 1, true) await capture('recommendation-narrow') win.setSize(1052, 901) await pause(250) check('no renderer errors', errors, []) fs.writeFileSync(path.join(output, 'report.json'), JSON.stringify({ result: 'PASS', checks, calls, screenshots, actionStyles, limits: 'Controlled host in isolated Electron; the shipped page and styles are used. No network, no real skill storage, no real install.' }, null, 2)) console.log(JSON.stringify({ result: 'PASS', checks: checks.length, output })) win.destroy() app.exit(0) } catch (error) { await capture('failure') fs.writeFileSync(path.join(output, 'failure.json'), JSON.stringify({ error: String(error), stack: error?.stack, checks, errors, calls, html: await read('document.body.innerHTML') }, null, 2)) console.error(error) win.destroy() app.exit(1) } })().catch(error => { console.error(error); electron.app.exit(1) }) }