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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user