Files
smartagent/test/test.ts

189 lines
6.1 KiB
TypeScript

import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartagent from '../ts/index.js';
import { filesystemTool, shellTool, httpTool, jsonTool, truncateOutput } from '../ts_tools/index.js';
import { compactMessages } from '../ts_compaction/index.js';
// ============================================================
// Core exports
// ============================================================
tap.test('should export runAgent function', async () => {
expect(smartagent.runAgent).toBeTypeOf('function');
});
tap.test('should export ToolRegistry class', async () => {
expect(smartagent.ToolRegistry).toBeTypeOf('function');
});
tap.test('should export ContextOverflowError class', async () => {
expect(smartagent.ContextOverflowError).toBeTypeOf('function');
});
tap.test('should export truncateOutput function', async () => {
expect(smartagent.truncateOutput).toBeTypeOf('function');
});
tap.test('should re-export tool helper', async () => {
expect(smartagent.tool).toBeTypeOf('function');
});
tap.test('should re-export z (zod)', async () => {
expect(smartagent.z).toBeTruthy();
});
tap.test('should re-export stepCountIs', async () => {
expect(smartagent.stepCountIs).toBeTypeOf('function');
});
// ============================================================
// ToolRegistry
// ============================================================
tap.test('ToolRegistry should register and return tools', async () => {
const registry = new smartagent.ToolRegistry();
const echoTool = smartagent.tool({
description: 'Echo tool',
inputSchema: smartagent.z.object({ text: smartagent.z.string() }),
execute: async ({ text }: { text: string }) => text,
});
registry.register('echo', echoTool);
const tools = registry.getTools();
expect(Object.keys(tools)).toContain('echo');
});
// ============================================================
// Truncation
// ============================================================
tap.test('truncateOutput should not truncate short strings', async () => {
const result = truncateOutput('hello world');
expect(result.truncated).toBeFalse();
expect(result.content).toEqual('hello world');
});
tap.test('truncateOutput should truncate strings over maxLines', async () => {
const lines = Array.from({ length: 3000 }, (_, i) => `line ${i}`).join('\n');
const result = truncateOutput(lines, { maxLines: 100 });
expect(result.truncated).toBeTrue();
expect(result.notice).toBeTruthy();
expect(result.content).toInclude('[Output truncated');
});
tap.test('truncateOutput should truncate strings over maxBytes', async () => {
const big = 'x'.repeat(100_000);
const result = truncateOutput(big, { maxBytes: 1000 });
expect(result.truncated).toBeTrue();
});
// ============================================================
// Tool factories
// ============================================================
tap.test('filesystemTool returns expected tool names', async () => {
const tools = filesystemTool();
const names = Object.keys(tools);
expect(names).toContain('read_file');
expect(names).toContain('write_file');
expect(names).toContain('list_directory');
expect(names).toContain('delete_file');
});
tap.test('shellTool returns expected tool names', async () => {
const tools = shellTool();
const names = Object.keys(tools);
expect(names).toContain('run_command');
});
tap.test('httpTool returns expected tool names', async () => {
const tools = httpTool();
const names = Object.keys(tools);
expect(names).toContain('http_get');
expect(names).toContain('http_post');
});
tap.test('jsonTool returns expected tool names', async () => {
const tools = jsonTool();
const names = Object.keys(tools);
expect(names).toContain('json_validate');
expect(names).toContain('json_transform');
});
tap.test('json_validate tool should validate valid JSON', async () => {
const tools = jsonTool();
const result = await (tools.json_validate as any).execute({
jsonString: '{"name":"test","value":42}',
});
expect(result).toInclude('Valid JSON');
});
tap.test('json_validate tool should detect invalid JSON', async () => {
const tools = jsonTool();
const result = await (tools.json_validate as any).execute({
jsonString: '{invalid json',
});
expect(result).toInclude('Invalid JSON');
});
tap.test('json_validate tool should check required fields', async () => {
const tools = jsonTool();
const result = await (tools.json_validate as any).execute({
jsonString: '{"name":"test"}',
requiredFields: ['name', 'missing_field'],
});
expect(result).toInclude('missing_field');
});
tap.test('json_transform tool should pretty-print JSON', async () => {
const tools = jsonTool();
const result = await (tools.json_transform as any).execute({
jsonString: '{"a":1,"b":2}',
});
expect(result).toInclude(' "a": 1');
});
// ============================================================
// Compaction export
// ============================================================
tap.test('compactMessages should be a function', async () => {
expect(compactMessages).toBeTypeOf('function');
});
// ============================================================
// Filesystem tool read/write round-trip
// ============================================================
tap.test('filesystem tool should write and read a file', async () => {
const tmpDir = '/tmp/smartagent-test-' + Date.now();
const tools = filesystemTool({ rootDir: tmpDir });
await (tools.write_file as any).execute({
path: tmpDir + '/hello.txt',
content: 'Hello, world!',
});
const content = await (tools.read_file as any).execute({
path: tmpDir + '/hello.txt',
});
expect(content).toInclude('Hello, world!');
// Cleanup
await (tools.delete_file as any).execute({
path: tmpDir + '/hello.txt',
});
});
tap.test('filesystem tool should enforce rootDir restriction', async () => {
const tools = filesystemTool({ rootDir: '/tmp/restricted' });
let threw = false;
try {
await (tools.read_file as any).execute({ path: '/etc/passwd' });
} catch (e) {
threw = true;
expect((e as Error).message).toInclude('Access denied');
}
expect(threw).toBeTrue();
});
export default tap.start();