chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,184 @@
|
||||
// Isolated production renderer/preload/IPC -> actual OMP -> controlled upstream + actual SDK MCP.
|
||||
const fs = require('node:fs/promises'), path = require('node:path'), assert = require('node:assert/strict')
|
||||
const { pathToFileURL } = require('node:url')
|
||||
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
if (!process.versions.electron) {
|
||||
const { spawn } = require('node:child_process')
|
||||
;(async () => {
|
||||
const directory = await fs.mkdtemp(path.resolve('.tmp/capability-flows-'))
|
||||
await Promise.all(['data', 'home', 'source-skill', 'workspace'].map(name => fs.mkdir(path.join(directory, name))))
|
||||
await fs.writeFile(path.join(directory, 'source-skill', 'SKILL.md'), '---\nname: capability-acceptance\ndescription: Controlled capability integration skill\nversion: 1.0\n---\n\nCAPABILITY_SKILL_BODY_MARKER\nUse the configured echo connector when requested.\n')
|
||||
await fs.writeFile(path.join(directory, 'source-skill', 'reference.txt'), 'CAPABILITY_RESOURCE_MARKER')
|
||||
const omp = process.env.YCODE_OMP_PATH || path.join(process.env.LOCALAPPDATA, 'Ygent', 'omp.exe')
|
||||
assert.ok((await fs.stat(omp)).isFile())
|
||||
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_QA_NODE: process.execPath, YCODE_USER_DATA_DIR: path.join(directory, 'data'), YCODE_OMP_HOME: path.join(directory, 'home'), YCODE_OMP_PATH: omp, YCODE_QA_OFFSCREEN: '1', YCODE_DISABLE_GPU: '1' }
|
||||
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL; delete env.YCODE_MODEL_API_KEY
|
||||
for (const stage of ['run', 'restart']) {
|
||||
const code = await new Promise(resolve => {
|
||||
const child = spawn(require('electron'), ['--no-sandbox', '--disable-gpu-sandbox', __filename], { env: { ...env, YCODE_QA_STAGE: stage }, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
let output = ''; child.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('CAPABILITY_')) process.stdout.write(bytes) }); child.stderr.on('data', bytes => { output += bytes })
|
||||
const timer = setTimeout(() => child.kill(), 160000)
|
||||
child.once('exit', async code => { clearTimeout(timer); await fs.writeFile(path.join(directory, stage + '-process.log'), output); resolve(code ?? 1) })
|
||||
})
|
||||
const failedReport = await fs.stat(path.join(directory, stage + '-failure.json')).then(() => true, () => false)
|
||||
const report = await fs.readFile(path.join(directory, 'report.json'), 'utf8').then(JSON.parse, () => null)
|
||||
if (code || failedReport || report?.result !== 'PASS') { console.log(JSON.stringify({ directory, stage, result: 'FAIL' })); process.exitCode = code || 1; return }
|
||||
}
|
||||
console.log(JSON.stringify({ directory, result: 'PASS' }))
|
||||
})().catch(error => { console.error(error); process.exitCode = 1 })
|
||||
} else {
|
||||
const { app, BrowserWindow, dialog } = require('electron'), { createServer } = require('node:http')
|
||||
const directory = process.env.YCODE_QA_DIRECTORY
|
||||
assert.ok(path.relative(path.resolve('.tmp'), directory).startsWith('capability-flows-'))
|
||||
app.disableHardwareAcceleration(); app.setPath('userData', path.join(directory, 'data')); app.setPath('home', path.join(directory, 'home'))
|
||||
const checks = [], requests = [], stage = process.env.YCODE_QA_STAGE
|
||||
let main, upstream, phase = 'boot'
|
||||
const check = (label, actual, expected = true) => { assert.deepEqual(actual, expected, label); checks.push({ label, pass: true }) }
|
||||
async function read(expression) { try { return await main.webContents.executeJavaScript(expression) } catch (error) { throw Error(String(error) + ' :: ' + expression.slice(0, 340)) } }
|
||||
async function wait(expression, label, timeout = 30000) { const start = Date.now(); while (Date.now() - start < timeout) { const value = await read(expression); if (value) return value; await pause(40) } throw Error('Timeout: ' + label) }
|
||||
async function click(selector) { await read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e||e.disabled)throw Error('Missing enabled control '+${JSON.stringify(selector)});e.click()})()`); await pause(60) }
|
||||
async function clickText(selector, text, exact = true) { await read(`(()=>{const text=${JSON.stringify(text)};const e=[...document.querySelectorAll(${JSON.stringify(selector)})].find(e=>${exact ? 'e.textContent.trim()===text' : 'e.textContent.includes(text)'});if(!e||e.disabled)throw Error('Missing enabled text '+text);e.click()})()`); await pause(60) }
|
||||
async function fill(selector, value) { await read(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});if(!e||e.disabled)throw Error('Missing input');const p=e.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;Object.getOwnPropertyDescriptor(p,'value').set.call(e,${JSON.stringify(value)});e.dispatchEvent(new Event('input',{bubbles:true}))})()`); await pause(50) }
|
||||
async function hoverAddMenuItem(id){const selector='.cr-add-menu__popover [data-add-menu-id="'+id+'"]';await wait('!!document.querySelector('+JSON.stringify(selector)+')','source add menu row');const point=await read('(()=>{const b=document.querySelector('+JSON.stringify(selector)+').getBoundingClientRect();return{x:Math.round(b.x+b.width/2),y:Math.round(b.y+b.height/2)}})()');const trigger=await read('(()=>{const b=document.querySelector(".wb-composer__add-button").getBoundingClientRect();return{x:Math.round(b.x+b.width/2),y:Math.round(b.y+b.height/2)}})()');main.webContents.sendInputEvent({type:'mouseMove',...trigger});await pause(25);main.webContents.sendInputEvent({type:'mouseMove',...point});await pause(220)}
|
||||
async function shot(name) { await read('document.fonts.ready.then(()=>true)'); await pause(120); const image = await main.webContents.capturePage(undefined, { stayHidden: true, stayAwake: true }); await fs.writeFile(path.join(directory, name + '.png'), image.toPNG()) }
|
||||
async function step(name) { phase = name; await fs.writeFile(path.join(directory, stage + '-phase.txt'), name) }
|
||||
async function boot() {
|
||||
dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [path.join(directory, 'source-skill')] }) // Only OS picker selection is controlled; production installer and IPC run unchanged.
|
||||
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href); await app.whenReady()
|
||||
for (let i = 0; i < 300; i++) { main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode'); if (main && !main.webContents.isLoading()) break; await pause(30) }
|
||||
assert.ok(main); main.setContentSize(1336, 901)
|
||||
await wait('!!window.ycode && !!document.querySelector(".wb-composer__input")', 'production UI')
|
||||
check('isolated production userData', path.resolve((await read('window.ycode.app.info()')).configDir), path.join(directory, 'data'))
|
||||
check('actual OMP available', (await read('window.ycode.app.info()')).omp.available)
|
||||
}
|
||||
async function openCenter(tab) {
|
||||
await clickText('.sidebar__tab', '专家·技能·连接器')
|
||||
await wait('!!document.querySelector(".yc-capability-center")', 'capability center')
|
||||
if (tab) await clickText('.um-tab', tab)
|
||||
await pause(100)
|
||||
}
|
||||
function completion(response, model, content, toolCall) {
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
||||
const chunk = delta => ({ id: 'capability-completion', object: 'chat.completion.chunk', created: 1700000000, model, choices: [{ index: 0, delta, finish_reason: null }] })
|
||||
response.write('data: ' + JSON.stringify(chunk({ role: 'assistant', ...(toolCall ? { tool_calls: [{ index: 0, id: 'cap-call-' + requests.length, type: 'function', function: { name: toolCall.name, arguments: JSON.stringify(toolCall.args) } }] } : { content }) })) + '\n\n')
|
||||
const final = chunk({}); final.choices[0].finish_reason = toolCall ? 'tool_calls' : 'stop'; response.write('data: ' + JSON.stringify(final) + '\n\ndata: [DONE]\n\n'); response.end()
|
||||
}
|
||||
async function cleanup() { if (upstream) { upstream.closeAllConnections(); await new Promise(resolve => upstream.close(resolve)) } }
|
||||
;(async () => {
|
||||
await boot()
|
||||
if (stage === 'restart') {
|
||||
await step('restart')
|
||||
const previous = JSON.parse(await fs.readFile(path.join(directory, 'report.json'), 'utf8'))
|
||||
const skills = await read('window.ycode.skills.list()'), servers = await wait('window.ycode.mcp.list().then(s=>s.some(x=>x.status==="connected")&&s)', 'startup MCP reconnect')
|
||||
check('skill persists after new Electron process', skills.some(item => item.id === previous.skill.id && item.enabled))
|
||||
check('MCP reconnects and discovers tools after restart', servers.some(item => item.id === previous.server.id && item.status === 'connected' && item.tools.length === 4))
|
||||
const history = await read(`window.ycode.session.messages(${JSON.stringify(previous.sessionId)})`)
|
||||
check('skill resource link persists', history[0].skills[0].uri, 'skill://capability-acceptance')
|
||||
check('real connector result persists', history[1].tools.some(tool => tool.output.includes('stdio:CAPABILITY_ECHO_INPUT')))
|
||||
previous.restartChecks = checks; previous.result = 'PASS'; await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(previous, null, 2))
|
||||
console.log('CAPABILITY_RESTART_PASS ' + directory); app.quit(); return
|
||||
}
|
||||
upstream = createServer(async (request, response) => {
|
||||
try {
|
||||
let raw = ''; for await (const bytes of request) raw += bytes
|
||||
const body = JSON.parse(raw), text = JSON.stringify(body.messages), names = (body.tools ?? []).map(tool => tool.function.name)
|
||||
requests.push({ stream: body.stream, selectedSkillBody: text.includes('CAPABILITY_SKILL_BODY_MARKER'), toolNames: names, hasEchoResult: text.includes('stdio:CAPABILITY_ECHO_INPUT'), hasResourceResult: text.includes('CAPABILITY_RESOURCE_MARKER') })
|
||||
if (!body.stream) { response.writeHead(200, { 'content-type': 'application/json' }); response.end(JSON.stringify({ id: 'test', object: 'chat.completion', choices: [{ index: 0, message: { role: 'assistant', content: 'OK' }, finish_reason: 'stop' }] })); return }
|
||||
const lastUser = body.messages.findLast(message => message.role === 'user')?.content ?? ''
|
||||
if (JSON.stringify(lastUser).includes('CAPABILITY_AUTO_LOAD')) {
|
||||
const hasLoadResult = body.messages.some(message => message.role === 'tool' && String(message.content).includes('CAPABILITY_SKILL_BODY_MARKER'))
|
||||
if (!hasLoadResult) { const match = text.match(/user:[a-f0-9]{24}/); completion(response, body.model, '', { name: 'ycode_load_skill', args: { id: match?.[0] } }); return }
|
||||
completion(response, body.model, 'CAPABILITY_AUTO_OK'); return
|
||||
}
|
||||
const hasEchoResult = body.messages.some(message => message.role === 'tool' && String(message.content).includes('stdio:CAPABILITY_ECHO_INPUT'))
|
||||
if (!hasEchoResult) { const name = names.find(name => name.startsWith('mcp_echo_')); assert.ok(name); completion(response, body.model, '', { name, args: { text: 'CAPABILITY_ECHO_INPUT' } }); return }
|
||||
completion(response, body.model, 'CAPABILITY_OK')
|
||||
} catch (error) { response.writeHead(500); response.end('Controlled upstream failure'); requests.push({ error: String(error) }) }
|
||||
})
|
||||
await new Promise(resolve => upstream.listen(0, '127.0.0.1', resolve))
|
||||
const profile = { id: 'capability-model', name: 'Capability Fixture', providerId: 'custom', model: 'capability-fixture', baseUrl: `http://127.0.0.1:${upstream.address().port}/v1`, apiKey: '', authMode: 'none', supportsToolCall: true, supportsImages: false, maxInputTokens: 32000, maxOutputTokens: 1000 }
|
||||
await read(`window.ycode.models.save(${JSON.stringify(profile)})`); await read('window.ycode.models.select("capability-model")')
|
||||
const space = await read(`window.ycode.workspace.attach(${JSON.stringify(path.join(directory, 'workspace'))}).then(v=>v.workspaces.find(x=>x.id===v.attachedWorkspaceId))`)
|
||||
check('space registered', !!space)
|
||||
main.webContents.reload(); await pause(500); await wait('!!document.querySelector(".wb-composer__input")', 'reloaded controlled models')
|
||||
await fill('.wb-composer__input', 'DRAFT_SURVIVES_CAPABILITY_CENTER')
|
||||
await step('center-draft-and-source-layout'); await openCenter()
|
||||
await shot('experts-market')
|
||||
const bounds = await read(`(()=>{const r=document.querySelector('.yc-capability-center').getBoundingClientRect(),h=document.querySelector('.yc-capability-center header').getBoundingClientRect();return{page:[r.x,r.y,r.width,r.height],header:[h.x,h.y,h.width,h.height]}})()`)
|
||||
await fs.writeFile(path.join(directory, 'layout.json'), JSON.stringify(bounds, null, 2))
|
||||
check('source topbar height', bounds.header[3], 64)
|
||||
await clickText('button', '我的专家'); await shot('my-experts-empty')
|
||||
await clickText('.sidebar__tab', '新建任务')
|
||||
check('draft survives source center roundtrip', await read('document.querySelector(".wb-composer__input").value'), 'DRAFT_SURVIVES_CAPABILITY_CENTER')
|
||||
await openCenter('技能')
|
||||
await shot('skills-market')
|
||||
await step('real-skill-install')
|
||||
// The UI exposes source add -> upload -> choose folder. Use stable accessible labels.
|
||||
await click('[aria-label="添加技能"]'); await clickText('button,[role="menuitem"]', '上传技能')
|
||||
await click('[aria-label="选择文件夹"]')
|
||||
await wait('window.ycode.skills.list().then(s=>s.length===1)', 'real folder installer')
|
||||
const skill = (await read('window.ycode.skills.list()'))[0]
|
||||
check('source folder retained', await fs.readFile(path.join(directory, 'source-skill', 'reference.txt'), 'utf8'), 'CAPABILITY_RESOURCE_MARKER')
|
||||
if (await read('!!document.querySelector(".sm-import-modal")')) await click('[aria-label="关闭导入技能"]')
|
||||
await wait(`!document.querySelector('[aria-label="导入技能"]')`, 'import dialog closes after actual install')
|
||||
if (await read(`!!document.querySelector('[aria-label="已安装技能"]')`)) await click('[aria-label="已安装技能"]')
|
||||
await shot('skills-installed')
|
||||
await step('real-mcp-configuration')
|
||||
if (!(await read('!!document.querySelector(".um-tab")'))) await click('[aria-label="返回专家中心"]')
|
||||
await clickText('.um-tab', '连接器'); await clickText('button', '自定义连接器')
|
||||
await shot('mcp-empty')
|
||||
await clickText('button', '添加 MCP', false)
|
||||
const mcpConfig = { mcpServers: { 'capability-echo': { command: process.env.YCODE_QA_NODE, args: [path.resolve('scripts/test-mcp-server.mjs'), 'stdio', path.join(directory, 'mcp-events.jsonl')] } } }
|
||||
await wait('!!document.querySelector(".mcp-config-textarea,.mcp-config-editor-monaco-wrapper .monaco-editor textarea")', 'real JSON editor')
|
||||
if (await read('!!document.querySelector(".mcp-config-textarea")')) await fill('.mcp-config-textarea', JSON.stringify(mcpConfig, null, 2))
|
||||
else {
|
||||
await pause(500)
|
||||
main.webContents.focus()
|
||||
const point = await read(`(()=>{const r=document.querySelector('.mcp-config-editor-monaco-wrapper .view-lines').getBoundingClientRect();return{x:Math.round(r.left+80),y:Math.round(r.top+10)}})()`)
|
||||
main.webContents.sendInputEvent({ type: 'mouseDown', ...point, button: 'left', clickCount: 1 }); main.webContents.sendInputEvent({ type: 'mouseUp', ...point, button: 'left', clickCount: 1 }); await pause(60)
|
||||
main.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A', modifiers: ['control'] }); main.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'A', modifiers: ['control'] })
|
||||
await read(`(()=>{const transfer=new DataTransfer();transfer.setData('text/plain',${JSON.stringify(JSON.stringify(mcpConfig, null, 2))});document.activeElement.dispatchEvent(new ClipboardEvent('paste',{clipboardData:transfer,bubbles:true,cancelable:true}));return true})()`)
|
||||
await pause(100)
|
||||
}
|
||||
await clickText('button', '保存')
|
||||
await wait('window.ycode.mcp.list().then(s=>s.length===1)', 'real MCP registration')
|
||||
// Trust/enable through the real confirmation UI, following source configuration hash semantics.
|
||||
const server = (await read('window.ycode.mcp.list()'))[0]
|
||||
if (!server.trusted) { await clickText('button', '批准配置', false) }
|
||||
await wait('window.ycode.mcp.list().then(s=>s[0]?.trusted)', 'config trust')
|
||||
if (!(await read('window.ycode.mcp.list().then(s=>s[0].enabled)'))) await click('[role="switch"][aria-label*="capability-echo"]')
|
||||
await wait('window.ycode.mcp.list().then(s=>s[0]?.status==="connected"&&s[0].tools.length===4)', 'actual SDK handshake and two tool pages')
|
||||
await shot('mcp-connected')
|
||||
await click('.mcp-modal-close')
|
||||
await clickText('.sidebar__tab', '新建任务')
|
||||
await fill('.wb-composer__input', 'CAPABILITY_MANUAL_MCP')
|
||||
await click('.wb-composer__add-button'); await hoverAddMenuItem('skills')
|
||||
await wait('!!document.querySelector("[data-skill-option]")', 'skill menu from actual registry')
|
||||
await shot('skill-picker'); await click('[data-skill-option]')
|
||||
check('structured skill chip added', await read(`!!document.querySelector('[data-skill-uri="skill://capability-acceptance"]')`))
|
||||
await step('actual-omp-skill-mcp-turn')
|
||||
await click('.cr-send-button:not(.cr-send-button--stop)')
|
||||
await wait('[...document.querySelectorAll("button")].some(e=>e.textContent.includes("允许一次"))', 'MCP runtime approval')
|
||||
await shot('mcp-tool-approval'); await clickText('button', '允许一次', false)
|
||||
const session = await wait('window.ycode.session.list().then(s=>s[0])', 'persisted session')
|
||||
const assistant = await wait(`window.ycode.session.messages(${JSON.stringify(session.id)}).then(m=>m[1]?.status!=="streaming"&&m[1])`, 'terminal actual OMP reply')
|
||||
check('actual OMP capability reply', assistant.content, 'CAPABILITY_OK'); check('actual OMP terminal', assistant.status, 'done')
|
||||
check('MCP actual tool result', assistant.tools.some(tool => tool.output.includes('stdio:CAPABILITY_ECHO_INPUT')))
|
||||
check('actual MCP identity retained in persisted tool metadata', assistant.tools.some(tool => tool.metadata?.serverName === 'capability-echo' && tool.metadata?.toolName === 'echo'))
|
||||
await wait(`!!document.querySelector('[data-tool-name="mcp_call_tool"]')`, 'source MCP dedicated renderer')
|
||||
check('selected skill body reached real upstream', requests.some(request => request.selectedSkillBody))
|
||||
await shot('capability-chat-result')
|
||||
await step('automatic-skill-loading')
|
||||
await fill('.wb-composer__input', 'CAPABILITY_AUTO_LOAD'); await click('.cr-send-button:not(.cr-send-button--stop)')
|
||||
const automatic = await wait(`window.ycode.session.messages(${JSON.stringify(session.id)}).then(m=>m.length===4&&m[3].status!=="streaming"&&m[3])`, 'automatic skill load')
|
||||
check('automatic skill actually loaded by tool', automatic.tools.some(tool => tool.name === 'ycode_load_skill' && tool.output.includes('CAPABILITY_SKILL_BODY_MARKER')))
|
||||
check('automatic skill turn done', automatic.content, 'CAPABILITY_AUTO_OK')
|
||||
await step('complete')
|
||||
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'PASS', checks, requests, skill, server, sessionId: session.id, bounds, limitations: ['OS file-picker selection is controlled; actual production UI/IPC/install/runtime run unchanged.', 'Original cloud catalogs, expert packages and whole-state pixel equivalence are not asserted.'] }, null, 2))
|
||||
console.log('CAPABILITY_FLOWS_PASS ' + directory); await cleanup(); app.quit()
|
||||
})().catch(async error => {
|
||||
await fs.writeFile(path.join(directory, stage + '-failure.json'), JSON.stringify({ result: 'FAIL', phase, error: String(error), stack: error.stack, checks, requests }, null, 2)).catch(() => {})
|
||||
if (main && !main.isDestroyed()) await shot(stage + '-failure').catch(() => {})
|
||||
console.log('CAPABILITY_FLOWS_FAIL ' + phase + ' ' + String(error)); await cleanup().catch(() => {}); process.exitCode = 1; app.quit()
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user