Phase 2 - Breaking Changes (v2.0.0): - Added typed result interfaces (FileMetadata, DirectoryMetadata, VaultInfo, SearchResult, SearchMatch) - Unified parameter naming: list_notes now uses 'path' parameter (removed 'folder') - Enhanced tool responses with structured JSON for all tools - list_notes: Returns array of FileMetadata/DirectoryMetadata with full metadata - search_notes: Returns SearchResult with line numbers, snippets, and match ranges - get_vault_info: Returns VaultInfo with comprehensive statistics - Updated all tool descriptions to document structured responses - Version bumped to 2.0.0 (breaking changes) Phase 2.1 - Post-Testing Fixes: - Fixed root listing to exclude vault root folder itself (handles path '', '/', and isRoot()) - Fixed alphabetical sorting to be case-insensitive for stable ordering - Improved directory metadata with better timestamp detection and error handling - Fixed parent folder validation order (check if file before checking existence) - Updated documentation with root path examples and leading slash warnings - Added comprehensive test suite for sorting and root listing behavior - Fixed test mocks to use proper TFile/TFolder instances Tests: All 64 tests passing Build: Successful, no errors
193 lines
5.6 KiB
TypeScript
193 lines
5.6 KiB
TypeScript
import { VaultTools } from '../src/tools/vault-tools';
|
|
import { App, TFile, TFolder } from 'obsidian';
|
|
import { FileMetadata, DirectoryMetadata } from '../src/types/mcp-types';
|
|
|
|
describe('VaultTools - list_notes sorting', () => {
|
|
let app: App;
|
|
let vaultTools: VaultTools;
|
|
|
|
beforeEach(() => {
|
|
// Mock App with vault
|
|
app = {
|
|
vault: {
|
|
getAllLoadedFiles: jest.fn(),
|
|
}
|
|
} as any;
|
|
|
|
vaultTools = new VaultTools(app);
|
|
});
|
|
|
|
describe('Case-insensitive alphabetical sorting', () => {
|
|
it('should sort directories case-insensitively', async () => {
|
|
// Create mock folders with mixed case names
|
|
const folders = [
|
|
createMockFolder('construction Game', 'construction Game'),
|
|
createMockFolder('CTP Lancaster', 'CTP Lancaster'),
|
|
createMockFolder('Archive', 'Archive'),
|
|
createMockFolder('daily', 'daily'),
|
|
];
|
|
|
|
(app.vault.getAllLoadedFiles as jest.Mock).mockReturnValue(folders);
|
|
|
|
const result = await vaultTools.listNotes();
|
|
const items = JSON.parse(result.content[0].text) as Array<FileMetadata | DirectoryMetadata>;
|
|
|
|
// Extract directory names
|
|
const dirNames = items
|
|
.filter(item => item.kind === 'directory')
|
|
.map(item => item.name);
|
|
|
|
// Expected order (case-insensitive alphabetical)
|
|
expect(dirNames).toEqual([
|
|
'Archive',
|
|
'construction Game',
|
|
'CTP Lancaster',
|
|
'daily'
|
|
]);
|
|
});
|
|
|
|
it('should sort files case-insensitively', async () => {
|
|
const files = [
|
|
createMockFile('Zebra.md', 'Zebra.md'),
|
|
createMockFile('apple.md', 'apple.md'),
|
|
createMockFile('Banana.md', 'Banana.md'),
|
|
createMockFile('cherry.md', 'cherry.md'),
|
|
];
|
|
|
|
(app.vault.getAllLoadedFiles as jest.Mock).mockReturnValue(files);
|
|
|
|
const result = await vaultTools.listNotes();
|
|
const items = JSON.parse(result.content[0].text) as Array<FileMetadata | DirectoryMetadata>;
|
|
|
|
const fileNames = items
|
|
.filter(item => item.kind === 'file')
|
|
.map(item => item.name);
|
|
|
|
// Expected order (case-insensitive alphabetical)
|
|
expect(fileNames).toEqual([
|
|
'apple.md',
|
|
'Banana.md',
|
|
'cherry.md',
|
|
'Zebra.md'
|
|
]);
|
|
});
|
|
|
|
it('should place all directories before all files', async () => {
|
|
const items = [
|
|
createMockFile('zebra.md', 'zebra.md'),
|
|
createMockFolder('Archive', 'Archive'),
|
|
createMockFile('apple.md', 'apple.md'),
|
|
createMockFolder('daily', 'daily'),
|
|
];
|
|
|
|
(app.vault.getAllLoadedFiles as jest.Mock).mockReturnValue(items);
|
|
|
|
const result = await vaultTools.listNotes();
|
|
const parsed = JSON.parse(result.content[0].text) as Array<FileMetadata | DirectoryMetadata>;
|
|
|
|
// First items should be directories
|
|
expect(parsed[0].kind).toBe('directory');
|
|
expect(parsed[1].kind).toBe('directory');
|
|
// Last items should be files
|
|
expect(parsed[2].kind).toBe('file');
|
|
expect(parsed[3].kind).toBe('file');
|
|
});
|
|
});
|
|
|
|
describe('Root path handling', () => {
|
|
it('should list root when path is undefined', async () => {
|
|
const items = [
|
|
createMockFolder('folder1', 'folder1'),
|
|
createMockFile('root-file.md', 'root-file.md'),
|
|
];
|
|
|
|
(app.vault.getAllLoadedFiles as jest.Mock).mockReturnValue(items);
|
|
|
|
const result = await vaultTools.listNotes();
|
|
const parsed = JSON.parse(result.content[0].text) as Array<FileMetadata | DirectoryMetadata>;
|
|
|
|
expect(parsed.length).toBe(2);
|
|
});
|
|
|
|
it('should list root when path is empty string', async () => {
|
|
const items = [
|
|
createMockFolder('folder1', 'folder1'),
|
|
createMockFile('root-file.md', 'root-file.md'),
|
|
];
|
|
|
|
(app.vault.getAllLoadedFiles as jest.Mock).mockReturnValue(items);
|
|
|
|
const result = await vaultTools.listNotes('');
|
|
const parsed = JSON.parse(result.content[0].text) as Array<FileMetadata | DirectoryMetadata>;
|
|
|
|
expect(parsed.length).toBe(2);
|
|
});
|
|
|
|
it('should list root when path is dot', async () => {
|
|
const items = [
|
|
createMockFolder('folder1', 'folder1'),
|
|
createMockFile('root-file.md', 'root-file.md'),
|
|
];
|
|
|
|
(app.vault.getAllLoadedFiles as jest.Mock).mockReturnValue(items);
|
|
|
|
const result = await vaultTools.listNotes('.');
|
|
const parsed = JSON.parse(result.content[0].text) as Array<FileMetadata | DirectoryMetadata>;
|
|
|
|
expect(parsed.length).toBe(2);
|
|
});
|
|
|
|
it('should only return direct children of root', async () => {
|
|
const items = [
|
|
createMockFolder('folder1', 'folder1'),
|
|
createMockFile('root-file.md', 'root-file.md'),
|
|
// These should NOT be included (nested)
|
|
createMockFile('nested.md', 'folder1/nested.md', 'folder1'),
|
|
];
|
|
|
|
(app.vault.getAllLoadedFiles as jest.Mock).mockReturnValue(items);
|
|
|
|
const result = await vaultTools.listNotes();
|
|
const parsed = JSON.parse(result.content[0].text) as Array<FileMetadata | DirectoryMetadata>;
|
|
|
|
// Should only have 2 items (folder1 and root-file.md)
|
|
expect(parsed.length).toBe(2);
|
|
expect(parsed.some(item => item.name === 'nested.md')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Helper functions
|
|
function createMockFolder(name: string, path: string, parentPath: string = ''): any {
|
|
const folder = Object.create(TFolder.prototype);
|
|
Object.assign(folder, {
|
|
name,
|
|
path,
|
|
parent: parentPath ? { path: parentPath } : null,
|
|
children: [],
|
|
stat: {
|
|
mtime: Date.now(),
|
|
ctime: Date.now(),
|
|
size: 0
|
|
}
|
|
});
|
|
return folder;
|
|
}
|
|
|
|
function createMockFile(name: string, path: string, parentPath: string = ''): any {
|
|
const file = Object.create(TFile.prototype);
|
|
Object.assign(file, {
|
|
name,
|
|
path,
|
|
basename: name.replace(/\.[^.]+$/, ''),
|
|
extension: name.split('.').pop() || '',
|
|
parent: parentPath ? { path: parentPath } : null,
|
|
stat: {
|
|
mtime: Date.now(),
|
|
ctime: Date.now(),
|
|
size: 1024
|
|
}
|
|
});
|
|
return file;
|
|
}
|
|
});
|