From 59433bc896f3e43cfff715a7050474ff23115c9d Mon Sep 17 00:00:00 2001 From: Bill Ballou Date: Sat, 31 Jan 2026 17:13:53 -0500 Subject: [PATCH] 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. --- src/tools/note-tools.ts | 17 ++++++++++++++++- tests/note-tools.test.ts | 16 ++++++++-------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/tools/note-tools.ts b/src/tools/note-tools.ts index d6e8137..1c616e8 100644 --- a/src/tools/note-tools.ts +++ b/src/tools/note-tools.ts @@ -905,7 +905,8 @@ export class NoteTools { path: string, edits: SectionEdit[], ifMatch?: string, - validateLinks: boolean = true + validateLinks: boolean = true, + force: boolean = false ): Promise { // 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); diff --git a/tests/note-tools.test.ts b/tests/note-tools.test.ts index 60981fe..68a12de 100644 --- a/tests/note-tools.test.ts +++ b/tests/note-tools.test.ts @@ -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);