mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
- Change ChatMode enum values from numeric (0,1,2) to string literals ("read_only", "edit", "planning")
- Move chat mode state management from ChatWindow to SettingsService with persistence
- Add chat mode awareness to system prompts and tool validation
- Replace Map with WeakMap for settings subscribers to prevent memory leaks
- Add type-safe event handlers to EventService
- Update esbuild and TypeScript targets from ES2018/ES2020 to ES2022
- Add requiresEditModeEnabled() validation to prevent tool hallucination in read-only mode
- Update all test fixtures to include chatMode in mock settings
917 lines
28 KiB
TypeScript
917 lines
28 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { AIToolService } from '../../Services/AIServices/AIToolService';
|
|
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
|
import { Services } from '../../Services/Services';
|
|
import { AITool } from '../../Enums/AITool';
|
|
import { TFile } from 'obsidian';
|
|
import { Exception } from '../../Helpers/Exception';
|
|
import { AbortService } from '../../Services/AbortService';
|
|
import { ChatMode } from '../../Enums/ChatMode';
|
|
|
|
/**
|
|
* INTEGRATION TESTS - AIToolService
|
|
*
|
|
* Tests the AI function dispatcher with real FileSystemService integration.
|
|
* Mocks only the Obsidian API layer (VaultService).
|
|
*
|
|
* Tests all AI functions:
|
|
* - SearchVaultFiles
|
|
* - ReadVaultFiles
|
|
* - WriteVaultFile
|
|
* - PatchVaultFile
|
|
* - DeleteVaultFiles
|
|
* - MoveVaultFiles
|
|
* - RequestWebSearch (Gemini only)
|
|
*/
|
|
|
|
describe('AIToolService - Integration Tests', () => {
|
|
let service: AIToolService;
|
|
let mockFileSystemService: any;
|
|
let abortService: AbortService;
|
|
|
|
beforeEach(() => {
|
|
// Mock FileSystemService with common operations
|
|
mockFileSystemService = {
|
|
searchVaultFiles: vi.fn(),
|
|
listFilesInDirectory: vi.fn(),
|
|
readFilePath: vi.fn(),
|
|
writeToFilePath: vi.fn(),
|
|
patchFileAtPath: vi.fn(),
|
|
deleteFile: vi.fn(),
|
|
moveFile: vi.fn()
|
|
};
|
|
|
|
// Create real AbortService instance
|
|
abortService = new AbortService();
|
|
|
|
// Register the mocks
|
|
RegisterSingleton(Services.FileSystemService, mockFileSystemService);
|
|
RegisterSingleton(Services.AbortService, abortService);
|
|
RegisterSingleton(Services.MemoriesService, {
|
|
readMemories: vi.fn().mockResolvedValue(''),
|
|
updateMemories: vi.fn().mockResolvedValue(undefined)
|
|
});
|
|
RegisterSingleton(Services.SettingsService, {
|
|
settings: { enableMemories: false, allowUpdatingMemories: false, chatMode: ChatMode.Edit }
|
|
});
|
|
RegisterSingleton(Services.WebViewerService, {
|
|
getWebViewContent: vi.fn(),
|
|
takeScreenshot: vi.fn()
|
|
});
|
|
|
|
// Mock Exception.log
|
|
vi.spyOn(Exception, 'log').mockImplementation(() => {});
|
|
|
|
// Create service - it will resolve the mock FileSystemService and real AbortService
|
|
service = new AIToolService();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clear singleton registry to prevent memory leaks
|
|
DeregisterAllServices();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
// Helper to create mock TFile
|
|
function createMockFile(path: string, basename: string): TFile {
|
|
const file = new TFile();
|
|
file.path = path;
|
|
file.basename = basename;
|
|
file.name = basename + '.md';
|
|
file.extension = 'md';
|
|
return file;
|
|
}
|
|
|
|
describe('performAITool - SearchVaultFiles', () => {
|
|
it('should return search results with snippets', async () => {
|
|
const mockMatches = [
|
|
{
|
|
file: createMockFile('notes/test.md', 'test'),
|
|
snippets: [
|
|
{ text: 'This is a test note', matchIndex: 10 },
|
|
{ text: 'Another test match', matchIndex: 5 }
|
|
]
|
|
},
|
|
{
|
|
file: createMockFile('docs/guide.md', 'guide'),
|
|
snippets: [
|
|
{ text: 'Guide for testing', matchIndex: 0 }
|
|
]
|
|
}
|
|
];
|
|
|
|
mockFileSystemService.searchVaultFiles.mockResolvedValue(mockMatches);
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.SearchVaultFiles,
|
|
arguments: { search_terms: ['test'], user_message: 'test search' },
|
|
toolId: 'tool_1'
|
|
} as any);
|
|
|
|
expect(result.name).toBe(AITool.SearchVaultFiles);
|
|
expect(result.toolId).toBe('tool_1');
|
|
expect(result.payload.response).toEqual([{searchTerm: 'test', results: [
|
|
{
|
|
path: 'notes/test.md',
|
|
snippets: [
|
|
{ text: 'This is a test note', matchPosition: 10 },
|
|
{ text: 'Another test match', matchPosition: 5 }
|
|
]
|
|
},
|
|
{
|
|
path: 'docs/guide.md',
|
|
snippets: [
|
|
{ text: 'Guide for testing', matchPosition: 0 }
|
|
]
|
|
}
|
|
]}]);
|
|
});
|
|
|
|
it('should return empty array when search term is empty', async () => {
|
|
// Mock returns empty array for empty search term
|
|
mockFileSystemService.searchVaultFiles.mockResolvedValue([]);
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.SearchVaultFiles,
|
|
arguments: { search_terms: [''], user_message: 'test search' },
|
|
toolId: 'tool_2'
|
|
} as any);
|
|
|
|
// Empty search terms return empty results
|
|
expect(result.payload.response).toEqual([{searchTerm: '', results: []}]);
|
|
});
|
|
|
|
it('should return empty array when search term is whitespace', async () => {
|
|
// Mock returns empty array for whitespace search term
|
|
mockFileSystemService.searchVaultFiles.mockResolvedValue([]);
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.SearchVaultFiles,
|
|
arguments: { search_terms: [' '], user_message: 'test search' },
|
|
toolId: 'tool_3'
|
|
} as any);
|
|
|
|
// Whitespace search terms return empty results (after trim)
|
|
expect(result.payload.response).toEqual([{searchTerm: ' ', results: []}]);
|
|
});
|
|
|
|
it('should return empty array when no matches found', async () => {
|
|
mockFileSystemService.searchVaultFiles.mockResolvedValue([]);
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.SearchVaultFiles,
|
|
arguments: { search_terms: ['nonexistent'], user_message: 'test search' },
|
|
toolId: 'tool_4'
|
|
} as any);
|
|
|
|
// No matches returns empty results
|
|
expect(result.payload.response).toEqual([{searchTerm: 'nonexistent', results: []}]);
|
|
});
|
|
|
|
it('should handle single match', async () => {
|
|
const mockMatches = [
|
|
{
|
|
file: createMockFile('single.md', 'single'),
|
|
snippets: [{ text: 'Single result', matchIndex: 0 }]
|
|
}
|
|
];
|
|
|
|
mockFileSystemService.searchVaultFiles.mockResolvedValue(mockMatches);
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.SearchVaultFiles,
|
|
arguments: { search_terms: ['single'], user_message: 'test search' },
|
|
toolId: 'tool_5'
|
|
} as any);
|
|
|
|
expect(result.payload.response).toHaveLength(1);
|
|
expect((result.payload.response as any)[0].searchTerm).toBe('single');
|
|
expect((result.payload.response as any)[0].results).toHaveLength(1);
|
|
expect((result.payload.response as any)[0].results[0].path).toBe('single.md');
|
|
});
|
|
});
|
|
|
|
describe('performAITool - ReadVaultFiles', () => {
|
|
it('should read multiple files successfully', async () => {
|
|
mockFileSystemService.readFilePath
|
|
.mockResolvedValueOnce('Content of file 1')
|
|
.mockResolvedValueOnce('Content of file 2')
|
|
.mockResolvedValueOnce('Content of file 3');
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.ReadVaultFiles,
|
|
arguments: { file_paths: ['file1.md', 'file2.md', 'file3.md'], user_message: 'test search' },
|
|
toolId: 'tool_6'
|
|
} as any);
|
|
|
|
expect(result.payload.response).toEqual({
|
|
results: [
|
|
{ type: 'md', path: 'file1.md', contents: 'Content of file 1' },
|
|
{ type: 'md', path: 'file2.md', contents: 'Content of file 2' },
|
|
{ type: 'md', path: 'file3.md', contents: 'Content of file 3' }
|
|
]
|
|
});
|
|
});
|
|
|
|
it('should handle missing files with error messages', async () => {
|
|
const error1 = new Error('File not found');
|
|
const error2 = new Error('File not found');
|
|
|
|
mockFileSystemService.readFilePath
|
|
.mockResolvedValueOnce('Existing content')
|
|
.mockResolvedValueOnce(error1)
|
|
.mockResolvedValueOnce(error2);
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.ReadVaultFiles,
|
|
arguments: { file_paths: ['exists.md', 'missing1.md', 'missing2.md'], user_message: 'test search' },
|
|
toolId: 'tool_7'
|
|
} as any);
|
|
|
|
expect(result.payload.response).toEqual({
|
|
results: [
|
|
{ type: 'md', path: 'exists.md', contents: 'Existing content' },
|
|
{ path: 'missing1.md', error: 'File not found' },
|
|
{ path: 'missing2.md', error: 'File not found' }
|
|
]
|
|
});
|
|
});
|
|
|
|
it('should handle mixed success and failure', async () => {
|
|
mockFileSystemService.readFilePath
|
|
.mockResolvedValueOnce('Content A')
|
|
.mockResolvedValueOnce(new Error('File not found'))
|
|
.mockResolvedValueOnce('Content B');
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.ReadVaultFiles,
|
|
arguments: { file_paths: ['a.md', 'missing.md', 'b.md'], user_message: 'test search' },
|
|
toolId: 'tool_8'
|
|
} as any);
|
|
|
|
const results = (result.payload.response as any).results;
|
|
expect(results.find((r: any) => r.contents === 'Content A')).toBeDefined();
|
|
expect(results.find((r: any) => r.contents === 'Content B')).toBeDefined();
|
|
expect(results.find((r: any) => r.error === 'File not found')).toBeDefined();
|
|
});
|
|
|
|
it('should handle empty file list', async () => {
|
|
const result = await service.performAITool({
|
|
name: AITool.ReadVaultFiles,
|
|
arguments: { file_paths: [], user_message: 'test search' },
|
|
toolId: 'tool_9'
|
|
} as any);
|
|
|
|
expect(result.payload.response).toEqual({ message: 'Files retrieved successfully. The contents of the files are included below.', count: 0 });
|
|
});
|
|
|
|
it('should handle single file read', async () => {
|
|
mockFileSystemService.readFilePath.mockResolvedValue('Single file content');
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.ReadVaultFiles,
|
|
arguments: { file_paths: ['single.md'], user_message: 'test search' },
|
|
toolId: 'tool_10'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).results).toHaveLength(1);
|
|
expect((result.payload.response as any).results[0].contents).toBe('Single file content');
|
|
});
|
|
});
|
|
|
|
describe('performAITool - WriteVaultFile', () => {
|
|
it('should write file successfully', async () => {
|
|
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.WriteVaultFile,
|
|
arguments: {
|
|
file_path: 'notes/new-note.md',
|
|
content: '# New Note\n\nContent here',
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_11'
|
|
} as any);
|
|
|
|
expect(mockFileSystemService.writeToFilePath).toHaveBeenCalledWith(
|
|
'notes/new-note.md',
|
|
'# New Note\n\nContent here'
|
|
);
|
|
expect(result.payload.response).toEqual({ success: true });
|
|
});
|
|
|
|
it('should handle write failure', async () => {
|
|
const error = new Error('Permission denied');
|
|
mockFileSystemService.writeToFilePath.mockResolvedValue(error);
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.WriteVaultFile,
|
|
arguments: {
|
|
file_path: 'protected.md',
|
|
content: 'Content',
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_12'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).success).toBe(false);
|
|
expect((result.payload.response as any).error).toBeDefined();
|
|
});
|
|
|
|
it('should normalize file path', async () => {
|
|
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
|
|
|
|
await service.performAITool({
|
|
name: AITool.WriteVaultFile,
|
|
arguments: {
|
|
file_path: 'folder\\subfolder\\file.md',
|
|
content: 'Content',
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_13'
|
|
} as any);
|
|
|
|
// normalizePath should convert backslashes to forward slashes
|
|
expect(mockFileSystemService.writeToFilePath).toHaveBeenCalledWith(
|
|
expect.stringContaining('/'),
|
|
'Content'
|
|
);
|
|
});
|
|
|
|
it('should handle empty content', async () => {
|
|
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.WriteVaultFile,
|
|
arguments: {
|
|
file_path: 'empty.md',
|
|
content: '',
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_14'
|
|
} as any);
|
|
|
|
expect(mockFileSystemService.writeToFilePath).toHaveBeenCalledWith('empty.md', '');
|
|
expect((result.payload.response as any).success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('performAITool - PatchVaultFile', () => {
|
|
it('should apply patch successfully', async () => {
|
|
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('notes/test.md', 'test'));
|
|
|
|
const oldContent = ['old content'];
|
|
const newContent = ['new content'];
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.PatchVaultFile,
|
|
arguments: {
|
|
file_path: 'notes/test.md',
|
|
oldContent: oldContent,
|
|
newContent: newContent,
|
|
user_message: 'Updating test note'
|
|
},
|
|
toolId: 'tool_patch_1'
|
|
} as any);
|
|
|
|
expect(mockFileSystemService.patchFileAtPath).toHaveBeenCalledWith('notes/test.md', oldContent, newContent);
|
|
expect(result.payload.response).toEqual({ success: true });
|
|
});
|
|
|
|
it('should handle patch failure', async () => {
|
|
const error = new Error('Content to replace was not found in the file');
|
|
mockFileSystemService.patchFileAtPath.mockResolvedValue(error);
|
|
|
|
const oldContent = ['old content'];
|
|
const newContent = ['new content'];
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.PatchVaultFile,
|
|
arguments: {
|
|
file_path: 'notes/test.md',
|
|
oldContent: oldContent,
|
|
newContent: newContent,
|
|
user_message: 'Updating note'
|
|
},
|
|
toolId: 'tool_patch_2'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).success).toBe(false);
|
|
expect((result.payload.response as any).error).toBe('Content to replace was not found in the file');
|
|
});
|
|
|
|
it('should normalize file path', async () => {
|
|
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('folder/file.md', 'file'));
|
|
|
|
const oldContent = ['old'];
|
|
const newContent = ['new'];
|
|
|
|
await service.performAITool({
|
|
name: AITool.PatchVaultFile,
|
|
arguments: {
|
|
file_path: 'folder\\subfolder\\file.md',
|
|
oldContent: oldContent,
|
|
newContent: newContent,
|
|
user_message: 'Patching file'
|
|
},
|
|
toolId: 'tool_patch_3'
|
|
} as any);
|
|
|
|
// normalizePath should convert backslashes to forward slashes
|
|
expect(mockFileSystemService.patchFileAtPath).toHaveBeenCalledWith(
|
|
expect.stringContaining('/'),
|
|
oldContent,
|
|
newContent
|
|
);
|
|
});
|
|
|
|
it('should handle file not found error', async () => {
|
|
const error = new Error('File does not exist: missing.md');
|
|
mockFileSystemService.patchFileAtPath.mockResolvedValue(error);
|
|
|
|
const oldContent = ['old'];
|
|
const newContent = ['new'];
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.PatchVaultFile,
|
|
arguments: {
|
|
file_path: 'missing.md',
|
|
oldContent: oldContent,
|
|
newContent: newContent,
|
|
user_message: 'Patching missing file'
|
|
},
|
|
toolId: 'tool_patch_4'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).success).toBe(false);
|
|
expect((result.payload.response as any).error).toContain('File does not exist');
|
|
});
|
|
|
|
it('should handle complex multi-line replacement', async () => {
|
|
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('complex.md', 'complex'));
|
|
|
|
const oldContent = ['# Title\nOld line 1\nContext line'];
|
|
const newContent = ['# Title\nNew line 1\nContext line'];
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.PatchVaultFile,
|
|
arguments: {
|
|
file_path: 'complex.md',
|
|
oldContent: oldContent,
|
|
newContent: newContent,
|
|
user_message: 'Applying complex patch'
|
|
},
|
|
toolId: 'tool_patch_5'
|
|
} as any);
|
|
|
|
expect(mockFileSystemService.patchFileAtPath).toHaveBeenCalledWith('complex.md', oldContent, newContent);
|
|
expect((result.payload.response as any).success).toBe(true);
|
|
});
|
|
|
|
it('should handle adding new content', async () => {
|
|
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('additions.md', 'additions'));
|
|
|
|
const oldContent = ['# Title\nExisting content'];
|
|
const newContent = ['# Title\nExisting content\nNew line 1\nNew line 2'];
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.PatchVaultFile,
|
|
arguments: {
|
|
file_path: 'additions.md',
|
|
oldContent: oldContent,
|
|
newContent: newContent,
|
|
user_message: 'Adding new lines'
|
|
},
|
|
toolId: 'tool_patch_6'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).success).toBe(true);
|
|
});
|
|
|
|
it('should handle removing content', async () => {
|
|
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('deletions.md', 'deletions'));
|
|
|
|
const oldContent = ['# Title\nLine to remove 1\nLine to remove 2\nRemaining content'];
|
|
const newContent = ['# Title\nRemaining content'];
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.PatchVaultFile,
|
|
arguments: {
|
|
file_path: 'deletions.md',
|
|
oldContent: oldContent,
|
|
newContent: newContent,
|
|
user_message: 'Removing lines'
|
|
},
|
|
toolId: 'tool_patch_7'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).success).toBe(true);
|
|
});
|
|
|
|
it('should handle permission denied error', async () => {
|
|
const error = new Error('Permission denied');
|
|
mockFileSystemService.patchFileAtPath.mockResolvedValue(error);
|
|
|
|
const oldContent = ['old'];
|
|
const newContent = ['new'];
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.PatchVaultFile,
|
|
arguments: {
|
|
file_path: 'protected.md',
|
|
oldContent: oldContent,
|
|
newContent: newContent,
|
|
user_message: 'Patching protected file'
|
|
},
|
|
toolId: 'tool_patch_8'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).success).toBe(false);
|
|
expect((result.payload.response as any).error).toBe('Permission denied');
|
|
});
|
|
|
|
it('should return correct toolId in response', async () => {
|
|
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('test.md', 'test'));
|
|
|
|
const oldContent = 'old';
|
|
const newContent = 'new';
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.PatchVaultFile,
|
|
arguments: {
|
|
file_path: 'test.md',
|
|
oldContent: oldContent,
|
|
newContent: newContent,
|
|
user_message: 'Test patch'
|
|
},
|
|
toolId: 'unique_tool_id_123'
|
|
} as any);
|
|
|
|
expect(result.toolId).toBe('unique_tool_id_123');
|
|
expect(result.name).toBe(AITool.PatchVaultFile);
|
|
});
|
|
|
|
it('should handle invalid arguments', async () => {
|
|
const result = await service.performAITool({
|
|
name: AITool.PatchVaultFile,
|
|
arguments: {
|
|
// missing required fields
|
|
file_path: 'test.md'
|
|
// oldContent, newContent and user_message are missing
|
|
},
|
|
toolId: 'tool_patch_invalid'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).error).toContain('Invalid arguments for patch_vault_file');
|
|
expect(mockFileSystemService.patchFileAtPath).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('performAITool - DeleteVaultFiles', () => {
|
|
it('should delete multiple files successfully', async () => {
|
|
mockFileSystemService.deleteFile
|
|
.mockResolvedValueOnce({ success: true })
|
|
.mockResolvedValueOnce({ success: true })
|
|
.mockResolvedValueOnce({ success: true });
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.DeleteVaultFiles,
|
|
arguments: {
|
|
file_paths: ['file1.md', 'file2.md', 'file3.md'],
|
|
confirm_deletion: true,
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_15'
|
|
} as any);
|
|
|
|
expect(result.payload.response).toEqual({
|
|
results: [
|
|
{ path: 'file1.md', success: true },
|
|
{ path: 'file2.md', success: true },
|
|
{ path: 'file3.md', success: true }
|
|
]
|
|
});
|
|
});
|
|
|
|
it('should reject deletion when confirmation is false', async () => {
|
|
const result = await service.performAITool({
|
|
name: AITool.DeleteVaultFiles,
|
|
arguments: {
|
|
file_paths: ['file1.md', 'file2.md'],
|
|
confirm_deletion: false,
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_16'
|
|
} as any);
|
|
|
|
expect(result.payload.response).toEqual({
|
|
error: 'Confirmation was false, no action taken'
|
|
});
|
|
expect(mockFileSystemService.deleteFile).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle mixed success and failure', async () => {
|
|
const error = new Error('File not found');
|
|
|
|
mockFileSystemService.deleteFile
|
|
.mockResolvedValueOnce(undefined) // void = success
|
|
.mockResolvedValueOnce(error)
|
|
.mockResolvedValueOnce(undefined); // void = success
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.DeleteVaultFiles,
|
|
arguments: {
|
|
file_paths: ['a.md', 'missing.md', 'c.md'],
|
|
confirm_deletion: true,
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_17'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).results).toEqual([
|
|
{ path: 'a.md', success: true },
|
|
{ path: 'missing.md', success: false, error: 'File not found' },
|
|
{ path: 'c.md', success: true }
|
|
]);
|
|
});
|
|
|
|
it('should handle all failures', async () => {
|
|
mockFileSystemService.deleteFile
|
|
.mockResolvedValueOnce(new Error('Error 1'))
|
|
.mockResolvedValueOnce(new Error('Error 2'));
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.DeleteVaultFiles,
|
|
arguments: {
|
|
file_paths: ['file1.md', 'file2.md'],
|
|
confirm_deletion: true,
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_18'
|
|
} as any);
|
|
|
|
const results = (result.payload.response as any).results;
|
|
expect(results[0].success).toBe(false);
|
|
expect(results[1].success).toBe(false);
|
|
});
|
|
|
|
it('should handle empty file list with confirmation', async () => {
|
|
const result = await service.performAITool({
|
|
name: AITool.DeleteVaultFiles,
|
|
arguments: {
|
|
file_paths: [],
|
|
confirm_deletion: true,
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_19'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).results).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('performAITool - MoveVaultFiles', () => {
|
|
it('should move multiple files successfully', async () => {
|
|
mockFileSystemService.moveFile
|
|
.mockResolvedValueOnce({ success: true })
|
|
.mockResolvedValueOnce({ success: true })
|
|
.mockResolvedValueOnce({ success: true });
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.MoveVaultFiles,
|
|
arguments: {
|
|
source_paths: ['a.md', 'b.md', 'c.md'],
|
|
destination_paths: ['dest/a.md', 'dest/b.md', 'dest/c.md'],
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_20'
|
|
} as any);
|
|
|
|
expect(result.payload.response).toEqual({
|
|
results: [
|
|
{ path: 'dest/a.md', success: true },
|
|
{ path: 'dest/b.md', success: true },
|
|
{ path: 'dest/c.md', success: true }
|
|
]
|
|
});
|
|
});
|
|
|
|
it('should reject when array lengths dont match', async () => {
|
|
const result = await service.performAITool({
|
|
name: AITool.MoveVaultFiles,
|
|
arguments: {
|
|
source_paths: ['a.md', 'b.md'],
|
|
destination_paths: ['dest/a.md'],
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_21'
|
|
} as any);
|
|
|
|
expect(result.payload.response).toEqual({
|
|
error: 'Source paths array length does not equal destination paths array length'
|
|
});
|
|
expect(mockFileSystemService.moveFile).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle mixed success and failure', async () => {
|
|
const error = new Error('Destination exists');
|
|
|
|
mockFileSystemService.moveFile
|
|
.mockResolvedValueOnce(undefined) // void = success
|
|
.mockResolvedValueOnce(error)
|
|
.mockResolvedValueOnce(undefined); // void = success
|
|
|
|
const result = await service.performAITool({
|
|
name: AITool.MoveVaultFiles,
|
|
arguments: {
|
|
source_paths: ['a.md', 'b.md', 'c.md'],
|
|
destination_paths: ['new/a.md', 'existing.md', 'new/c.md'],
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_22'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).results).toEqual([
|
|
{ path: 'new/a.md', success: true },
|
|
{ path: 'existing.md', success: false, error: 'Destination exists' },
|
|
{ path: 'new/c.md', success: true }
|
|
]);
|
|
});
|
|
|
|
it('should call moveFile with correct parameters', async () => {
|
|
mockFileSystemService.moveFile.mockResolvedValue(undefined); // void = success
|
|
|
|
await service.performAITool({
|
|
name: AITool.MoveVaultFiles,
|
|
arguments: {
|
|
source_paths: ['old/file.md'],
|
|
destination_paths: ['new/file.md'],
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_23'
|
|
} as any);
|
|
|
|
expect(mockFileSystemService.moveFile).toHaveBeenCalledWith('old/file.md', 'new/file.md');
|
|
});
|
|
|
|
it('should handle empty arrays', async () => {
|
|
const result = await service.performAITool({
|
|
name: AITool.MoveVaultFiles,
|
|
arguments: {
|
|
source_paths: [],
|
|
destination_paths: [],
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'tool_24'
|
|
} as any);
|
|
|
|
expect((result.payload.response as any).results).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('performAITool - RequestWebSearch', () => {
|
|
it('should return empty object for Gemini web search', async () => {
|
|
const result = await service.performAITool({
|
|
name: AITool.RequestWebSearch,
|
|
arguments: {},
|
|
toolId: 'tool_25'
|
|
} as any);
|
|
|
|
expect(result.name).toBe(AITool.RequestWebSearch);
|
|
expect(result.payload.response).toEqual({});
|
|
expect(result.toolId).toBe('tool_25');
|
|
});
|
|
|
|
it('should handle web search without arguments', async () => {
|
|
const result = await service.performAITool({
|
|
name: AITool.RequestWebSearch,
|
|
arguments: {},
|
|
toolId: 'tool_26'
|
|
} as any);
|
|
|
|
expect(result.payload.response).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('performAITool - Unknown Function', () => {
|
|
it('should return unknown response for unknown function', async () => {
|
|
const result = await service.performAITool({
|
|
name: 'UnknownFunction' as any,
|
|
arguments: {},
|
|
toolId: 'tool_27'
|
|
} as any);
|
|
expect(result.name).toBe('unknown');
|
|
expect(result.toolId).toBe('tool_27');
|
|
expect(result.payload.response).toEqual({ error: 'Unknown function request unknown' });
|
|
});
|
|
|
|
it('should return unknown response for invalid function', async () => {
|
|
const result = await service.performAITool({
|
|
name: 'InvalidFunction' as any,
|
|
arguments: {},
|
|
toolId: 'tool_error'
|
|
} as any);
|
|
expect(result.name).toBe('unknown');
|
|
expect(result.toolId).toBe('tool_error');
|
|
expect(result.payload.response).toEqual({ error: 'Unknown function request unknown' });
|
|
});
|
|
});
|
|
|
|
describe('Integration - Complete Workflows', () => {
|
|
it('should handle search -> read workflow', async () => {
|
|
// First search
|
|
const mockMatches = [
|
|
{
|
|
file: createMockFile('found.md', 'found'),
|
|
snippets: [{ text: 'Found content', matchIndex: 0 }]
|
|
}
|
|
];
|
|
mockFileSystemService.searchVaultFiles.mockResolvedValue(mockMatches);
|
|
|
|
const searchResult = await service.performAITool({
|
|
name: AITool.SearchVaultFiles,
|
|
arguments: { search_terms: ['test'], user_message: 'test search' },
|
|
toolId: 'search_1'
|
|
} as any);
|
|
|
|
const foundPath = (searchResult.payload.response as any)[0].results[0].path;
|
|
|
|
// Then read
|
|
mockFileSystemService.readFilePath.mockResolvedValue('File content here');
|
|
|
|
const readResult = await service.performAITool({
|
|
name: AITool.ReadVaultFiles,
|
|
arguments: { file_paths: [foundPath], user_message: 'test search' },
|
|
toolId: 'read_1'
|
|
} as any);
|
|
|
|
expect((readResult.payload.response as any).results[0].contents).toBe('File content here');
|
|
expect((readResult.payload.response as any).results[0].error).toBeUndefined();
|
|
});
|
|
|
|
it('should handle write -> move workflow', async () => {
|
|
// First write
|
|
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
|
|
|
|
const writeResult = await service.performAITool({
|
|
name: AITool.WriteVaultFile,
|
|
arguments: {
|
|
file_path: 'temp.md',
|
|
content: 'Temporary content',
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'write_1'
|
|
} as any);
|
|
|
|
expect((writeResult.payload.response as any).success).toBe(true);
|
|
|
|
// Then move
|
|
mockFileSystemService.moveFile.mockResolvedValue({ success: true });
|
|
|
|
const moveResult = await service.performAITool({
|
|
name: AITool.MoveVaultFiles,
|
|
arguments: {
|
|
source_paths: ['temp.md'],
|
|
destination_paths: ['archive/temp.md'],
|
|
user_message: 'test search'
|
|
},
|
|
toolId: 'move_1'
|
|
} as any);
|
|
|
|
expect((moveResult.payload.response as any).results[0].success).toBe(true);
|
|
});
|
|
|
|
it('should handle read -> patch workflow', async () => {
|
|
// First read the file
|
|
mockFileSystemService.readFilePath.mockResolvedValue('# Original Title\n\nOriginal content');
|
|
|
|
const readResult = await service.performAITool({
|
|
name: AITool.ReadVaultFiles,
|
|
arguments: { file_paths: ['document.md'], user_message: 'Reading file' },
|
|
toolId: 'read_2'
|
|
} as any);
|
|
|
|
expect((readResult.payload.response as any).results[0].contents).toContain('Original');
|
|
|
|
// Then patch it
|
|
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('document.md', 'document'));
|
|
|
|
const oldContent = ['# Original Title'];
|
|
const newContent = ['# Updated Title'];
|
|
|
|
const patchResult = await service.performAITool({
|
|
name: AITool.PatchVaultFile,
|
|
arguments: {
|
|
file_path: 'document.md',
|
|
oldContent: oldContent,
|
|
newContent: newContent,
|
|
user_message: 'Updating title'
|
|
},
|
|
toolId: 'patch_1'
|
|
} as any);
|
|
|
|
expect((patchResult.payload.response as any).success).toBe(true);
|
|
expect(mockFileSystemService.patchFileAtPath).toHaveBeenCalledWith('document.md', oldContent, newContent);
|
|
});
|
|
});
|
|
});
|