Files
hermes-web-ui/tests/ekko-agent/browser-tools.test.ts
2026-07-05 15:54:39 +08:00

167 lines
5.5 KiB
TypeScript

import { EventEmitter } from 'node:events'
import { createHash } from 'node:crypto'
import { writeSync } from 'node:fs'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('node:child_process', () => ({
spawn: vi.fn(),
}))
import { spawn } from 'node:child_process'
import {
AgentBrowserTool,
createBrowserTools,
} from '../../packages/ekko-agent/src/index'
const mockedSpawn = vi.mocked(spawn)
beforeEach(() => {
vi.clearAllMocks()
process.env.AGENT_BROWSER_BIN = '/tmp/agent-browser'
})
afterEach(() => {
delete process.env.AGENT_BROWSER_BIN
delete process.env.OPENAI_API_KEY
})
describe('ekko-agent browser tools', () => {
it('exposes browser tool definitions even before the CLI is available', () => {
delete process.env.AGENT_BROWSER_BIN
expect(createBrowserTools().map(tool => tool.definition.name).sort()).toEqual([
'browser_back',
'browser_click',
'browser_console',
'browser_get_images',
'browser_navigate',
'browser_press',
'browser_scroll',
'browser_snapshot',
'browser_type',
'browser_vision',
])
})
it('exposes the full Hermes browser tool surface', () => {
expect(createBrowserTools().map(tool => tool.definition.name).sort()).toEqual([
'browser_back',
'browser_click',
'browser_console',
'browser_get_images',
'browser_navigate',
'browser_press',
'browser_scroll',
'browser_snapshot',
'browser_type',
'browser_vision',
])
})
it('runs browser_navigate through agent-browser and auto snapshots the page', async () => {
mockBrowserSpawn({ success: true, data: { url: 'https://example.com/', title: 'Example' } })
mockBrowserSpawn({ success: true, data: { snapshot: 'button "Continue" [ref=@e1]', refs: { '@e1': {} } } })
const tool = createBrowserTools().find(item => item.definition.name === 'browser_navigate')
const result = await tool?.execute({ url: 'example.com' }, { sessionId: 'session:123', cwd: process.cwd() })
expect(result).toMatchObject({ ok: true })
expect(JSON.parse(result?.content || '{}')).toMatchObject({
success: true,
url: 'https://example.com/',
title: 'Example',
snapshot: 'button "Continue" [ref=@e1]',
element_count: 1,
})
expect(mockedSpawn).toHaveBeenNthCalledWith(
1,
'/tmp/agent-browser',
['--session', hashedSession('session:123'), '--json', 'open', 'https://example.com'],
expect.objectContaining({ shell: false }),
)
expect(mockedSpawn).toHaveBeenNthCalledWith(
2,
'/tmp/agent-browser',
['--session', hashedSession('session:123'), '--json', 'snapshot', '-c'],
expect.objectContaining({ shell: false }),
)
})
it('passes a sanitized browser environment to the CLI', async () => {
process.env.OPENAI_API_KEY = 'should-not-leak'
process.env.BROWSERBASE_API_KEY = 'browser-key'
mockBrowserSpawn({ success: true, data: { snapshot: 'ok', refs: {} } })
const tool = createBrowserTools().find(item => item.definition.name === 'browser_snapshot')
await tool?.execute({}, { browserSessionId: 'browser-session' })
const options = mockedSpawn.mock.calls[0]?.[2] as { env?: NodeJS.ProcessEnv }
expect(options.env?.BROWSERBASE_API_KEY).toBe('browser-key')
expect(options.env?.OPENAI_API_KEY).toBeUndefined()
expect(options.env?.AGENT_BROWSER_SOCKET_DIR).toBe(`/tmp/eab_${hashedSession('browser-session')}`)
expect((options.env?.AGENT_BROWSER_SOCKET_DIR || '').length).toBeLessThan(40)
})
it('blocks sensitive console expressions before spawning the browser CLI', async () => {
const tool = new AgentBrowserTool({
name: 'browser_console',
parameters: { type: 'object' },
})
const result = await tool.execute({ expression: 'document.cookie' }, { sessionId: 's1' })
expect(result).toMatchObject({
ok: false,
error: 'Blocked: browser_console expression attempts to access sensitive browser or runtime state.',
})
expect(mockedSpawn).not.toHaveBeenCalled()
})
it('parses image extraction results from browser_get_images', async () => {
mockBrowserSpawn({
success: true,
data: {
result: JSON.stringify([{ src: 'https://example.com/logo.png', alt: 'Logo', width: 64, height: 64 }]),
},
})
const tool = createBrowserTools().find(item => item.definition.name === 'browser_get_images')
const result = await tool?.execute({}, { sessionId: 'images' })
expect(result).toMatchObject({ ok: true })
expect(JSON.parse(result?.content || '{}')).toMatchObject({
success: true,
count: 1,
images: [{ src: 'https://example.com/logo.png', alt: 'Logo', width: 64, height: 64 }],
})
expect(mockedSpawn.mock.calls[0]?.[1]).toEqual([
'--session',
hashedSession('images'),
'--json',
'eval',
expect.stringContaining('document.images'),
])
})
})
function hashedSession(value: string): string {
return `e_${createHash('sha256').update(value).digest('hex').slice(0, 10)}`
}
function mockBrowserSpawn(payload: unknown, exitCode = 0): void {
mockedSpawn.mockImplementationOnce((_command, _args, options) => {
const child = new EventEmitter() as EventEmitter & {
kill: ReturnType<typeof vi.fn>
}
child.kill = vi.fn()
const stdio = Array.isArray(options?.stdio) ? options.stdio : []
if (typeof stdio[1] === 'number') {
writeSync(stdio[1], JSON.stringify(payload))
}
process.nextTick(() => {
child.emit('close', exitCode)
})
return child as any
})
}