mirror of https://github.com/buster-so/buster.git
265 lines
9.1 KiB
TypeScript
265 lines
9.1 KiB
TypeScript
import { NoSuchToolError } from 'ai';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { ZodError } from 'zod';
|
|
import {
|
|
type HealableStreamError,
|
|
healStreamingToolError,
|
|
isHealableStreamError,
|
|
} from '../../../src/utils/streaming/tool-healing';
|
|
|
|
// Mock interfaces for testing
|
|
interface MockToolError extends Error {
|
|
toolCallId?: string;
|
|
toolName?: string;
|
|
args?: string;
|
|
cause?: ZodError;
|
|
}
|
|
|
|
interface MockToolResult {
|
|
result?: {
|
|
error?: string;
|
|
success?: boolean;
|
|
};
|
|
}
|
|
|
|
describe('tool-healing', () => {
|
|
describe('isHealableStreamError', () => {
|
|
it('should identify NoSuchToolError as healable', () => {
|
|
const error = new NoSuchToolError({
|
|
toolName: 'unknownTool',
|
|
availableTools: ['tool1', 'tool2'],
|
|
});
|
|
|
|
expect(isHealableStreamError(error)).toBe(true);
|
|
});
|
|
|
|
it('should identify InvalidToolArgumentsError as healable', () => {
|
|
const error = new Error('Invalid tool arguments');
|
|
error.name = 'AI_InvalidToolArgumentsError';
|
|
|
|
expect(isHealableStreamError(error)).toBe(true);
|
|
});
|
|
|
|
it('should not identify other errors as healable', () => {
|
|
const error = new Error('Some other error');
|
|
|
|
expect(isHealableStreamError(error)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('healStreamingToolError - NoSuchToolError', () => {
|
|
it('should provide available tools list for NoSuchToolError', () => {
|
|
const error = new NoSuchToolError({
|
|
toolName: 'badTool',
|
|
availableTools: ['tool1', 'tool2'],
|
|
});
|
|
(error as MockToolError).toolCallId = 'test-call-id';
|
|
|
|
const availableTools = {
|
|
'create-metrics-file': {},
|
|
'execute-sql': {},
|
|
'sequential-thinking': {},
|
|
};
|
|
|
|
const result = healStreamingToolError(error, availableTools);
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.healed).toBe(false);
|
|
expect(result?.healingMessage.role).toBe('tool');
|
|
expect(result?.healingMessage.content[0]).toMatchObject({
|
|
type: 'tool-result',
|
|
toolCallId: 'test-call-id',
|
|
toolName: 'badTool',
|
|
result: {
|
|
error: expect.stringContaining('Tool "badTool" is not available'),
|
|
},
|
|
});
|
|
const toolResult = result?.healingMessage.content[0] as MockToolResult;
|
|
expect(toolResult?.result?.error).toContain('create-metrics-file');
|
|
expect(toolResult?.result?.error).toContain('execute-sql');
|
|
expect(toolResult?.result?.error).toContain('sequential-thinking');
|
|
});
|
|
});
|
|
|
|
describe('healStreamingToolError - InvalidToolArgumentsError for visualization tools', () => {
|
|
it('should heal double-escaped JSON in files parameter for create-metrics-file', () => {
|
|
const error = new Error('Invalid tool arguments');
|
|
error.name = 'AI_InvalidToolArgumentsError';
|
|
(error as MockToolError).toolCallId = 'test-call-id';
|
|
(error as MockToolError).toolName = 'create-metrics-file';
|
|
|
|
// Simulate double-escaped JSON files parameter
|
|
const doubleEscapedArgs = JSON.stringify({
|
|
files: JSON.stringify([
|
|
{ name: 'test-metric', yml_content: 'name: Test Metric\\nsql: SELECT 1' },
|
|
]),
|
|
});
|
|
(error as MockToolError).args = doubleEscapedArgs;
|
|
|
|
const zodError = new ZodError([
|
|
{
|
|
code: 'invalid_type',
|
|
expected: 'array',
|
|
received: 'string',
|
|
path: ['files'],
|
|
message: 'Expected array, received string',
|
|
},
|
|
]);
|
|
(error as MockToolError).cause = zodError;
|
|
|
|
const availableTools = { 'create-metrics-file': {} };
|
|
const result = healStreamingToolError(error, availableTools);
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.healed).toBe(true);
|
|
expect(result?.healedArgs).toBeDefined();
|
|
expect(result?.healedArgs.files).toBeInstanceOf(Array);
|
|
expect(result?.healedArgs.files[0]).toMatchObject({
|
|
name: 'test-metric',
|
|
yml_content: 'name: Test Metric\\nsql: SELECT 1',
|
|
});
|
|
const toolResult = result?.healingMessage.content[0] as MockToolResult;
|
|
expect(toolResult?.result?.success).toBe(true);
|
|
});
|
|
|
|
it('should heal double-escaped JSON for modify-dashboards-file', () => {
|
|
const error = new Error('Invalid tool arguments');
|
|
error.name = 'AI_InvalidToolArgumentsError';
|
|
(error as MockToolError).toolCallId = 'test-call-id';
|
|
(error as MockToolError).toolName = 'modify-dashboards-file';
|
|
|
|
const doubleEscapedArgs = JSON.stringify({
|
|
files: JSON.stringify([{ name: 'dashboard.json', content: '{"type": "dashboard"}' }]),
|
|
});
|
|
(error as MockToolError).args = doubleEscapedArgs;
|
|
|
|
const zodError = new ZodError([
|
|
{
|
|
code: 'invalid_type',
|
|
expected: 'array',
|
|
received: 'string',
|
|
path: ['files'],
|
|
message: 'Expected array, received string',
|
|
},
|
|
]);
|
|
(error as MockToolError).cause = zodError;
|
|
|
|
const availableTools = { 'modify-dashboards-file': {} };
|
|
const result = healStreamingToolError(error, availableTools);
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.healed).toBe(true);
|
|
expect(result?.healedArgs.files).toBeInstanceOf(Array);
|
|
});
|
|
|
|
it('should provide guidance when files parameter is malformed JSON string', () => {
|
|
const error = new Error('Invalid tool arguments');
|
|
error.name = 'AI_InvalidToolArgumentsError';
|
|
(error as MockToolError).toolCallId = 'test-call-id';
|
|
(error as MockToolError).toolName = 'create-metrics-file';
|
|
|
|
// Malformed JSON that can't be parsed
|
|
const malformedArgs = JSON.stringify({
|
|
files: '[{broken json',
|
|
});
|
|
(error as MockToolError).args = malformedArgs;
|
|
|
|
const zodError = new ZodError([
|
|
{
|
|
code: 'invalid_type',
|
|
expected: 'array',
|
|
received: 'string',
|
|
path: ['files'],
|
|
message: 'Expected array, received string',
|
|
},
|
|
]);
|
|
(error as MockToolError).cause = zodError;
|
|
|
|
const availableTools = { 'create-metrics-file': {} };
|
|
const result = healStreamingToolError(error, availableTools);
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.healed).toBe(false);
|
|
const toolResult = result?.healingMessage.content[0] as MockToolResult;
|
|
expect(toolResult?.result?.error).toContain("files' parameter should be an array");
|
|
});
|
|
|
|
it('should provide generic Zod error for visualization tools with other argument issues', () => {
|
|
const error = new Error('Invalid tool arguments');
|
|
error.name = 'AI_InvalidToolArgumentsError';
|
|
(error as MockToolError).toolCallId = 'test-call-id';
|
|
(error as MockToolError).toolName = 'create-dashboards-file';
|
|
|
|
// Valid JSON but different error (missing required field)
|
|
const validArgs = JSON.stringify({
|
|
files: [{ name: 'test' }], // missing yml_content
|
|
});
|
|
(error as MockToolError).args = validArgs;
|
|
|
|
const zodError = new ZodError([
|
|
{
|
|
code: 'invalid_type',
|
|
expected: 'string',
|
|
received: 'undefined',
|
|
path: ['files', 0, 'yml_content'],
|
|
message: 'Required',
|
|
},
|
|
]);
|
|
(error as MockToolError).cause = zodError;
|
|
|
|
const availableTools = { 'create-dashboards-file': {} };
|
|
const result = healStreamingToolError(error, availableTools);
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.healed).toBe(false);
|
|
const toolResult = result?.healingMessage.content[0] as MockToolResult;
|
|
expect(toolResult?.result?.error).toContain('files.0.yml_content: Required');
|
|
});
|
|
});
|
|
|
|
describe('healStreamingToolError - InvalidToolArgumentsError for non-visualization tools', () => {
|
|
it('should provide generic Zod error for non-visualization tools', () => {
|
|
const error = new Error('Invalid tool arguments');
|
|
error.name = 'AI_InvalidToolArgumentsError';
|
|
(error as MockToolError).toolCallId = 'test-call-id';
|
|
(error as MockToolError).toolName = 'execute-sql';
|
|
|
|
const args = JSON.stringify({
|
|
query: 123, // should be string
|
|
});
|
|
(error as MockToolError).args = args;
|
|
|
|
const zodError = new ZodError([
|
|
{
|
|
code: 'invalid_type',
|
|
expected: 'string',
|
|
received: 'number',
|
|
path: ['query'],
|
|
message: 'Expected string, received number',
|
|
},
|
|
]);
|
|
(error as MockToolError).cause = zodError;
|
|
|
|
const availableTools = { 'execute-sql': {} };
|
|
const result = healStreamingToolError(error, availableTools);
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.healed).toBe(false);
|
|
const toolResult = result?.healingMessage.content[0] as MockToolResult;
|
|
expect(toolResult?.result?.error).toContain('Invalid arguments for execute-sql');
|
|
expect(toolResult?.result?.error).toContain('query: Expected string, received number');
|
|
});
|
|
});
|
|
|
|
describe('healStreamingToolError - unsupported errors', () => {
|
|
it('should return null for non-healable errors', () => {
|
|
const error = new Error('Some other error');
|
|
const availableTools = {};
|
|
|
|
const result = healStreamingToolError(error as HealableStreamError, availableTools);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
});
|