Change withLineNumbers default from false to true so AI assistants can reference specific line numbers when discussing notes. - Apply line numbers to both simple and parseFrontmatter paths - Add totalLines to ParsedNote type - Update schema description to document new default - Update tests to expect line-numbered content by default BREAKING CHANGE: read_note now returns line-numbered content by default. Pass withLineNumbers: false to get raw content.
1483 lines
55 KiB
TypeScript
1483 lines
55 KiB
TypeScript
import { NoteTools } from '../src/tools/note-tools';
|
|
import { createMockVaultAdapter, createMockFileManagerAdapter, createMockMetadataCacheAdapter, 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) : '';
|
|
})
|
|
}
|
|
}));
|
|
|
|
// Mock LinkUtils for link validation tests
|
|
jest.mock('../src/utils/link-utils', () => ({
|
|
LinkUtils: {
|
|
validateLinks: jest.fn().mockReturnValue({
|
|
valid: [],
|
|
brokenNotes: [],
|
|
brokenHeadings: [],
|
|
summary: 'No links found'
|
|
})
|
|
}
|
|
}));
|
|
|
|
// 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 mockMetadata: ReturnType<typeof createMockMetadataCacheAdapter>;
|
|
let mockApp: App;
|
|
|
|
beforeEach(() => {
|
|
mockVault = createMockVaultAdapter();
|
|
mockFileManager = createMockFileManagerAdapter();
|
|
mockMetadata = createMockMetadataCacheAdapter();
|
|
mockApp = new App();
|
|
noteTools = new NoteTools(mockVault, mockFileManager, mockMetadata, mockApp);
|
|
|
|
// Reset all mocks
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('readNote', () => {
|
|
it('should read note content successfully with line numbers by default', 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();
|
|
// Now returns JSON with content (line-numbered by default) and wordCount
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.content).toBe('1→# Test Note\n2→\n3→This is test content.');
|
|
expect(parsed.totalLines).toBe(3);
|
|
expect(parsed.wordCount).toBe(7); // Test Note This is test 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();
|
|
});
|
|
|
|
it('should include word count when withContent is true', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const content = '# Test Note\n\nThis is a test note with some words.';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
|
|
const result = await noteTools.readNote('test.md', { withContent: true, withLineNumbers: false });
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.content).toBe(content);
|
|
expect(parsed.wordCount).toBe(11); // Test Note This is a test note with some words
|
|
});
|
|
|
|
it('should include word count when parseFrontmatter is true', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const content = '---\ntitle: Test\n---\n\nThis is content.';
|
|
|
|
(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.wordCount).toBe(3); // "This is content."
|
|
});
|
|
|
|
it('should exclude frontmatter from word count', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const content = '---\ntitle: Test Note\ntags: [test, example]\n---\n\nActual content words.';
|
|
|
|
(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.wordCount).toBe(3); // "Actual content words."
|
|
});
|
|
|
|
it('should exclude Obsidian comments from word count', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const content = 'Visible text %% Hidden comment %% more visible.';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
|
|
const result = await noteTools.readNote('test.md', { withContent: true });
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.wordCount).toBe(4); // "Visible text more visible"
|
|
});
|
|
|
|
it('should return 0 word count for empty file', async () => {
|
|
const mockFile = createMockTFile('empty.md');
|
|
const content = '';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
|
|
const result = await noteTools.readNote('empty.md', { withContent: true });
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.wordCount).toBe(0);
|
|
});
|
|
|
|
it('should return JSON format with raw content when withLineNumbers is false', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
const content = '# Test Note\n\nContent here.';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
|
|
const result = await noteTools.readNote('test.md', { withLineNumbers: false });
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
// Returns JSON with raw content when line numbers disabled
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.content).toBe(content);
|
|
expect(parsed.wordCount).toBe(5); // Test Note Content here
|
|
});
|
|
|
|
it('should return numbered lines by default', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
const content = '# Title\n\nParagraph text\nMore text';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
|
|
const result = await noteTools.readNote('test.md');
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.content).toBe('1→# Title\n2→\n3→Paragraph text\n4→More text');
|
|
expect(parsed.totalLines).toBe(4);
|
|
expect(parsed.versionId).toBe('AXrGSV5GxqntccmzWCNwe7'); // SHA-256 hash of "2000-100"
|
|
expect(parsed.wordCount).toBe(6); // # Title Paragraph text More text
|
|
});
|
|
|
|
it('should return raw content when withLineNumbers is false', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
const content = '# Test';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue(content);
|
|
|
|
const result = await noteTools.readNote('test.md', { withLineNumbers: false });
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.content).toBe('# Test');
|
|
expect(parsed.totalLines).toBeUndefined();
|
|
expect(parsed.versionId).toBe('AXrGSV5GxqntccmzWCNwe7'); // SHA-256 hash of "2000-100"
|
|
});
|
|
|
|
it('should return numbered lines in parseFrontmatter path by default', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
const content = '---\ntitle: Test\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.content).toBe('1→---\n2→title: Test\n3→---\n4→\n5→Content here');
|
|
expect(parsed.totalLines).toBe(5);
|
|
});
|
|
|
|
it('should return raw content in parseFrontmatter path when withLineNumbers is false', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
const content = '---\ntitle: Test\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, withLineNumbers: false });
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.content).toBe(content);
|
|
expect(parsed.totalLines).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
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);
|
|
mockFileManager.trashFile = 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(mockFileManager.trashFile).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);
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.success).toBe(true);
|
|
expect(parsed.path).toBe('test.md');
|
|
expect(parsed.wordCount).toBeDefined();
|
|
});
|
|
|
|
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);
|
|
mockFileManager.trashFile = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.deleteNote('test.md', true, false);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockFileManager.trashFile).toHaveBeenCalledWith(mockFile);
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.deleted).toBe(true);
|
|
expect(parsed.soft).toBe(true);
|
|
expect(parsed.destination).toBe('trash');
|
|
});
|
|
|
|
it('should permanently delete note', async () => {
|
|
const mockFile = createMockTFile('test.md');
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockFileManager.trashFile = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.deleteNote('test.md', false, false);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockFileManager.trashFile).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);
|
|
mockFileManager.trashFile = jest.fn().mockResolvedValue(undefined);
|
|
|
|
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(parsed.destination).toBe('trash');
|
|
expect(mockFileManager.trashFile).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);
|
|
mockFileManager.trashFile = 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' }
|
|
], undefined, true, true); // validateLinks=true, force=true
|
|
|
|
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' }
|
|
], undefined, true, true); // validateLinks=true, force=true
|
|
|
|
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' }
|
|
], undefined, true, true); // validateLinks=true, force=true
|
|
|
|
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' }
|
|
], undefined, true, true); // validateLinks=true, force=true
|
|
|
|
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' }
|
|
], undefined, true, true); // validateLinks=true, force=true
|
|
|
|
expect(result.isError).toBe(true);
|
|
expect(result.content[0].text).toContain('not a file');
|
|
});
|
|
|
|
it('should return error when ifMatch not provided and force not set', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
const content = 'Line 1\nLine 2';
|
|
|
|
(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: 1, endLine: 1, content: 'New' }
|
|
]);
|
|
|
|
expect(result.isError).toBe(true);
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.error).toBe('Version check required');
|
|
expect(parsed.message).toContain('ifMatch parameter is required');
|
|
expect(mockVault.modify).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should proceed without ifMatch when force is true', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
const content = 'Line 1\nLine 2';
|
|
|
|
(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: 1, endLine: 1, content: 'New Line 1' }],
|
|
undefined, // no ifMatch
|
|
true, // validateLinks
|
|
true // force
|
|
);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.modify).toHaveBeenCalled();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.success).toBe(true);
|
|
});
|
|
|
|
it('should proceed with valid ifMatch without force', async () => {
|
|
const mockFile = createMockTFile('test.md', {
|
|
ctime: 1000,
|
|
mtime: 2000,
|
|
size: 100
|
|
});
|
|
const content = 'Line 1\nLine 2';
|
|
|
|
(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: 1, endLine: 1, content: 'New Line 1' }],
|
|
'AXrGSV5GxqntccmzWCNwe7' // valid ifMatch (SHA-256 hash of "2000-100")
|
|
);
|
|
|
|
expect(result.isError).toBeUndefined();
|
|
expect(mockVault.modify).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
|
|
describe('Word Count and Link Validation', () => {
|
|
beforeEach(() => {
|
|
// Setup default mocks for all word count/link validation tests
|
|
(PathUtils.isValidVaultPath as jest.Mock).mockReturnValue(true);
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
|
(PathUtils.resolveFile as jest.Mock).mockImplementation((app: any, path: string) => {
|
|
// Return null for non-existent files
|
|
return null;
|
|
});
|
|
});
|
|
|
|
describe('createNote with word count and link validation', () => {
|
|
beforeEach(() => {
|
|
// Setup mocks for these tests
|
|
(PathUtils.fileExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.folderExists as jest.Mock).mockReturnValue(false);
|
|
(PathUtils.getParentPath as jest.Mock).mockReturnValue('');
|
|
});
|
|
|
|
it('should return word count when creating a note', async () => {
|
|
const content = 'This is a test note with some words.';
|
|
const mockFile = createMockTFile('test-note.md');
|
|
|
|
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
|
|
|
const result = await noteTools.createNote('test-note.md', content);
|
|
|
|
expect(result.isError).toBeFalsy();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.wordCount).toBe(8);
|
|
});
|
|
|
|
it('should return link validation structure when creating a note', async () => {
|
|
const content = 'This note has some [[links]].';
|
|
const mockFile = createMockTFile('test-note.md');
|
|
|
|
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
|
|
|
const result = await noteTools.createNote('test-note.md', content);
|
|
|
|
expect(result.isError).toBeFalsy();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.linkValidation).toBeDefined();
|
|
expect(parsed.linkValidation).toHaveProperty('valid');
|
|
expect(parsed.linkValidation).toHaveProperty('brokenNotes');
|
|
expect(parsed.linkValidation).toHaveProperty('brokenHeadings');
|
|
expect(parsed.linkValidation).toHaveProperty('summary');
|
|
});
|
|
|
|
it('should skip link validation when validateLinks is false', async () => {
|
|
const content = 'This note links to [[Some Note]].';
|
|
const mockFile = createMockTFile('test-note.md');
|
|
|
|
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
|
|
|
const result = await noteTools.createNote('test-note.md', content, false, 'error', false);
|
|
|
|
expect(result.isError).toBeFalsy();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.wordCount).toBeDefined();
|
|
expect(parsed.linkValidation).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('updateNote with word count and link validation', () => {
|
|
it('should return word count when updating a note', async () => {
|
|
const mockFile = createMockTFile('update-test.md');
|
|
const newContent = 'This is updated content with several more words.';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue('Old content');
|
|
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.updateNote('update-test.md', newContent);
|
|
|
|
expect(result.isError).toBeFalsy();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.wordCount).toBe(8);
|
|
});
|
|
|
|
it('should return link validation structure when updating a note', async () => {
|
|
const mockFile = createMockTFile('update-test.md');
|
|
const newContent = 'Updated with [[Referenced]] link.';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue('Old content');
|
|
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.updateNote('update-test.md', newContent);
|
|
|
|
expect(result.isError).toBeFalsy();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.linkValidation).toBeDefined();
|
|
expect(parsed.linkValidation).toHaveProperty('valid');
|
|
expect(parsed.linkValidation).toHaveProperty('brokenNotes');
|
|
expect(parsed.linkValidation).toHaveProperty('brokenHeadings');
|
|
});
|
|
|
|
it('should skip link validation when validateLinks is false', async () => {
|
|
const mockFile = createMockTFile('update-test.md');
|
|
const newContent = 'Updated content with [[Some Link]].';
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue('Old content');
|
|
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.updateNote('update-test.md', newContent, false);
|
|
|
|
expect(result.isError).toBeFalsy();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.wordCount).toBeDefined();
|
|
expect(parsed.linkValidation).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('updateSections with word count and link validation', () => {
|
|
it('should return word count for entire note after section update', async () => {
|
|
const mockFile = createMockTFile('sections-test.md');
|
|
const edits = [{ startLine: 2, endLine: 2, content: 'Updated line two with more words' }];
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue('Line 1\nLine 2\nLine 3');
|
|
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.updateSections('sections-test.md', edits, undefined, true, true); // validateLinks=true, force=true
|
|
|
|
expect(result.isError).toBeFalsy();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.wordCount).toBeGreaterThan(0);
|
|
expect(parsed.sectionsUpdated).toBe(1);
|
|
});
|
|
|
|
it('should return link validation structure for entire note after section update', async () => {
|
|
const mockFile = createMockTFile('sections-test.md');
|
|
const edits = [{ startLine: 2, endLine: 2, content: 'See [[Link Target]] here' }];
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue('Line 1\nLine 2\nLine 3');
|
|
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.updateSections('sections-test.md', edits, undefined, true, true); // validateLinks=true, force=true
|
|
|
|
expect(result.isError).toBeFalsy();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.linkValidation).toBeDefined();
|
|
expect(parsed.linkValidation).toHaveProperty('valid');
|
|
expect(parsed.linkValidation).toHaveProperty('brokenNotes');
|
|
});
|
|
|
|
it('should skip link validation when validateLinks is false', async () => {
|
|
const mockFile = createMockTFile('sections-test.md');
|
|
const edits = [{ startLine: 1, endLine: 1, content: 'Updated with [[Link]]' }];
|
|
|
|
(PathUtils.resolveFile as jest.Mock).mockReturnValue(mockFile);
|
|
mockVault.read = jest.fn().mockResolvedValue('Line 1\nLine 2\nLine 3');
|
|
mockVault.modify = jest.fn().mockResolvedValue(undefined);
|
|
|
|
const result = await noteTools.updateSections('sections-test.md', edits, undefined, false, true); // validateLinks=false, force=true
|
|
|
|
expect(result.isError).toBeFalsy();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.wordCount).toBeDefined();
|
|
expect(parsed.linkValidation).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Word count with frontmatter and comments', () => {
|
|
it('should exclude frontmatter from word count', async () => {
|
|
const content = `---
|
|
title: Test Note
|
|
tags: [test]
|
|
---
|
|
|
|
This is the actual content with words.`;
|
|
const mockFile = createMockTFile('test-note.md');
|
|
|
|
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
|
|
|
const result = await noteTools.createNote('test-note.md', content);
|
|
|
|
expect(result.isError).toBeFalsy();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.wordCount).toBe(7); // "This is the actual content with words."
|
|
});
|
|
|
|
it('should exclude Obsidian comments from word count', async () => {
|
|
const content = `This is visible. %% This is hidden %% More visible.`;
|
|
const mockFile = createMockTFile('test-note.md');
|
|
|
|
mockVault.create = jest.fn().mockResolvedValue(mockFile);
|
|
|
|
const result = await noteTools.createNote('test-note.md', content);
|
|
|
|
expect(result.isError).toBeFalsy();
|
|
const parsed = JSON.parse(result.content[0].text);
|
|
expect(parsed.wordCount).toBe(5); // "This is visible. More visible." = 5 words
|
|
});
|
|
});
|
|
});
|
|
}); |