mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-13 20:20:46 +00:00
167 lines
5.5 KiB
TypeScript
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
|
|
})
|
|
}
|