/** Real React hook in isolated Electron; all MCP calls are controlled, with no network/user registry. */ 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' import { createHash } from 'node:crypto' const require = createRequire(import.meta.url), electronModule = require('electron') const electron = typeof electronModule === 'object' && electronModule.app ? electronModule : null const root = path.resolve('.'), base = path.join(root, '.tmp') if (!electron) { fs.mkdirSync(base, { recursive: true }) const output = fs.mkdtempSync(path.join(base, 'composer-connectors-')), env = { ...process.env } delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL const child = spawn(electronModule, ['--no-sandbox', '--disable-gpu-sandbox', path.resolve('scripts/test-composer-connectors.mjs'), output], { env, windowsHide: true, stdio: 'inherit' }) const timer = setTimeout(() => child.kill(), 60000) process.exitCode = await new Promise(resolve => child.once('exit', code => { clearTimeout(timer); resolve(code ?? 1) })) } else void (async () => { const { app, BrowserWindow } = electron, output = path.resolve(process.argv.at(-1)) assert.ok(path.relative(base, output).startsWith('composer-connectors-') && !path.relative(base, output).includes('..')) fs.mkdirSync(path.join(output, 'data')); app.setPath('userData', path.join(output, 'data')); app.disableHardwareAcceleration(); await app.whenReady() const sourceFile = path.join(root, 'src/renderer/src/capabilities/useComposerConnectors.ts') const sourceSha256 = createHash('sha256').update(fs.readFileSync(sourceFile)).digest('hex') fs.writeFileSync(path.join(output, 'fixture.tsx'), ` import React,{useState}from'react';import{createRoot}from'react-dom/client';import{useComposerConnectors}from ${JSON.stringify(sourceFile.replaceAll('\\', '/'))}; const w=window as any,clone=value=>JSON.parse(JSON.stringify(value)); const server=(id,enabled=false,trusted=true,status='disconnected')=>({id,name:id,transport:'stdio',command:'CONTROLLED',enabled,trusted,status,configHash:'QA',tools:[],envKeys:[],headerKeys:[]}); const q=w.__qa={calls:[],plans:{},pending:[],results:{},promises:{},history:[],servers:[server('a'),server('b',true),server('untrusted',false,false)],server}; q.plan=(method,type='defer',error='CONTROLLED_FAILURE')=>(q.plans[method]??=[]).push({type,error}); async function host(method,args){q.calls.push([method,...args]);const plan=q.plans[method]?.shift();if(plan?.type==='reject')throw Error(plan.error);if(plan?.type==='defer')await new Promise((resolve,reject)=>q.pending.push({method,resolve,reject}));if(method==='list')return clone(q.servers);const item=q.servers.find(item=>item.id===args[0]);if(!item)throw Error('MCP 服务已移除');if(method==='setEnabled'){if(args[1]&&!item.trusted)throw Error('请先批准当前 MCP 配置');item.enabled=args[1];item.status=args[1]?'connected':'disconnected'}if(method==='connect'){if(!item.trusted)throw Error('请先批准当前 MCP 配置');item.status='connected'}if(method==='disconnect')item.status='disconnected';if(plan?.type==='statusError'){item.status='error';item.error=plan.error}else delete item.error;return clone(item)} w.ycode={mcp:Object.fromEntries(['list','connect','disconnect','setEnabled','trust'].map(method=>[method,(...args)=>host(method,args)]))}; q.resolve=(index=0)=>q.pending.splice(index,1)[0].resolve();q.reject=(text,index=0)=>q.pending.splice(index,1)[0].reject(Error(text)); function Fixture(){const[owner,setOwner]=useState('A');q.setOwner=setOwner;const hook=useComposerConnectors(owner);q.hook=hook;q.view=clone({owner,items:hook.items,loading:hook.loading,error:hook.error,pendingId:hook.pendingId});q.history.push(q.view);return
{JSON.stringify(q.view)}
} const reactRoot=createRoot(document.getElementById('root')!);q.mount=()=>reactRoot.render();q.unmount=()=>reactRoot.render(null);q.invoke=(method,args,key)=>{const promise=q.hook[method](...args);q.promises[key]=promise;promise.then(()=>q.results[key]='ok',error=>q.results[key]=error.message);return true};q.mount(); `) await require('esbuild').build({ entryPoints: [path.join(output, 'fixture.tsx')], bundle: true, platform: 'browser', format: 'iife', jsx: 'automatic', outfile: path.join(output, 'fixture.js'), logLevel: 'warning' }) fs.writeFileSync(path.join(output, 'index.html'), '
') const win = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true, nodeIntegration: false, backgroundThrottling: false } }), checks = [], errors = [] win.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) errors.push(event.message) }) const read = code => win.webContents.executeJavaScript(code), pause = ms => new Promise(resolve => setTimeout(resolve, ms)) const check = (label, actual, expected = true) => { assert.deepEqual(actual, expected, label); checks.push({ label, pass: true }) } const wait = async code => { for (let i = 0; i < 160; i++) { if (await read(code)) return; await pause(15) } throw Error('Timeout: ' + code) } const invoke = async (method, args, key) => { await read(`__qa.invoke(${JSON.stringify(method)},${JSON.stringify(args)},${JSON.stringify(key)})`); await wait(`!!__qa.results[${JSON.stringify(key)}]`) } try { await win.loadFile(path.join(output, 'index.html')); await wait('!!window.__qa?.view&&!__qa.view.loading') check('initial load lists only and does not auto-connect', await read('__qa.calls'), [['list']]) check('actual disabled registry state is retained', await read('__qa.view.items[0].enabled'), false) await invoke('change', ['a'], 'enable'); check('undefined enables disabled connector via real setEnabled', await read('__qa.calls.slice(1)'), [['list'], ['setEnabled', 'a', true], ['list']]) check('enabled result comes from final list', await read('[__qa.view.items[0].enabled,__qa.view.items[0].status,__qa.view.pendingId??null,__qa.view.loading]'), [true, 'connected', null, false]) await invoke('change', ['a'], 'already'); check('already connected undefined action does not reconnect', await read('__qa.calls.slice(-2)'), [['list'], ['list']]) await invoke('change', ['b'], 'connect'); check('enabled disconnected connector calls connect', await read('__qa.calls.slice(-3)'), [['list'], ['connect', 'b'], ['list']]) await invoke('change', ['a', false], 'off'); check('explicit disable calls real setEnabled false', await read('__qa.calls.slice(-2)'), [['setEnabled', 'a', false], ['list']]); check('disabled view reflects host', await read('__qa.view.items[0].status'), 'disconnected') await invoke('change', ['a', true], 'on'); check('explicit enable calls setEnabled once without duplicate connect', await read('__qa.calls.slice(-2)'), [['setEnabled', 'a', true], ['list']]) await invoke('change', ['untrusted'], 'trust'); check('untrusted configuration returns actual approval error', await read('[__qa.results.trust,__qa.view.error]'), ['请先批准当前 MCP 配置', '请先批准当前 MCP 配置']); check('no implicit trust or enabling failed server', await read('__qa.calls.every(call=>call[0]!=="trust")&&!__qa.view.items.find(s=>s.id==="untrusted").enabled')) await read('__qa.servers[1].status="disconnected";__qa.plan("connect","statusError","CONTROLLED_STATUS_ERROR");true'); await invoke('change', ['b'], 'status'); check('resolved status:error is reported rather than fake success', await read('[__qa.results.status,__qa.view.error,__qa.view.items[1].status]'), ['CONTROLLED_STATUS_ERROR', 'CONTROLLED_STATUS_ERROR', 'error']) await read('__qa.plan("connect","reject","CONTROLLED_CONNECT_REJECTION");true'); await invoke('change', ['b'], 'reject'); check('rejection remains visible after successful final refresh', await read('__qa.view.error'), 'CONTROLLED_CONNECT_REJECTION'); check('failed operation still refreshes actual state', await read('__qa.calls.at(-1)'), ['list']) await read('__qa.plan("list","reject","CONTROLLED_LIST_ERROR");__qa.hook.refresh();true'); await wait('!__qa.view.loading&&__qa.view.error==="CONTROLLED_LIST_ERROR"'); check('list failure clears loading without attempting connect', await read('__qa.calls.at(-1)'), ['list']) await read('__qa.plan("connect");__qa.invoke("change",["b"],"slow");true'); await wait('__qa.pending.some(p=>p.method==="connect")'); check('inflight pending id is exposed', await read('__qa.view.pendingId'), 'b') await read('__qa.invoke("change",["b"],"duplicate");true'); check('identical duplicate reuses promise', await read('__qa.promises.slow===__qa.promises.duplicate')); check('only one connect request while pending', await read('__qa.pending.filter(p=>p.method==="connect").length'), 1) await invoke('change', ['b', false], 'conflict'); check('conflicting change rejected without config mutation', await read('__qa.results.conflict'), '连接器正在操作,请稍后重试') await read('__qa.invoke("cancel",["b"],"cancel");true'); await wait('__qa.calls.at(-1)[0]==="disconnect"'); check('cancel immediately invokes real disconnect during connect', await read('__qa.servers[1].status'), 'disconnected') await read('__qa.invoke("cancel",["b"],"cancelDuplicate");true'); check('duplicate cancellation reuses promise', await read('__qa.promises.cancel===__qa.promises.cancelDuplicate')) await read('__qa.resolve();true'); await wait('__qa.results.cancel==="ok"'); check('cancel disconnects again after pending connect settles', await read('__qa.calls.slice(-3)'), [['disconnect', 'b'], ['disconnect', 'b'], ['list']]); check('final cancel view reflects disconnected host', await read('[__qa.view.items[1].status,__qa.view.pendingId??null,__qa.view.error]'), ['disconnected', null, '']) await read('__qa.plan("disconnect","reject","CONTROLLED_DISCONNECT_ERROR");true'); await invoke('cancel', ['b'], 'cancelFail'); check('disconnect failure is reported and actual state refreshed', await read('[__qa.results.cancelFail,__qa.view.error,__qa.calls.at(-1)]'), ['CONTROLLED_DISCONNECT_ERROR', 'CONTROLLED_DISCONNECT_ERROR', ['list']]) await read('__qa.servers[0].enabled=false;__qa.servers[0].status="disconnected";__qa.plan("list");__qa.invoke("change",["a"],"preflight");true'); await wait('__qa.pending.length===1'); await read('__qa.invoke("cancel",["a"],"preflightCancel");true'); await wait('__qa.calls.at(-1)[0]==="disconnect"'); await read('__qa.resolve();true'); await wait('__qa.results.preflightCancel==="ok"'); check('cancel during disabled-enable preflight waits then disconnects created transport', await read('__qa.calls.slice(-3)'), [['setEnabled', 'a', true], ['disconnect', 'a'], ['list']]); check('preflight cancellation retains real global enabled but disconnected state', await read('[__qa.view.items[0].enabled,__qa.view.items[0].status]'), [true, 'disconnected']) await read('__qa.plan("list");__qa.plan("list");__qa.hook.refresh();__qa.hook.refresh();true'); await wait('__qa.pending.length===2'); await read('__qa.servers[0].name="newer";__qa.resolve(1);true'); await wait('!__qa.view.loading&&__qa.view.items[0].name==="newer"'); await read('__qa.servers[0].name="stale";__qa.resolve(0);true'); await pause(40); check('older list response cannot replace newer refresh', await read('__qa.view.items[0].name'), 'newer') await read('__qa.plan("connect");__qa.invoke("change",["b"],"oldOwner");true'); await wait('__qa.pending.length===1'); await read('__qa.plan("list");__qa.setOwner("B");true'); await wait('__qa.view.owner==="B"&&__qa.pending.length===2'); check('owner change immediately clears list/error/pending and loads independently', await read('[__qa.view.items,__qa.view.error,__qa.view.pendingId??null,__qa.view.loading]'), [[], '', null, true]) await read('__qa.reject("STALE_OWNER_ACTION",0);true'); await wait('__qa.results.oldOwner==="STALE_OWNER_ACTION"'); check('old-owner action failure cannot poison new owner UI', await read('[__qa.view.items,__qa.view.error,__qa.view.pendingId??null,__qa.view.loading]'), [[], '', null, true]); await read('__qa.resolve();true'); await wait('!__qa.view.loading'); check('global registry is read normally for new owner', await read('__qa.view.items.length'), 3) await read('__qa.plan("list");__qa.hook.refresh();true'); await wait('__qa.pending.length===1'); await read('__qa.plan("list");__qa.setOwner("A");true'); await wait('__qa.pending.length===2&&__qa.view.owner==="A"'); await read('__qa.servers[0].name="A-fresh";__qa.resolve(1);true'); await wait('!__qa.view.loading'); await read('__qa.servers[0].name="B-stale";__qa.resolve();true'); await pause(40); check('A-B-A scopes use identity rather than only owner string', await read('__qa.view.items[0].name'), 'A-fresh') await read('__qa.oldHook=__qa.hook;__qa.plan("list");__qa.hook.refresh();true'); await wait('__qa.pending.length===1'); await read('__qa.setOwner("B");true'); await wait('__qa.view.owner==="B"&&!__qa.view.loading'); await read('__qa.plan("list");__qa.setOwner("A");true'); await wait('__qa.view.owner==="A"&&__qa.pending.length===2'); await read('__qa.servers[0].name="old-A-result";__qa.resolve(0);true'); await pause(30); check('returning to same owner still ignores its previous incarnation list', await read('[__qa.view.items,__qa.view.loading,__qa.view.error]'), [[], true, '']); await read('__qa.servers[0].name="new-A-result";__qa.resolve();true'); await wait('!__qa.view.loading'); check('new same-owner scope accepts only its own list', await read('__qa.view.items[0].name'), 'new-A-result'); await read('__qa.oldHook.change("a",false).then(()=>__qa.staleCallback="unexpected",e=>__qa.staleCallback=e.message);true'); await wait('!!__qa.staleCallback'); check('stale same-owner callback cannot mutate registry or new UI', await read('[__qa.staleCallback,__qa.servers[0].enabled,__qa.view.error]'), ['连接器菜单已切换,请重试', true, '']) await read('__qa.plan("list");__qa.hook.refresh();true'); await wait('__qa.pending.length===1'); await read('__qa.unmount();true'); await pause(30); const history = await read('__qa.history.length'); await read('__qa.reject("UNMOUNTED_ERROR");true'); await pause(40); check('unmount prevents late list state updates', await read('__qa.history.length'), history) await read('__qa.mount();true'); await wait('!__qa.view.loading'); check('remount starts clean without previous errors', await read('__qa.view.error'), '') await read('__qa.originalList=window.ycode.mcp.list;window.ycode.mcp.list=()=>{throw Error("SYNC_HOST_ERROR")};__qa.hook.refresh();true'); await wait('!__qa.view.loading&&__qa.view.error==="SYNC_HOST_ERROR"'); check('synchronous host failure handled by refresh', await read('__qa.view.error'), 'SYNC_HOST_ERROR'); await read('window.ycode.mcp.list=__qa.originalList;true') await read('__qa.servers[1].status="disconnected";__qa.plan("connect");__qa.invoke("change",["b"],"unmountAction");true'); await wait('__qa.pending.length===1'); await read('__qa.unmount();true'); await pause(30); const before = await read('__qa.history.length'); await read('__qa.resolve();true'); await wait('__qa.results.unmountAction==="ok"'); check('unmount prevents late action/final refresh UI updates', await read('__qa.history.length'), before) check('no implicit approval throughout tests', await read('__qa.calls.every(call=>call[0]!=="trust")')); check('no renderer exceptions', errors, []) check('production hook source unchanged during QA', createHash('sha256').update(fs.readFileSync(sourceFile)).digest('hex'), sourceSha256) fs.writeFileSync(path.join(output, 'report.json'), JSON.stringify({ result: 'PASS', checks, errors, sourceSha256, limitations: 'Actual React/Electron hook with controlled MCP API. No user MCP connection, original UI, or production IPC/network exercised.' }, null, 2)) console.log(JSON.stringify({ result: 'PASS', checks: checks.length, output })); win.destroy(); app.exit(0) } catch (error) { fs.writeFileSync(path.join(output, 'report.json'), JSON.stringify({ result: 'FAIL', error: error.stack, checks, errors, state: await read('JSON.stringify({view:__qa.view,calls:__qa.calls,results:__qa.results,pending:__qa.pending.map(p=>p.method)})') }, null, 2)) console.error(JSON.stringify({ result: 'FAIL', error: String(error), output })); win.destroy(); app.exit(1) } })().catch(error => { console.error(error); electron.app.exit(1) })