From 5760ac9b8b753580839a973f2a9f0a08fb7a6519 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 20 Oct 2025 00:20:12 -0400 Subject: [PATCH] test: add comprehensive VaultTools coverage tests Added extensive test coverage for VaultTools to increase coverage from 54.9% to 93.83%: getVaultInfo tests: - Return vault info with total notes and size - Handle empty vault - Handle files with missing stat info - Handle errors gracefully - Format large file sizes correctly (KB, MB, GB) search tests: - Search for literal text - Search with regex pattern - Handle invalid regex pattern - Filter by folder - Respect maxResults limit - Handle file read errors gracefully - Handle case sensitive search - Extract snippets correctly - Handle zero-width regex matches - Handle general search errors Waypoint tests (searchWaypoints, getFolderWaypoint, isFolderNote): - Search for waypoints in vault - Filter waypoints by folder - Extract waypoint from file - Detect folder notes - Handle file not found errors - Handle general errors resolveWikilink tests: - Resolve wikilink successfully - Provide suggestions for unresolved links - Handle errors gracefully - Handle invalid source path getBacklinks unlinked mentions tests: - Find unlinked mentions - Skip files that already have linked backlinks - Skip target file itself in unlinked mentions - Not return unlinked mentions when includeUnlinked is false list edge case tests: - Handle invalid path - Handle non-existent folder - Handle path pointing to file instead of folder - Handle cursor not found in pagination validateWikilinks edge case tests: - Handle invalid path --- tests/vault-tools.test.ts | 527 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 527 insertions(+) diff --git a/tests/vault-tools.test.ts b/tests/vault-tools.test.ts index d20ae2a..2a2f225 100644 --- a/tests/vault-tools.test.ts +++ b/tests/vault-tools.test.ts @@ -577,5 +577,532 @@ describe('VaultTools', () => { expect(parsed.resolvedLinks.length).toBe(1); expect(parsed.unresolvedLinks.length).toBe(1); }); + + it('should handle invalid path', async () => { + mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null); + + const result = await vaultTools.validateWikilinks('../invalid'); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('not found'); + }); + }); + + describe('resolveWikilink', () => { + it('should return error if source file not found', async () => { + mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null); + + const result = await vaultTools.resolveWikilink('nonexistent.md', 'target'); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('not found'); + }); + + it('should resolve wikilink successfully', async () => { + const sourceFile = createMockTFile('source.md'); + const targetFile = createMockTFile('target.md'); + + mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(sourceFile); + mockMetadata.getFirstLinkpathDest = jest.fn().mockReturnValue(targetFile); + + const result = await vaultTools.resolveWikilink('source.md', 'target'); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.resolved).toBe(true); + expect(parsed.targetPath).toBe('target.md'); + expect(parsed.suggestions).toBeUndefined(); + }); + + it('should provide suggestions for unresolved links', async () => { + const sourceFile = createMockTFile('source.md'); + const similarFile = createMockTFile('target-similar.md'); + + mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(sourceFile); + mockMetadata.getFirstLinkpathDest = jest.fn().mockReturnValue(null); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([similarFile]); + + const result = await vaultTools.resolveWikilink('source.md', 'target'); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.resolved).toBe(false); + expect(parsed.suggestions).toBeDefined(); + expect(Array.isArray(parsed.suggestions)).toBe(true); + }); + + it('should handle errors gracefully', async () => { + const sourceFile = createMockTFile('source.md'); + + mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(sourceFile); + mockMetadata.getFirstLinkpathDest = jest.fn().mockImplementation(() => { + throw new Error('Cache error'); + }); + + const result = await vaultTools.resolveWikilink('source.md', 'target'); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('error'); + }); + + it('should handle invalid source path', async () => { + mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null); + + const result = await vaultTools.resolveWikilink('../invalid', 'target'); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('not found'); + }); + }); + + describe('getVaultInfo', () => { + it('should return vault info with total notes and size', async () => { + const mockFiles = [ + createMockTFile('note1.md', { size: 100, ctime: 1000, mtime: 2000 }), + createMockTFile('note2.md', { size: 200, ctime: 1000, mtime: 2000 }) + ]; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue(mockFiles); + mockVault.stat = jest.fn() + .mockReturnValueOnce({ size: 100, ctime: 1000, mtime: 2000 }) + .mockReturnValueOnce({ size: 200, ctime: 1000, mtime: 2000 }); + + const result = await vaultTools.getVaultInfo(); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.totalNotes).toBe(2); + expect(parsed.totalSize).toBe(300); + expect(parsed.sizeFormatted).toBe('300 Bytes'); + }); + + it('should handle empty vault', async () => { + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([]); + + const result = await vaultTools.getVaultInfo(); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.totalNotes).toBe(0); + expect(parsed.totalSize).toBe(0); + expect(parsed.sizeFormatted).toBe('0 Bytes'); + }); + + it('should handle files with missing stat info', async () => { + const mockFiles = [ + createMockTFile('note1.md'), + createMockTFile('note2.md') + ]; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue(mockFiles); + mockVault.stat = jest.fn() + .mockReturnValueOnce(null) + .mockReturnValueOnce({ size: 100, ctime: 1000, mtime: 2000 }); + + const result = await vaultTools.getVaultInfo(); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.totalNotes).toBe(2); + expect(parsed.totalSize).toBe(100); // Only counts the file with valid stat + }); + + it('should handle errors gracefully', async () => { + mockVault.getMarkdownFiles = jest.fn().mockImplementation(() => { + throw new Error('Vault access error'); + }); + + const result = await vaultTools.getVaultInfo(); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('Get vault info error'); + }); + + it('should format large file sizes correctly', async () => { + const mockFiles = [ + createMockTFile('large.md', { size: 1024 * 1024 * 5, ctime: 1000, mtime: 2000 }) + ]; + + mockVault.getMarkdownFiles = jest.fn().mockReturnValue(mockFiles); + mockVault.stat = jest.fn().mockReturnValue({ size: 1024 * 1024 * 5, ctime: 1000, mtime: 2000 }); + + const result = await vaultTools.getVaultInfo(); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.sizeFormatted).toContain('MB'); + }); + }); + + describe('search', () => { + it('should search for literal text', async () => { + const mockFile = createMockTFile('test.md'); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([mockFile]); + mockVault.read = jest.fn().mockResolvedValue('Hello world\nThis is a test'); + + const result = await vaultTools.search({ query: 'test' }); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.totalMatches).toBeGreaterThan(0); + expect(parsed.matches[0].path).toBe('test.md'); + }); + + it('should search with regex pattern', async () => { + const mockFile = createMockTFile('test.md'); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([mockFile]); + mockVault.read = jest.fn().mockResolvedValue('test123\ntest456'); + + const result = await vaultTools.search({ query: 'test\\d+', isRegex: true }); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.isRegex).toBe(true); + expect(parsed.totalMatches).toBeGreaterThan(0); + }); + + it('should handle invalid regex pattern', async () => { + const result = await vaultTools.search({ query: '[invalid(regex', isRegex: true }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('Invalid regex pattern'); + }); + + it('should filter by folder', async () => { + const mockFile1 = createMockTFile('folder/test.md'); + const mockFile2 = createMockTFile('other/test.md'); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([mockFile1, mockFile2]); + mockVault.read = jest.fn().mockResolvedValue('test content'); + + const result = await vaultTools.search({ query: 'test', folder: 'folder' }); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.filesSearched).toBe(1); + }); + + it('should respect maxResults limit', async () => { + const mockFile = createMockTFile('test.md'); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([mockFile]); + mockVault.read = jest.fn().mockResolvedValue('test test test test test'); + + const result = await vaultTools.search({ query: 'test', maxResults: 2 }); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.totalMatches).toBeLessThanOrEqual(2); + }); + + it('should handle file read errors gracefully', async () => { + const mockFile = createMockTFile('test.md'); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([mockFile]); + mockVault.read = jest.fn().mockRejectedValue(new Error('Read error')); + + const result = await vaultTools.search({ query: 'test' }); + + // Should not throw, just skip the file + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.totalMatches).toBe(0); + }); + + it('should handle case sensitive search', async () => { + const mockFile = createMockTFile('test.md'); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([mockFile]); + mockVault.read = jest.fn().mockResolvedValue('Test test TEST'); + + const result = await vaultTools.search({ query: 'test', caseSensitive: true }); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + // Should only match lowercase 'test' + expect(parsed.totalMatches).toBe(1); + }); + + it('should extract snippets correctly', async () => { + const mockFile = createMockTFile('test.md'); + const longLine = 'a'.repeat(200) + 'target' + 'b'.repeat(200); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([mockFile]); + mockVault.read = jest.fn().mockResolvedValue(longLine); + + const result = await vaultTools.search({ query: 'target', returnSnippets: true, snippetLength: 100 }); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.matches[0].snippet.length).toBeLessThanOrEqual(100); + }); + + it('should handle zero-width regex matches', async () => { + const mockFile = createMockTFile('test.md'); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([mockFile]); + mockVault.read = jest.fn().mockResolvedValue('test'); + + const result = await vaultTools.search({ query: '(?=test)', isRegex: true, maxResults: 10 }); + + expect(result.isError).toBeUndefined(); + // Should handle zero-width matches without infinite loop + }); + + it('should handle general search errors', async () => { + mockVault.getMarkdownFiles = jest.fn().mockImplementation(() => { + throw new Error('Vault error'); + }); + + const result = await vaultTools.search({ query: 'test' }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('Search error'); + }); + }); + + describe('searchWaypoints', () => { + it('should search for waypoints in vault', async () => { + const mockFile = createMockTFile('test.md'); + mockApp.vault = { + getMarkdownFiles: jest.fn().mockReturnValue([mockFile]) + } as any; + + // Mock SearchUtils + const SearchUtils = require('../src/utils/search-utils').SearchUtils; + SearchUtils.searchWaypoints = jest.fn().mockResolvedValue([ + { path: 'test.md', waypointRange: { start: 0, end: 10 } } + ]); + + const result = await vaultTools.searchWaypoints(); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.totalWaypoints).toBeDefined(); + expect(parsed.filesSearched).toBeDefined(); + }); + + it('should filter waypoints by folder', async () => { + const mockFile1 = createMockTFile('folder1/test.md'); + const mockFile2 = createMockTFile('folder2/test.md'); + mockApp.vault = { + getMarkdownFiles: jest.fn().mockReturnValue([mockFile1, mockFile2]) + } as any; + + const SearchUtils = require('../src/utils/search-utils').SearchUtils; + SearchUtils.searchWaypoints = jest.fn().mockResolvedValue([]); + + const result = await vaultTools.searchWaypoints('folder1'); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.filesSearched).toBe(1); + }); + + it('should handle search errors', async () => { + const SearchUtils = require('../src/utils/search-utils').SearchUtils; + SearchUtils.searchWaypoints = jest.fn().mockRejectedValue(new Error('Search failed')); + + const result = await vaultTools.searchWaypoints(); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('Waypoint search error'); + }); + }); + + describe('getFolderWaypoint', () => { + it('should return error if file not found', async () => { + const PathUtils = require('../src/utils/path-utils').PathUtils; + PathUtils.resolveFile = jest.fn().mockReturnValue(null); + + const result = await vaultTools.getFolderWaypoint('nonexistent.md'); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('not found'); + }); + + it('should extract waypoint from file', async () => { + const mockFile = createMockTFile('test.md'); + const PathUtils = require('../src/utils/path-utils').PathUtils; + const WaypointUtils = require('../src/utils/waypoint-utils').WaypointUtils; + + PathUtils.resolveFile = jest.fn().mockReturnValue(mockFile); + mockApp.vault = { + read: jest.fn().mockResolvedValue('%% Begin Waypoint %%\nContent\n%% End Waypoint %%') + } as any; + WaypointUtils.extractWaypointBlock = jest.fn().mockReturnValue({ + hasWaypoint: true, + waypointRange: { start: 0, end: 10 }, + links: ['link1'], + rawContent: 'Content' + }); + + const result = await vaultTools.getFolderWaypoint('test.md'); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.hasWaypoint).toBe(true); + }); + + it('should handle errors', async () => { + const PathUtils = require('../src/utils/path-utils').PathUtils; + PathUtils.resolveFile = jest.fn().mockImplementation(() => { + throw new Error('File error'); + }); + + const result = await vaultTools.getFolderWaypoint('test.md'); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('Get folder waypoint error'); + }); + }); + + describe('isFolderNote', () => { + it('should return error if file not found', async () => { + const PathUtils = require('../src/utils/path-utils').PathUtils; + PathUtils.resolveFile = jest.fn().mockReturnValue(null); + + const result = await vaultTools.isFolderNote('nonexistent.md'); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('not found'); + }); + + it('should detect folder notes', async () => { + const mockFile = createMockTFile('test.md'); + const PathUtils = require('../src/utils/path-utils').PathUtils; + const WaypointUtils = require('../src/utils/waypoint-utils').WaypointUtils; + + PathUtils.resolveFile = jest.fn().mockReturnValue(mockFile); + WaypointUtils.isFolderNote = jest.fn().mockResolvedValue({ + isFolderNote: true, + reason: 'basename_match', + folderPath: 'test' + }); + + const result = await vaultTools.isFolderNote('test.md'); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.isFolderNote).toBe(true); + }); + + it('should handle errors', async () => { + const PathUtils = require('../src/utils/path-utils').PathUtils; + PathUtils.resolveFile = jest.fn().mockImplementation(() => { + throw new Error('File error'); + }); + + const result = await vaultTools.isFolderNote('test.md'); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('Is folder note error'); + }); + }); + + describe('getBacklinks - unlinked mentions', () => { + it('should find unlinked mentions', async () => { + const targetFile = createMockTFile('target.md'); + const sourceFile = createMockTFile('source.md'); + + mockVault.getAbstractFileByPath = jest.fn() + .mockReturnValueOnce(targetFile) + .mockReturnValue(sourceFile); + mockVault.read = jest.fn().mockResolvedValue('This mentions target in text'); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([sourceFile]); + mockMetadata.resolvedLinks = {}; + + const result = await vaultTools.getBacklinks('target.md', true, true); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.backlinks.some((b: any) => b.type === 'unlinked')).toBe(true); + }); + + it('should not return unlinked mentions when includeUnlinked is false', async () => { + const targetFile = createMockTFile('target.md'); + + mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(targetFile); + mockMetadata.resolvedLinks = {}; + + const result = await vaultTools.getBacklinks('target.md', false, true); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.backlinks.every((b: any) => b.type !== 'unlinked')).toBe(true); + }); + + it('should skip files that already have linked backlinks', async () => { + const targetFile = createMockTFile('target.md'); + const sourceFile = createMockTFile('source.md'); + + mockVault.getAbstractFileByPath = jest.fn() + .mockReturnValueOnce(targetFile) + .mockReturnValue(sourceFile); + mockVault.read = jest.fn().mockResolvedValue('This links to [[target]] and mentions target'); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([sourceFile]); + mockMetadata.resolvedLinks = { + 'source.md': { 'target.md': 1 } + }; + mockMetadata.getFirstLinkpathDest = jest.fn().mockReturnValue(targetFile); + + const result = await vaultTools.getBacklinks('target.md', true, true); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + // Should have linked mention but not duplicate unlinked + expect(parsed.backlinks.filter((b: any) => b.sourcePath === 'source.md').length).toBe(1); + }); + + it('should skip target file itself in unlinked mentions', async () => { + const targetFile = createMockTFile('target.md'); + + mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(targetFile); + mockVault.read = jest.fn().mockResolvedValue('This file mentions target'); + mockVault.getMarkdownFiles = jest.fn().mockReturnValue([targetFile]); + mockMetadata.resolvedLinks = {}; + + const result = await vaultTools.getBacklinks('target.md', true, true); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + expect(parsed.backlinks.every((b: any) => b.sourcePath !== 'target.md')).toBe(true); + }); + }); + + describe('list - edge cases', () => { + it('should handle invalid path in list', async () => { + const result = await vaultTools.list({ path: '../invalid' }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('Invalid path'); + }); + + it('should handle non-existent folder', async () => { + mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null); + + const result = await vaultTools.list({ path: 'nonexistent' }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('not found'); + }); + + it('should handle path pointing to file instead of folder', async () => { + const mockFile = createMockTFile('test.md'); + mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(mockFile); + + const result = await vaultTools.list({ path: 'test.md' }); + + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('not a folder'); + }); + + it('should handle cursor not found in pagination', async () => { + const mockFile = createMockTFile('test.md'); + const mockRoot = createMockTFolder('', [mockFile]); + + mockVault.getRoot = jest.fn().mockReturnValue(mockRoot); + + const result = await vaultTools.list({ cursor: 'nonexistent.md' }); + + expect(result.isError).toBeUndefined(); + const parsed = JSON.parse(result.content[0].text); + // Should return from beginning when cursor not found + expect(parsed.items.length).toBeGreaterThan(0); + }); }); }); \ No newline at end of file