Files
obsidian-mcp-server/tests/list-notes-sorting.test.ts
Bill 9d07ec64e2 Phase 2: API Unification & Typed Results + Phase 2.1 Fixes
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
2025-10-16 22:49:28 -04:00

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