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
This commit is contained in:
2025-10-20 00:20:12 -04:00
parent 2e30b81f01
commit 5760ac9b8b

View File

@@ -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);
});
});
});