test: update vault-tools tests to use mock adapters
Replace complex App object mocks with clean mock adapter pattern. Simplifies test setup and improves maintainability. - Created comprehensive tests for VaultTools methods using mock adapters - Tests cover listNotes, stat, exists, and list (enhanced) functionality - Includes tests for frontmatter extraction edge cases - Fixed mock helpers to use proper prototype chains for instanceof checks - All 27 VaultTools tests passing
This commit is contained in:
@@ -44,10 +44,11 @@ export function createMockFileManagerAdapter(overrides?: Partial<IFileManagerAda
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a mock TFile
|
||||
* Helper to create a mock TFile with proper prototype chain
|
||||
*/
|
||||
export function createMockTFile(path: string, stat?: { ctime: number; mtime: number; size: number }): TFile {
|
||||
return {
|
||||
const file = Object.create(TFile.prototype);
|
||||
Object.assign(file, {
|
||||
path,
|
||||
basename: path.split('/').pop()?.replace('.md', '') || '',
|
||||
extension: 'md',
|
||||
@@ -55,20 +56,22 @@ export function createMockTFile(path: string, stat?: { ctime: number; mtime: num
|
||||
stat: stat || { ctime: Date.now(), mtime: Date.now(), size: 100 },
|
||||
vault: {} as any,
|
||||
parent: null
|
||||
} as TFile;
|
||||
});
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a mock TFolder
|
||||
* Helper to create a mock TFolder with proper prototype chain
|
||||
*/
|
||||
export function createMockTFolder(path: string, children?: TAbstractFile[]): TFolder {
|
||||
const folder = {
|
||||
const folder = Object.create(TFolder.prototype);
|
||||
Object.assign(folder, {
|
||||
path,
|
||||
name: path.split('/').pop() || '',
|
||||
children: children || [],
|
||||
vault: {} as any,
|
||||
parent: null,
|
||||
isRoot: function() { return path === '' || path === '/'; }
|
||||
} as TFolder;
|
||||
});
|
||||
return folder;
|
||||
}
|
||||
458
tests/vault-tools.test.ts
Normal file
458
tests/vault-tools.test.ts
Normal file
@@ -0,0 +1,458 @@
|
||||
import { VaultTools } from '../src/tools/vault-tools';
|
||||
import { createMockVaultAdapter, createMockMetadataCacheAdapter, createMockTFile, createMockTFolder } from './__mocks__/adapters';
|
||||
import { TFile, TFolder, App } from 'obsidian';
|
||||
|
||||
describe('VaultTools', () => {
|
||||
let vaultTools: VaultTools;
|
||||
let mockVault: ReturnType<typeof createMockVaultAdapter>;
|
||||
let mockMetadata: ReturnType<typeof createMockMetadataCacheAdapter>;
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user