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'; import { Artifact } from '../../Conversations/Artifact'; /** * 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(), readFile: vi.fn(), readBinaryFile: 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 as any).count).toBe(0); expect((result.payload.response as any).message).toContain('attached to the conversation'); }); 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 produce an artifact tracking the before/after content on success', async () => { mockFileSystemService.readFilePath.mockResolvedValue('Old content'); mockFileSystemService.writeToFilePath.mockResolvedValue(createMockFile('notes/new-note.md', 'new-note')); mockFileSystemService.readFile.mockResolvedValue('# New Note\n\nContent here'); 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_11b' } as any); expect(result.payload.artifacts).toHaveLength(1); const artifact = result.payload.artifacts[0]; expect(artifact).toBeInstanceOf(Artifact); expect(artifact.filePath).toBe('notes/new-note.md'); expect(artifact.originalContent).toBe('Old content'); expect(artifact.updatedContent).toBe('# New Note\n\nContent here'); }); it('should track empty originalContent when the file did not previously exist', async () => { mockFileSystemService.readFilePath.mockResolvedValue(new Error('File does not exist')); mockFileSystemService.writeToFilePath.mockResolvedValue(createMockFile('brand-new.md', 'brand-new')); mockFileSystemService.readFile.mockResolvedValue('Brand new content'); const result = await service.performAITool({ name: AITool.WriteVaultFile, arguments: { file_path: 'brand-new.md', content: 'Brand new content', user_message: 'test search' }, toolId: 'tool_11c' } as any); expect(result.payload.artifacts[0].originalContent).toBe(''); expect(result.payload.artifacts[0].updatedContent).toBe('Brand new content'); }); it('should not produce an artifact when the write fails', async () => { mockFileSystemService.writeToFilePath.mockResolvedValue(new Error('Permission denied')); const result = await service.performAITool({ name: AITool.WriteVaultFile, arguments: { file_path: 'protected.md', content: 'Content', user_message: 'test search' }, toolId: 'tool_11d' } as any); expect(result.payload.artifacts).toEqual([]); }); 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 produce an artifact tracking the before/after content on success', async () => { mockFileSystemService.readFilePath.mockResolvedValue('# Title\nold content'); mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('notes/test.md', 'test')); mockFileSystemService.readFile.mockResolvedValue('# Title\nnew content'); const result = await service.performAITool({ name: AITool.PatchVaultFile, arguments: { file_path: 'notes/test.md', oldContent: ['old content'], newContent: ['new content'], user_message: 'Updating test note' }, toolId: 'tool_patch_1b' } as any); expect(result.payload.artifacts).toHaveLength(1); const artifact = result.payload.artifacts[0]; expect(artifact).toBeInstanceOf(Artifact); expect(artifact.filePath).toBe('notes/test.md'); expect(artifact.originalContent).toBe('# Title\nold content'); expect(artifact.updatedContent).toBe('# Title\nnew content'); }); it('should not produce an artifact when the patch fails', async () => { mockFileSystemService.readFilePath.mockResolvedValue('# Title\nold content'); mockFileSystemService.patchFileAtPath.mockResolvedValue(new Error('Content to replace was not found in the file')); const result = await service.performAITool({ name: AITool.PatchVaultFile, arguments: { file_path: 'notes/test.md', oldContent: ['missing content'], newContent: ['new content'], user_message: 'Updating test note' }, toolId: 'tool_patch_1c' } as any); expect(result.payload.artifacts).toEqual([]); }); 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([]); }); it('should produce artifacts capturing content of deleted text files', async () => { mockFileSystemService.readFilePath.mockResolvedValue('Content before deletion'); mockFileSystemService.deleteFile.mockResolvedValue(undefined); const result = await service.performAITool({ name: AITool.DeleteVaultFiles, arguments: { file_paths: ['notes/gone.md'], confirm_deletion: true, user_message: 'test search' }, toolId: 'tool_19b' } as any); expect(result.payload.artifacts).toHaveLength(1); const artifact = result.payload.artifacts[0]; expect(artifact).toBeInstanceOf(Artifact); expect(artifact.filePath).toBe('notes/gone.md'); expect(artifact.originalContent).toBe('Content before deletion'); expect(artifact.updatedContent).toBe(''); }); it('should produce base64 artifacts capturing content of deleted binary files', async () => { const buffer = new TextEncoder().encode('binary-bytes').buffer; mockFileSystemService.readBinaryFile.mockResolvedValue(buffer); mockFileSystemService.deleteFile.mockResolvedValue(undefined); const result = await service.performAITool({ name: AITool.DeleteVaultFiles, arguments: { file_paths: ['images/gone.png'], confirm_deletion: true, user_message: 'test search' }, toolId: 'tool_19c' } as any); expect(result.payload.artifacts).toHaveLength(1); const artifact = result.payload.artifacts[0]; expect(artifact.filePath).toBe('images/gone.png'); expect(artifact.base64).toBeDefined(); expect(mockFileSystemService.readFilePath).not.toHaveBeenCalled(); }); it('should not collect artifacts when confirmation is false', async () => { const result = await service.performAITool({ name: AITool.DeleteVaultFiles, arguments: { file_paths: ['notes/gone.md'], confirm_deletion: false, user_message: 'test search' }, toolId: 'tool_19d' } as any); expect(result.payload.artifacts).toEqual([]); expect(mockFileSystemService.readFilePath).not.toHaveBeenCalled(); }); }); describe('performAITool - DeleteVaultFolder', () => { it('should delete folder successfully and produce artifacts for its contents', async () => { mockFileSystemService.listDirectoryContents = vi.fn().mockResolvedValue([ createMockFile('folder/a.md', 'a'), createMockFile('folder/b.md', 'b') ]); mockFileSystemService.readFilePath .mockResolvedValueOnce('Content A') .mockResolvedValueOnce('Content B'); mockFileSystemService.deleteFolder = vi.fn().mockResolvedValue(undefined); const result = await service.performAITool({ name: AITool.DeleteVaultFolder, arguments: { path: 'folder', confirm_deletion: true, user_message: 'test search' }, toolId: 'tool_folder_1' } as any); expect(mockFileSystemService.deleteFolder).toHaveBeenCalledWith('folder'); expect(result.payload.response).toEqual({ path: 'folder', success: true }); expect(result.payload.artifacts).toHaveLength(2); expect(result.payload.artifacts.map((a: Artifact) => a.filePath)).toEqual(['folder/a.md', 'folder/b.md']); expect(result.payload.artifacts[0].originalContent).toBe('Content A'); }); it('should reject deletion when confirmation is false', async () => { mockFileSystemService.listDirectoryContents = vi.fn(); mockFileSystemService.deleteFolder = vi.fn(); const result = await service.performAITool({ name: AITool.DeleteVaultFolder, arguments: { path: 'folder', confirm_deletion: false, user_message: 'test search' }, toolId: 'tool_folder_2' } as any); expect(result.payload.response).toEqual({ error: 'Confirmation was false, no action taken' }); expect(mockFileSystemService.deleteFolder).not.toHaveBeenCalled(); }); it('should still return collected artifacts when the delete fails', async () => { mockFileSystemService.listDirectoryContents = vi.fn().mockResolvedValue([ createMockFile('folder/a.md', 'a') ]); mockFileSystemService.readFilePath.mockResolvedValue('Content A'); mockFileSystemService.deleteFolder = vi.fn().mockResolvedValue(new Error('Permission denied')); const result = await service.performAITool({ name: AITool.DeleteVaultFolder, arguments: { path: 'folder', confirm_deletion: true, user_message: 'test search' }, toolId: 'tool_folder_3' } as any); expect((result.payload.response as any).success).toBe(false); expect(result.payload.artifacts).toHaveLength(1); }); }); 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); }); }); });