// Real isolated production App/preload/IPC, native focus/keys + controlled Monaco paste event. import fs from 'node:fs/promises' import path from 'node:path' import { spawn } from 'node:child_process' import { createRequire } from 'node:module' import assert from 'node:assert/strict' const directory = await fs.mkdtemp(path.resolve('.tmp/mcp-native-ui-')) await Promise.all(['data', 'sessions', 'home'].map(name => fs.mkdir(path.join(directory, name)))) const bootstrap = path.join(directory, 'bootstrap.cjs') await fs.writeFile(bootstrap, '(' + nativeQa.toString() + ')()') const env = { ...process.env, YCODE_MCP_UI_DIRECTORY: directory, YCODE_QA_NODE: process.execPath, YCODE_USER_DATA_DIR: path.join(directory, 'data'), 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 const code = await new Promise(resolve => { const child = spawn(createRequire(import.meta.url)('electron'), ['--no-sandbox', '--disable-gpu-sandbox', bootstrap], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) const timer = setTimeout(() => child.kill(), 50000) child.stdout.on('data', () => {}); child.stderr.on('data', () => {}) child.on('exit', code => { clearTimeout(timer); resolve(code ?? 1) }) }) assert.equal(code, 0, 'Native UI failed: ' + directory) const report = JSON.parse(await fs.readFile(path.join(directory, 'report.json'), 'utf8')) assert.equal(report.result, 'PASS', 'Native UI report failed: ' + directory) console.log('MCP_NATIVE_UI_PASS ' + report.checks.length + ' ' + directory) function nativeQa() { const { app, BrowserWindow } = require('electron') const fs = require('node:fs/promises'), path = require('node:path'), { pathToFileURL } = require('node:url'), { createHash } = require('node:crypto') const directory = process.env.YCODE_MCP_UI_DIRECTORY app.disableHardwareAcceleration(); app.setPath('userData', path.join(directory, 'data')); app.setPath('sessionData', path.join(directory, 'sessions')); app.setPath('home', path.join(directory, 'home')) const production = import(pathToFileURL(path.resolve('out/main/index.mjs')).href) const checks = [], styles = {} let main, phase = 'boot' const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) const check = (label, actual, expected) => { const pass = JSON.stringify(actual) === JSON.stringify(expected); checks.push({ label, actual, expected, pass }); if (!pass) throw Error(label) } const read = expression => main.webContents.executeJavaScript(expression) async function wait(expression, label) { for (let i = 0; i < 500; i++) { const value = await read(expression); if (value) return value; await pause(30) } throw Error('Timeout: ' + label) } async function click(text, selector = 'button') { await read(`(()=>{const e=[...document.querySelectorAll(${JSON.stringify(selector)})].find(e=>e.textContent.trim()===${JSON.stringify(text)});if(!e||e.disabled)throw Error('Missing enabled button');e.click()})()`); await pause(70) } async function shot(name) { const png = (await main.webContents.capturePage(undefined, { stayHidden: true, stayAwake: true })).toPNG(); await fs.writeFile(path.join(directory, name + '.png'), png) } ;(async () => { await production; await app.whenReady() for (let i = 0; i < 300; i++) { main = BrowserWindow.getAllWindows().find(window => window.getTitle() === 'YCode'); if (main && !main.webContents.isLoading()) break; await pause(30) } main.setContentSize(1336, 901) await wait('!!window.ycode&&!!document.querySelector(".sidebar__tab")', 'actual App') check('real App configDir isolated', path.resolve((await read('window.ycode.app.info()')).configDir), path.join(directory, 'data')) phase = 'open-real-mcp-manager' await click('专家·技能·连接器', '.sidebar__tab'); await click('连接器', '.um-tab'); await click('自定义连接器') await wait('!!document.querySelector(".mcp-modal")', 'source MCP modal') await read('Promise.all([...document.querySelector(".mcp-modal").getAnimations(),...document.querySelector(".mcp-modal-mask").getAnimations()].map(a=>a.finished.catch(()=>undefined))).then(()=>true)') styles.light = await read(`(()=>{const e=document.querySelector('.mcp-modal'),b=e.querySelector('.wb-button'),s=getComputedStyle(e),r=e.getBoundingClientRect();return{portalHasThemeScope:!!e.closest('.expert-center-page'),bounds:[r.x,r.y,r.width,r.height],bg:s.backgroundColor,color:s.color,buttonColor:getComputedStyle(b).color,buttonHeight:b.getBoundingClientRect().height}})()`) check('portal retains source theme scope', styles.light.portalHasThemeScope, true) check('light modal opaque source background', styles.light.bg, 'rgb(255, 255, 255)') check('source modal dimensions', styles.light.bounds.slice(2), [640, 700]) check('source button height', styles.light.buttonHeight, 32) check('source secondary button foreground', styles.light.buttonColor, 'rgba(0, 0, 0, 0.9)') await shot('light-manager') await read('document.documentElement.dataset.theme="dark"'); await pause(120) styles.dark = await read(`(()=>{const e=document.querySelector('.mcp-modal'),b=e.querySelector('.wb-button'),probe=document.createElement('span');probe.style.background='var(--wb-bg-primary)';e.append(probe);const bg=getComputedStyle(probe).backgroundColor;probe.remove();return{bg:getComputedStyle(e).backgroundColor,expectedBg:bg,color:getComputedStyle(e).color,buttonHeight:b.getBoundingClientRect().height}})()`) check('dark portal resolves inherited background token', styles.dark.bg, styles.dark.expectedBg) check('dark background changes with source theme', styles.dark.bg !== styles.light.bg, true) check('dark button retains 32px height', styles.dark.buttonHeight, 32) await shot('dark-manager'); await read('document.documentElement.dataset.theme="light"'); await pause(80) phase = 'native-monaco-paste' await click('添加 MCP') await wait('!!document.querySelector(".mcp-config-editor .monaco-editor textarea")', 'actual editable source Monaco') const input = { mcpServers: { 'Native paste fixture': { command: process.env.YCODE_QA_NODE, args: [path.resolve('scripts/test-mcp-server.mjs'), 'stdio', path.join(directory, 'events.jsonl')] } } } const text = JSON.stringify(input, null, 2) const point = await read(`(()=>{const e=document.querySelector('.mcp-config-editor .monaco-editor .view-lines'),r=e.getBoundingClientRect();return{x:Math.round(r.x+60),y:Math.round(r.y+12)}})()`) main.focus(); main.webContents.focus() main.webContents.sendInputEvent({ type: 'mouseDown', button: 'left', clickCount: 1, ...point }); main.webContents.sendInputEvent({ type: 'mouseUp', button: 'left', clickCount: 1, ...point }); await pause(40) main.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'A', modifiers: ['control'] }); main.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'A', modifiers: ['control'] }); await pause(30) // Exercise Monaco's paste handler, not its typing/IME path or direct model setters. // Controlled DataTransfer leaves the operating-system clipboard untouched. await read(`(()=>{const data=new DataTransfer();data.setData('text/plain',${JSON.stringify(text)});return document.activeElement.dispatchEvent(new ClipboardEvent('paste',{clipboardData:data,bubbles:true,cancelable:true}))})()`) await pause(160) await click('保存') const saved = await wait('window.ycode.mcp.list().then(s=>s.find(x=>x.name==="Native paste fixture"))', 'native paste saved through real preload IPC') check('native pasted JSON preserves full command', saved.command, process.env.YCODE_QA_NODE) check('native pasted JSON preserves args without auto-inserted braces', saved.args, input.mcpServers['Native paste fixture'].args) check('new config never auto-approved', saved.trusted, false) check('new config never auto-enabled', saved.enabled, false) await shot('saved-untrusted') phase = 'complete' const bytes = await fs.readFile('out/main/index.mjs') await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'PASS', phase, checks, styles, buildSha256: createHash('sha256').update(bytes).digest('hex'), scope: 'Real isolated production App/preload/IPC; native source Monaco focus/keys plus controlled renderer ClipboardEvent paste and save. No system clipboard access or model/upstream calls. Navigation buttons use DOM click. No original runtime actions.' }, null, 2)) app.quit() })().catch(async error => { await shot('failure').catch(() => undefined); await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'FAIL', phase, checks, styles, error: String(error) }, null, 2)); process.exitCode = 1; app.quit() }) }