mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
feat: add multi-patch support with whitespace-flexible matching
Enable PatchVaultFile to apply multiple patches in a single operation by converting oldContent and newContent from strings to arrays. Add whitespace-flexible regex matching as fallback when exact string matching fails, improving robustness for code with varying indentation. Update schemas, tool definitions, and all tests to reflect the new array-based API.
This commit is contained in:
parent
f5ae83edaf
commit
984ed9eb82
9 changed files with 137 additions and 69 deletions
|
|
@ -23,8 +23,8 @@ export const WriteVaultFileArgsSchema = z.object({
|
|||
|
||||
export const PatchVaultFileArgsSchema = z.object({
|
||||
file_path: z.string(),
|
||||
oldContent: z.string(),
|
||||
newContent: z.string(),
|
||||
oldContent: z.array(z.string()),
|
||||
newContent: z.array(z.string()),
|
||||
user_message: z.string()
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export const PatchVaultFile: IAIToolDefinition = {
|
|||
name: AITool.PatchVaultFile,
|
||||
description: `Apply targeted changes to an existing file in the vault by finding and replacing specific content.
|
||||
|
||||
This tool modifies specific sections of a file by matching exact content and replacing it with new content. It works by performing a direct string match and replace operation.
|
||||
This tool modifies specific sections of a file by matching exact content and replacing it with new content. It works by performing direct string match and replace operations. Multiple patches can be applied in a single call by providing corresponding arrays of old and new content.
|
||||
|
||||
**CRITICAL:** The content to match must be EXACTLY as it appears in the file - including all whitespace, indentation, blank lines, and line breaks.
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ Call this function:
|
|||
- When making edits in the middle of a file where you have clear surrounding context
|
||||
- When making simple line additions, deletions, or replacements with minimal changes
|
||||
- When you know the exact content that needs to be changed
|
||||
- When making multiple independent edits to the same file (use multiple entries in the arrays)
|
||||
|
||||
Do NOT use this function:
|
||||
- When rewriting most or all of a file's content
|
||||
|
|
@ -27,26 +28,33 @@ Do NOT use this function:
|
|||
description: "The full path to the file within the vault (e.g., 'folder/note.md')"
|
||||
},
|
||||
oldContent: {
|
||||
type: "string",
|
||||
description: `The exact content to find and replace in the file.
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
description: `An array of exact content strings to find in the file. Each entry corresponds to a matching entry in newContent at the same index.
|
||||
|
||||
CRITICAL MATCHING REQUIREMENTS:
|
||||
- Must match the file content EXACTLY character-for-character
|
||||
- Each entry must match the file content EXACTLY character-for-character
|
||||
- Include all whitespace, indentation, and line breaks exactly as they appear
|
||||
- Include enough context to make the match unique within the file
|
||||
- Include enough context to make each match unique within the file
|
||||
- Typically include 2-3 lines before and after the change for context
|
||||
- Match complete lines/statements to avoid breaking code structure
|
||||
- Preserve all blank lines that exist in the original
|
||||
|
||||
WARNING: The replacement will fail if:
|
||||
- The content doesn't exist in the file
|
||||
- Any content entry doesn't exist in the file
|
||||
- Whitespace/indentation doesn't match exactly
|
||||
- The match is ambiguous (appears multiple times in the file)
|
||||
- Line breaks are missing or incorrect`
|
||||
- Any match is ambiguous (appears multiple times in the file)
|
||||
- Line breaks are missing or incorrect
|
||||
- The number of oldContent entries doesn't match the number of newContent entries`
|
||||
},
|
||||
newContent: {
|
||||
type: "string",
|
||||
description: `The new content that will replace the old content. Ensure proper indentation and formatting matches the surrounding code.`
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
},
|
||||
description: `An array of new content strings that will replace the corresponding old content at the same index. Each entry must have a matching entry in oldContent. Ensure proper indentation and formatting matches the surrounding code.`
|
||||
},
|
||||
user_message: {
|
||||
type: "string",
|
||||
|
|
|
|||
|
|
@ -37,6 +37,12 @@ export abstract class StringTools {
|
|||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
// Builds a regex from a string that matches flexibly on whitespace but strictly on all other characters.
|
||||
public static toWhitespaceFlexibleRegex(input: string): RegExp {
|
||||
const pattern = this.escapeRegex(input).replace(/(\\\s|\s)+/g, "\\s+");
|
||||
return new RegExp(pattern);
|
||||
}
|
||||
|
||||
public static asRegex(input: string, requiredFlags: string[]): RegExp | null {
|
||||
let regex: RegExp;
|
||||
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ export class AIToolService {
|
|||
return { success: true };
|
||||
}
|
||||
|
||||
private async patchVaultFile(filePath: string, oldContent: string, newContent: string): Promise<object> {
|
||||
private async patchVaultFile(filePath: string, oldContent: string[], newContent: string[]): Promise<object> {
|
||||
const result = await this.fileSystemService.patchFile(normalizePath(filePath), oldContent, newContent);
|
||||
if (result instanceof Error) {
|
||||
return { success: false, error: result.message };
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export class FileSystemService {
|
|||
return await this.vaultService.modifyBinary(file, data, allowAccessToPluginRoot);
|
||||
}
|
||||
|
||||
public async patchFile(filePath: string, oldContent: string, newContent: string, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise<TFile | Error> {
|
||||
public async patchFile(filePath: string, oldContent: string[], newContent: string[], allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise<TFile | Error> {
|
||||
const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot);
|
||||
|
||||
let fileToPatch: TFile;
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ export class VaultService {
|
|||
});
|
||||
}
|
||||
|
||||
public async patch(file: TFile, oldContent: string, newContent: string, allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise<TFile | Error> {
|
||||
public async patch(file: TFile, oldContent: string[], newContent: string[], allowAccessToPluginRoot: boolean = false, requiresConfirmation: boolean = true): Promise<TFile | Error> {
|
||||
const filePath = this.sanitiserService.sanitize(file.path);
|
||||
if (this.isExclusion(file.path, allowAccessToPluginRoot)) {
|
||||
Exception.log(`Plugin attempted to patch a file that is in the exclusion list: ${filePath}`);
|
||||
|
|
@ -154,7 +154,11 @@ export class VaultService {
|
|||
}
|
||||
|
||||
if (isFileType(pathExtname(filePath), FileType.PDF)) {
|
||||
return Exception.new("Creating PDF files is not supported");
|
||||
return Exception.new("Patching PDF files is not supported");
|
||||
}
|
||||
|
||||
if (oldContent.length !== newContent.length) {
|
||||
return Exception.new(`Mismatched patch arrays: ${oldContent.length} old content entries but ${newContent.length} new content entries. Each old content entry must have a corresponding new content entry.`);
|
||||
}
|
||||
|
||||
const currentContent = await this.read(file, allowAccessToPluginRoot);
|
||||
|
|
@ -163,11 +167,20 @@ export class VaultService {
|
|||
return currentContent;
|
||||
}
|
||||
|
||||
if (!currentContent.includes(oldContent)) {
|
||||
return Exception.new(`Content to replace was not found in the file. The old content must match exactly.`);
|
||||
for (let content of oldContent) {
|
||||
if (!currentContent.includes(content) && !StringTools.toWhitespaceFlexibleRegex(content).test(currentContent)) {
|
||||
return Exception.new(`Content to replace was not found in the file, the old content must match exactly. No changes have been made. Unmatched content: "${content}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedContent = currentContent.replace(oldContent, newContent);
|
||||
let updatedContent = currentContent;
|
||||
for (let i = 0; i < oldContent.length; i++) {
|
||||
if (updatedContent.includes(oldContent[i])) {
|
||||
updatedContent = updatedContent.replace(oldContent[i], newContent[i]);
|
||||
} else {
|
||||
updatedContent = updatedContent.replace(StringTools.toWhitespaceFlexibleRegex(oldContent[i]), newContent[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return this.proposeChange(file.name, file.name, currentContent, updatedContent, requiresConfirmation, async () => {
|
||||
await this.vault.process(file, () => updatedContent);
|
||||
|
|
|
|||
|
|
@ -348,8 +348,8 @@ describe('AIToolService - Integration Tests', () => {
|
|||
it('should apply patch successfully', async () => {
|
||||
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('notes/test.md', 'test'));
|
||||
|
||||
const oldContent = 'old content';
|
||||
const newContent = 'new content';
|
||||
const oldContent = ['old content'];
|
||||
const newContent = ['new content'];
|
||||
|
||||
const result = await service.performAITool({
|
||||
name: AITool.PatchVaultFile,
|
||||
|
|
@ -370,8 +370,8 @@ describe('AIToolService - Integration Tests', () => {
|
|||
const error = new Error('Content to replace was not found in the file');
|
||||
mockFileSystemService.patchFile.mockResolvedValue(error);
|
||||
|
||||
const oldContent = 'old content';
|
||||
const newContent = 'new content';
|
||||
const oldContent = ['old content'];
|
||||
const newContent = ['new content'];
|
||||
|
||||
const result = await service.performAITool({
|
||||
name: AITool.PatchVaultFile,
|
||||
|
|
@ -391,8 +391,8 @@ describe('AIToolService - Integration Tests', () => {
|
|||
it('should normalize file path', async () => {
|
||||
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('folder/file.md', 'file'));
|
||||
|
||||
const oldContent = 'old';
|
||||
const newContent = 'new';
|
||||
const oldContent = ['old'];
|
||||
const newContent = ['new'];
|
||||
|
||||
await service.performAITool({
|
||||
name: AITool.PatchVaultFile,
|
||||
|
|
@ -417,8 +417,8 @@ describe('AIToolService - Integration Tests', () => {
|
|||
const error = new Error('File does not exist: missing.md');
|
||||
mockFileSystemService.patchFile.mockResolvedValue(error);
|
||||
|
||||
const oldContent = 'old';
|
||||
const newContent = 'new';
|
||||
const oldContent = ['old'];
|
||||
const newContent = ['new'];
|
||||
|
||||
const result = await service.performAITool({
|
||||
name: AITool.PatchVaultFile,
|
||||
|
|
@ -438,8 +438,8 @@ describe('AIToolService - Integration Tests', () => {
|
|||
it('should handle complex multi-line replacement', async () => {
|
||||
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('complex.md', 'complex'));
|
||||
|
||||
const oldContent = '# Title\nOld line 1\nContext line';
|
||||
const newContent = '# Title\nNew line 1\nContext line';
|
||||
const oldContent = ['# Title\nOld line 1\nContext line'];
|
||||
const newContent = ['# Title\nNew line 1\nContext line'];
|
||||
|
||||
const result = await service.performAITool({
|
||||
name: AITool.PatchVaultFile,
|
||||
|
|
@ -459,8 +459,8 @@ describe('AIToolService - Integration Tests', () => {
|
|||
it('should handle adding new content', async () => {
|
||||
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('additions.md', 'additions'));
|
||||
|
||||
const oldContent = '# Title\nExisting content';
|
||||
const newContent = '# Title\nExisting content\nNew line 1\nNew line 2';
|
||||
const oldContent = ['# Title\nExisting content'];
|
||||
const newContent = ['# Title\nExisting content\nNew line 1\nNew line 2'];
|
||||
|
||||
const result = await service.performAITool({
|
||||
name: AITool.PatchVaultFile,
|
||||
|
|
@ -479,8 +479,8 @@ describe('AIToolService - Integration Tests', () => {
|
|||
it('should handle removing content', async () => {
|
||||
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('deletions.md', 'deletions'));
|
||||
|
||||
const oldContent = '# Title\nLine to remove 1\nLine to remove 2\nRemaining content';
|
||||
const newContent = '# Title\nRemaining content';
|
||||
const oldContent = ['# Title\nLine to remove 1\nLine to remove 2\nRemaining content'];
|
||||
const newContent = ['# Title\nRemaining content'];
|
||||
|
||||
const result = await service.performAITool({
|
||||
name: AITool.PatchVaultFile,
|
||||
|
|
@ -500,8 +500,8 @@ describe('AIToolService - Integration Tests', () => {
|
|||
const error = new Error('Permission denied');
|
||||
mockFileSystemService.patchFile.mockResolvedValue(error);
|
||||
|
||||
const oldContent = 'old';
|
||||
const newContent = 'new';
|
||||
const oldContent = ['old'];
|
||||
const newContent = ['new'];
|
||||
|
||||
const result = await service.performAITool({
|
||||
name: AITool.PatchVaultFile,
|
||||
|
|
@ -884,8 +884,8 @@ describe('AIToolService - Integration Tests', () => {
|
|||
// Then patch it
|
||||
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('document.md', 'document'));
|
||||
|
||||
const oldContent = '# Original Title';
|
||||
const newContent = '# Updated Title';
|
||||
const oldContent = ['# Original Title'];
|
||||
const newContent = ['# Updated Title'];
|
||||
|
||||
const patchResult = await service.performAITool({
|
||||
name: AITool.PatchVaultFile,
|
||||
|
|
|
|||
|
|
@ -191,8 +191,8 @@ describe('FileSystemService', () => {
|
|||
describe('patchFile', () => {
|
||||
it('should patch existing file successfully', async () => {
|
||||
const mockFile = createMockFile('existing.md');
|
||||
const oldContent = 'old content';
|
||||
const newContent = 'new content';
|
||||
const oldContent = ['old content'];
|
||||
const newContent = ['new content'];
|
||||
|
||||
mockVaultService.getAbstractFileByPath = vi.fn().mockReturnValue(mockFile);
|
||||
mockVaultService.patch = vi.fn().mockResolvedValue(mockFile);
|
||||
|
|
@ -205,8 +205,8 @@ describe('FileSystemService', () => {
|
|||
|
||||
it('should create file with empty content when file does not exist', async () => {
|
||||
const mockFile = createMockFile('new.md');
|
||||
const oldContent = '';
|
||||
const newContent = '# New File\nContent here';
|
||||
const oldContent = [''];
|
||||
const newContent = ['# New File\nContent here'];
|
||||
|
||||
// Mock sequence: first call returns null (file doesn't exist), second call returns null (writeFile checks), then create succeeds
|
||||
mockVaultService.getAbstractFileByPath = vi.fn()
|
||||
|
|
@ -225,8 +225,8 @@ describe('FileSystemService', () => {
|
|||
|
||||
it('should return error when patch fails', async () => {
|
||||
const mockFile = createMockFile('existing.md');
|
||||
const oldContent = 'old';
|
||||
const newContent = 'new';
|
||||
const oldContent = ['old'];
|
||||
const newContent = ['new'];
|
||||
const error = new Error('Content to replace was not found in the file');
|
||||
|
||||
mockVaultService.getAbstractFileByPath = vi.fn().mockReturnValue(mockFile);
|
||||
|
|
@ -239,8 +239,8 @@ describe('FileSystemService', () => {
|
|||
});
|
||||
|
||||
it('should return error when creating empty file fails', async () => {
|
||||
const oldContent = '';
|
||||
const newContent = 'New line';
|
||||
const oldContent = [''];
|
||||
const newContent = ['New line'];
|
||||
const error = new Error('Create failed');
|
||||
|
||||
mockVaultService.getAbstractFileByPath = vi.fn().mockReturnValue(null);
|
||||
|
|
@ -255,8 +255,8 @@ describe('FileSystemService', () => {
|
|||
|
||||
it('should respect allowAccessToPluginRoot parameter', async () => {
|
||||
const mockFile = createMockFile('plugin/config.md');
|
||||
const oldContent = 'setting=old';
|
||||
const newContent = 'setting=new';
|
||||
const oldContent = ['setting=old'];
|
||||
const newContent = ['setting=new'];
|
||||
|
||||
mockVaultService.getAbstractFileByPath = vi.fn().mockReturnValue(mockFile);
|
||||
mockVaultService.patch = vi.fn().mockResolvedValue(mockFile);
|
||||
|
|
@ -269,8 +269,8 @@ describe('FileSystemService', () => {
|
|||
|
||||
it('should handle complex multi-line replacement', async () => {
|
||||
const mockFile = createMockFile('document.md');
|
||||
const oldContent = '# Title\nOld intro\nContent';
|
||||
const newContent = '# Title\nNew intro\nContent';
|
||||
const oldContent = ['# Title\nOld intro\nContent'];
|
||||
const newContent = ['# Title\nNew intro\nContent'];
|
||||
|
||||
mockVaultService.getAbstractFileByPath = vi.fn().mockReturnValue(mockFile);
|
||||
mockVaultService.patch = vi.fn().mockResolvedValue(mockFile);
|
||||
|
|
@ -282,8 +282,8 @@ describe('FileSystemService', () => {
|
|||
});
|
||||
|
||||
it('should handle file creation when getAbstractFileByPath returns null after create', async () => {
|
||||
const oldContent = '';
|
||||
const newContent = 'content';
|
||||
const oldContent = [''];
|
||||
const newContent = ['content'];
|
||||
|
||||
mockVaultService.getAbstractFileByPath = vi.fn().mockReturnValue(null);
|
||||
mockVaultService.create = vi.fn().mockResolvedValue(createMockFile('new.md'));
|
||||
|
|
@ -298,8 +298,8 @@ describe('FileSystemService', () => {
|
|||
|
||||
it('should respect requiresConfirmation parameter', async () => {
|
||||
const mockFile = createMockFile('test.md');
|
||||
const oldContent = 'old';
|
||||
const newContent = 'new';
|
||||
const oldContent = ['old'];
|
||||
const newContent = ['new'];
|
||||
|
||||
mockVaultService.getAbstractFileByPath = vi.fn().mockReturnValue(mockFile);
|
||||
mockVaultService.patch = vi.fn().mockResolvedValue(mockFile);
|
||||
|
|
|
|||
|
|
@ -416,8 +416,8 @@ describe('VaultService - Integration Tests', () => {
|
|||
describe('patch', () => {
|
||||
it('should apply patch successfully when file is not excluded', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
const oldContent = 'old content';
|
||||
const newContent = 'new content';
|
||||
const oldContent = ['old content'];
|
||||
const newContent = ['new content'];
|
||||
const currentContent = '# Title\nold content\nmore lines';
|
||||
|
||||
mockVault.read.mockResolvedValue(currentContent);
|
||||
|
|
@ -434,8 +434,8 @@ describe('VaultService - Integration Tests', () => {
|
|||
|
||||
it('should return error when file is excluded', async () => {
|
||||
const mockFile = createMockFile('Vaultkeeper AI/test.md');
|
||||
const oldContent = 'old';
|
||||
const newContent = 'new';
|
||||
const oldContent = ['old'];
|
||||
const newContent = ['new'];
|
||||
|
||||
const result = await vaultService.patch(mockFile, oldContent, newContent, false);
|
||||
|
||||
|
|
@ -446,8 +446,8 @@ describe('VaultService - Integration Tests', () => {
|
|||
|
||||
it('should return error when old content is not found in file', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
const oldContent = 'old';
|
||||
const newContent = 'new';
|
||||
const oldContent = ['old'];
|
||||
const newContent = ['new'];
|
||||
const currentContent = '# Different content';
|
||||
|
||||
mockVault.read.mockResolvedValue(currentContent);
|
||||
|
|
@ -461,8 +461,8 @@ describe('VaultService - Integration Tests', () => {
|
|||
|
||||
it('should allow patching excluded files when allowAccessToPluginRoot is true', async () => {
|
||||
const mockFile = createMockFile('Vaultkeeper AI/config.md');
|
||||
const oldContent = 'setting=old';
|
||||
const newContent = 'setting=new';
|
||||
const oldContent = ['setting=old'];
|
||||
const newContent = ['setting=new'];
|
||||
const currentContent = 'setting=old';
|
||||
|
||||
mockVault.read.mockResolvedValue(currentContent);
|
||||
|
|
@ -477,8 +477,8 @@ describe('VaultService - Integration Tests', () => {
|
|||
|
||||
it('should request diff confirmation when requiresConfirmation is true', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
const oldContent = 'old';
|
||||
const newContent = 'new';
|
||||
const oldContent = ['old'];
|
||||
const newContent = ['new'];
|
||||
const currentContent = 'old';
|
||||
const updatedContent = 'new';
|
||||
|
||||
|
|
@ -498,8 +498,8 @@ describe('VaultService - Integration Tests', () => {
|
|||
|
||||
it('should replace only the first occurrence of old content', async () => {
|
||||
const mockFile = createMockFile('document.md');
|
||||
const oldContent = 'duplicate text';
|
||||
const newContent = 'replaced text';
|
||||
const oldContent = ['duplicate text'];
|
||||
const newContent = ['replaced text'];
|
||||
const currentContent = 'duplicate text\nSome content\nduplicate text';
|
||||
const expectedContent = 'replaced text\nSome content\nduplicate text';
|
||||
|
||||
|
|
@ -519,8 +519,8 @@ describe('VaultService - Integration Tests', () => {
|
|||
|
||||
it('should call vault.process with function that returns updated content', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
const oldContent = 'old';
|
||||
const newContent = 'new';
|
||||
const oldContent = ['old'];
|
||||
const newContent = ['new'];
|
||||
const updatedContent = 'new';
|
||||
let processCallback: any;
|
||||
|
||||
|
|
@ -536,10 +536,51 @@ describe('VaultService - Integration Tests', () => {
|
|||
expect(processCallback()).toBe(updatedContent);
|
||||
});
|
||||
|
||||
it('should fall back to whitespace-flexible matching when exact match fails', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
const oldContent = [' if (x) {\n return true;\n }'];
|
||||
const newContent = [' if (x) {\n return false;\n }'];
|
||||
const currentContent = ' if (x) {\n return true;\n }';
|
||||
|
||||
mockVault.read.mockResolvedValue(currentContent);
|
||||
mockDiffService.requestDiff.mockResolvedValue({ accepted: true });
|
||||
|
||||
let processCallback: any;
|
||||
mockVault.process.mockImplementation((_file, fn) => {
|
||||
processCallback = fn;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
const result = await vaultService.patch(mockFile, oldContent, newContent);
|
||||
|
||||
expect(result).toBe(mockFile);
|
||||
expect(processCallback()).toBe(' if (x) {\n return false;\n }');
|
||||
});
|
||||
|
||||
it('should prefer exact match over whitespace-flexible match', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
const oldContent = ['hello world'];
|
||||
const newContent = ['goodbye world'];
|
||||
const currentContent = 'hello world';
|
||||
|
||||
mockVault.read.mockResolvedValue(currentContent);
|
||||
mockDiffService.requestDiff.mockResolvedValue({ accepted: true });
|
||||
|
||||
let processCallback: any;
|
||||
mockVault.process.mockImplementation((_file, fn) => {
|
||||
processCallback = fn;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await vaultService.patch(mockFile, oldContent, newContent);
|
||||
|
||||
expect(processCallback()).toBe('goodbye world');
|
||||
});
|
||||
|
||||
it('should skip confirmation when requiresConfirmation is false', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
const oldContent = 'old';
|
||||
const newContent = 'new';
|
||||
const oldContent = ['old'];
|
||||
const newContent = ['new'];
|
||||
|
||||
mockVault.read.mockResolvedValue('old');
|
||||
mockVault.process.mockResolvedValue(undefined);
|
||||
|
|
|
|||
Loading…
Reference in a new issue