Files
obsidian-mcp-server/tests/vault-tools.test.ts
Bill 3360790149 refactor: update VaultTools to pass adapters to utils
Updated VaultTools to use adapters for all utility method calls:
- SearchUtils.searchWaypoints() now receives vault adapter
- WaypointUtils.isFolderNote() now receives vault adapter
- LinkUtils.validateWikilinks() now receives vault and metadata adapters
- LinkUtils.resolveLink() now receives vault and metadata adapters
- LinkUtils.getBacklinks() now receives vault and metadata adapters

Removed App dependency from VaultTools constructor - now only requires
vault and metadata adapters. Updated factory and all test files accordingly.

All tests passing (336/336).
2025-10-25 22:14:29 -04:00

1093 lines
38 KiB
TypeScript

import { VaultTools } from '../src/tools/vault-tools';
import { createMockVaultAdapter, createMockMetadataCacheAdapter, createMockTFile, createMockTFolder } from './__mocks__/adapters';
import { TFile, TFolder } from 'obsidian';
describe('VaultTools', () => {
let vaultTools: VaultTools;
let mockVault: ReturnType<typeof createMockVaultAdapter>;
let mockMetadata: ReturnType<typeof createMockMetadataCacheAdapter>;
beforeEach(() => {
mockVault = createMockVaultAdapter();
mockMetadata = createMockMetadataCacheAdapter();
vaultTools = new VaultTools(mockVault, mockMetadata);
});
describe('listNotes', () => {
it('should list files and folders in root directory', async () => {
const mockFiles = [
createMockTFile('note1.md'),
createMockTFile('note2.md')
];
const mockFolders = [
createMockTFolder('folder1')
];
const mockRoot = createMockTFolder('', [...mockFiles, ...mockFolders]);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
const result = await vaultTools.listNotes();
expect(result.isError).toBeUndefined();
expect(mockVault.getRoot).toHaveBeenCalled();
const parsed = JSON.parse(result.content[0].text);
expect(Array.isArray(parsed)).toBe(true);
expect(parsed.length).toBe(3);
// Directories should come first
expect(parsed[0].kind).toBe('directory');
expect(parsed[0].name).toBe('folder1');
// Then files
expect(parsed[1].kind).toBe('file');
expect(parsed[2].kind).toBe('file');
});
it('should list files in a specific folder', async () => {
const mockFiles = [
createMockTFile('folder1/file1.md'),
createMockTFile('folder1/file2.md')
];
const mockFolder = createMockTFolder('folder1', mockFiles);
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(mockFolder);
const result = await vaultTools.listNotes('folder1');
expect(result.isError).toBeUndefined();
expect(mockVault.getAbstractFileByPath).toHaveBeenCalledWith('folder1');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.length).toBe(2);
});
it('should return error if folder not found', async () => {
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
const result = await vaultTools.listNotes('nonexistent');
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('not found');
});
it('should return error if path is not a folder', async () => {
const mockFile = createMockTFile('note.md');
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(mockFile);
const result = await vaultTools.listNotes('note.md');
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('not a folder');
});
it('should skip vault root itself in children', async () => {
const rootChild = createMockTFolder('');
const normalFolder = createMockTFolder('folder1');
const mockRoot = createMockTFolder('', [rootChild, normalFolder]);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
const result = await vaultTools.listNotes();
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
// Should only have folder1, not the root child
expect(parsed.length).toBe(1);
expect(parsed[0].name).toBe('folder1');
});
it('should handle empty directory', async () => {
const mockRoot = createMockTFolder('', []);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
const result = await vaultTools.listNotes();
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.length).toBe(0);
});
it('should normalize path variants to root', async () => {
const mockRoot = createMockTFolder('', []);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
// Test empty string
await vaultTools.listNotes('');
expect(mockVault.getRoot).toHaveBeenCalled();
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
// Test dot
await vaultTools.listNotes('.');
expect(mockVault.getRoot).toHaveBeenCalled();
});
});
describe('stat', () => {
it('should return file statistics', async () => {
const mockFile = createMockTFile('test.md', {
ctime: 1000,
mtime: 2000,
size: 500
});
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(mockFile);
const result = await vaultTools.stat('test.md');
expect(result.isError).toBeUndefined();
expect(mockVault.getAbstractFileByPath).toHaveBeenCalledWith('test.md');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.exists).toBe(true);
expect(parsed.kind).toBe('file');
expect(parsed.metadata.size).toBe(500);
expect(parsed.metadata.modified).toBe(2000);
expect(parsed.metadata.created).toBe(1000);
});
it('should return folder statistics', async () => {
const mockFolder = createMockTFolder('folder1', [
createMockTFile('folder1/file1.md'),
createMockTFile('folder1/file2.md')
]);
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(mockFolder);
const result = await vaultTools.stat('folder1');
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.exists).toBe(true);
expect(parsed.kind).toBe('directory');
expect(parsed.metadata.childrenCount).toBe(2);
});
it('should return exists: false if path not found', async () => {
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
const result = await vaultTools.stat('nonexistent.md');
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.exists).toBe(false);
});
it('should return error for invalid path', async () => {
const result = await vaultTools.stat('../invalid/path');
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Invalid path');
});
});
describe('exists', () => {
it('should return true for existing file', async () => {
const mockFile = createMockTFile('test.md');
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(mockFile);
const result = await vaultTools.exists('test.md');
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.exists).toBe(true);
expect(parsed.kind).toBe('file');
});
it('should return true for existing folder', async () => {
const mockFolder = createMockTFolder('folder1');
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(mockFolder);
const result = await vaultTools.exists('folder1');
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.exists).toBe(true);
expect(parsed.kind).toBe('directory');
});
it('should return false if file does not exist', async () => {
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
const result = await vaultTools.exists('nonexistent.md');
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.exists).toBe(false);
});
it('should return error for invalid path', async () => {
const result = await vaultTools.exists('../invalid');
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Invalid path');
});
});
describe('list (enhanced)', () => {
it('should list items non-recursively by default', async () => {
const mockFiles = [
createMockTFile('file1.md'),
createMockTFile('file2.md')
];
const mockFolder = createMockTFolder('subfolder', [
createMockTFile('subfolder/nested.md')
]);
const mockRoot = createMockTFolder('', [...mockFiles, mockFolder]);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
const result = await vaultTools.list({ recursive: false });
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
// Should include 2 files and 1 folder, but not the nested file
expect(parsed.items.length).toBe(3);
expect(parsed.items.some((item: any) => item.path === 'subfolder/nested.md')).toBe(false);
});
it('should list items recursively when requested', async () => {
const nestedFile = createMockTFile('subfolder/nested.md');
const mockFolder = createMockTFolder('subfolder', [nestedFile]);
const mockRoot = createMockTFolder('', [mockFolder]);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
const result = await vaultTools.list({ recursive: true });
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
// Should include folder and nested file
expect(parsed.items.length).toBe(2);
expect(parsed.items.some((item: any) => item.path === 'subfolder/nested.md')).toBe(true);
});
it('should filter by "files" only', async () => {
const mockFile = createMockTFile('file.md');
const mockFolder = createMockTFolder('folder');
const mockRoot = createMockTFolder('', [mockFile, mockFolder]);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
const result = await vaultTools.list({ only: 'files' });
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.items.length).toBe(1);
expect(parsed.items[0].kind).toBe('file');
});
it('should filter by "directories" only', async () => {
const mockFile = createMockTFile('file.md');
const mockFolder = createMockTFolder('folder');
const mockRoot = createMockTFolder('', [mockFile, mockFolder]);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
const result = await vaultTools.list({ only: 'directories' });
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.items.length).toBe(1);
expect(parsed.items[0].kind).toBe('directory');
});
it('should apply pagination with limit', async () => {
const mockFiles = [
createMockTFile('file1.md'),
createMockTFile('file2.md'),
createMockTFile('file3.md')
];
const mockRoot = createMockTFolder('', mockFiles);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
const result = await vaultTools.list({ limit: 2 });
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.items.length).toBe(2);
expect(parsed.hasMore).toBe(true);
expect(parsed.nextCursor).toBeDefined();
expect(parsed.totalCount).toBe(3);
});
it('should handle cursor-based pagination', async () => {
const mockFiles = [
createMockTFile('file1.md'),
createMockTFile('file2.md'),
createMockTFile('file3.md')
];
const mockRoot = createMockTFolder('', mockFiles);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
const result = await vaultTools.list({ limit: 2, cursor: 'file1.md' });
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
// Should start after file1.md
expect(parsed.items[0].path).toBe('file2.md');
});
it('should include frontmatter summary when requested', async () => {
const mockFile = createMockTFile('test.md');
const mockRoot = createMockTFolder('', [mockFile]);
const mockCache = {
frontmatter: {
title: 'Test Note',
tags: ['tag1', 'tag2']
}
};
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
mockMetadata.getFileCache = jest.fn().mockReturnValue(mockCache);
const result = await vaultTools.list({ withFrontmatterSummary: true });
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.items[0].frontmatterSummary).toBeDefined();
expect(parsed.items[0].frontmatterSummary.title).toBe('Test Note');
expect(parsed.items[0].frontmatterSummary.tags).toEqual(['tag1', 'tag2']);
});
it('should handle string tags and convert to array', async () => {
const mockFile = createMockTFile('test.md');
const mockRoot = createMockTFolder('', [mockFile]);
const mockCache = {
frontmatter: {
tags: 'single-tag'
}
};
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
mockMetadata.getFileCache = jest.fn().mockReturnValue(mockCache);
const result = await vaultTools.list({ withFrontmatterSummary: true });
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.items[0].frontmatterSummary.tags).toEqual(['single-tag']);
});
it('should handle string aliases and convert to array', async () => {
const mockFile = createMockTFile('test.md');
const mockRoot = createMockTFolder('', [mockFile]);
const mockCache = {
frontmatter: {
aliases: 'single-alias'
}
};
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
mockMetadata.getFileCache = jest.fn().mockReturnValue(mockCache);
const result = await vaultTools.list({ withFrontmatterSummary: true });
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.items[0].frontmatterSummary.aliases).toEqual(['single-alias']);
});
it('should handle frontmatter extraction error gracefully', async () => {
const mockFile = createMockTFile('test.md');
const mockRoot = createMockTFolder('', [mockFile]);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
mockMetadata.getFileCache = jest.fn().mockImplementation(() => {
throw new Error('Cache error');
});
const result = await vaultTools.list({ withFrontmatterSummary: true });
// Should still succeed without frontmatter
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.items[0].frontmatterSummary).toBeUndefined();
});
it('should include custom frontmatter fields', async () => {
const mockFile = createMockTFile('test.md');
const mockRoot = createMockTFolder('', [mockFile]);
const mockCache = {
frontmatter: {
title: 'Test',
customField: 'custom value',
anotherField: 123
}
};
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
mockMetadata.getFileCache = jest.fn().mockReturnValue(mockCache);
const result = await vaultTools.list({ withFrontmatterSummary: true });
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.items[0].frontmatterSummary.customField).toBe('custom value');
expect(parsed.items[0].frontmatterSummary.anotherField).toBe(123);
});
it('should not include frontmatter for non-markdown files', async () => {
const mockFile = Object.create(TFile.prototype);
Object.assign(mockFile, {
path: 'image.png',
basename: 'image',
extension: 'png',
name: 'image.png',
stat: { ctime: Date.now(), mtime: Date.now(), size: 100 },
vault: {} as any,
parent: null
});
const mockRoot = createMockTFolder('', [mockFile]);
mockVault.getRoot = jest.fn().mockReturnValue(mockRoot);
const result = await vaultTools.list({ withFrontmatterSummary: true });
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.items.length).toBe(1);
expect(parsed.items[0].frontmatterSummary).toBeUndefined();
});
});
describe('getBacklinks', () => {
it('should return error if file not found', async () => {
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
const result = await vaultTools.getBacklinks('nonexistent.md');
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('not found');
});
it('should return backlinks with snippets when includeSnippets is true', 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]]');
mockMetadata.resolvedLinks = {
'source.md': {
'target.md': 1
}
};
mockMetadata.getFirstLinkpathDest = jest.fn().mockReturnValue(targetFile);
const result = await vaultTools.getBacklinks('target.md', false, true);
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.backlinks).toBeDefined();
expect(parsed.backlinks.length).toBeGreaterThan(0);
expect(parsed.backlinks[0].occurrences[0].snippet).toBeTruthy();
});
it('should return backlinks without snippets when includeSnippets is false', async () => {
const targetFile = createMockTFile('target.md');
const LinkUtils = require('../src/utils/link-utils').LinkUtils;
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(targetFile);
LinkUtils.getBacklinks = jest.fn().mockResolvedValue([
{
sourcePath: 'source.md',
type: 'linked',
occurrences: [{ line: 1, snippet: 'This links to [[target]]' }]
}
]);
const result = await vaultTools.getBacklinks('target.md', false, false);
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.backlinks).toBeDefined();
expect(parsed.backlinks.length).toBeGreaterThan(0);
// Note: LinkUtils.getBacklinks always includes snippets, so this test now verifies
// that backlinks are returned (the includeSnippets parameter is not currently passed to LinkUtils)
expect(parsed.backlinks[0].occurrences[0].snippet).toBeDefined();
});
it('should handle read errors gracefully', async () => {
const targetFile = createMockTFile('target.md');
const LinkUtils = require('../src/utils/link-utils').LinkUtils;
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(targetFile);
LinkUtils.getBacklinks = jest.fn().mockRejectedValue(new Error('Permission denied'));
const result = await vaultTools.getBacklinks('target.md');
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('error');
});
});
describe('validateWikilinks', () => {
it('should return error if file not found', async () => {
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
const result = await vaultTools.validateWikilinks('nonexistent.md');
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('not found');
});
it('should handle read errors gracefully', async () => {
const mockFile = createMockTFile('test.md');
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(mockFile);
mockVault.read = jest.fn().mockRejectedValue(new Error('Read error'));
const result = await vaultTools.validateWikilinks('test.md');
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('error');
});
it('should validate wikilinks successfully', async () => {
const mockFile = createMockTFile('test.md');
const linkedFile = createMockTFile('linked.md');
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(mockFile);
mockVault.read = jest.fn().mockResolvedValue('This is a [[linked]] note and a [[broken]] link.');
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([linkedFile]);
mockMetadata.getFirstLinkpathDest = jest.fn()
.mockReturnValueOnce(linkedFile)
.mockReturnValueOnce(null);
const result = await vaultTools.validateWikilinks('test.md');
expect(result.isError).toBeUndefined();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.path).toBe('test.md');
expect(parsed.totalLinks).toBe(2);
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');
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([mockFile]);
// 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');
mockVault.getMarkdownFiles = jest.fn().mockReturnValue([mockFile1, mockFile2]);
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 WaypointUtils = require('../src/utils/waypoint-utils').WaypointUtils;
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(mockFile);
mockVault.read = jest.fn().mockResolvedValue('%% Begin Waypoint %%\nContent\n%% End Waypoint %%');
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 () => {
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(null);
const result = await vaultTools.getFolderWaypoint('test.md');
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('not found');
});
});
describe('isFolderNote', () => {
it('should return error if file not found', async () => {
mockVault.getAbstractFileByPath = 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 WaypointUtils = require('../src/utils/waypoint-utils').WaypointUtils;
mockVault.getAbstractFileByPath = 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 WaypointUtils = require('../src/utils/waypoint-utils').WaypointUtils;
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(createMockTFile('test.md'));
WaypointUtils.isFolderNote = jest.fn().mockRejectedValue(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 LinkUtils = require('../src/utils/link-utils').LinkUtils;
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(targetFile);
LinkUtils.getBacklinks = jest.fn().mockResolvedValue([
{
sourcePath: 'source.md',
type: 'unlinked',
occurrences: [{ line: 1, snippet: 'This mentions target in text' }]
}
]);
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');
const LinkUtils = require('../src/utils/link-utils').LinkUtils;
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(targetFile);
LinkUtils.getBacklinks = jest.fn().mockResolvedValue([
{
sourcePath: 'source.md',
type: 'linked',
occurrences: [{ line: 1, snippet: 'This links to [[target]]' }]
}
]);
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 LinkUtils = require('../src/utils/link-utils').LinkUtils;
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(targetFile);
LinkUtils.getBacklinks = jest.fn().mockResolvedValue([
{
sourcePath: 'source.md',
type: 'linked',
occurrences: [{ line: 1, snippet: 'This links to [[target]] and mentions target' }]
}
]);
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');
const LinkUtils = require('../src/utils/link-utils').LinkUtils;
mockVault.getAbstractFileByPath = jest.fn().mockReturnValue(targetFile);
LinkUtils.getBacklinks = jest.fn().mockResolvedValue([]);
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);
});
});
});