diff --git a/tests/__mocks__/adapters.ts b/tests/__mocks__/adapters.ts index 1e657dd..6c64c62 100644 --- a/tests/__mocks__/adapters.ts +++ b/tests/__mocks__/adapters.ts @@ -44,10 +44,11 @@ export function createMockFileManagerAdapter(overrides?: Partial { + let vaultTools: VaultTools; + let mockVault: ReturnType; + let mockMetadata: ReturnType; + let mockApp: App; + + beforeEach(() => { + mockVault = createMockVaultAdapter(); + mockMetadata = createMockMetadataCacheAdapter(); + mockApp = {} as App; // Minimal mock for methods not yet migrated + + vaultTools = new VaultTools(mockVault, mockMetadata, mockApp); + }); + + 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(); + }); + }); +}); \ No newline at end of file