* feat: add richer JSON Schema types and AJV validation utility - Extended ToolParameter with enum, items, properties, additionalProperties (max depth 3) - Added ToolError class with ToolErrorCategory enum - Added ToolValidationError interface - Created AJV-based validateToolArgs() with lazy loading and schema depth enforcement - 57 new tests covering types and validation * feat: add validation gate, parallel execution, and structured error feedback - Arsenal.execute() validates args against JSON Schema before handler execution - Tool execution errors categorized (scope_denied, tool_not_found, timeout, execution_error, validation_error) - ToolError import fix in agent (was missing, causing silent ReferenceError) - Semaphore-based parallel tool execution with configurable concurrency (default 5) - Results maintain original call order regardless of completion order - Structured error feedback to LLM with category only (no raw error messages) - Scope/approval gates checked per-call before execution - 35 new tests covering validation, parallel execution, and error feedback * feat: add Ollama native function calling with probe + cache + fallback - Added nativeTools config option to LLMConfig - LocalAdapter probes model support for native tools on first call - Probe result cached per model name to avoid re-probing - Native tools formatted as Ollama /api/chat tools parameter - Tool calls parsed from Ollama's tool_calls response format - Falls back to text-based renderToolContract when native not supported - OpenAI-compatible wire format also supported (for vLLM, LM Studio) - 10 new tests covering probe, cache, fallback, and config overrides * fix(tools): harden validation and native fallback --------- Co-authored-by: sronaal <sronaal@users.noreply.github.com> Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
139 lines
4.6 KiB
TypeScript
139 lines
4.6 KiB
TypeScript
/**
|
|
* ToolError Type Tests
|
|
*
|
|
* Tests for ToolErrorCategory enum and ToolError class — structured error
|
|
* handling for tool validation, execution, and scope enforcement.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { ToolError, ToolErrorCategory, type ToolValidationError } from '../types/index.js';
|
|
|
|
describe('ToolErrorCategory', () => {
|
|
it('should have all expected category values', () => {
|
|
expect(ToolErrorCategory.ScopeDenied).toBe('scope_denied');
|
|
expect(ToolErrorCategory.ToolNotFound).toBe('tool_not_found');
|
|
expect(ToolErrorCategory.Timeout).toBe('timeout');
|
|
expect(ToolErrorCategory.ExecutionError).toBe('execution_error');
|
|
expect(ToolErrorCategory.ValidationError).toBe('validation_error');
|
|
});
|
|
|
|
it('should have exactly 5 categories', () => {
|
|
const values = Object.values(ToolErrorCategory);
|
|
expect(values).toHaveLength(5);
|
|
});
|
|
});
|
|
|
|
describe('ToolError', () => {
|
|
it('should construct with category and message', () => {
|
|
const err = new ToolError(ToolErrorCategory.ValidationError, 'Invalid argument');
|
|
expect(err.category).toBe(ToolErrorCategory.ValidationError);
|
|
expect(err.message).toBe('Invalid argument');
|
|
expect(err.name).toBe('ToolError');
|
|
});
|
|
|
|
it('should be an instance of Error', () => {
|
|
const err = new ToolError(ToolErrorCategory.Timeout, 'Timed out');
|
|
expect(err).toBeInstanceOf(Error);
|
|
expect(err).toBeInstanceOf(ToolError);
|
|
});
|
|
|
|
it('should accept optional toolName', () => {
|
|
const err = new ToolError(ToolErrorCategory.ToolNotFound, 'Not found', 'nmap_scan');
|
|
expect(err.toolName).toBe('nmap_scan');
|
|
});
|
|
|
|
it('should have undefined toolName when not provided', () => {
|
|
const err = new ToolError(ToolErrorCategory.ExecutionError, 'Failed');
|
|
expect(err.toolName).toBeUndefined();
|
|
});
|
|
|
|
it('should preserve stack trace', () => {
|
|
const err = new ToolError(ToolErrorCategory.ScopeDenied, 'Denied');
|
|
expect(err.stack).toBeDefined();
|
|
expect(err.stack).toContain('ToolError');
|
|
});
|
|
|
|
it('should be throwable and catchable', () => {
|
|
expect(() => {
|
|
throw new ToolError(ToolErrorCategory.ValidationError, 'Bad input');
|
|
}).toThrow(ToolError);
|
|
});
|
|
|
|
it('should allow checking category in catch block', () => {
|
|
try {
|
|
throw new ToolError(ToolErrorCategory.Timeout, 'Tool timed out', 'nuclei');
|
|
} catch (e) {
|
|
expect(e).toBeInstanceOf(ToolError);
|
|
if (e instanceof ToolError) {
|
|
expect(e.category).toBe(ToolErrorCategory.Timeout);
|
|
expect(e.toolName).toBe('nuclei');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should work with all categories', () => {
|
|
const categories = [
|
|
ToolErrorCategory.ScopeDenied,
|
|
ToolErrorCategory.ToolNotFound,
|
|
ToolErrorCategory.Timeout,
|
|
ToolErrorCategory.ExecutionError,
|
|
ToolErrorCategory.ValidationError,
|
|
];
|
|
for (const cat of categories) {
|
|
const err = new ToolError(cat, `Error for ${cat}`);
|
|
expect(err.category).toBe(cat);
|
|
}
|
|
});
|
|
|
|
it('should have readonly category and toolName', () => {
|
|
const err = new ToolError(ToolErrorCategory.ValidationError, 'msg', 'tool');
|
|
// TypeScript readonly — runtime still allows assignment but the intent is clear
|
|
expect(err.category).toBe(ToolErrorCategory.ValidationError);
|
|
expect(err.toolName).toBe('tool');
|
|
});
|
|
});
|
|
|
|
describe('ToolValidationError', () => {
|
|
it('should have all required fields', () => {
|
|
const error: ToolValidationError = {
|
|
field: 'port',
|
|
message: 'Port must be a number',
|
|
received: 'abc',
|
|
expected: 'number',
|
|
};
|
|
expect(error.field).toBe('port');
|
|
expect(error.message).toBe('Port must be a number');
|
|
expect(error.received).toBe('abc');
|
|
expect(error.expected).toBe('number');
|
|
});
|
|
|
|
it('should accept unknown received values', () => {
|
|
const error: ToolValidationError = {
|
|
field: 'config',
|
|
message: 'Invalid type',
|
|
received: undefined,
|
|
expected: 'object',
|
|
};
|
|
expect(error.received).toBeUndefined();
|
|
});
|
|
|
|
it('should accept complex received values', () => {
|
|
const error: ToolValidationError = {
|
|
field: 'headers',
|
|
message: 'Array item type mismatch',
|
|
received: { key: 123 },
|
|
expected: 'string',
|
|
};
|
|
expect(error.received).toEqual({ key: 123 });
|
|
});
|
|
|
|
it('should be usable in arrays for multi-field validation', () => {
|
|
const errors: ToolValidationError[] = [
|
|
{ field: 'host', message: 'Required', received: undefined, expected: 'string' },
|
|
{ field: 'port', message: 'Out of range', received: 99999, expected: '1-65535' },
|
|
];
|
|
expect(errors).toHaveLength(2);
|
|
expect(errors.map(e => e.field)).toEqual(['host', 'port']);
|
|
});
|
|
});
|