mirror of
https://github.com/aaronsb/obsidian-mcp-plugin.git
synced 2026-07-22 06:45:14 +00:00
- Added support for targeting specific document structures in patch operations
- Heading targeting: Use 'Section::Subsection' syntax to target nested headings
- Block targeting: Target blocks by their ID (^blockId)
- Frontmatter targeting: Add, update, or append to frontmatter fields
- Maintains backward compatibility with oldText/newText replacement
- Added comprehensive unit tests for all patch modes
Example usage:
- Target heading: {targetType: 'heading', target: 'Section One', operation: 'append'}
- Target block: {targetType: 'block', target: 'myBlockId', operation: 'prepend'}
- Target frontmatter: {targetType: 'frontmatter', target: 'tags', operation: 'replace'}
Resolves the disconnect between API design and implementation
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
235 lines
No EOL
5.7 KiB
TypeScript
235 lines
No EOL
5.7 KiB
TypeScript
import { ObsidianAPI } from '../src/utils/obsidian-api';
|
|
import { App, TFile } from 'obsidian';
|
|
|
|
// Mock the instanceof check for TFile
|
|
jest.mock('obsidian', () => {
|
|
const originalModule = jest.requireActual('../tests/__mocks__/obsidian');
|
|
return {
|
|
...originalModule,
|
|
TFile: class TFile {
|
|
static [Symbol.hasInstance](instance: any) {
|
|
return instance && instance._isTFile;
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
describe('Patch Operations', () => {
|
|
let api: ObsidianAPI;
|
|
let mockApp: App;
|
|
let mockFile: any;
|
|
let mockVault: any;
|
|
|
|
beforeEach(() => {
|
|
// Create minimal mocks for testing
|
|
mockFile = {
|
|
path: 'test.md',
|
|
name: 'test.md',
|
|
extension: 'md',
|
|
_isTFile: true // Mark as TFile for instanceof check
|
|
};
|
|
|
|
mockVault = {
|
|
getAbstractFileByPath: jest.fn().mockReturnValue(mockFile),
|
|
read: jest.fn(),
|
|
modify: jest.fn().mockResolvedValue(undefined)
|
|
};
|
|
|
|
mockApp = {
|
|
vault: mockVault
|
|
} as any;
|
|
|
|
api = new ObsidianAPI(mockApp);
|
|
});
|
|
|
|
describe('Structured Patch - Heading', () => {
|
|
it('should append content to a heading section', async () => {
|
|
const originalContent = `# Main Title
|
|
|
|
## Section One
|
|
Original content here.
|
|
|
|
## Section Two
|
|
Different content.`;
|
|
|
|
const expectedContent = `# Main Title
|
|
|
|
## Section One
|
|
Original content here.
|
|
|
|
|
|
New appended content.
|
|
## Section Two
|
|
Different content.`;
|
|
|
|
mockVault.read.mockResolvedValue(originalContent);
|
|
|
|
const result = await api.patchVaultFile('test.md', {
|
|
targetType: 'heading',
|
|
target: 'Section One',
|
|
operation: 'append',
|
|
content: 'New appended content.'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockVault.modify).toHaveBeenCalledWith(mockFile, expectedContent);
|
|
});
|
|
|
|
it('should prepend content after a heading', async () => {
|
|
const originalContent = `# Main Title
|
|
|
|
## Section One
|
|
Original content here.`;
|
|
|
|
const expectedContent = `# Main Title
|
|
|
|
## Section One
|
|
|
|
New prepended content.
|
|
Original content here.`;
|
|
|
|
mockVault.read.mockResolvedValue(originalContent);
|
|
|
|
const result = await api.patchVaultFile('test.md', {
|
|
targetType: 'heading',
|
|
target: 'Section One',
|
|
operation: 'prepend',
|
|
content: 'New prepended content.'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockVault.modify).toHaveBeenCalledWith(mockFile, expectedContent);
|
|
});
|
|
|
|
it('should handle nested headings with :: syntax', async () => {
|
|
const originalContent = `# Main Title
|
|
|
|
## Section One
|
|
|
|
### Subsection
|
|
Original subsection content.
|
|
|
|
## Section Two`;
|
|
|
|
const expectedContent = `# Main Title
|
|
|
|
## Section One
|
|
|
|
### Subsection
|
|
Original subsection content.
|
|
|
|
|
|
New content in subsection.
|
|
## Section Two`;
|
|
|
|
mockVault.read.mockResolvedValue(originalContent);
|
|
|
|
const result = await api.patchVaultFile('test.md', {
|
|
targetType: 'heading',
|
|
target: 'Section One::Subsection',
|
|
operation: 'append',
|
|
content: 'New content in subsection.'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockVault.modify).toHaveBeenCalledWith(mockFile, expectedContent);
|
|
});
|
|
});
|
|
|
|
describe('Structured Patch - Frontmatter', () => {
|
|
it('should add a new frontmatter field', async () => {
|
|
const originalContent = `---
|
|
title: Test Document
|
|
---
|
|
|
|
# Content`;
|
|
|
|
const expectedContent = `---
|
|
title: Test Document
|
|
status: published
|
|
---
|
|
|
|
# Content`;
|
|
|
|
mockVault.read.mockResolvedValue(originalContent);
|
|
|
|
const result = await api.patchVaultFile('test.md', {
|
|
targetType: 'frontmatter',
|
|
target: 'status',
|
|
operation: 'replace',
|
|
content: 'published'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockVault.modify).toHaveBeenCalledWith(mockFile, expectedContent);
|
|
});
|
|
|
|
it('should create frontmatter if it does not exist', async () => {
|
|
const originalContent = `# Content without frontmatter`;
|
|
|
|
const expectedContent = `---
|
|
tags: new-tag
|
|
---
|
|
|
|
# Content without frontmatter`;
|
|
|
|
mockVault.read.mockResolvedValue(originalContent);
|
|
|
|
const result = await api.patchVaultFile('test.md', {
|
|
targetType: 'frontmatter',
|
|
target: 'tags',
|
|
operation: 'replace',
|
|
content: 'new-tag'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockVault.modify).toHaveBeenCalledWith(mockFile, expectedContent);
|
|
});
|
|
});
|
|
|
|
describe('Structured Patch - Block', () => {
|
|
it('should append content to a block', async () => {
|
|
const originalContent = `# Document
|
|
|
|
This is a paragraph with a block ID. ^myblock
|
|
|
|
Another paragraph.`;
|
|
|
|
const expectedContent = `# Document
|
|
|
|
This is a paragraph with a block ID. Additional content ^myblock
|
|
|
|
Another paragraph.`;
|
|
|
|
mockVault.read.mockResolvedValue(originalContent);
|
|
|
|
const result = await api.patchVaultFile('test.md', {
|
|
targetType: 'block',
|
|
target: 'myblock',
|
|
operation: 'append',
|
|
content: 'Additional content'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockVault.modify).toHaveBeenCalledWith(mockFile, expectedContent);
|
|
});
|
|
});
|
|
|
|
describe('Legacy Patch Operations', () => {
|
|
it('should still support old text replacement', async () => {
|
|
const originalContent = `This is the original text.`;
|
|
const expectedContent = `This is the modified text.`;
|
|
|
|
mockVault.read.mockResolvedValue(originalContent);
|
|
|
|
const result = await api.patchVaultFile('test.md', {
|
|
operation: 'replace',
|
|
old_text: 'original',
|
|
new_text: 'modified'
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(mockVault.modify).toHaveBeenCalledWith(mockFile, expectedContent);
|
|
});
|
|
});
|
|
}); |