mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-13 20:20:46 +00:00
153 lines
4.4 KiB
TypeScript
153 lines
4.4 KiB
TypeScript
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
AgentToolError,
|
|
ReadFileTool,
|
|
TerminalExecTool,
|
|
WriteFileTool,
|
|
createDefaultToolRegistry,
|
|
} from '../../packages/ekko-agent/src/index'
|
|
|
|
let workspaceRoot = ''
|
|
|
|
beforeEach(async () => {
|
|
workspaceRoot = await mkdtemp(path.join(os.tmpdir(), 'ekko-agent-tools-'))
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await rm(workspaceRoot, { recursive: true, force: true })
|
|
delete process.env.AGENT_BROWSER_BIN
|
|
})
|
|
|
|
describe('ekko-agent tools', () => {
|
|
it('writes and reads files inside the workspace', async () => {
|
|
const writer = new WriteFileTool()
|
|
const reader = new ReadFileTool()
|
|
|
|
await expect(writer.execute({
|
|
path: 'notes/todo.txt',
|
|
content: 'ship tools',
|
|
}, { workspaceRoot })).resolves.toMatchObject({
|
|
ok: true,
|
|
data: {
|
|
bytes: 10,
|
|
},
|
|
})
|
|
|
|
await expect(readFile(path.join(workspaceRoot, 'notes/todo.txt'), 'utf8')).resolves.toBe('ship tools')
|
|
await expect(reader.execute({ path: 'notes/todo.txt' }, { workspaceRoot })).resolves.toMatchObject({
|
|
ok: true,
|
|
content: 'ship tools',
|
|
})
|
|
})
|
|
|
|
it('blocks file paths outside workspaceRoot', async () => {
|
|
const reader = new ReadFileTool()
|
|
|
|
await expect(reader.execute({ path: '../outside.txt' }, { workspaceRoot })).rejects.toBeInstanceOf(AgentToolError)
|
|
await expect(reader.execute({ path: '../outside.txt' }, { workspaceRoot })).rejects.toMatchObject({
|
|
code: 'PATH_OUTSIDE_WORKSPACE',
|
|
})
|
|
})
|
|
|
|
it('runs terminal commands with argument arrays', async () => {
|
|
const terminal = new TerminalExecTool()
|
|
|
|
await expect(terminal.execute({
|
|
command: process.execPath,
|
|
args: ['-e', 'process.stdout.write(process.argv[1])', 'hello-terminal'],
|
|
}, { workspaceRoot })).resolves.toMatchObject({
|
|
ok: true,
|
|
content: 'hello-terminal',
|
|
data: {
|
|
args: ['-e', 'process.stdout.write(process.argv[1])', 'hello-terminal'],
|
|
exitCode: 0,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('normalizes shell-like terminal command strings when args are omitted', async () => {
|
|
const terminal = new TerminalExecTool()
|
|
|
|
await expect(terminal.execute({
|
|
command: `${process.execPath} -e "process.stdout.write(process.argv[1])" hello-split`,
|
|
}, { workspaceRoot })).resolves.toMatchObject({
|
|
ok: true,
|
|
content: 'hello-split',
|
|
data: {
|
|
command: process.execPath,
|
|
args: ['-e', 'process.stdout.write(process.argv[1])', 'hello-split'],
|
|
exitCode: 0,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('does not start terminal commands when the signal is already aborted', async () => {
|
|
const terminal = new TerminalExecTool()
|
|
const controller = new AbortController()
|
|
controller.abort()
|
|
|
|
await expect(terminal.execute({
|
|
command: process.execPath,
|
|
args: ['-e', 'process.stdout.write("should-not-run")'],
|
|
}, { workspaceRoot, signal: controller.signal })).resolves.toMatchObject({
|
|
ok: false,
|
|
content: 'Command aborted.',
|
|
error: 'Command aborted.',
|
|
data: {
|
|
aborted: true,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('reports non-zero terminal exits without throwing', async () => {
|
|
const terminal = new TerminalExecTool()
|
|
|
|
await expect(terminal.execute({
|
|
command: process.execPath,
|
|
args: ['-e', 'process.stderr.write("bad"); process.exit(7)'],
|
|
}, { workspaceRoot })).resolves.toMatchObject({
|
|
ok: false,
|
|
content: 'bad',
|
|
error: 'Command exited with code 7',
|
|
data: {
|
|
exitCode: 7,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('registers default tools and exposes model definitions', async () => {
|
|
const registry = createDefaultToolRegistry()
|
|
|
|
expect(registry.definitions().map(definition => 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',
|
|
'read_file',
|
|
'terminal_exec',
|
|
'write_file',
|
|
])
|
|
|
|
await expect(registry.execute('write_file', {
|
|
path: 'from-registry.txt',
|
|
content: 'ok',
|
|
}, { workspaceRoot })).resolves.toMatchObject({ ok: true })
|
|
|
|
await expect(registry.execute('read_file', {
|
|
path: 'from-registry.txt',
|
|
}, { workspaceRoot })).resolves.toMatchObject({
|
|
ok: true,
|
|
content: 'ok',
|
|
})
|
|
})
|
|
})
|