feat: ship skill and MCP workflows with signed updater for v0.0.7

This commit is contained in:
xiaohan.yuan committed 2026-10-21 00:04:17 +08:00
1 parent 1b52df0603
commit a01085c656
136 files changed
+18646 -217

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
// Production app + real GitHub package, isolated from the user's skill registry.
const fs = require('node:fs/promises')
const path = require('node:path')
const assert = require('node:assert/strict')
if (!process.versions.electron) {
;(async () => {
const directory = await fs.mkdtemp(path.resolve('.tmp/recommended-app-'))
await Promise.all(['data', 'home', 'workspace'].map(name => fs.mkdir(path.join(directory, name))))
const now = Date.now()
await fs.writeFile(path.join(directory, 'data', 'sessions.json'), JSON.stringify({ sessions: [{
id: 'skill-existing', title: 'Skill existing draft', cwd: path.join(directory, 'workspace'),
createdAt: now, updatedAt: now, messages: []
}] }))
const env = { ...process.env, YCODE_QA_DIRECTORY: directory,
YCODE_USER_DATA_DIR: path.join(directory, 'data'), HOME: path.join(directory, 'home'),
USERPROFILE: path.join(directory, 'home'), YCODE_QA_OFFSCREEN: '1',
YCODE_DISABLE_GPU: '1', YCODE_DISABLE_SYSTEM_INTEGRATION: '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', value => { output += value; if (String(value).includes('RECOMMENDED_APP_')) process.stdout.write(value) })
child.stderr.on('data', value => { output += value })
const timer = setTimeout(() => child.kill(), 200000)
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 !== 0) console.error(output.slice(-7000))
process.exitCode = code === 0 ? 0 : 1
})
})().catch(error => { console.error(error); process.exitCode = 1 })
} else {
const { app, BrowserWindow } = require('electron')
const { pathToFileURL } = require('node:url')
const directory = path.resolve(process.env.YCODE_QA_DIRECTORY)
const rel = path.relative(path.resolve('.tmp'), directory)
assert.ok(rel.startsWith('recommended-app-') && !rel.includes('..'))
const checks = [], errors = []
let stage = 'boot', win
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
const read = source => win.webContents.executeJavaScript(source)
const check = (label, value) => { assert.ok(value, label); checks.push(label) }
async function click(selector) {
const point = await read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e)throw Error('Missing target');e.scrollIntoView({block:'nearest'});const r=e.getBoundingClientRect();const x=Math.round(r.x+r.width/2),y=Math.round(r.y+r.height/2);const hit=document.elementFromPoint(x,y);if(!e.contains(hit))throw Error('Target is covered: '+${JSON.stringify(selector)});return{x,y}})()`)
win.webContents.sendInputEvent({ type: 'mouseDown', ...point, button: 'left', clickCount: 1 })
win.webContents.sendInputEvent({ type: 'mouseUp', ...point, button: 'left', clickCount: 1 })
await pause(100)
}
async function wait(source, label, timeout = 15000) {
const end = Date.now() + timeout
while (Date.now() < end) { const value = await read(source); if (value) return value; await pause(80) }
throw Error('Timeout: ' + label)
}
async function shot(name) {
win.webContents.invalidate()
await win.webContents.capturePage()
await pause(250)
await fs.writeFile(path.join(directory, name + '.png'), (await win.webContents.capturePage()).toPNG())
}
app.on('browser-window-created', (_event, window) => {
window.setSkipTaskbar(true)
window.on('show', () => window.hide())
window.webContents.on('console-message', (event) => { if (event.level === 'error') errors.push(event.message) })
})
;(async () => {
try {
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href)
await app.whenReady()
for (let index = 0; index < 200; index++) {
win = BrowserWindow.getAllWindows().find(window => window.getTitle() === 'YCode')
if (win && !win.webContents.isLoading()) break
await pause(30)
}
check('production window loaded', !!win)
await wait('!!window.ycode?.skills?.recommendations', 'real preload')
await wait('!!document.querySelector("[data-session-id=skill-existing]")', 'original session')
await click('[data-session-id=skill-existing]')
await wait('!!document.querySelector(".chat-pane .wb-composer textarea")', 'initial composer')
await read('document.querySelector(".chat-pane .wb-composer textarea").focus()')
await win.webContents.insertText('KEEP_ORIGINAL_DRAFT')
const catalog = await read('window.ycode.skills.recommendations()')
check('fixed Archify recommendation is exposed by real IPC', catalog.some(item => item.id === 'archify' && item.commit === '25cf37080fc8b1dbe758a4efc3935b55f3327083'))
await read(`(()=>{window.__installEvents=[];window.ycode.skills.onInstallProgress(event=>window.__installEvents.push(event));window.__skillChanges=0;window.ycode.skills.onChanged(()=>window.__skillChanges++);window.dispatchEvent(new CustomEvent('ycode:open-capabilities',{detail:{tab:'skills'}}));return true})()`)
await wait('!!document.querySelector("[data-recommendation-id=archify]")', 'recommendation card')
await shot('recommendation')
stage = 'install'
// Use the real card and confirmation; no direct import and no picker stub.
await click('[data-recommendation-id=archify] [aria-label="安装 archify"]')
await wait('!!document.querySelector("[aria-label=确认安装推荐技能]")', 'install confirmation')
check('the modal host escapes the isolated capability center', await read('document.querySelector(".yc-skill-modal-host")?.parentElement===document.body'))
check('the background root is inert while the skill modal is open', await read('document.getElementById("root").inert'))
check('sidebar sticky headers are behind the global mask', await read(`(()=>{const e=document.querySelector('.conversation-section-label');if(!e)return false;const r=e.getBoundingClientRect();return !!document.elementFromPoint(r.left+5,r.top+5)?.closest('.yc-skill-modal-host')})()`))
await read('window.__sameDetail=document.querySelector(".yc-skill-detail-shell")')
await shot('confirm-full-window')
await read('document.documentElement.dataset.theme="dark"')
await pause(350); await shot('confirm-dark')
const originalSize = win.getSize()
win.setSize(720, 900); await pause(350)
check('the full-window skill detail fits a narrow viewport', await read('(()=>{const r=document.querySelector(".yc-skill-detail-shell").getBoundingClientRect();return r.left>=0&&r.right<=innerWidth&&r.top>=0&&r.bottom<=innerHeight})()'))
await shot('confirm-narrow')
win.setSize(...originalSize); await read('document.documentElement.dataset.theme="light"'); await pause(300)
await click('[aria-label=确认安装推荐技能]')
await wait(`window.__installEvents.some(event=>['installed','failed','cancelled'].includes(event.phase))`, 'native installation', 150000)
const progress = await read('window.__installEvents')
check('actual download, validation and commit phases are observed', ['downloading', 'validating', 'installing', 'installed'].every(phase => progress.some(event => event.phase === phase)))
const installed = await read('window.ycode.skills.list()')
const skill = installed.find(item => item.name === 'archify')
check('installed skill is enabled, intact and has source provenance', skill?.enabled && !skill.userModified && skill.origin?.commit === catalog[0].commit)
const document = await read(`window.ycode.skills.read(${JSON.stringify(skill.id)})`)
check('skill body and packaged CLI resources are present', document.content.includes('Archify') && document.files.includes('bin/archify.mjs') && document.files.length > 100)
check('committed mutation broadcasts a skill refresh', await read('window.__skillChanges>0'))
await wait(`document.querySelector('[data-recommendation-id=archify]')?.textContent.includes('已安装')`, 'installed card refresh')
check('installation keeps the same detail DOM mounted', await read('window.__sameDetail===document.querySelector(".yc-skill-detail-shell")'))
await wait('document.querySelector("[role=dialog] .sm-skill-detail-action-row")?.textContent.includes("去使用")', 'post-install action')
check('the avatar is a packaged offline image', await read('(()=>{const i=document.querySelector("[role=dialog] .sm-skill-detail-icon-wrap img");return i?.complete&&i.naturalWidth>0&&!i.src.startsWith("http")})()'))
await shot('installed')
stage = 'idempotency'
const again = await read(`window.ycode.skills.installRecommended('archify',{operationId:crypto.randomUUID()})`)
check('same unchanged version returns existing install', again.id === skill.id && again.origin.installedAt === skill.origin.installedAt)
await read(`window.ycode.skills.setEnabled(${JSON.stringify(skill.id)},false)`)
const disabled = await read(`window.ycode.skills.installRecommended('archify',{operationId:crypto.randomUUID()})`)
check('idempotent install does not silently re-enable disabled skill', disabled.enabled === false)
await read(`window.ycode.skills.setEnabled(${JSON.stringify(skill.id)},true)`)
stage = 'use-skill'
await wait(`!!document.querySelector('[role=dialog] [aria-label="使用推荐技能 archify"]')`, 'use installed recommendation')
await click('[role=dialog] [aria-label="使用推荐技能 archify"]')
await wait(`document.querySelector('.chat-pane .wb-composer')?.textContent.includes('archify')`, 'skill reference in the real composer')
check('using the installed recommendation fills the real composer', true)
check('use opens a clean new draft instead of overwriting the old draft', await read('!document.querySelector(".chat-pane .wb-composer").textContent.includes("KEEP_ORIGINAL_DRAFT")'))
check('the background is no longer inert after use', await read('!document.getElementById("root").inert'))
await shot('use-skill')
check('browsing, installation and use do not create persisted chat sessions', (await read('window.ycode.session.list()')).length === 1)
await click('[data-session-id=skill-existing]')
await wait('document.querySelector(".chat-pane .wb-composer textarea")?.value.includes("KEEP_ORIGINAL_DRAFT")', 'original session draft restored')
check('the original session draft remains intact after using the skill', true)
check('no renderer errors', errors.length === 0)
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'PASS', checks, progress, skill, files: document.files,
errors, limits: 'Real fixed GitHub package and production IPC; no skill commands or model requests executed.' }, null, 2))
console.log('RECOMMENDED_APP_PASS ' + directory)
app.quit()
} catch (error) {
await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({ stage, error: String(error.stack ?? error), checks, errors }, null, 2))
if (win) await shot('failure').catch(() => {})
console.error('RECOMMENDED_APP_FAIL ' + String(error))
app.exit(1)
}
})()
}