/** Own native fixture, real production skills module + isolated FS/IPC. OS picker only is controlled. */
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'), electron = typeof electronModule === 'object' && electronModule.app ? electronModule : null
if (electron) { electron.app.disableHardwareAcceleration(); electron.app.commandLine.appendSwitch('use-angle', 'swiftshader'); electron.app.commandLine.appendSwitch('disable-gpu-compositing') }
const repository = path.resolve('.'), base = path.join(repository, '.tmp')
if (!electron) {
const output = fs.mkdtempSync(path.join(base, 'skills-ui-'))
assert.ok(path.relative(base, output).startsWith('skills-ui-') && !path.relative(base, output).includes('..'))
const env = { ...process.env }; delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
const child = spawn(require('electron'), ['--no-sandbox','--disable-gpu-sandbox',path.resolve('scripts/test-skills-ui.mjs'), output], { stdio: 'inherit', windowsHide:true, env })
process.exitCode = await new Promise(resolve => child.on('exit', code => 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('skills-ui-') && !relative.includes('..') && !path.isAbsolute(relative))
const userData = path.join(output, 'data'); fs.mkdirSync(userData); app.setPath('userData', userData)
const workspace = path.join(output, 'workspace'), source = path.join(output, 'source'), checks = [], errors = []
fs.mkdirSync(workspace); fs.mkdirSync(source)
fs.writeFileSync(path.join(source, 'SKILL.md'), '---\nname: Native Local Skill\ndescription: Controlled native UI fixture\n---\n# Actual skill instructions\nNATIVE_SKILL_PROOF\n')
const imports = []
await app.whenReady()
await require('esbuild').build({ entryPoints: [path.join(repository,'src/main/capabilities/skills.ts')], bundle: true, platform:'node',format:'cjs',external:['electron'],outfile:path.join(output,'skills.cjs'),logLevel:'warning' })
const api = require(path.join(output,'skills.cjs'))
ipcMain.handle('skills-qa',async (_event,method,args)=>{
if(method==='list')return api.listSkills(...args)
if(method==='read')return api.readSkill(...args)
if(method==='setEnabled')return api.setSkillEnabled(...args)
if(method==='remove')return api.removeSkill(...args)
if(method==='export')return api.exportSkill(args[0],path.join(output,'export.zip'))
if(method==='importPath'){imports.push(args);return api.importSkill(args[2]==='zip'?path.join(output,'export.zip'):source,args[1])}
throw Error('Unknown QA method')
})
fs.writeFileSync(path.join(output,'preload.cjs'), `const{contextBridge,ipcRenderer}=require('electron');contextBridge.exposeInMainWorld('ycode',{skills:Object.fromEntries(['list','read','setEnabled','remove','export','importPath'].map(method=>[method,(...args)=>ipcRenderer.invoke('skills-qa',method,args)]))});`)
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.__skillsQa={uses:[],created:0,find:0};createRoot(document.getElementById('root')).render(
{}} onUseSkill={skill=>window.__skillsQa.uses.push(skill)} onCreateSkill={()=>window.__skillsQa.created++} onFindSkill={()=>window.__skillsQa.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=(name,actual,expected)=>{assert.deepEqual(actual,expected,name);checks.push(name)}
const wait=async code=>{for(let index=0;index<100;index++){if(await read(code))return;await new Promise(resolve=>setTimeout(resolve,50))}throw Error('Timeout '+code)}
await win.loadFile(path.join(output,'index.html'));win.webContents.debugger.attach('1.3');await win.webContents.debugger.sendCommand('Page.enable');win.showInactive()
const cdp=(method,args)=>win.webContents.debugger.sendCommand(method,args)
const settle=async()=>{await cdp('Page.captureScreenshot',{format:'png',fromSurface:true});await new Promise(resolve=>setTimeout(resolve,150))}
const click=async selector=>{const hover=await read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});const r=e?.closest('.sm-card')?.getBoundingClientRect();return r?{x:r.x+r.width/2,y:r.y+r.height/2}:null})()`);if(hover){await cdp('Input.dispatchMouseEvent',{type:'mouseMoved',...hover});await new Promise(resolve=>setTimeout(resolve,100))}const point=await read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e)throw Error('Missing '+${JSON.stringify(selector)});const r=e.getBoundingClientRect();return{x:r.x+r.width/2,y:r.y+r.height/2}})()`);await cdp('Input.dispatchMouseEvent',{type:'mouseMoved',...point});await new Promise(resolve=>setTimeout(resolve,100));for(const type of ['mousePressed','mouseReleased'])await cdp('Input.dispatchMouseEvent',{type,...point,button:'left',clickCount:1});await settle()}
const clickText=async(selector,label)=>{const match=await read(`(()=>{const elements=[...document.querySelectorAll(${JSON.stringify(selector)})];const e=elements.find(e=>e.textContent.trim()===${JSON.stringify(label)});if(!e)throw Error('Missing text '+${JSON.stringify(label)});e.dataset.qaTarget='true';return '[data-qa-target="true"]'})()`);await click(match);await read(`document.querySelector(${JSON.stringify(match)})?.removeAttribute('data-qa-target')`)}
const capture=async name=>fs.writeFileSync(path.join(output,name+'.png'),Buffer.from((await cdp('Page.captureScreenshot',{format:'png',fromSurface:true})).data,'base64'))
const key=async(key,shift=false)=>{for(const type of ['keyDown','keyUp'])await cdp('Input.dispatchKeyEvent',{type,key,code:key,modifiers:shift?8:0,windowsVirtualKeyCode:key==='Escape'?27:key==='Enter'?13:9})}
try {
await wait('!!document.querySelector(".um-tab")');await clickText('.um-tab','技能');await wait('!!document.querySelector(".yc-skills-page")')
check('one shared topbar, no duplicate page header',await read('document.querySelectorAll(".workbuddy-topbar").length'),1)
check('remote market has no invented cards',await read('document.querySelectorAll(".sm-card").length'),0)
await capture('market');await click('[aria-label="添加技能"]');check('source add menu shows all three routes',await read('[...document.querySelectorAll(".yc-skill-add-menu button")].map(e=>e.textContent)'),['查找技能','上传技能','创建技能']);await capture('add-menu')
await clickText('.yc-skill-add-menu button','上传技能');await wait('!!document.querySelector(".skill-upload-modal")');check('import modal has source DOM and accessible label',await read('document.querySelector(".skill-upload-modal").getAttribute("aria-label")'),'导入技能');await capture('import-modal')
check('import modal takes focus',await read('document.activeElement.getAttribute("aria-label")'),'关闭导入技能');await key('Tab',true);check('ShiftTab wraps to last modal control',await read('document.activeElement.matches(".yc-skill-overwrite input")'),true);await key('Tab');check('Tab wraps back to first modal control',await read('document.activeElement.getAttribute("aria-label")'),'关闭导入技能')
await click('[aria-label="选择文件夹"]');await wait('document.querySelectorAll(".sm-card").length===1');check('picker reaches real import with folder kind',imports[0],[undefined,false,'directory'])
check('actual managed package is installed',(await api.listSkills(workspace))[0].name,'Native Local Skill');await capture('installed')
const style=await read(`(()=>{const card=document.querySelector('.sm-card'),s=getComputedStyle(card),r=card.getBoundingClientRect();return{padding:s.padding,radius:s.borderRadius,shadow:s.boxShadow,width:r.width,switchWidth:document.querySelector('.wb-switch__track').getBoundingClientRect().width}})()`)
check('source installed card padding and radius',[style.padding,style.radius],['20px','16px']);check('source medium switch geometry',style.switchWidth,26)
check('success notice does not affect normal flow',await read('getComputedStyle(document.querySelector(".yc-skill-notice")).position'),'absolute');await wait('!document.querySelector(".yc-skill-notice")');check('success notice disappears automatically',await read('!!document.querySelector(".yc-skill-notice")'),false)
await clickText('.sm-installed-batch-manage','批量管理');await click('.sm-card');await clickText('.sm-batch-toolbar button','禁用');check('batch disable persists real state',(await api.listSkills(workspace))[0].enabled,false);await clickText('.sm-batch-toolbar button','启用');check('batch enable persists real state',(await api.listSkills(workspace))[0].enabled,true);await clickText('.sm-installed-batch-manage','完成')
await click('.wb-switch__input');check('native toggle persists actual disabled state',(await api.listSkills(workspace))[0].enabled,false)
await click('.wb-switch__input');await click('.sm-card-name');await wait('document.querySelector(".yc-skill-description-body")?.textContent.includes("NATIVE_SKILL_PROOF")');check('detail renders real SKILL.md markdown',await read('document.querySelector(".yc-skill-description-body").textContent.includes("Actual skill instructions")'),true);await capture('detail')
await click('[aria-label="使用技能"]');check('use callback receives actual enabled identity',await read('window.__skillsQa.uses[0].id'),(await api.listSkills(workspace))[0].id)
await click('.sm-card-name');await wait('!!document.querySelector(".sm-skill-detail-card")');await clickText('.sm-skill-detail-action-row button','导出');check('export writes real ZIP',fs.existsSync(path.join(output,'export.zip')),true)
await click('[aria-label="卸载技能"]');await key('Escape');await settle();check('nested Escape closes only top uninstall dialog',await read('!!document.querySelector(".sm-skill-detail-card")&&!document.querySelector("[aria-label=确认卸载]")'),true)
await click('[aria-label="关闭技能详情"]');await click('[aria-label="搜索技能"]');await cdp('Input.insertText',{text:'NO_MATCH'});await settle();check('real search filters installed list',await read('document.querySelectorAll(".sm-card").length'),0);await click('[aria-label="清空技能搜索"]');await wait('document.querySelectorAll(".sm-card").length===1')
await click('.sm-card-pin');check('pin updates shared slash menu memory',await read(`JSON.parse(localStorage.getItem(${JSON.stringify('ycode.skills.menu.v1:'+workspace.replaceAll('\\','/').toLowerCase())})).pinned.length`),1)
await click('.sm-card-more');await clickText('.sm-card-menu button','卸载技能');await wait('!!document.querySelector("[aria-label=确认卸载]")');await key('Escape');check('Escape cancels uninstall without deleting',(await api.listSkills(workspace)).length,1)
await click('.sm-card-more');await clickText('.sm-card-menu button','卸载技能');await click('[aria-label="确认卸载"]');await wait('document.querySelectorAll(".sm-card").length===0');check('confirmed uninstall removes managed registration',(await api.listSkills(workspace)).length,0);check('uninstall preserves source',fs.existsSync(path.join(source,'SKILL.md')),true)
await click('[aria-label="添加技能"]');await clickText('.yc-skill-add-menu button','上传技能');await click('[aria-label="关闭导入技能"]');check('import close is usable',await read('!!document.querySelector(".skill-upload-modal")'),false)
check('closed import restores focus to add trigger',await read('document.activeElement.getAttribute("aria-label")'),'添加技能')
await click('[aria-label="添加技能"]');await clickText('.yc-skill-add-menu button','查找技能');check('find invokes real host callback',await read('window.__skillsQa.find'),1)
await click('[aria-label="添加技能"]');await clickText('.yc-skill-add-menu button','创建技能');check('create invokes real host callback',await read('window.__skillsQa.created'),1)
await click('[aria-label="添加技能"]');await clickText('.yc-skill-add-menu button','上传技能');await clickText('.skill-upload-modal button','选择 ZIP 文件');await wait('document.querySelectorAll(".sm-card").length===1');check('ZIP picker sends explicit kind',imports.at(-1)[2],'zip');check('ZIP picker imports actual exported package',(await api.listSkills(workspace))[0].name,'Native Local Skill')
await click('[aria-label="添加技能"]');await clickText('.yc-skill-add-menu button','上传技能');await click('[aria-label="选择文件夹"]');await wait('document.querySelector(".skill-upload-modal [role=alert]")?.textContent.includes("同名")');check('duplicate import displays actual backend conflict',await read('!!document.querySelector(".skill-upload-modal [role=alert]")'),true);await click('.yc-skill-overwrite input');await click('[aria-label="选择文件夹"]');await wait('!document.querySelector(".skill-upload-modal")');check('explicit overwrite flag reaches real import',imports.at(-1)[1],true)
const damaged=(await api.listSkills(workspace))[0];fs.writeFileSync(path.join(damaged.path,'SKILL.md'),'invalid metadata')
await clickText('.sm-installed-batch-manage','批量管理');await click('.sm-card');await clickText('.sm-batch-toolbar button','禁用');await clickText('.sm-installed-batch-manage','完成');await wait('document.querySelector(".sm-card-desc")?.textContent.includes("不可用")');check('invalid package is visible without blocking list',await read('document.querySelectorAll(".sm-card").length'),1);check('invalid package toggle disabled',await read('document.querySelector(".wb-switch__input").disabled'),true);await capture('invalid-package');await click('.sm-card-more');await clickText('.sm-card-menu button','卸载技能');await click('[aria-label="确认卸载"]');await wait('document.querySelectorAll(".sm-card").length===0');check('UI can remove invalid registration',(await api.listSkills(workspace)).length,0)
check('no renderer errors',errors,[])
fs.writeFileSync(path.join(output,'report.json'),JSON.stringify({result:'PASS',checks,style,limits:'Controlled native fixture; actual production storage and IPC, controlled picker; not WorkBuddy full-pixel acceptance.'},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),checks,errors,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)})
}