mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
Update delete functionality to support folders and use trash
- Enhance DeleteVaultFiles to handle both files and folders - Change VaultService.delete to use vault.trash instead of permanent deletion - Remove TFile type check to allow folder deletion - Simplify delete method signature by removing unused force parameter - Increase default snippet size limit from 150 to 300 characters - Fix IPrompt to use SettingsService instead of direct plugin access - Update all tests to reflect trash behavior and folder support
This commit is contained in:
parent
ee8da0e6a5
commit
4d8edc8b96
8 changed files with 38 additions and 33 deletions
|
|
@ -3,8 +3,8 @@ import type { IAIFunctionDefinition } from "../IAIFunctionDefinition";
|
|||
|
||||
export const DeleteVaultFiles: IAIFunctionDefinition = {
|
||||
name: AIFunction.DeleteVaultFiles,
|
||||
description: `Permanently removes files from the vault. Use this when the user explicitly
|
||||
requests to delete file(s), when a file is no longer needed, or when removing outdated
|
||||
description: `Permanently removes files and folders from the vault. Use this when the user explicitly
|
||||
requests to delete file(s) or folder(s), when a file or folder is no longer needed, or when removing outdated
|
||||
content. IMPORTANT: This action is irreversible - always confirm the exact file path(s)
|
||||
before deletion. Prefer archiving or moving files to a trash folder over permanent
|
||||
deletion when uncertain. Only call this after verifying the file(s) exist and confirming
|
||||
|
|
@ -17,7 +17,7 @@ export const DeleteVaultFiles: IAIFunctionDefinition = {
|
|||
items: {
|
||||
type: "string"
|
||||
},
|
||||
description: "An array of file paths to be deleted (e.g., ['folder/note.md', 'note2.md']). Must be an exact match to existing files."
|
||||
description: "An array of file paths to be deleted (e.g., ['folder/note.md', 'note2.md', 'folder/subfolder']). Must be an exact match to existing files or folders."
|
||||
},
|
||||
user_message: {
|
||||
type: "string",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Resolve } from "Services/DependencyService";
|
|||
import { Services } from "Services/Services";
|
||||
import { SystemInstruction } from "./SystemPrompt";
|
||||
import type { FileSystemService } from "Services/FileSystemService";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
|
||||
export interface IPrompt {
|
||||
systemInstruction(): string;
|
||||
|
|
@ -12,10 +13,12 @@ export interface IPrompt {
|
|||
export class AIPrompt implements IPrompt {
|
||||
|
||||
private readonly plugin: AIAgentPlugin;
|
||||
private readonly settingsService: SettingsService;
|
||||
private readonly fileSystemService: FileSystemService;
|
||||
|
||||
public constructor() {
|
||||
this.plugin = Resolve<AIAgentPlugin>(Services.AIAgentPlugin);
|
||||
this.settingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
this.fileSystemService = Resolve<FileSystemService>(Services.FileSystemService);
|
||||
}
|
||||
|
||||
|
|
@ -24,7 +27,7 @@ export class AIPrompt implements IPrompt {
|
|||
}
|
||||
|
||||
public async userInstruction(): Promise<string> {
|
||||
const userInstruction: string | null = await this.fileSystemService.readFile(this.plugin.settings.userInstruction, true);
|
||||
const userInstruction: string | null = await this.fileSystemService.readFile(this.settingsService.settings.userInstruction, true);
|
||||
return userInstruction ?? "";
|
||||
}
|
||||
}
|
||||
|
|
@ -48,11 +48,11 @@ export class FileSystemService {
|
|||
public async deleteFile(filePath: string, allowAccessToPluginRoot: boolean = false): Promise<{ success: true } | { success: false, error: string }> {
|
||||
const file: TAbstractFile | null = this.vaultService.getAbstractFileByPath(filePath, allowAccessToPluginRoot);
|
||||
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
if (!file) {
|
||||
return { success: false, error: "File not found" };
|
||||
}
|
||||
|
||||
return await this.vaultService.delete(file, undefined, allowAccessToPluginRoot);
|
||||
return await this.vaultService.delete(file, allowAccessToPluginRoot);
|
||||
}
|
||||
|
||||
public async moveFile(sourcePath: string, destinationPath: string, allowAccessToPluginRoot: boolean = false): Promise<{ success: true } | { success: false, error: string }> {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const DEFAULT_SETTINGS: IAIAgentSettings = {
|
|||
exclusions: [],
|
||||
|
||||
searchResultsLimit: 15,
|
||||
snippetSizeLimit: 150
|
||||
snippetSizeLimit: 300
|
||||
}
|
||||
|
||||
export interface IAIAgentSettings {
|
||||
|
|
|
|||
|
|
@ -91,13 +91,13 @@ export class VaultService {
|
|||
await this.vault.process(file, () => content);
|
||||
}
|
||||
|
||||
public async delete(file: TAbstractFile, force?: boolean, allowAccessToPluginRoot: boolean = false): Promise<{ success: true } | { success: false, error: string }> {
|
||||
public async delete(file: TAbstractFile, allowAccessToPluginRoot: boolean = false): Promise<{ success: true } | { success: false, error: string }> {
|
||||
if (this.isExclusion(file.path, allowAccessToPluginRoot)) {
|
||||
console.error(`Plugin attempted to delete a file that is in the exclusions list: ${file.path}`)
|
||||
return { success: false, error: "File is in exclusion list" };
|
||||
}
|
||||
try {
|
||||
await this.vault.delete(file, force);
|
||||
await this.vault.trash(file, true);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error(`Error deleting file ${file.path}:`, error);
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ describe('FileSystemService', () => {
|
|||
const result = await fileSystemService.deleteFile('delete-me.md');
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockVaultService.delete).toHaveBeenCalledWith(mockFile, undefined, false);
|
||||
expect(mockVaultService.delete).toHaveBeenCalledWith(mockFile, false);
|
||||
});
|
||||
|
||||
it('should return error when file does not exist', async () => {
|
||||
|
|
@ -254,18 +254,19 @@ describe('FileSystemService', () => {
|
|||
await fileSystemService.deleteFile('plugin/temp.json', true);
|
||||
|
||||
expect(mockVaultService.getAbstractFileByPath).toHaveBeenCalledWith('plugin/temp.json', true);
|
||||
expect(mockVaultService.delete).toHaveBeenCalledWith(mockFile, undefined, true);
|
||||
expect(mockVaultService.delete).toHaveBeenCalledWith(mockFile, true);
|
||||
});
|
||||
|
||||
it('should return error when path is not a file', async () => {
|
||||
it('should delete folder successfully (supports both files and folders)', async () => {
|
||||
const mockFolder = createMockFolder('folder');
|
||||
|
||||
mockVaultService.getAbstractFileByPath = vi.fn().mockReturnValue(mockFolder);
|
||||
mockVaultService.delete = vi.fn().mockResolvedValue({ success: true });
|
||||
|
||||
const result = await fileSystemService.deleteFile('folder');
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'File not found' });
|
||||
expect(mockVaultService.delete).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockVaultService.delete).toHaveBeenCalledWith(mockFolder, false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ describe('SettingsService', () => {
|
|||
expect(settingsService.settings.exclusions).toEqual([]);
|
||||
expect(settingsService.settings.userInstruction).toBe('');
|
||||
expect(settingsService.settings.searchResultsLimit).toBe(15);
|
||||
expect(settingsService.settings.snippetSizeLimit).toBe(150);
|
||||
expect(settingsService.settings.snippetSizeLimit).toBe(300);
|
||||
});
|
||||
|
||||
it('should merge loaded settings with defaults', () => {
|
||||
|
|
@ -80,7 +80,7 @@ describe('SettingsService', () => {
|
|||
expect(settingsService.settings.exclusions).toEqual([]); // Default
|
||||
expect(settingsService.settings.userInstruction).toBe(''); // Default
|
||||
expect(settingsService.settings.searchResultsLimit).toBe(15); // Default
|
||||
expect(settingsService.settings.snippetSizeLimit).toBe(150); // Default
|
||||
expect(settingsService.settings.snippetSizeLimit).toBe(300); // Default
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ describe('SettingsService', () => {
|
|||
exclusions: [],
|
||||
userInstruction: '',
|
||||
searchResultsLimit: 15,
|
||||
snippetSizeLimit: 150
|
||||
snippetSizeLimit: 300
|
||||
};
|
||||
settingsService = new SettingsService(loadedSettings);
|
||||
});
|
||||
|
|
@ -137,7 +137,7 @@ describe('SettingsService', () => {
|
|||
exclusions: [],
|
||||
userInstruction: '',
|
||||
searchResultsLimit: 15,
|
||||
snippetSizeLimit: 150
|
||||
snippetSizeLimit: 300
|
||||
};
|
||||
settingsService = new SettingsService(loadedSettings);
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ describe('SettingsService', () => {
|
|||
exclusions: [],
|
||||
userInstruction: '',
|
||||
searchResultsLimit: 15,
|
||||
snippetSizeLimit: 150
|
||||
snippetSizeLimit: 300
|
||||
};
|
||||
settingsService = new SettingsService(loadedSettings);
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ describe('SettingsService', () => {
|
|||
exclusions: [],
|
||||
userInstruction: '',
|
||||
searchResultsLimit: 15,
|
||||
snippetSizeLimit: 150
|
||||
snippetSizeLimit: 300
|
||||
};
|
||||
settingsService = new SettingsService(loadedSettings);
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ describe('SettingsService', () => {
|
|||
exclusions: [],
|
||||
userInstruction: '',
|
||||
searchResultsLimit: 15,
|
||||
snippetSizeLimit: 150
|
||||
snippetSizeLimit: 300
|
||||
};
|
||||
settingsService = new SettingsService(loadedSettings);
|
||||
});
|
||||
|
|
@ -276,7 +276,7 @@ describe('SettingsService', () => {
|
|||
exclusions: ['node_modules'],
|
||||
userInstruction: 'Be helpful',
|
||||
searchResultsLimit: 15,
|
||||
snippetSizeLimit: 150
|
||||
snippetSizeLimit: 300
|
||||
};
|
||||
settingsService = new SettingsService(loadedSettings);
|
||||
});
|
||||
|
|
@ -403,7 +403,7 @@ describe('SettingsService', () => {
|
|||
|
||||
it('should use default snippetSizeLimit when not specified', () => {
|
||||
settingsService = new SettingsService({});
|
||||
expect(settingsService.settings.snippetSizeLimit).toBe(150);
|
||||
expect(settingsService.settings.snippetSizeLimit).toBe(300);
|
||||
});
|
||||
|
||||
it('should allow custom searchResultsLimit values', () => {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const mockVault = {
|
|||
create: vi.fn(),
|
||||
process: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
trash: vi.fn(),
|
||||
createFolder: vi.fn(),
|
||||
getFiles: vi.fn(),
|
||||
getAllFolders: vi.fn(),
|
||||
|
|
@ -53,7 +54,7 @@ const mockSettings: IAIAgentSettings = {
|
|||
exclusions: [],
|
||||
userInstruction: '',
|
||||
searchResultsLimit: 15,
|
||||
snippetSizeLimit: 150
|
||||
snippetSizeLimit: 300
|
||||
};
|
||||
|
||||
const mockPlugin = {
|
||||
|
|
@ -104,7 +105,7 @@ describe('VaultService - Integration Tests', () => {
|
|||
// Reset settings to defaults
|
||||
mockSettings.exclusions = [];
|
||||
mockSettings.searchResultsLimit = 15;
|
||||
mockSettings.snippetSizeLimit = 150;
|
||||
mockSettings.snippetSizeLimit = 300;
|
||||
|
||||
// Set default mock for adapter.exists (can be overridden in individual tests)
|
||||
mockVault.adapter.exists.mockResolvedValue(false);
|
||||
|
|
@ -399,36 +400,36 @@ describe('VaultService - Integration Tests', () => {
|
|||
describe('delete', () => {
|
||||
it('should delete file successfully when not excluded', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
mockVault.delete.mockResolvedValue(undefined);
|
||||
mockVault.trash.mockResolvedValue(undefined);
|
||||
|
||||
const result = await vaultService.delete(mockFile);
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockVault.delete).toHaveBeenCalledWith(mockFile, undefined);
|
||||
expect(mockVault.trash).toHaveBeenCalledWith(mockFile, true);
|
||||
});
|
||||
|
||||
it('should not delete file and return error when excluded', async () => {
|
||||
const mockFile = createMockFile('AI Agent/test.md');
|
||||
|
||||
const result = await vaultService.delete(mockFile, false, false);
|
||||
const result = await vaultService.delete(mockFile, false);
|
||||
|
||||
expect(result).toEqual({ success: false, error: 'File is in exclusion list' });
|
||||
expect(mockVault.delete).not.toHaveBeenCalled();
|
||||
expect(mockVault.trash).not.toHaveBeenCalled();
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass force parameter to vault.delete', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
mockVault.delete.mockResolvedValue(undefined);
|
||||
mockVault.trash.mockResolvedValue(undefined);
|
||||
|
||||
await vaultService.delete(mockFile, true);
|
||||
|
||||
expect(mockVault.delete).toHaveBeenCalledWith(mockFile, true);
|
||||
expect(mockVault.trash).toHaveBeenCalledWith(mockFile, true);
|
||||
});
|
||||
|
||||
it('should return error when deletion fails', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
mockVault.delete.mockRejectedValue(new Error('Deletion failed'));
|
||||
mockVault.trash.mockRejectedValue(new Error('Deletion failed'));
|
||||
|
||||
const result = await vaultService.delete(mockFile);
|
||||
|
||||
|
|
@ -438,7 +439,7 @@ describe('VaultService - Integration Tests', () => {
|
|||
|
||||
it('should handle non-Error objects in catch block', async () => {
|
||||
const mockFile = createMockFile('note.md');
|
||||
mockVault.delete.mockRejectedValue('string error');
|
||||
mockVault.trash.mockRejectedValue('string error');
|
||||
|
||||
const result = await vaultService.delete(mockFile);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue