Add test for read_excalidraw with includeCompressed option to cover line 647. Add test for update_frontmatter on files without existing frontmatter to cover line 771. Coverage for note-tools.ts now at 100% line coverage (99.6% statement, 92.82% branch, 90.9% function).
1020 lines
36 KiB
TypeScript
1020 lines
36 KiB
TypeScript
import { NoteTools } from '../src/tools/note-tools';
|
|
import { createMockVaultAdapter, createMockFileManagerAdapter, createMockTFile, createMockTFolder } from './__mocks__/adapters';
|
|
import { App, Vault, TFile, TFolder } from 'obsidian';
|
|
|
|
// Mock PathUtils since NoteTools uses it extensively
|
|
jest.mock('../src/utils/path-utils', () => ({
|
|
PathUtils: {
|
|
normalizePath: jest.fn((path: string) => path),
|
|
isValidVaultPath: jest.fn(() => true),
|
|
resolveFile: jest.fn(),
|
|
fileExists: jest.fn(),
|
|
folderExists: jest.fn(),
|
|
pathExists: jest.fn(),
|
|
getParentPath: jest.fn((path: string) => {
|
|
const lastSlash = path.lastIndexOf('/');
|
|
return lastSlash > 0 ? path.substring(0, lastSlash) : '';
|
|
})
|
|
}
|
|
}));
|
|
|
|
// Import the mocked PathUtils
|
|
import { PathUtils } from '../src/utils/path-utils';
|
|
|
|
describe('NoteTools', () => {
|
|
let noteTools: NoteTools;
|
|
let mockVault: ReturnType<typeof createMockVaultAdapter>;
|
|
let mockFileManager: ReturnType<typeof createMockFileManagerAdapter>;
|
|
let mockApp: App;
|
|
|
|
beforeEach(() => {
|
|
mockVault = createMockVaultAdapter();
|
|
mockFileManager = createMockFileManagerAdapter();
|
|
mockApp = new App();
|
|
noteTools = new NoteTools(mockVault, mockFileManager, mockApp);
|
|
|
|
// Reset all mocks
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('readNote', () => {
|
|
it('should read note content successfully', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const content = '# Test Note\n\nThis is test content.';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
|
|
const result = await noteTools.readNote('test.md');
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(result.content[0].text).toBe(content);
|
|
expect(mockVault.read).toHaveBeenCalledWith(mockFile);
|
|
});
|
|
|
|
it('should return error if file not found', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
|
|
const result = await noteTools.readNote('nonexistent.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not found');
|
|
});
|
|
|
|
it('should return error if path is a folder', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await noteTools.readNote('folder');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not a file');
|
|
});
|
|
|
|
it('should handle read errors', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockRejectedValue(new Error('Read permission denied'));
|
|
|
|
const result = await noteTools.readNote('test.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Read permission denied');
|
|
});
|
|
|
|
it('should parse frontmatter when requested', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const content = '---\ntitle: Test\ntags: [test, example]\n---\n\nContent here';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
|
|
const result = await noteTools.readNote('test.md', { parseFrontmatter: true });
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.hasFrontmatter).toBe(true);
|
|
expect(parsed.path).toBe('test.md');
|
|
// frontmatter field is the raw YAML string
|
|
expect(parsed.frontmatter).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('createNote', () => {
|
|
it('should create note successfully', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
|
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
|
|
|
const result = await noteTools.createNote('test.md', 'content');
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.create).toHaveBeenCalledWith('test.md', 'content');
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.success).toBe(true);
|
|
expect(parsed.path).toBe('test.md');
|
|
});
|
|
|
|
it('should return error if file exists and strategy is error', async () => {
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await noteTools.createNote('test.md', 'content', false, 'error');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('already exists');
|
|
});
|
|
|
|
it('should overwrite if strategy is overwrite', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(true);
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.delete = jest.fn().mockResolvedValue(undefined);
|
|
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
|
|
|
const result = await noteTools.createNote('test.md', 'content', false, 'overwrite');
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.delete).toHaveBeenCalledWith(mockFile);
|
|
expect(mockVault.create).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should rename if strategy is rename', async () => {
|
|
const mockFile = createMockTFile('test 1.md');
|
|
|
|
(PathUtils.fileExists as jest.Mock)
|
|
.mockReturnValueOnce(true) // Original exists
|
|
.mockReturnValueOnce(false); // test 1.md doesn't exist
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
|
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
|
|
|
const result = await noteTools.createNote('test.md', 'content', false, 'rename');
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.create).toHaveBeenCalledWith('test 1.md', 'content');
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.renamed).toBe(true);
|
|
expect(parsed.originalPath).toBe('test.md');
|
|
});
|
|
|
|
it('should create file with incremented counter when conflicts exist', async () => {
|
|
const mockFile = createMockTFile('test 3.md');
|
|
|
|
(PathUtils.fileExists as jest.Mock)
|
|
.mockReturnValueOnce(true) // Original test.md exists
|
|
.mockReturnValueOnce(true) // test 1.md exists
|
|
.mockReturnValueOnce(true) // test 2.md exists
|
|
.mockReturnValueOnce(false); // test 3.md doesn't exist
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
|
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
|
|
|
const result = await noteTools.createNote('test.md', 'content', false, 'rename');
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.create).toHaveBeenCalledWith('test 3.md', 'content');
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.renamed).toBe(true);
|
|
expect(parsed.originalPath).toBe('test.md');
|
|
expect(parsed.path).toBe('test 3.md');
|
|
});
|
|
|
|
it('should return error if parent folder does not exist and createParents is false', async () => {
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock).mockReturnValue('folder');
|
|
(PathUtils.pathExists as jest.Mock).mockReturnValue(false);
|
|
|
|
const result = await noteTools.createNote('folder/file.md', 'content', false);
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Parent folder');
|
|
});
|
|
|
|
it('should create parent folders when createParents is true', async () => {
|
|
const mockFile = createMockTFile('folder/file.md');
|
|
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock)
|
|
.mockReturnValueOnce('folder') // getParentPath('folder/file.md') in createNote
|
|
.mockReturnValueOnce(''); // getParentPath('folder') in createParentFolders - stops recursion
|
|
(PathUtils.pathExists as jest.Mock)
|
|
.mockReturnValueOnce(false) // Check in createNote: parentPath exists?
|
|
.mockReturnValueOnce(false); // Check in createParentFolders: folder exists?
|
|
mockVault.createFolder = jest.fn().mockResolvedValue(undefined);
|
|
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
|
|
|
const result = await noteTools.createNote('folder/file.md', 'content', true);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.createFolder).toHaveBeenCalledWith('folder');
|
|
expect(mockVault.create).toHaveBeenCalledWith('folder/file.md', 'content');
|
|
});
|
|
|
|
it('should handle create errors', async () => {
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
|
mockVault.create = jest.fn().mockRejectedValue(new Error('Disk full'));
|
|
|
|
const result = await noteTools.createNote('test.md', 'content');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Disk full');
|
|
});
|
|
|
|
it('should return error if parent path is a file', async () => {
|
|
(PathUtils.fileExists as jest.Mock)
|
|
.mockReturnValueOnce(false) // test.md doesn't exist
|
|
.mockReturnValueOnce(true); // parent is a file
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock).mockReturnValue('parent');
|
|
|
|
const result = await noteTools.createNote('parent/test.md', 'content');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not a folder');
|
|
});
|
|
|
|
it('should return error if path is a folder', async () => {
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await noteTools.createNote('folder', 'content');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not a file');
|
|
});
|
|
});
|
|
|
|
describe('updateNote', () => {
|
|
it('should update note successfully', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const currentContent = 'old content';
|
|
const newContent = 'new content';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(currentContent);
|
|
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.updateNote('test.md', newContent);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.modify).toHaveBeenCalledWith(mockFile, newContent);
|
|
expect(result.content[0].text).toContain('updated successfully');
|
|
});
|
|
|
|
it('should return error if file not found', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
|
|
const result = await noteTools.updateNote('nonexistent.md', 'content');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not found');
|
|
});
|
|
|
|
it('should return error if path is a folder', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await noteTools.updateNote('folder', 'content');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not a file');
|
|
});
|
|
|
|
it('should handle update errors', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue('old content');
|
|
mockVault.modify = jest.fn().mockRejectedValue(new Error('File locked'));
|
|
|
|
const result = await noteTools.updateNote('test.md', 'new content');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('File locked');
|
|
});
|
|
|
|
it('should prevent waypoint modification', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const waypointContent = 'before\n%% Begin Waypoint %%\nwaypoint content\n%% End Waypoint %%\nafter';
|
|
const newContent = 'before\nmodified waypoint\nafter';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(waypointContent);
|
|
|
|
const result = await noteTools.updateNote('test.md', newContent);
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Waypoint');
|
|
});
|
|
});
|
|
|
|
describe('deleteNote', () => {
|
|
it('should soft delete note successfully', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.trash = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.deleteNote('test.md', true, false);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.trash).toHaveBeenCalledWith(mockFile, true);
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.deleted).toBe(true);
|
|
expect(parsed.soft).toBe(true);
|
|
});
|
|
|
|
it('should permanently delete note', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.delete = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.deleteNote('test.md', false, false);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.delete).toHaveBeenCalledWith(mockFile);
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.deleted).toBe(true);
|
|
expect(parsed.soft).toBe(false);
|
|
});
|
|
|
|
it('should handle dry run', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
|
|
const result = await noteTools.deleteNote('test.md', true, true);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.deleted).toBe(false);
|
|
expect(parsed.dryRun).toBe(true);
|
|
expect(mockVault.trash).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return error if file not found', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
|
|
const result = await noteTools.deleteNote('nonexistent.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not found');
|
|
});
|
|
|
|
it('should return error if path is a folder', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await noteTools.deleteNote('folder');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Path is a folder');
|
|
});
|
|
|
|
it('should handle delete errors', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.trash = jest.fn().mockRejectedValue(new Error('Cannot delete'));
|
|
|
|
const result = await noteTools.deleteNote('test.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Cannot delete');
|
|
});
|
|
|
|
it('should check version if ifMatch provided', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
|
|
// Wrong version
|
|
const result = await noteTools.deleteNote('test.md', true, false, '1000-50');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Version mismatch');
|
|
expect(mockVault.trash).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('renameFile', () => {
|
|
it('should rename file successfully', async () => {
|
|
const mockFile = createMockTFile('old.md');
|
|
const renamedFile = createMockTFile('new.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock)
|
|
.mockReturnValueOnce(mockFile) // Source file
|
|
.mockReturnValueOnce(renamedFile); // After rename
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.pathExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
|
mockFileManager.renameFile = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.renameFile('old.md', 'new.md');
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockFileManager.renameFile).toHaveBeenCalledWith(mockFile, 'new.md');
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.success).toBe(true);
|
|
expect(parsed.oldPath).toBe('old.md');
|
|
expect(parsed.newPath).toBe('new.md');
|
|
});
|
|
|
|
it('should return error if source file not found', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
|
|
const result = await noteTools.renameFile('nonexistent.md', 'new.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not found');
|
|
});
|
|
|
|
it('should return error if source path is a folder', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await noteTools.renameFile('folder', 'new.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not a file');
|
|
});
|
|
|
|
it('should return error if destination exists', async () => {
|
|
const mockFile = createMockTFile('old.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await noteTools.renameFile('old.md', 'existing.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('already exists');
|
|
});
|
|
|
|
it('should return error if destination path is a folder', async () => {
|
|
const mockFile = createMockTFile('old.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await noteTools.renameFile('old.md', 'existing-folder');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not a file');
|
|
});
|
|
|
|
it('should handle rename errors', async () => {
|
|
const mockFile = createMockTFile('old.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.pathExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
|
mockFileManager.renameFile = jest.fn().mockRejectedValue(new Error('Name conflict'));
|
|
|
|
const result = await noteTools.renameFile('old.md', 'new.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Name conflict');
|
|
});
|
|
|
|
it('should check version if ifMatch provided', async () => {
|
|
const mockFile = createMockTFile('old.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
|
|
// Wrong version
|
|
const result = await noteTools.renameFile('old.md', 'new.md', true, '1000-50');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Version mismatch');
|
|
expect(mockFileManager.renameFile).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should create parent folders if needed', async () => {
|
|
const mockFile = createMockTFile('old.md');
|
|
const renamedFile = createMockTFile('folder/new.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock)
|
|
.mockReturnValueOnce(mockFile)
|
|
.mockReturnValueOnce(renamedFile);
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock)
|
|
.mockReturnValueOnce('folder') // getParentPath('folder/new.md') in renameFile
|
|
.mockReturnValueOnce(''); // getParentPath('folder') in createParentFolders
|
|
(PathUtils.pathExists as jest.Mock)
|
|
.mockReturnValueOnce(false) // Check in renameFile: parentPath exists?
|
|
.mockReturnValueOnce(false); // Check in createParentFolders: folder exists?
|
|
mockVault.createFolder = jest.fn().mockResolvedValue(undefined);
|
|
mockFileManager.renameFile = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.renameFile('old.md', 'folder/new.md');
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.createFolder).toHaveBeenCalledWith('folder');
|
|
});
|
|
});
|
|
|
|
describe('readExcalidraw', () => {
|
|
it('should read Excalidraw file successfully', async () => {
|
|
const mockFile = createMockTFile('drawing.md');
|
|
// Excalidraw files must have the Drawing section with json code block
|
|
const excalidrawContent = `# Text Elements
|
|
Some text
|
|
|
|
## Drawing
|
|
\`\`\`json
|
|
{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[{"id":"1","type":"rectangle"}],"appState":{"viewBackgroundColor":"#ffffff"},"files":{}}
|
|
\`\`\``;
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(excalidrawContent);
|
|
|
|
const result = await noteTools.readExcalidraw('drawing.md');
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.isExcalidraw).toBe(true);
|
|
});
|
|
|
|
it('should include compressed data when includeCompressed is true', async () => {
|
|
const mockFile = createMockTFile('drawing.md');
|
|
const excalidrawContent = `# Text Elements
|
|
Some text
|
|
|
|
## Drawing
|
|
\`\`\`json
|
|
{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[{"id":"1","type":"rectangle"}],"appState":{"viewBackgroundColor":"#ffffff"},"files":{}}
|
|
\`\`\``;
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(excalidrawContent);
|
|
|
|
const result = await noteTools.readExcalidraw('drawing.md', { includeCompressed: true });
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.isExcalidraw).toBe(true);
|
|
expect(parsed.compressedData).toBe(excalidrawContent);
|
|
});
|
|
|
|
it('should return error for non-Excalidraw files', async () => {
|
|
const mockFile = createMockTFile('regular.md');
|
|
const content = '# Regular Note\n\nNot an Excalidraw file';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
|
|
const result = await noteTools.readExcalidraw('regular.md');
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.isExcalidraw).toBe(false);
|
|
});
|
|
|
|
it('should return error if file not found', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
|
|
const result = await noteTools.readExcalidraw('nonexistent.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not found');
|
|
});
|
|
|
|
it('should return error if path is a folder', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await noteTools.readExcalidraw('folder');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not a file');
|
|
});
|
|
|
|
it('should handle read errors', async () => {
|
|
const mockFile = createMockTFile('drawing.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockRejectedValue(new Error('Read error'));
|
|
|
|
const result = await noteTools.readExcalidraw('drawing.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Read error');
|
|
});
|
|
});
|
|
|
|
describe('updateFrontmatter', () => {
|
|
it('should update frontmatter successfully', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
const content = '---\ntitle: Old\n---\n\nContent';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.updateFrontmatter('test.md', { title: 'New', author: 'Test' });
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.modify).toHaveBeenCalled();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.success).toBe(true);
|
|
expect(parsed.updatedFields).toContain('title');
|
|
expect(parsed.updatedFields).toContain('author');
|
|
});
|
|
|
|
it('should add frontmatter to file without existing frontmatter', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
const content = 'Regular content without frontmatter';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.updateFrontmatter('test.md', { title: 'New Title', tags: ['test'] });
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.modify).toHaveBeenCalled();
|
|
const modifyCall = (mockVault.modify as jest.Mock).mock.calls[0];
|
|
const newContent = modifyCall[1];
|
|
// Should have frontmatter at the beginning followed by original content
|
|
expect(newContent).toContain('---\n');
|
|
expect(newContent).toContain('title:');
|
|
expect(newContent).toContain('tags:');
|
|
expect(newContent).toContain('Regular content without frontmatter');
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.success).toBe(true);
|
|
expect(parsed.updatedFields).toContain('title');
|
|
expect(parsed.updatedFields).toContain('tags');
|
|
});
|
|
|
|
it('should remove frontmatter fields', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
const content = '---\ntitle: Test\nauthor: Me\n---\n\nContent';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.updateFrontmatter('test.md', undefined, ['author']);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.removedFields).toContain('author');
|
|
});
|
|
|
|
it('should return error if no operations provided', async () => {
|
|
const result = await noteTools.updateFrontmatter('test.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('No operations provided');
|
|
});
|
|
|
|
it('should return error if file not found', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
|
|
const result = await noteTools.updateFrontmatter('nonexistent.md', { title: 'Test' });
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not found');
|
|
});
|
|
|
|
it('should return error if path is a folder', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await noteTools.updateFrontmatter('folder', { title: 'Test' });
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not a file');
|
|
});
|
|
|
|
it('should check version if ifMatch provided', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
|
|
// Wrong version
|
|
const result = await noteTools.updateFrontmatter('test.md', { title: 'Test' }, [], '1000-50');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Version mismatch');
|
|
expect(mockVault.modify).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle update errors', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const content = '---\ntitle: Test\n---\n\nContent';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
mockVault.modify = jest.fn().mockRejectedValue(new Error('Write error'));
|
|
|
|
const result = await noteTools.updateFrontmatter('test.md', { title: 'New' });
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Write error');
|
|
});
|
|
});
|
|
|
|
describe('updateSections', () => {
|
|
it('should update sections successfully', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
const content = 'Line 1\nLine 2\nLine 3\nLine 4';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.updateSections('test.md', [
|
|
{ startLine: 2, endLine: 3, content: 'New Line 2\nNew Line 3' }
|
|
]);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.modify).toHaveBeenCalled();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.success).toBe(true);
|
|
expect(parsed.sectionsUpdated).toBe(1);
|
|
});
|
|
|
|
it('should return error if no edits provided', async () => {
|
|
const result = await noteTools.updateSections('test.md', []);
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('No edits provided');
|
|
});
|
|
|
|
it('should return error for invalid line range', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const content = 'Line 1\nLine 2\nLine 3';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
|
|
const result = await noteTools.updateSections('test.md', [
|
|
{ startLine: 1, endLine: 10, content: 'New' }
|
|
]);
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Invalid line range');
|
|
});
|
|
|
|
it('should check version if ifMatch provided', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
|
|
// Wrong version
|
|
const result = await noteTools.updateSections('test.md', [
|
|
{ startLine: 1, endLine: 1, content: 'New' }
|
|
], '1000-50');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Version mismatch');
|
|
expect(mockVault.modify).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle update errors', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const content = 'Line 1\nLine 2';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
mockVault.modify = jest.fn().mockRejectedValue(new Error('Update error'));
|
|
|
|
const result = await noteTools.updateSections('test.md', [
|
|
{ startLine: 1, endLine: 1, content: 'New' }
|
|
]);
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Update error');
|
|
});
|
|
|
|
it('should return error if file not found', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
|
|
const result = await noteTools.updateSections('nonexistent.md', [
|
|
{ startLine: 1, endLine: 1, content: 'New' }
|
|
]);
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not found');
|
|
});
|
|
|
|
it('should return error if path is a folder', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(null);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(true);
|
|
|
|
const result = await noteTools.updateSections('folder', [
|
|
{ startLine: 1, endLine: 1, content: 'New' }
|
|
]);
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not a file');
|
|
});
|
|
});
|
|
|
|
describe('path validation', () => {
|
|
beforeEach(() => {
|
|
(PathUtils.isValidVaultPath as jest.Mock).mockReturnValue(false);
|
|
});
|
|
|
|
it('should validate path in readNote', async () => {
|
|
const result = await noteTools.readNote('../../../etc/passwd');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Invalid path');
|
|
});
|
|
|
|
it('should validate path in createNote', async () => {
|
|
const result = await noteTools.createNote('../bad.md', 'content');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Invalid path');
|
|
});
|
|
|
|
it('should validate path in updateNote', async () => {
|
|
const result = await noteTools.updateNote('/absolute/path.md', 'content');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Invalid path');
|
|
});
|
|
|
|
it('should validate path in deleteNote', async () => {
|
|
const result = await noteTools.deleteNote('bad//path.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Invalid path');
|
|
});
|
|
|
|
it('should validate source path in renameFile', async () => {
|
|
const result = await noteTools.renameFile('../bad.md', 'good.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Invalid path');
|
|
});
|
|
|
|
it('should validate destination path in renameFile', async () => {
|
|
(PathUtils.isValidVaultPath as jest.Mock)
|
|
.mockReturnValueOnce(true) // source is valid
|
|
.mockReturnValueOnce(false); // destination is invalid
|
|
|
|
const result = await noteTools.renameFile('good.md', '../bad.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Invalid path');
|
|
});
|
|
|
|
it('should validate path in readExcalidraw', async () => {
|
|
const result = await noteTools.readExcalidraw('../../bad.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Invalid path');
|
|
});
|
|
|
|
it('should validate path in updateFrontmatter', async () => {
|
|
const result = await noteTools.updateFrontmatter('../bad.md', { title: 'Test' });
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Invalid path');
|
|
});
|
|
|
|
it('should validate path in updateSections', async () => {
|
|
const result = await noteTools.updateSections('../bad.md', [
|
|
{ startLine: 1, endLine: 1, content: 'New' }
|
|
]);
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('Invalid path');
|
|
});
|
|
});
|
|
|
|
describe('empty path validation', () => {
|
|
it('should reject empty path in readNote', async () => {
|
|
const result = await noteTools.readNote('');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('empty');
|
|
});
|
|
|
|
it('should reject empty path in createNote', async () => {
|
|
const result = await noteTools.createNote('', 'content');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('empty');
|
|
});
|
|
|
|
it('should reject empty path in updateNote', async () => {
|
|
const result = await noteTools.updateNote('', 'content');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('empty');
|
|
});
|
|
|
|
it('should reject empty path in deleteNote', async () => {
|
|
const result = await noteTools.deleteNote('');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('empty');
|
|
});
|
|
|
|
it('should reject empty source path in renameFile', async () => {
|
|
const result = await noteTools.renameFile('', 'new.md');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('empty');
|
|
});
|
|
|
|
it('should reject empty destination path in renameFile', async () => {
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(createMockTFile('old.md'));
|
|
|
|
const result = await noteTools.renameFile('old.md', '');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('empty');
|
|
});
|
|
|
|
it('should reject empty path in readExcalidraw', async () => {
|
|
const result = await noteTools.readExcalidraw('');
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('empty');
|
|
});
|
|
|
|
it('should reject empty path in updateFrontmatter', async () => {
|
|
const result = await noteTools.updateFrontmatter('', { title: 'Test' });
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('empty');
|
|
});
|
|
|
|
it('should reject empty path in updateSections', async () => {
|
|
const result = await noteTools.updateSections('', [
|
|
{ startLine: 1, endLine: 1, content: 'New' }
|
|
]);
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('empty');
|
|
});
|
|
});
|
|
}); |