Files
obsidian_ollama/tests/tool-executor.test.ts

484 lines
15 KiB
TypeScript
Executable File

import { ToolExecutor } from '../src/tool-executor';
import { ToolCall, ToolResult } from '../src/types';
import { ErrorHandler } from '../src/error-handler';
// Mock Obsidian types
interface MockVault {
create: (path: string, content: string) => Promise<any>;
}
interface MockApp {
// Mock app properties if needed
}
interface MockNotice {
(message: string): void;
}
// Mock Obsidian module
jest.mock('obsidian', () => ({
Vault: jest.fn(),
App: jest.fn(),
Notice: jest.fn(),
}));
// 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),
};
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('unknown tool', () => {
it('should return failure for unknown tool', async () => {
const call: ToolCall = {
id: 'call_27',
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' });
});
});
});
});