784 lines
25 KiB
TypeScript
Executable File
784 lines
25 KiB
TypeScript
Executable File
import { ToolExecutor } from '../src/tool-executor';
|
|
import { TFile } from 'obsidian';
|
|
import { ToolCall, ToolResult } from '../src/types';
|
|
import { ErrorHandler } from '../src/error-handler';
|
|
|
|
// Mock Obsidian types
|
|
interface MockVault {
|
|
create: (path: string, content: string) => Promise<any>;
|
|
getAbstractFileByPath: (path: string) => any;
|
|
cachedRead: (file: any) => Promise<string>;
|
|
getMarkdownFiles: () => any[];
|
|
}
|
|
interface MockApp {
|
|
// Mock app properties if needed
|
|
}
|
|
interface MockNotice {
|
|
(message: string): void;
|
|
}
|
|
|
|
// Mock Obsidian module
|
|
jest.mock('obsidian', () => {
|
|
class TFile {}
|
|
return {
|
|
Vault: jest.fn(),
|
|
App: jest.fn(),
|
|
Notice: jest.fn(),
|
|
TFile,
|
|
};
|
|
});
|
|
|
|
// Mock ErrorHandler
|
|
jest.mock('../src/error-handler', () => ({
|
|
ErrorHandler: {
|
|
handleError: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('ToolExecutor', () => {
|
|
let executor: ToolExecutor;
|
|
let mockVault: MockVault;
|
|
let mockApp: MockApp;
|
|
|
|
beforeEach(() => {
|
|
mockVault = {
|
|
create: jest.fn().mockResolvedValue(null),
|
|
getAbstractFileByPath: jest.fn(),
|
|
cachedRead: jest.fn().mockResolvedValue(''),
|
|
getMarkdownFiles: jest.fn().mockReturnValue([]),
|
|
};
|
|
mockApp = {} as MockApp;
|
|
executor = new ToolExecutor(mockVault as unknown as any, mockApp as unknown as any);
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('handleToolCall', () => {
|
|
describe('create_file tool', () => {
|
|
it('should successfully create a file with valid arguments', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_1',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith('test-file.md', 'Test content');
|
|
});
|
|
|
|
it('should handle object arguments directly', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_2',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: {
|
|
path: 'obj-args-file.md',
|
|
content: 'Object args content',
|
|
} as unknown as string,
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith('obj-args-file.md', 'Object args content');
|
|
});
|
|
|
|
it('should successfully create a file in a subdirectory', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_3',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'subdirectory/test-file.md',
|
|
content: 'Subdir content',
|
|
}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith(
|
|
'subdirectory/test-file.md',
|
|
'Subdir content'
|
|
);
|
|
});
|
|
|
|
it('should handle multiple slashes gracefully by normalizing path', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_4',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test//file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith('test//file.md', 'Test content');
|
|
});
|
|
|
|
it('should handle empty content gracefully', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_5',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'empty-file.md',
|
|
content: '',
|
|
}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith('empty-file.md', '');
|
|
});
|
|
|
|
it('should allow filenames with consecutive dots', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_6',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'project..notes.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result).toEqual({ success: true, message: 'File created successfully' });
|
|
expect(mockVault.create).toHaveBeenCalledWith('project..notes.md', 'Test content');
|
|
});
|
|
|
|
it('should reject path traversal attempts with ..', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_7',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '../test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject path traversal attempts with .\\', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_8',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '.\\test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject path traversal attempts with /..', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_9',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '/../test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject absolute paths starting with /', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_10',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '/var/test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject absolute paths starting with \\', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_11',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '\\var\\test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject Windows drive letters', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_12',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'C:\\test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject empty path', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_13',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: '',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject undefined path', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_14',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject path with invalid characters <', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_15',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test<file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject path with invalid characters >', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_16',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test>file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject path with invalid characters :', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_17',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test:file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject path with invalid characters |', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_18',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test|file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject path with invalid characters ?', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_19',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test?file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject path with invalid characters *', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_20',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test*file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject path longer than 200 characters', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_21',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'a'.repeat(201) + '.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject path with ~ character', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_22',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test~file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject non-string content', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_23',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test-file.md',
|
|
content: 123,
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should reject non-string path', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_24',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 123,
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle vault.create rejection gracefully', async () => {
|
|
mockVault.create = jest.fn().mockRejectedValue(new Error('Permission denied'));
|
|
const call: ToolCall = {
|
|
id: 'call_25',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test-file.md',
|
|
content: 'Test content',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
});
|
|
|
|
it('should handle invalid JSON in arguments', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_26',
|
|
type: 'function',
|
|
function: {
|
|
name: 'create_file',
|
|
arguments: 'invalid json',
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
expect(mockVault.create).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('read_vault_file tool', () => {
|
|
it('should successfully read an existing file', async () => {
|
|
const mockFile = {
|
|
path: 'test-file.md',
|
|
basename: 'test-file.md',
|
|
};
|
|
// Create a proper mock TFile class for instanceof checks
|
|
class MockTFile extends TFile {
|
|
path: string;
|
|
basename: string;
|
|
extension: string;
|
|
constructor(path: string) {
|
|
super();
|
|
this.path = path;
|
|
this.basename = path.split('/').pop() || path;
|
|
this.extension = this.basename.split('.').pop() || '';
|
|
}
|
|
}
|
|
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(new MockTFile('test-file.md'));
|
|
mockVault.cachedRead = jest.fn().mockResolvedValue('File content');
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([]);
|
|
|
|
const call: ToolCall = {
|
|
id: 'call_28',
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_vault_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test-file.md',
|
|
}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toBe('File read successfully');
|
|
expect(result.data).toEqual({ path: 'test-file.md', content: 'File content' });
|
|
});
|
|
|
|
it('should reject path traversal attempts', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_29',
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_vault_file',
|
|
arguments: JSON.stringify({
|
|
path: '../test-file.md',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
});
|
|
|
|
it('should reject invalid characters in path', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_30',
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_vault_file',
|
|
arguments: JSON.stringify({
|
|
path: 'test<file.md',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
|
});
|
|
|
|
it('should throw error when file not found', async () => {
|
|
// Return null to simulate file not found
|
|
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
|
|
mockVault.cachedRead = jest.fn().mockResolvedValue('');
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([]);
|
|
|
|
const call: ToolCall = {
|
|
id: 'call_31',
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_vault_file',
|
|
arguments: JSON.stringify({
|
|
path: 'nonexistent.md',
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow(
|
|
'File not found: nonexistent.md'
|
|
);
|
|
});
|
|
|
|
it('should reject non-string path', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_32',
|
|
type: 'function',
|
|
function: {
|
|
name: 'read_vault_file',
|
|
arguments: JSON.stringify({
|
|
path: 123,
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow('Path must be a string');
|
|
});
|
|
});
|
|
|
|
describe('search_vault_files tool', () => {
|
|
it('should successfully search vault files', async () => {
|
|
const mockFiles = [
|
|
{ path: 'file1.md', basename: 'file1.md' },
|
|
{ path: 'file2.md', basename: 'file2.md' },
|
|
];
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([
|
|
{ path: 'query.md', basename: 'query.md' },
|
|
{ path: 'other.md', basename: 'other.md' },
|
|
{ path: 'query2.md', basename: 'query2.md' },
|
|
{ path: 'unrelated.md', basename: 'unrelated.md' },
|
|
] as unknown as any[]);
|
|
|
|
const call: ToolCall = {
|
|
id: 'call_33',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 'query',
|
|
}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toContain('Found 2 matching files');
|
|
expect(result.data).toHaveLength(2);
|
|
expect(result.data).toContainEqual({ path: 'query.md', basename: 'query.md' });
|
|
expect(result.data).toContainEqual({ path: 'query2.md', basename: 'query2.md' });
|
|
});
|
|
|
|
it('should limit results based on limit parameter', async () => {
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([
|
|
{ path: 'result1.md', basename: 'result1.md' },
|
|
{ path: 'result2.md', basename: 'result2.md' },
|
|
{ path: 'result3.md', basename: 'result3.md' },
|
|
{ path: 'result4.md', basename: 'result4.md' },
|
|
{ path: 'result5.md', basename: 'result5.md' },
|
|
] as unknown as any[]);
|
|
|
|
const call: ToolCall = {
|
|
id: 'call_34',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 'result',
|
|
limit: 3,
|
|
}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result.data).toHaveLength(3);
|
|
});
|
|
|
|
it('should use default limit of 10 when no limit specified', async () => {
|
|
const mockFiles = Array.from({ length: 15 }, (_, i) => ({
|
|
path: `match${i}.md`,
|
|
basename: `match${i}.md`,
|
|
}));
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue(mockFiles as unknown as any[]);
|
|
|
|
const call: ToolCall = {
|
|
id: 'call_35',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 'match',
|
|
}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result.data).toHaveLength(10);
|
|
});
|
|
|
|
it('should handle case-insensitive search', async () => {
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([
|
|
{ path: 'QUERY.md', basename: 'QUERY.md' },
|
|
{ path: 'Query.md', basename: 'Query.md' },
|
|
{ path: 'query.md', basename: 'query.md' },
|
|
{ path: 'other.md', basename: 'other.md' },
|
|
] as unknown as any[]);
|
|
|
|
const call: ToolCall = {
|
|
id: 'call_36',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 'query',
|
|
}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result.data).toHaveLength(3);
|
|
});
|
|
|
|
it('should reject non-string query', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_37',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 123,
|
|
}),
|
|
},
|
|
};
|
|
await expect(executor.handleToolCall(call)).rejects.toThrow('Query must be a string');
|
|
});
|
|
|
|
it('should return empty array when no matches found', async () => {
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([
|
|
{ path: 'unrelated1.md', basename: 'unrelated1.md' },
|
|
{ path: 'unrelated2.md', basename: 'unrelated2.md' },
|
|
] as unknown as any[]);
|
|
|
|
const call: ToolCall = {
|
|
id: 'call_38',
|
|
type: 'function',
|
|
function: {
|
|
name: 'search_vault_files',
|
|
arguments: JSON.stringify({
|
|
query: 'nomatches',
|
|
}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toHaveLength(0);
|
|
expect(result.message).toContain('Found 0 matching files');
|
|
});
|
|
});
|
|
|
|
describe('executeTool method', () => {
|
|
it('should execute create_file tool successfully', async () => {
|
|
const result = await executor.executeTool('create_file', {
|
|
path: 'test-file.md',
|
|
content: 'Test content',
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(mockVault.create).toHaveBeenCalledWith('test-file.md', 'Test content');
|
|
});
|
|
|
|
it('should execute read_vault_file tool successfully', async () => {
|
|
// Create a proper mock TFile class for instanceof checks
|
|
class MockTFile extends TFile {
|
|
path: string;
|
|
basename: string;
|
|
extension: string;
|
|
constructor(path: string) {
|
|
super();
|
|
this.path = path;
|
|
this.basename = path.split('/').pop() || path;
|
|
this.extension = this.basename.split('.').pop() || '';
|
|
}
|
|
}
|
|
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(new MockTFile('test-file.md'));
|
|
mockVault.cachedRead = jest.fn().mockResolvedValue('File content');
|
|
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([]);
|
|
|
|
const result = await executor.executeTool('read_vault_file', {
|
|
path: 'test-file.md',
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('should execute search_vault_files tool successfully', async () => {
|
|
mockVault.getMarkdownFiles = jest
|
|
.fn()
|
|
.mockReturnValue([{ path: 'match.md', basename: 'match.md' }] as unknown as any[]);
|
|
|
|
const result = await executor.executeTool('search_vault_files', {
|
|
query: 'match',
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toHaveLength(1);
|
|
});
|
|
|
|
it('should handle unknown tool', async () => {
|
|
const result = await executor.executeTool('unknown_tool', {});
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toContain('Unknown tool: unknown_tool');
|
|
});
|
|
});
|
|
|
|
describe('unknown tool', () => {
|
|
it('should return failure for unknown tool', async () => {
|
|
const call: ToolCall = {
|
|
id: 'call_39',
|
|
type: 'function',
|
|
function: {
|
|
name: 'unknown_tool',
|
|
arguments: JSON.stringify({}),
|
|
},
|
|
};
|
|
const result = await executor.handleToolCall(call);
|
|
expect(result).toEqual({ success: false, message: 'Unknown tool: unknown_tool' });
|
|
});
|
|
});
|
|
});
|
|
});
|