Files
waggle-os/packages/agent/tests/system-tools.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

203 lines
7.2 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createSystemTools } from '../src/system-tools.js';
import type { ToolDefinition } from '../src/tools.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
describe('createSystemTools', () => {
let workspace: string;
let tools: ToolDefinition[];
beforeEach(() => {
workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-test-'));
tools = createSystemTools(workspace);
});
afterEach(async () => {
// On Windows, killed child processes may briefly hold locks on cwd
for (let i = 0; i < 5; i++) {
try {
fs.rmSync(workspace, { recursive: true, force: true });
return;
} catch {
await new Promise((r) => setTimeout(r, 200));
}
}
});
function getTool(name: string): ToolDefinition {
const tool = tools.find((t) => t.name === name);
if (!tool) throw new Error(`Tool "${name}" not found`);
return tool;
}
it('creates all system tools', () => {
const names = tools.map((t) => t.name);
expect(names).toContain('bash');
expect(names).toContain('read_file');
expect(names).toContain('write_file');
expect(names).toContain('edit_file');
expect(names).toContain('search_files');
expect(names).toContain('search_content');
expect(names).toContain('web_search');
expect(names).toContain('web_fetch');
expect(names).toContain('multi_edit');
expect(names).toContain('get_task_output');
expect(names).toContain('kill_task');
expect(names).toContain('run_code');
expect(tools).toHaveLength(12);
});
describe('bash', () => {
it('executes a simple command', async () => {
const bash = getTool('bash');
const result = await bash.execute({ command: 'echo hello world' });
expect(result.trim()).toBe('hello world');
});
it('returns stderr on failure', async () => {
const bash = getTool('bash');
// Use a command that reliably writes to stderr cross-platform
const result = await bash.execute({ command: 'node -e "process.stderr.write(String.fromCharCode(101,114,114)); process.exit(1)"' });
expect(result).toContain('err');
});
it('respects timeout', async () => {
const bash = getTool('bash');
// Use ping which reliably blocks on both Windows and Unix
const isWindows = process.platform === 'win32';
const sleepCmd = isWindows ? 'ping -n 30 127.0.0.1' : 'sleep 30';
const result = await bash.execute({ command: sleepCmd, timeout: 1000 });
expect(result.toLowerCase()).toContain('timeout');
}, 10_000);
});
describe('read_file', () => {
it('reads a file within workspace', async () => {
const filePath = path.join(workspace, 'test.txt');
fs.writeFileSync(filePath, 'file contents here');
const readFile = getTool('read_file');
const result = await readFile.execute({ path: 'test.txt' });
expect(result).toBe('file contents here');
});
it('rejects paths outside workspace', async () => {
const readFile = getTool('read_file');
const result = await readFile.execute({ path: '../../etc/passwd' });
expect(result.toLowerCase()).toContain('outside');
});
});
describe('write_file', () => {
it('creates a new file', async () => {
const writeFile = getTool('write_file');
await writeFile.execute({ path: 'new-file.txt', content: 'new content' });
const written = fs.readFileSync(path.join(workspace, 'new-file.txt'), 'utf-8');
expect(written).toBe('new content');
});
it('creates intermediate directories', async () => {
const writeFile = getTool('write_file');
await writeFile.execute({ path: 'a/b/c/deep.txt', content: 'deep content' });
const written = fs.readFileSync(path.join(workspace, 'a', 'b', 'c', 'deep.txt'), 'utf-8');
expect(written).toBe('deep content');
});
});
describe('edit_file', () => {
it('replaces exact string', async () => {
const filePath = path.join(workspace, 'edit-me.txt');
fs.writeFileSync(filePath, 'hello world, hello universe');
const editFile = getTool('edit_file');
const result = await editFile.execute({
path: 'edit-me.txt',
old_string: 'world',
new_string: 'planet',
});
const edited = fs.readFileSync(filePath, 'utf-8');
expect(edited).toBe('hello planet, hello universe');
expect(result.toLowerCase()).toContain('success');
});
it('fails if old_string not found', async () => {
const filePath = path.join(workspace, 'edit-me.txt');
fs.writeFileSync(filePath, 'hello world');
const editFile = getTool('edit_file');
const result = await editFile.execute({
path: 'edit-me.txt',
old_string: 'nonexistent',
new_string: 'replacement',
});
expect(result.toLowerCase()).toContain('not found');
});
it('fails if old_string appears more than once', async () => {
const filePath = path.join(workspace, 'edit-me.txt');
fs.writeFileSync(filePath, 'hello hello');
const editFile = getTool('edit_file');
const result = await editFile.execute({
path: 'edit-me.txt',
old_string: 'hello',
new_string: 'hi',
});
expect(result.toLowerCase()).toContain('multiple');
});
});
describe('search_files', () => {
it('finds files by glob pattern', async () => {
fs.writeFileSync(path.join(workspace, 'foo.ts'), '');
fs.writeFileSync(path.join(workspace, 'bar.ts'), '');
fs.writeFileSync(path.join(workspace, 'baz.js'), '');
const searchFiles = getTool('search_files');
const result = await searchFiles.execute({ pattern: '**/*.ts' });
expect(result).toContain('foo.ts');
expect(result).toContain('bar.ts');
expect(result).not.toContain('baz.js');
});
it('ignores node_modules', async () => {
const nmDir = path.join(workspace, 'node_modules', 'pkg');
fs.mkdirSync(nmDir, { recursive: true });
fs.writeFileSync(path.join(nmDir, 'index.ts'), '');
fs.writeFileSync(path.join(workspace, 'app.ts'), '');
const searchFiles = getTool('search_files');
const result = await searchFiles.execute({ pattern: '**/*.ts' });
expect(result).toContain('app.ts');
expect(result).not.toContain('node_modules');
});
});
describe('search_content', () => {
it('finds content in files', async () => {
fs.writeFileSync(path.join(workspace, 'a.txt'), 'line1\nfind me here\nline3');
fs.writeFileSync(path.join(workspace, 'b.txt'), 'nothing relevant');
const searchContent = getTool('search_content');
const result = await searchContent.execute({ pattern: 'find me' });
expect(result).toContain('a.txt');
expect(result).toContain('find me here');
expect(result).not.toContain('b.txt');
});
it('supports regex patterns', async () => {
fs.writeFileSync(path.join(workspace, 'code.ts'), 'const foo = 123;\nconst bar = 456;');
const searchContent = getTool('search_content');
const result = await searchContent.execute({ pattern: 'const \\w+ = \\d+' });
expect(result).toContain('code.ts');
expect(result).toContain('const foo = 123');
});
});
});