// Real hidden Electron Chromium + host tools + HTTP gateway and installed OMP, all against loopback fixtures. const fs = require('node:fs/promises') const path = require('node:path') if (!process.versions.electron) { ;(async () => { const directory = await fs.mkdtemp(path.resolve('.tmp/browser-automation-')) const modulePath = path.join(directory, 'host.cjs') require('esbuild').buildSync({ stdin: { resolveDir: process.cwd(), contents: [ "export * from './src/main/browser-automation';", "export * from './src/main/agent/tools';", "export * from './src/main/agent/tools/browser';", "export * from './src/main/agent/conversation-tools';", "export * from './src/main/agent/loop';", "export * from './src/main/agent/omp-turn';", "export * from './src/main/agent/history';", "export * from './src/shared/message-parts';", "export * from './src/main/gateway';", "export * from './src/main/gateway/responses';", "export * from './src/main/gateway/native-protocols';", "export * from './src/main/omp';", "export * from './src/main/omp-model';" ].join('\n') }, bundle: true, platform: 'node', format: 'cjs', external: ['electron'], outfile: modulePath, logLevel: 'warning' }) const env = { ...process.env, YCODE_BROWSER_TEST_DIRECTORY: directory, YCODE_OMP_HOME: path.join(directory, 'omp-home'), YCODE_USER_DATA_DIR: path.join(directory, 'data'), YCODE_DISABLE_SYSTEM_INTEGRATION: '1', YCODE_DISABLE_GPU: '1' } delete env.ELECTRON_RUN_AS_NODE 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', bytes => { output += bytes; process.stdout.write(bytes) }) child.stderr.on('data', bytes => { output += bytes }) const timer = setTimeout(() => child.kill(), 180000) child.once('exit', async code => { clearTimeout(timer); await fs.writeFile(path.join(directory, 'process.log'), output) const report = JSON.parse(await fs.readFile(path.join(directory, 'report.json'), 'utf8').catch(() => '{"result":"MISSING"}')) if (code || report.result !== 'PASS') console.error(output.slice(-6000)) console.log(JSON.stringify({ directory, exitCode: code, result: report.result })); process.exitCode = code || report.result !== 'PASS' ? 1 : 0 }) })().catch(error => { console.error(error); process.exitCode = 1 }) } else { const { app, BrowserWindow } = require('electron') const assert = require('node:assert/strict') const http = require('node:http') const directory = process.env.YCODE_BROWSER_TEST_DIRECTORY app.setPath('userData', path.join(directory, 'data')); app.disableHardwareAcceleration() app.on('window-all-closed', () => {}) const servers = [], checks = [], apiRequests = [] let host, kernel, phase = 'boot' const pause = ms => new Promise(resolve => setTimeout(resolve, ms)) function check(label, condition) { assert.ok(condition, label); checks.push(label); console.log('BROWSER_CHECK ' + label) } async function listen(handler) { const server = http.createServer(handler); servers.push(server) await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) return { server, base: 'http://127.0.0.1:' + server.address().port } } function page(next = false) { return `${next ? 'Next fixture' : 'Browser fixture'}

${next ? 'NEXT_PAGE' : 'BROWSER_FIXTURE'}

Waiting
` } const ctx = (sessionId = 'browser-a', extra = {}) => ({ workspaceRoot: directory, sessionId, runId: 'test-' + sessionId, toolCallId: 'browser-test', permissionMode: 'full-access', signal: new AbortController().signal, ...extra }) async function call(args, context = ctx()) { return host.executeTool('browser', args, context) } async function ok(args, context = ctx()) { const result = await call(args, context); assert.equal(result.ok, true, result.content); return result } async function rejected(args, pattern, context = ctx()) { const result = await call(args, context); assert.equal(result.ok, false); assert.match(result.content, pattern); return result } function sse(res, call, final) { res.writeHead(200, { 'content-type': 'text/event-stream' }) const delta = call ? { role: 'assistant', tool_calls: [{ index: 0, id: call.id, type: 'function', function: { name: 'browser', arguments: JSON.stringify(call.args) } }] } : { role: 'assistant', content: final } res.end([{ id: 'controlled-browser', object: 'chat.completion.chunk', model: 'browser-controlled', choices: [{ index: 0, delta, finish_reason: null }] }, { id: 'controlled-browser', object: 'chat.completion.chunk', model: 'browser-controlled', choices: [{ index: 0, delta: {}, finish_reason: call ? 'tool_calls' : 'stop' }], usage: { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120 } }] .map(row => 'data: ' + JSON.stringify(row) + '\n\n').join('') + 'data: [DONE]\n\n') } async function conversation(viaOmp, fixture, production = false, supportsImages = true) { const mode = production ? 'APP' : (viaOmp ? 'OMP' : 'HTTP') + (supportsImages ? '' : '_TEXT_ONLY') let stage = 0, observedImage = false, observedApplied = false const expected = mode + '_BROWSER_VALUE' const api = await listen(async (req, res) => { try { let source = ''; for await (const chunk of req) source += chunk const body = JSON.parse(source || '{}'); apiRequests.push({ mode, path: req.url, body }) if (req.url !== '/v1/chat/completions') { res.writeHead(404); res.end('{}'); return } assert.ok(body.tools.some(tool => tool.function.name === 'browser'), 'model actually receives browser tool') const serialized = JSON.stringify(body.messages) observedApplied ||= serialized.includes('APPLIED:' + expected + ':trusted=true') observedImage ||= serialized.includes('data:image/jpeg;base64,') const actions = [{ action: 'open', url: fixture + '/page' }, { action: 'type', selector: '#query', text: expected }, { action: 'click', selector: '#apply' }, { action: 'screenshot' }] if (stage < actions.length) sse(res, { id: mode + '-call-' + stage, args: actions[stage++] }) else { assert.ok(observedApplied, 'page changes returned to model'); assert.equal(observedImage, supportsImages, 'screenshot follows model image capabilities'); sse(res, undefined, mode + '_BROWSER_COMPLETE') } } catch (error) { res.writeHead(500, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: { message: String(error) } })) } }) const events = [], common = { runId: mode + '-run', sessionId: mode + '-session', workspaceRoot: directory, userText: 'Use the browser to fill the fixture and inspect its screenshot.', signal: new AbortController().signal, permissionMode: 'full-access', supportsImages, emit: event => events.push(event) } const config = { baseUrl: api.base + '/v1', api: 'openai-completions', model: 'browser-controlled', apiKey: 'loopback-only', maxOutputTokens: 1024, maxToolRounds: 8 } let result if (production) { // The harness already disabled acceleration before app readiness. delete process.env.YCODE_DISABLE_GPU delete process.env.YCODE_DISABLE_GPU_SANDBOX await import(require('node:url').pathToFileURL(path.resolve('out/main/index.mjs')).href) let main for (let index = 0; index < 300; index++) { main = BrowserWindow.getAllWindows().find(window => window.getTitle() === 'YCode') if (main && !main.webContents.isLoading()) break await pause(25) } assert.ok(main, 'production main window') const read = source => main.webContents.executeJavaScript(source) for (let index = 0; index < 200 && !await read('!!window.ycode'); index++) await pause(25) await read('window.__browserEvents=[];window.ycode.chat.onEvent(event=>window.__browserEvents.push(event));true') const profile = { id: 'browser-controlled', name: 'Browser controlled', providerId: 'custom', model: 'browser-controlled', protocol: 'openai', apiKey: 'loopback-only', baseUrl: config.baseUrl, endpoint: { api: 'openai-completions', baseUrl: config.baseUrl, checkedAt: Date.now() }, supportsToolCall: true, supportsImages: true, supportsReasoning: false, contextWindowTokens: 32000, maxOutputTokens: 1024, tokenBudgetVersion: 2 } await read(`window.ycode.models.save(${JSON.stringify(profile)})`) const workspace = await read(`window.ycode.workspace.attach(${JSON.stringify(directory)})`) const session = await read(`window.ycode.session.create('Browser APP',{modelId:'browser-controlled',workspaceId:${JSON.stringify(workspace.attachedWorkspaceId)}})`) await read(`window.ycode.chat.send({sessionId:${JSON.stringify(session.id)},text:'Control the browser fixture.',permissionMode:'full-access'})`) let assistant for (let index = 0; index < 1200; index++) { const messages = await read(`window.ycode.session.messages(${JSON.stringify(session.id)})`) assistant = messages.filter(message => message.role === 'assistant').at(-1) if (assistant && ['done', 'error', 'cancelled'].includes(assistant.status) && !await read(`window.ycode.chat.activeRun(${JSON.stringify(session.id)})`)) break await pause(40) } assert.equal(assistant?.status, 'done', JSON.stringify({ status: assistant?.status, error: assistant?.error, tools: assistant?.tools?.map(tool => ({ name: tool.name, status: tool.status })) })) result = { content: assistant.content, tools: assistant.tools } events.push(...await read('window.__browserEvents')) check('APP: production IPC persists the screenshot as an assistant image part', assistant.parts.some(part => part.type === 'image' && part.url.startsWith('data:image/jpeg;base64,'))) const persisted = JSON.parse(await fs.readFile(path.join(directory, 'data', 'sessions.json'), 'utf8')) check('APP: cold history contains successful browser actions and the real screenshot', persisted.sessions.some(item => item.id === session.id && item.messages.some(message => message.id === assistant.id && message.parts?.some(part => part.type === 'image')))) check('APP: the production host creates a hidden browser through its own registration', BrowserWindow.getAllWindows().some(window => window !== main && !window.isVisible())) await read(`window.ycode.session.remove(${JSON.stringify(session.id)})`) check('APP: deleting a session disposes its browser and native kernel', BrowserWindow.getAllWindows().length === 1) } else if (viaOmp) { const executable = host.detectOmpPath(); assert.ok(executable, 'installed OMP is available') const prepared = await host.prepareOmpModel({ config, profile: { id: 'browser-controlled', name: 'Browser controlled', providerId: 'custom', model: 'browser-controlled', baseUrl: config.baseUrl, hasApiKey: true, keyStorage: 'none', supportsToolCall: true, supportsImages, supportsReasoning: false, contextWindowTokens: 32000, maxOutputTokens: 1024, tokenBudgetVersion: 2 } }) kernel = new host.OmpKernel(executable, directory, prepared.profile, path.join(directory, 'omp-browser-session.jsonl'), prepared.model, prepared.env) result = await host.runOmpTurn({ ...common, kernel }) await kernel.dispose(); kernel = undefined } else result = await host.runTurn({ ...common, history: [], gateway: new host.Gateway(config), model: config.model, maxToolRounds: 8 }) check(mode + ': controlled model completes a real browser open/type/click/screenshot round trip', result.content.includes(mode + '_BROWSER_COMPLETE')) check(mode + ': tool events contain four successful browser actions', result.tools.filter(tool => tool.name === 'browser' && tool.status === 'ok').length === 4) check(mode + ': screenshot is visible in conversation image events', events.some(event => event.type === 'part' && event.part.type === 'image' && event.part.url.startsWith('data:image/jpeg;base64,'))) check(mode + ': page content and image capability are respected', observedApplied && observedImage === supportsImages) const final = apiRequests.filter(request => request.mode === mode).at(-1) if (!viaOmp && supportsImages) { const wire = { messages: final.body.messages } check('Responses fallback keeps browser screenshots as input_image', JSON.stringify(host.buildResponsesRequest(config, wire)).includes('input_image')) check('Anthropic fallback keeps browser screenshots as image blocks', JSON.stringify(host.buildAnthropicRequest(config, wire)).includes('"type":"image"')) check('Gemini fallback keeps browser screenshots as inlineData', JSON.stringify(host.buildGeminiRequest(config, wire)).includes('inlineData')) } if (!production) { const message = events.reduce((message, event) => host.applyAssistantEvent(message, event), { id: mode + '-assistant', requestId: common.runId, role: 'assistant', content: '', createdAt: 0 }) check(mode + ': compatible history replays screenshot images in subsequent turns', JSON.stringify(host.assistantWireHistory(message)).includes('data:image/jpeg;base64,')) check(mode + ': text-only history excludes screenshot input without losing tool results', !JSON.stringify(host.assistantWireHistory(message, { supportsImages: false })).includes('data:image/jpeg;base64,') && host.assistantWireHistory(message, { supportsImages: false }).some(row => row.role === 'tool')) } host.disposeSessionBrowsers(common.sessionId) } app.whenReady().then(async () => { host = require(path.join(directory, 'host.cjs')); host.configureBrowserTool(host.executeBrowserTool) const fixture = await listen((req, res) => { if (req.url === '/hang') return res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); res.end(page(req.url === '/next')) }) phase = 'direct-browser-actions' check('browser is registered in the host tool schema', host.toolSpecs().some(tool => tool.function.name === 'browser')) await rejected({ action: 'open', url: 'file:///C:/Windows/win.ini' }, /HTTP/) await rejected({ action: 'open', url: 'javascript:alert(1)' }, /HTTP/) await rejected({ action: 'open', url: 'http://user:password@localhost/' }, /HTTP/) check('file, script and credential URLs are refused', BrowserWindow.getAllWindows().length === 0) const blocked = await host.executeTurnTool('browser', { action: 'open', url: fixture.base + '/page' }, { ...ctx('mode-denied'), agentMode: 'plan', emit() {} }) check('plan mode cannot bypass the shared tool gate', !blocked.ok && BrowserWindow.getAllWindows().length === 0) const hostBrowserContext = ctx('host-browser', { permissionMode: 'default-sandbox', approve: async () => false }) const hostBrowser = await ok({ action: 'open', url: fixture.base + '/page' }, hostBrowserContext) check('host browser opens without sandbox approval', BrowserWindow.getAllWindows().length === 1) await ok({ action: 'close', tabId: hostBrowser.data.tabId }, hostBrowserContext) check('host browser close releases the unapproved tab', BrowserWindow.getAllWindows().length === 0) const first = await ok({ action: 'open', url: fixture.base + '/page' }) const id = first.data.tabId check('real hidden Chromium returns the rendered page and interactive refs', first.data.text.includes('BROWSER_FIXTURE') && first.data.elements.some(element => element.name === 'Apply')) check('browser remains hidden and has no renderer Node access', BrowserWindow.getAllWindows().every(window => !window.isVisible()) && first.data.text.includes('node:undefined/undefined')) check('password values do not appear in browser snapshots', !first.content.includes('never-expose-password')) const managed = BrowserWindow.getAllWindows()[0], prefs = managed.webContents.getLastWebPreferences() check('browser renderer uses isolation and a sandbox without preload', prefs.sandbox && prefs.contextIsolation && !prefs.nodeIntegration && !prefs.preload) await rejected({ action: 'snapshot', tabId: id }, /不属于/, ctx('browser-b')) check('another conversation cannot use the browser id', (await ok({ action: 'list' }, ctx('browser-b'))).data.length === 0) await managed.webContents.session.cookies.set({ url: fixture.base, name: 'isolation', value: 'private-a' }) const second = await ok({ action: 'open', url: fixture.base + '/page' }, ctx('browser-b')) check('browser cookies are isolated between conversations', !second.data.text.includes('private-a')) await rejected({ action: 'click', tabId: id, selector: 'button' }, /唯一匹配/) await ok({ action: 'type', tabId: id, selector: '#query', text: '' }) check('type can clear a text field', (await ok({ action: 'snapshot', tabId: id })).data.elements.find(element => element.tag === 'input' && element.type === 'text').value === '') await ok({ action: 'type', tabId: id, selector: '#query', text: 'typed real text' }) const applied = await ok({ action: 'click', tabId: id, ref: first.data.elements.find(element => element.name === 'Apply').ref }) check('typing and clicking produce actual trusted Chromium events', applied.data.text.includes('APPLIED:typed real text:trusted=true')) await ok({ action: 'type', tabId: id, selector: '#query', text: '+append', replace: false }) const appended = await ok({ action: 'snapshot', tabId: id }) check('type supports append without erasing existing input', appended.data.elements.find(element => element.tag === 'input' && element.type === 'text').value === 'typed real text+append') await ok({ action: 'press_key', tabId: id, key: 'Tab' }) await ok({ action: 'press_key', tabId: id, key: 'Enter' }) check('keyboard events can activate the focused button', (await ok({ action: 'snapshot', tabId: id })).data.text.includes('APPLIED:typed real text+append:trusted=true')) await ok({ action: 'click', tabId: id, selector: '#later' }) await ok({ action: 'wait', tabId: id, text: 'DELAYED_READY', timeoutMs: 1500 }) check('wait observes asynchronous rendered page changes', true) await rejected({ action: 'wait', tabId: id, text: 'never-present', timeoutMs: 100 }, /超时/) const scrolled = await ok({ action: 'scroll', tabId: id, y: 700 }) check('scroll changes the real page viewport', scrolled.data.scroll.y > 0) await ok({ action: 'scroll', tabId: id, y: -700 }) const capture = await ok({ action: 'screenshot', tabId: id }) const bytes = Buffer.from(capture.images[0].data, 'base64'); await fs.writeFile(path.join(directory, 'browser-actual.jpg'), bytes) check('screenshot returns a bounded real JPEG image', bytes[0] === 0xff && bytes[1] === 0xd8 && bytes.length > 2000 && bytes.length < 500 * 1024) check('taking a screenshot never opens a visible window', BrowserWindow.getAllWindows().every(window => !window.isVisible())) const oldRef = applied.data.elements.find(element => element.name === 'Apply').ref const next = await ok({ action: 'navigate', tabId: id, url: fixture.base + '/next' }) check('navigate reads the new document', next.data.text.includes('NEXT_PAGE')) await rejected({ action: 'click', tabId: id, ref: oldRef }, /ref/) check('refs from previous documents cannot trigger new-page controls', true) let approvals = 0 const noApproval = await ok({ action: 'open', url: fixture.base + '/page' }, ctx('approved', { permissionMode: 'default-sandbox', approve: async () => { approvals++; return true } })) check('legacy sandbox approval callback is never invoked', approvals === 0) await ok({ action: 'close', tabId: noApproval.data.tabId }, ctx('approved')) const cancellation = new AbortController(), started = Date.now() setTimeout(() => cancellation.abort(), 150) await rejected({ action: 'open', url: fixture.base + '/hang' }, /取消/, ctx('cancelled-open', { signal: cancellation.signal })) check('cancelled navigation stops promptly without leaving an orphan tab', Date.now() - started < 2000 && (await ok({ action: 'list' }, ctx('cancelled-open'))).data.length === 0) host.disposeSessionBrowsers('browser-a') check('session cleanup closes only its own browsers', (await ok({ action: 'list' })).data.length === 0 && (await ok({ action: 'list' }, ctx('browser-b'))).data.length === 1) await ok({ action: 'close', tabId: second.data.tabId }, ctx('browser-b')) check('close releases a browser through the public tool action', (await ok({ action: 'list' }, ctx('browser-b'))).data.length === 0) host.disposeBrowserAutomation() check('all browser contents are released', BrowserWindow.getAllWindows().length === 0) phase = 'http-conversation'; await conversation(false, fixture.base) phase = 'omp-conversation'; await conversation(true, fixture.base) phase = 'text-only-http-conversation'; await conversation(false, fixture.base, false, false) phase = 'text-only-omp-conversation'; await conversation(true, fixture.base, false, false) phase = 'production-conversation'; await conversation(true, fixture.base, true) await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'PASS', checks, apiRequests, phase }, null, 2)) console.log('BROWSER_AUTOMATION_PASS ' + directory) }).catch(async error => { console.error(error) await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'FAIL', phase, checks, error: String(error.stack || error), apiRequests }, null, 2)) process.exitCode = 1 }).finally(async () => { if (kernel) await kernel.dispose().catch(() => undefined) host?.disposeBrowserAutomation() for (const server of servers) { server.closeAllConnections(); server.close() } await pause(100); app.exit(process.exitCode || 0) }) }