feat(update_sections): require ifMatch with force opt-out

Add `force` parameter to updateSections method that allows bypassing
the ifMatch version check. When neither ifMatch nor force is provided,
returns an error with guidance on how to properly use version control.

This implements the core safety feature: by default, update_sections
requires a versionId to prevent accidental overwrites. Callers must
either:
1. Pass a valid ifMatch parameter from read_note's versionId
2. Explicitly set force:true to bypass the check (not recommended)

Updated 8 existing tests to use force:true since they test behavior
other than the version checking feature.
This commit is contained in:
2026-01-31 17:13:53 -05:00
parent abd712f694
commit 59433bc896
2 changed files with 24 additions and 9 deletions

View File

@@ -905,7 +905,8 @@ export class NoteTools {
path: string,
edits: SectionEdit[],
ifMatch?: string,
validateLinks: boolean = true
validateLinks: boolean = true,
force: boolean = false
): Promise<CallToolResult> {
// Validate path
if (!path || path.trim() === '') {
@@ -930,6 +931,20 @@ export class NoteTools {
};
}
// Require ifMatch unless force is true
if (!ifMatch && !force) {
return {
content: [{
type: "text",
text: JSON.stringify({
error: 'Version check required',
message: 'The ifMatch parameter is required to prevent overwriting concurrent changes. First call read_note with withLineNumbers:true to get the versionId, then pass it as ifMatch. To bypass this check, set force:true (not recommended).'
}, null, 2)
}],
isError: true
};
}
// Resolve file
const file = PathUtils.resolveFile(this.app, path);

View File

@@ -933,7 +933,7 @@ Some text
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();
@@ -958,7 +958,7 @@ Some text
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');
@@ -993,7 +993,7 @@ Some text
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');
@@ -1005,7 +1005,7 @@ Some text
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');
@@ -1017,7 +1017,7 @@ Some text
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');
@@ -1365,7 +1365,7 @@ Some text
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);
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);
@@ -1381,7 +1381,7 @@ Some text
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);
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);
@@ -1398,7 +1398,7 @@ Some text
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);
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);