mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
Add Exception helper class for consistent error handling and logging. Replace throw statements and console.error calls with Exception methods. Update service methods to return Error | T instead of mixed success/failure objects. Improve type safety in Claude.extractContents with explicit return type. Add WikiLinks helper to VaultCacheService for managing wiki link references. Update unit tests.
1351 lines
49 KiB
TypeScript
1351 lines
49 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { VaultService } from '../../Services/VaultService';
|
|
import { TFile, TFolder, TAbstractFile, FileManager } from 'obsidian';
|
|
import { Path } from '../../Enums/Path';
|
|
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
|
import { Services } from '../../Services/Services';
|
|
import { SanitiserService } from '../../Services/SanitiserService';
|
|
import { SettingsService, IVaultkeeperAISettings } from '../../Services/SettingsService';
|
|
import { AIProviderModel } from '../../Enums/ApiProvider';
|
|
import { Exception } from '../../Helpers/Exception';
|
|
|
|
/**
|
|
* INTEGRATION TESTS
|
|
*
|
|
* These tests use real dependencies (SanitiserService, DependencyService) and only mock
|
|
* the Obsidian API (Vault, FileManager, TFile, etc.) which is unavoidable in a test environment.
|
|
*
|
|
* This approach tests the actual integration between services and avoids complex mocking.
|
|
*/
|
|
|
|
// Create mock instances
|
|
const mockVault = {
|
|
getMarkdownFiles: vi.fn(),
|
|
getAbstractFileByPath: vi.fn(),
|
|
read: vi.fn(),
|
|
cachedRead: vi.fn(),
|
|
create: vi.fn(),
|
|
process: vi.fn(),
|
|
delete: vi.fn(),
|
|
trash: vi.fn(),
|
|
createFolder: vi.fn(),
|
|
getFiles: vi.fn(),
|
|
getAllFolders: vi.fn(),
|
|
on: vi.fn(),
|
|
adapter: {
|
|
exists: vi.fn()
|
|
}
|
|
};
|
|
|
|
const mockFileManager = {
|
|
renameFile: vi.fn(),
|
|
trashFile: vi.fn()
|
|
} as unknown as FileManager & {
|
|
renameFile: ReturnType<typeof vi.fn>;
|
|
trashFile: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
// Create a mutable settings object that tests can modify
|
|
const mockSettings: IVaultkeeperAISettings = {
|
|
firstTimeStart: false,
|
|
model: AIProviderModel.ClaudeSonnet_4_5,
|
|
apiKeys: {
|
|
claude: 'test-claude-key',
|
|
openai: 'test-openai-key',
|
|
gemini: 'test-gemini-key'
|
|
},
|
|
exclusions: [],
|
|
userInstruction: '',
|
|
searchResultsLimit: 15,
|
|
snippetSizeLimit: 300
|
|
};
|
|
|
|
const mockPlugin = {
|
|
app: {
|
|
vault: mockVault,
|
|
fileManager: mockFileManager
|
|
},
|
|
saveData: vi.fn().mockResolvedValue(undefined),
|
|
registerEvent: vi.fn()
|
|
};
|
|
|
|
// Helper to create mock TFile
|
|
function createMockFile(path: string): TFile {
|
|
const name = path.split('/').pop() || '';
|
|
const basename = name.split('.')[0];
|
|
const file = new TFile();
|
|
file.path = path;
|
|
file.name = name;
|
|
file.basename = basename;
|
|
file.extension = 'md';
|
|
file.stat = { ctime: Date.now(), mtime: Date.now(), size: 100 };
|
|
file.parent = null;
|
|
file.vault = mockVault as any;
|
|
return file;
|
|
}
|
|
|
|
// Helper to create mock TFolder
|
|
function createMockFolder(path: string, children: TAbstractFile[] = []): TFolder {
|
|
const name = path.split('/').pop() || '';
|
|
const folder = new TFolder();
|
|
folder.path = path;
|
|
folder.name = name;
|
|
folder.children = children;
|
|
folder.parent = null;
|
|
folder.vault = mockVault as any;
|
|
return folder;
|
|
}
|
|
|
|
describe('VaultService - Integration Tests', () => {
|
|
let vaultService: VaultService;
|
|
let settingsService: SettingsService;
|
|
let consoleErrorSpy: any;
|
|
|
|
beforeEach(() => {
|
|
// Reset all mocks
|
|
vi.clearAllMocks();
|
|
|
|
// Reset settings to defaults
|
|
mockSettings.exclusions = [];
|
|
mockSettings.searchResultsLimit = 15;
|
|
mockSettings.snippetSizeLimit = 300;
|
|
|
|
// Set default mock for adapter.exists (can be overridden in individual tests)
|
|
mockVault.adapter.exists.mockResolvedValue(false);
|
|
|
|
// Mock console.error to prevent noise in tests
|
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
// Mock Exception.log
|
|
vi.spyOn(Exception, 'log').mockImplementation(() => {});
|
|
|
|
// Register real dependencies in DependencyService
|
|
RegisterSingleton(Services.VaultkeeperAIPlugin, mockPlugin as any);
|
|
RegisterSingleton(Services.FileManager, mockFileManager);
|
|
RegisterSingleton(Services.SanitiserService, new SanitiserService());
|
|
|
|
// Create and register SettingsService
|
|
settingsService = new SettingsService(mockSettings);
|
|
RegisterSingleton(Services.SettingsService, settingsService);
|
|
|
|
// Create a fresh instance - it will resolve real dependencies
|
|
vaultService = new VaultService();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clear singleton registry to prevent memory leaks
|
|
DeregisterAllServices();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('getMarkdownFiles', () => {
|
|
it('should return all markdown files when no exclusions are set', () => {
|
|
const files = [
|
|
createMockFile('note1.md'),
|
|
createMockFile('note2.md'),
|
|
createMockFile('folder/note3.md')
|
|
];
|
|
mockVault.getMarkdownFiles.mockReturnValue(files);
|
|
|
|
const result = vaultService.getMarkdownFiles();
|
|
|
|
expect(result).toHaveLength(3);
|
|
expect(mockVault.getMarkdownFiles).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('should filter out files in the Vaultkeeper AI root directory by default', () => {
|
|
const files = [
|
|
createMockFile('note1.md'),
|
|
createMockFile('Vaultkeeper AI/conversation.md'),
|
|
createMockFile('Vaultkeeper AI/subfolder/data.md')
|
|
];
|
|
mockVault.getMarkdownFiles.mockReturnValue(files);
|
|
|
|
const result = vaultService.getMarkdownFiles(false);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].path).toBe('note1.md');
|
|
});
|
|
|
|
it('should allow access to Vaultkeeper AI directory when allowAccessToPluginRoot is true', () => {
|
|
const files = [
|
|
createMockFile('note1.md'),
|
|
createMockFile('Vaultkeeper AI/conversation.md')
|
|
];
|
|
mockVault.getMarkdownFiles.mockReturnValue(files);
|
|
|
|
const result = vaultService.getMarkdownFiles(true);
|
|
|
|
expect(result).toHaveLength(2);
|
|
});
|
|
|
|
it('should filter out user-defined exclusions', () => {
|
|
const files = [
|
|
createMockFile('public/note1.md'),
|
|
createMockFile('private/secret.md'),
|
|
createMockFile('public/note2.md')
|
|
];
|
|
mockVault.getMarkdownFiles.mockReturnValue(files);
|
|
|
|
// Update settings to include exclusion
|
|
settingsService.settings.exclusions = ['private/**'];
|
|
|
|
const result = vaultService.getMarkdownFiles();
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result.every(f => !f.path.startsWith('private/'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getAbstractFileByPath', () => {
|
|
it('should return file when path is not excluded', () => {
|
|
const mockFile = createMockFile('note.md');
|
|
mockVault.getAbstractFileByPath.mockReturnValue(mockFile);
|
|
|
|
const result = vaultService.getAbstractFileByPath('note.md');
|
|
|
|
expect(result).toBe(mockFile);
|
|
});
|
|
|
|
it('should return null when path is excluded', () => {
|
|
mockVault.getAbstractFileByPath.mockReturnValue(createMockFile('Vaultkeeper AI/test.md'));
|
|
|
|
const result = vaultService.getAbstractFileByPath('Vaultkeeper AI/test.md', false);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should sanitize the path before checking', () => {
|
|
// Path with illegal characters that will be sanitized
|
|
const unsanitizedPath = 'folder/file?.md';
|
|
const mockFile = createMockFile('folder/file.md');
|
|
mockVault.getAbstractFileByPath.mockReturnValue(mockFile);
|
|
|
|
vaultService.getAbstractFileByPath(unsanitizedPath);
|
|
|
|
// SanitiserService will remove the ? character
|
|
expect(mockVault.getAbstractFileByPath).toHaveBeenCalledWith('folder/file.md');
|
|
});
|
|
|
|
it('should allow access to excluded paths when allowAccessToPluginRoot is true', () => {
|
|
const mockFile = createMockFile('Vaultkeeper AI/conversation.md');
|
|
mockVault.getAbstractFileByPath.mockReturnValue(mockFile);
|
|
|
|
const result = vaultService.getAbstractFileByPath('Vaultkeeper AI/conversation.md', true);
|
|
|
|
expect(result).toBe(mockFile);
|
|
});
|
|
|
|
it('should exclude the Vaultkeeper AI directory itself', () => {
|
|
const mockFolder = createMockFolder('Vaultkeeper AI');
|
|
mockVault.getAbstractFileByPath.mockReturnValue(mockFolder);
|
|
|
|
const result = vaultService.getAbstractFileByPath('Vaultkeeper AI', false);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should allow access to Vaultkeeper AI directory when allowAccessToPluginRoot is true', () => {
|
|
const mockFolder = createMockFolder('Vaultkeeper AI');
|
|
mockVault.getAbstractFileByPath.mockReturnValue(mockFolder);
|
|
|
|
const result = vaultService.getAbstractFileByPath('Vaultkeeper AI', true);
|
|
|
|
expect(result).toBe(mockFolder);
|
|
});
|
|
});
|
|
|
|
describe('exists', () => {
|
|
it('should return true when file exists and is not excluded', async () => {
|
|
mockVault.adapter.exists.mockResolvedValue(true);
|
|
|
|
const result = await vaultService.exists('note.md');
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when file is excluded', async () => {
|
|
const result = await vaultService.exists('Vaultkeeper AI/test.md', false);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false when file does not exist', async () => {
|
|
mockVault.adapter.exists.mockResolvedValue(false);
|
|
|
|
const result = await vaultService.exists('nonexistent.md');
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return true when folder exists', async () => {
|
|
mockVault.adapter.exists.mockResolvedValue(true);
|
|
|
|
const result = await vaultService.exists('folder');
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('read', () => {
|
|
it('should read file content when file is not excluded', async () => {
|
|
const mockFile = createMockFile('note.md');
|
|
mockVault.read.mockResolvedValue('file content');
|
|
|
|
const result = await vaultService.read(mockFile);
|
|
|
|
expect(result).toBe('file content');
|
|
expect(mockVault.read).toHaveBeenCalledWith(mockFile);
|
|
});
|
|
|
|
it('should return empty string when file is excluded', async () => {
|
|
const mockFile = createMockFile('Vaultkeeper AI/test.md');
|
|
|
|
const result = await vaultService.read(mockFile, false);
|
|
|
|
expect(result).toBe('');
|
|
expect(mockVault.read).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should allow reading excluded files when allowAccessToPluginRoot is true', async () => {
|
|
const mockFile = createMockFile('Vaultkeeper AI/test.md');
|
|
mockVault.read.mockResolvedValue('content');
|
|
|
|
const result = await vaultService.read(mockFile, true);
|
|
|
|
expect(result).toBe('content');
|
|
expect(mockVault.read).toHaveBeenCalledWith(mockFile);
|
|
});
|
|
});
|
|
|
|
describe('create', () => {
|
|
it('should create a file with sanitized path', async () => {
|
|
const mockFile = createMockFile('note.md');
|
|
mockVault.getAbstractFileByPath.mockReturnValue(null); // No existing directories
|
|
mockVault.create.mockResolvedValue(mockFile);
|
|
mockVault.createFolder.mockResolvedValue(createMockFolder('folder'));
|
|
|
|
const result = await vaultService.create('note.md', 'content');
|
|
|
|
expect(mockVault.create).toHaveBeenCalledWith('note.md', 'content');
|
|
expect(result).toBe(mockFile);
|
|
});
|
|
|
|
it('should return error when trying to create file in excluded path', async () => {
|
|
const result = await vaultService.create('Vaultkeeper AI/test.md', 'content', false);
|
|
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toContain('Failed to create file, permission denied');
|
|
});
|
|
|
|
it('should create parent directories if they do not exist', async () => {
|
|
const mockFile = createMockFile('folder/subfolder/note.md');
|
|
mockVault.getAbstractFileByPath.mockReturnValue(null);
|
|
mockVault.create.mockResolvedValue(mockFile);
|
|
mockVault.createFolder.mockResolvedValue(createMockFolder('folder'));
|
|
|
|
await vaultService.create('folder/subfolder/note.md', 'content');
|
|
|
|
expect(mockVault.createFolder).toHaveBeenCalledTimes(2);
|
|
expect(mockVault.createFolder).toHaveBeenCalledWith('folder');
|
|
expect(mockVault.createFolder).toHaveBeenCalledWith('folder/subfolder');
|
|
});
|
|
|
|
it('should not create directories that already exist', async () => {
|
|
const mockFile = createMockFile('existing/note.md');
|
|
|
|
mockVault.adapter.exists.mockImplementation(async (path: string) => {
|
|
return path === 'existing';
|
|
});
|
|
mockVault.create.mockResolvedValue(mockFile);
|
|
|
|
await vaultService.create('existing/note.md', 'content');
|
|
|
|
// Should not call createFolder for 'existing' since it already exists
|
|
expect(mockVault.createFolder).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('modify', () => {
|
|
it('should modify file content when file is not excluded', async () => {
|
|
const mockFile = createMockFile('note.md');
|
|
mockVault.process.mockResolvedValue(undefined);
|
|
|
|
await vaultService.modify(mockFile, 'new content');
|
|
|
|
expect(mockVault.process).toHaveBeenCalledWith(mockFile, expect.any(Function));
|
|
});
|
|
|
|
it('should not modify file when file is excluded', async () => {
|
|
const mockFile = createMockFile('Vaultkeeper AI/test.md');
|
|
|
|
await vaultService.modify(mockFile, 'new content', false);
|
|
|
|
expect(mockVault.process).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should call vault.process with function that returns new content', async () => {
|
|
const mockFile = createMockFile('note.md');
|
|
let processCallback: any;
|
|
|
|
mockVault.process.mockImplementation((file, fn) => {
|
|
processCallback = fn;
|
|
return Promise.resolve();
|
|
});
|
|
|
|
await vaultService.modify(mockFile, 'new content');
|
|
|
|
expect(processCallback()).toBe('new content');
|
|
});
|
|
});
|
|
|
|
describe('delete', () => {
|
|
it('should delete file successfully when not excluded', async () => {
|
|
const mockFile = createMockFile('note.md');
|
|
mockFileManager.trashFile.mockResolvedValue(undefined);
|
|
|
|
const result = await vaultService.delete(mockFile);
|
|
|
|
expect(result).toBeUndefined(); // void = success
|
|
expect(mockFileManager.trashFile).toHaveBeenCalledWith(mockFile);
|
|
});
|
|
|
|
it('should not delete file and return error when excluded', async () => {
|
|
const mockFile = createMockFile('Vaultkeeper AI/test.md');
|
|
|
|
const result = await vaultService.delete(mockFile, false);
|
|
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toContain('File does not exist');
|
|
expect(mockFileManager.trashFile).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should call fileManager.trashFile to delete file', async () => {
|
|
const mockFile = createMockFile('note.md');
|
|
mockFileManager.trashFile.mockResolvedValue(undefined);
|
|
|
|
await vaultService.delete(mockFile, true);
|
|
|
|
expect(mockFileManager.trashFile).toHaveBeenCalledWith(mockFile);
|
|
});
|
|
|
|
it('should return error when deletion fails', async () => {
|
|
const mockFile = createMockFile('note.md');
|
|
mockFileManager.trashFile.mockRejectedValue(new Error('Deletion failed'));
|
|
|
|
const result = await vaultService.delete(mockFile);
|
|
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toBe('Deletion failed');
|
|
});
|
|
|
|
it('should handle non-Error objects in catch block', async () => {
|
|
const mockFile = createMockFile('note.md');
|
|
mockFileManager.trashFile.mockRejectedValue('string error');
|
|
|
|
const result = await vaultService.delete(mockFile);
|
|
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toBe('string error');
|
|
});
|
|
});
|
|
|
|
describe('move', () => {
|
|
it('should move file successfully when source is not excluded', async () => {
|
|
const mockFile = createMockFile('source.md');
|
|
mockVault.getAbstractFileByPath.mockReturnValue(mockFile);
|
|
mockFileManager.renameFile.mockResolvedValue(undefined);
|
|
|
|
const result = await vaultService.move('source.md', 'dest.md');
|
|
|
|
expect(result).toBeUndefined(); // void = success
|
|
expect(mockFileManager.renameFile).toHaveBeenCalledWith(mockFile, 'dest.md');
|
|
});
|
|
|
|
it('should return error when source file is excluded', async () => {
|
|
const result = await vaultService.move('Vaultkeeper AI/test.md', 'dest.md', false);
|
|
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toContain('File does not exist');
|
|
expect(mockFileManager.renameFile).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return error when source file does not exist', async () => {
|
|
mockVault.getAbstractFileByPath.mockReturnValue(null);
|
|
|
|
const result = await vaultService.move('nonexistent.md', 'dest.md');
|
|
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toContain('File does not exist');
|
|
expect(mockFileManager.renameFile).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should create destination directories if needed', async () => {
|
|
const mockFile = createMockFile('source.md');
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === 'source.md') return mockFile;
|
|
return null;
|
|
});
|
|
mockVault.createFolder.mockResolvedValue(createMockFolder('folder'));
|
|
mockFileManager.renameFile.mockResolvedValue(undefined);
|
|
|
|
await vaultService.move('source.md', 'folder/dest.md');
|
|
|
|
expect(mockVault.createFolder).toHaveBeenCalledWith('folder');
|
|
});
|
|
|
|
it('should return error when move operation fails', async () => {
|
|
const mockFile = createMockFile('source.md');
|
|
mockVault.getAbstractFileByPath.mockReturnValue(mockFile);
|
|
mockFileManager.renameFile.mockRejectedValue(new Error('Move failed'));
|
|
|
|
const result = await vaultService.move('source.md', 'dest.md');
|
|
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toBe('Move failed');
|
|
});
|
|
});
|
|
|
|
describe('createFolder', () => {
|
|
it('should create folder with sanitized path', async () => {
|
|
const mockFolder = createMockFolder('folder');
|
|
mockVault.createFolder.mockResolvedValue(mockFolder);
|
|
|
|
const result = await vaultService.createFolder('folder');
|
|
|
|
expect(mockVault.createFolder).toHaveBeenCalledWith('folder');
|
|
expect(result).toBe(mockFolder);
|
|
});
|
|
|
|
it('should return error when trying to create folder in excluded path', async () => {
|
|
const result = await vaultService.createFolder('Vaultkeeper AI/subfolder', false);
|
|
|
|
expect(result).toBeInstanceOf(Error);
|
|
expect((result as Error).message).toContain('Failed to create folder, permission denied');
|
|
});
|
|
});
|
|
|
|
describe('listFilesInDirectory', () => {
|
|
it('should list all files in directory non-recursively', async () => {
|
|
const file1 = createMockFile('folder/file1.md');
|
|
const file2 = createMockFile('folder/file2.md');
|
|
const folder = createMockFolder('folder', [file1, file2]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
|
|
const result = await vaultService.listFilesInDirectory('folder', false);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result).toContain(file1);
|
|
expect(result).toContain(file2);
|
|
});
|
|
|
|
it('should list all files recursively', async () => {
|
|
const file1 = createMockFile('folder/file1.md');
|
|
const file2 = createMockFile('folder/sub/file2.md');
|
|
const subfolder = createMockFolder('folder/sub', [file2]);
|
|
const folder = createMockFolder('folder', [file1, subfolder]);
|
|
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === 'folder') return folder;
|
|
if (path === 'folder/sub') return subfolder;
|
|
return null;
|
|
});
|
|
|
|
const result = await vaultService.listFilesInDirectory('folder', true);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result).toContain(file1);
|
|
expect(result).toContain(file2);
|
|
});
|
|
|
|
it('should filter out excluded files from results', async () => {
|
|
const file1 = createMockFile('folder/public.md');
|
|
const file2 = createMockFile('folder/private.md');
|
|
const folder = createMockFolder('folder', [file1, file2]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
settingsService.settings.exclusions = ['**/private.md'];
|
|
|
|
const result = await vaultService.listFilesInDirectory('folder', false);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].path).toBe('folder/public.md');
|
|
});
|
|
|
|
it('should return empty array when directory does not exist', async () => {
|
|
mockVault.getAbstractFileByPath.mockReturnValue(null);
|
|
|
|
const result = await vaultService.listFilesInDirectory('nonexistent');
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should return empty array when path is a file, not a directory', async () => {
|
|
const mockFile = createMockFile('file.md');
|
|
mockVault.getAbstractFileByPath.mockReturnValue(mockFile);
|
|
|
|
const result = await vaultService.listFilesInDirectory('file.md');
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should respect allowAccessToPluginRoot parameter when accessing directory', async () => {
|
|
const file1 = createMockFile('Vaultkeeper AI/test.md');
|
|
const file2 = createMockFile('Vaultkeeper AI/notes.md');
|
|
const agentFolder = createMockFolder('Vaultkeeper AI', [file1, file2]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(agentFolder);
|
|
|
|
// Create a spy on the vaultService.getAbstractFileByPath method
|
|
const getAbstractFileSpy = vi.spyOn(vaultService, 'getAbstractFileByPath');
|
|
|
|
// Call with allowAccessToPluginRoot = false (should block access)
|
|
await vaultService.listFilesInDirectory('Vaultkeeper AI', true, false);
|
|
|
|
// Verify getAbstractFileByPath was called with the correct parameter
|
|
expect(getAbstractFileSpy).toHaveBeenCalledWith('Vaultkeeper AI', false);
|
|
|
|
// Reset
|
|
getAbstractFileSpy.mockClear();
|
|
|
|
// Call with allowAccessToPluginRoot = true (should allow access)
|
|
await vaultService.listFilesInDirectory('Vaultkeeper AI', true, true);
|
|
|
|
// Verify getAbstractFileByPath was called with the correct parameter
|
|
expect(getAbstractFileSpy).toHaveBeenCalledWith('Vaultkeeper AI', true);
|
|
});
|
|
|
|
it('should not access excluded directory when allowAccessToPluginRoot is false', async () => {
|
|
// Mock to simulate VaultService exclusion behavior
|
|
mockVault.getAbstractFileByPath.mockReturnValue(null);
|
|
|
|
// Create a spy to verify the correct parameter is passed
|
|
const getAbstractFileSpy = vi.spyOn(vaultService, 'getAbstractFileByPath');
|
|
|
|
// Try to list files in Vaultkeeper AI directory with allowAccessToPluginRoot = false
|
|
const result = await vaultService.listFilesInDirectory('Vaultkeeper AI', true, false);
|
|
|
|
// Should call getAbstractFileByPath with false (not hardcoded true)
|
|
expect(getAbstractFileSpy).toHaveBeenCalledWith('Vaultkeeper AI', false);
|
|
|
|
// Should return empty array since directory is excluded
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('listFoldersInDirectory', () => {
|
|
it('should list all folders in directory non-recursively', async () => {
|
|
const folder1 = createMockFolder('parent/folder1', []);
|
|
const folder2 = createMockFolder('parent/folder2', []);
|
|
const parentFolder = createMockFolder('parent', [folder1, folder2]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(parentFolder);
|
|
|
|
const result = await vaultService.listFoldersInDirectory('parent', false);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result).toContain(folder1);
|
|
expect(result).toContain(folder2);
|
|
});
|
|
|
|
it('should list all folders recursively', async () => {
|
|
const subfolder1 = createMockFolder('parent/child/subfolder1', []);
|
|
const childFolder = createMockFolder('parent/child', [subfolder1]);
|
|
const folder1 = createMockFolder('parent/folder1', []);
|
|
const parentFolder = createMockFolder('parent', [folder1, childFolder]);
|
|
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === 'parent') return parentFolder;
|
|
if (path === 'parent/folder1') return folder1;
|
|
if (path === 'parent/child') return childFolder;
|
|
return null;
|
|
});
|
|
|
|
const result = await vaultService.listFoldersInDirectory('parent', true);
|
|
|
|
expect(result).toHaveLength(3);
|
|
expect(result).toContain(folder1);
|
|
expect(result).toContain(childFolder);
|
|
expect(result).toContain(subfolder1);
|
|
});
|
|
|
|
it('should filter out excluded folders from results', async () => {
|
|
const publicFolder = createMockFolder('parent/public', []);
|
|
const privateFolder = createMockFolder('parent/private', []);
|
|
const parentFolder = createMockFolder('parent', [publicFolder, privateFolder]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(parentFolder);
|
|
settingsService.settings.exclusions = ['**/private'];
|
|
|
|
const result = await vaultService.listFoldersInDirectory('parent', false);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].path).toBe('parent/public');
|
|
});
|
|
|
|
it('should return empty array when directory does not exist', async () => {
|
|
mockVault.getAbstractFileByPath.mockReturnValue(null);
|
|
|
|
const result = await vaultService.listFoldersInDirectory('nonexistent');
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should return empty array when path is a file, not a directory', async () => {
|
|
const mockFile = createMockFile('file.md');
|
|
mockVault.getAbstractFileByPath.mockReturnValue(mockFile);
|
|
|
|
const result = await vaultService.listFoldersInDirectory('file.md');
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should respect allowAccessToPluginRoot parameter', async () => {
|
|
const agentFolder = createMockFolder('Vaultkeeper AI', []);
|
|
const notesFolder = createMockFolder('notes', []);
|
|
const rootFolder = createMockFolder('/', [agentFolder, notesFolder]);
|
|
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === '/' || path === '') return rootFolder;
|
|
// Vaultkeeper AI blocked when allowAccessToPluginRoot = false
|
|
if (path === 'Vaultkeeper AI') return null;
|
|
if (path === 'notes') return notesFolder;
|
|
return null;
|
|
});
|
|
|
|
// With allowAccessToPluginRoot = false (should exclude Vaultkeeper AI)
|
|
const result1 = await vaultService.listFoldersInDirectory('/', true, false);
|
|
expect(result1.some((folder) => folder.path === 'Vaultkeeper AI')).toBe(false);
|
|
expect(result1.some((folder) => folder.path === 'notes')).toBe(true);
|
|
|
|
// With allowAccessToPluginRoot = true (should include Vaultkeeper AI)
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === '/' || path === '') return rootFolder;
|
|
if (path === 'Vaultkeeper AI') return agentFolder; // Now allowed
|
|
if (path === 'notes') return notesFolder;
|
|
return null;
|
|
});
|
|
|
|
const result2 = await vaultService.listFoldersInDirectory('/', true, true);
|
|
expect(result2.some((folder) => folder.path === 'Vaultkeeper AI')).toBe(true);
|
|
expect(result2.some((folder) => folder.path === 'notes')).toBe(true);
|
|
});
|
|
|
|
it('should not recurse into excluded folders', async () => {
|
|
const excludedSubfolder = createMockFolder('parent/excluded/sub', []);
|
|
const excludedFolder = createMockFolder('parent/excluded', [excludedSubfolder]);
|
|
const allowedFolder = createMockFolder('parent/allowed', []);
|
|
const parentFolder = createMockFolder('parent', [excludedFolder, allowedFolder]);
|
|
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === 'parent') return parentFolder;
|
|
if (path === 'parent/excluded') return excludedFolder; // Should not be accessed
|
|
if (path === 'parent/allowed') return allowedFolder;
|
|
return null;
|
|
});
|
|
|
|
// Use pattern that matches the folder itself and its contents
|
|
settingsService.settings.exclusions = ['parent/excluded/**', 'parent/excluded'];
|
|
|
|
const result = await vaultService.listFoldersInDirectory('parent', true);
|
|
|
|
// Should not include excluded folder or its subfolders
|
|
expect(result.some((folder) => folder.path === 'parent/excluded')).toBe(false);
|
|
expect(result.some((folder) => folder.path === 'parent/excluded/sub')).toBe(false);
|
|
expect(result.some((folder) => folder.path === 'parent/allowed')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('searchVaultFiles', () => {
|
|
it('should find matches in file content', async () => {
|
|
const file1 = createMockFile('note1.md');
|
|
const file2 = createMockFile('note2.md');
|
|
const folder = createMockFolder('/', [file1, file2]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockImplementation((file: TFile) => {
|
|
if (file.path === 'note1.md') return Promise.resolve('This is a test document with test word');
|
|
if (file.path === 'note2.md') return Promise.resolve('Another document');
|
|
return Promise.resolve('');
|
|
});
|
|
|
|
const results = await vaultService.searchVaultFiles('test');
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const match = results.find(r => r.file.path === 'note1.md');
|
|
expect(match).toBeDefined();
|
|
expect(match!.snippets.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should find filename matches', async () => {
|
|
const file = createMockFile('test-file.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('No matches in content');
|
|
|
|
const results = await vaultService.searchVaultFiles('test');
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const match = results.find(r => r.file.path === 'test-file.md');
|
|
expect(match).toBeDefined();
|
|
});
|
|
|
|
it('should handle invalid regex gracefully by escaping', async () => {
|
|
const file = createMockFile('note.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('Contains [special] characters');
|
|
|
|
// Should not throw error
|
|
const results = await vaultService.searchVaultFiles('[invalid regex');
|
|
|
|
expect(results).toBeDefined();
|
|
});
|
|
|
|
it('should extract snippets with context around matches', async () => {
|
|
const file = createMockFile('note.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
const content = 'a'.repeat(100) + 'MATCH' + 'b'.repeat(100);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue(content);
|
|
|
|
const results = await vaultService.searchVaultFiles('MATCH');
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const match = results[0];
|
|
expect(match.snippets[0].text.length).toBeGreaterThan('MATCH'.length);
|
|
expect(match.snippets[0].text).toContain('MATCH');
|
|
});
|
|
|
|
it('should merge overlapping snippets', async () => {
|
|
const file = createMockFile('note.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
// Two matches close together that should be merged
|
|
const content = 'test ' + 'a'.repeat(50) + ' test';
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue(content);
|
|
|
|
const results = await vaultService.searchVaultFiles('test');
|
|
|
|
// Should merge into one snippet since they overlap
|
|
expect(results.length).toBeGreaterThan(0);
|
|
// The exact behavior depends on implementation details
|
|
});
|
|
|
|
it('should randomly sample when more than searchResultsLimit matches', async () => {
|
|
// Create 25 files, each with a match
|
|
const files: TFile[] = [];
|
|
for (let i = 0; i < 25; i++) {
|
|
files.push(createMockFile(`note${i}.md`));
|
|
}
|
|
const folder = createMockFolder('/', files);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('This contains the search term');
|
|
|
|
const results = await vaultService.searchVaultFiles('search');
|
|
|
|
// Should have at most searchResultsLimit snippet matches (plus potentially filename matches)
|
|
const totalSnippets = results.reduce((sum, r) => sum + r.snippets.length, 0);
|
|
expect(totalSnippets).toBeLessThanOrEqual(settingsService.settings.searchResultsLimit);
|
|
});
|
|
|
|
it('should respect custom searchResultsLimit setting', async () => {
|
|
// Set custom limit
|
|
settingsService.settings.searchResultsLimit = 5;
|
|
|
|
// Create 10 files, each with a match
|
|
const files: TFile[] = [];
|
|
for (let i = 0; i < 10; i++) {
|
|
files.push(createMockFile(`note${i}.md`));
|
|
}
|
|
const folder = createMockFolder('/', files);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('This contains the search term');
|
|
|
|
const results = await vaultService.searchVaultFiles('search');
|
|
|
|
// Should have at most 5 snippet matches
|
|
const totalSnippets = results.reduce((sum, r) => sum + r.snippets.length, 0);
|
|
expect(totalSnippets).toBeLessThanOrEqual(5);
|
|
});
|
|
|
|
it('should respect custom snippetSizeLimit setting for snippet extraction', async () => {
|
|
// Set custom snippet size
|
|
settingsService.settings.snippetSizeLimit = 20;
|
|
|
|
const file = createMockFile('note.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
const content = 'a'.repeat(100) + 'MATCH' + 'b'.repeat(100);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue(content);
|
|
|
|
const results = await vaultService.searchVaultFiles('MATCH');
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const match = results[0];
|
|
// Snippet should be approximately snippetSizeLimit characters (10 before + 5 for MATCH + 10 after)
|
|
// Allow some margin for the match itself
|
|
expect(match.snippets[0].text.length).toBeLessThanOrEqual(settingsService.settings.snippetSizeLimit + 10);
|
|
});
|
|
|
|
it('should perform case-insensitive search', async () => {
|
|
const file = createMockFile('note.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('Test TEST tEsT');
|
|
|
|
const results = await vaultService.searchVaultFiles('test');
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
// Should find all three variants
|
|
});
|
|
|
|
it('should parse regex literal with case-insensitive flag', async () => {
|
|
const file = createMockFile('note.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('This note contains information about MAUI development');
|
|
|
|
// Build the regex pattern string to avoid vitest transformer issues
|
|
const pattern = '/' + 'maui' + '/' + 'i';
|
|
const results = await vaultService.searchVaultFiles(pattern);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const match = results.find(r => r.file.path === 'note.md');
|
|
expect(match).toBeDefined();
|
|
expect(match!.snippets.length).toBeGreaterThan(0);
|
|
expect(match!.snippets[0].text).toContain('MAUI');
|
|
});
|
|
|
|
it('should parse regex with word boundary patterns', async () => {
|
|
const file = createMockFile('note.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('docker dockerfiles and dockers');
|
|
|
|
// Should match only whole word "docker", not "dockers" or "dockerfiles"
|
|
const pattern = '/' + '\\b' + 'docker' + '\\b' + '/' + 'i';
|
|
const results = await vaultService.searchVaultFiles(pattern);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const match = results.find(r => r.file.path === 'note.md');
|
|
expect(match).toBeDefined();
|
|
});
|
|
|
|
it('should parse regex with alternation patterns', async () => {
|
|
const file = createMockFile('note.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('I prefer gray over grey');
|
|
|
|
// Should match both "gray" and "grey"
|
|
const pattern = '/' + 'gr(a|e)y' + '/' + 'i';
|
|
const results = await vaultService.searchVaultFiles(pattern);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const match = results.find(r => r.file.path === 'note.md');
|
|
expect(match).toBeDefined();
|
|
});
|
|
|
|
it('should parse regex with optional characters', async () => {
|
|
const file = createMockFile('note.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('kubernetes k8s and kube configurations');
|
|
|
|
// Should match kubernetes, k8s, or kube
|
|
const pattern = '/' + '(kubernetes|k8s|kube)' + '/' + 'i';
|
|
const results = await vaultService.searchVaultFiles(pattern);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const match = results.find(r => r.file.path === 'note.md');
|
|
expect(match).toBeDefined();
|
|
});
|
|
|
|
it('should parse regex with wildcard sequences', async () => {
|
|
const file = createMockFile('note.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('project alpha and proj_alpha are both valid');
|
|
|
|
// Should match "project alpha", "proj_alpha", "proj alpha", etc.
|
|
const pattern = '/' + 'proj.*alpha' + '/' + 'i';
|
|
const results = await vaultService.searchVaultFiles(pattern);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const match = results.find(r => r.file.path === 'note.md');
|
|
expect(match).toBeDefined();
|
|
});
|
|
|
|
it('should parse regex with character classes', async () => {
|
|
const file = createMockFile('note.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('Version v1.2 and v10.5 released');
|
|
|
|
// Should match version numbers like v1.2, v10.5, etc.
|
|
const pattern = '/' + 'v\\d+\\.\\d+' + '/';
|
|
const results = await vaultService.searchVaultFiles(pattern);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
const match = results.find(r => r.file.path === 'note.md');
|
|
expect(match).toBeDefined();
|
|
});
|
|
|
|
it('should not log errors when vault contains excluded directories', async () => {
|
|
// Setup: Create a vault structure with excluded "Vaultkeeper AI" directory
|
|
const normalFile = createMockFile('notes/document.md');
|
|
const excludedFile = createMockFile('Vaultkeeper AI/secret.md');
|
|
const notesFolder = createMockFolder('notes', [normalFile]);
|
|
const excludedFolder = createMockFolder('Vaultkeeper AI', [excludedFile]);
|
|
const rootFolder = createMockFolder('/', [normalFile, notesFolder, excludedFolder]);
|
|
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === '/' || path === '') return rootFolder;
|
|
if (path === 'notes') return notesFolder;
|
|
if (path === 'notes/document.md') return normalFile;
|
|
// Vaultkeeper AI directory should be blocked by getAbstractFileByPath
|
|
if (path === 'Vaultkeeper AI') return null;
|
|
return null;
|
|
});
|
|
|
|
mockVault.cachedRead.mockResolvedValue('test content');
|
|
|
|
// Spy on console.error
|
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
// Execute search
|
|
const results = await vaultService.searchVaultFiles('test');
|
|
|
|
// Should not log exclusion errors during normal search operation
|
|
expect(consoleErrorSpy).not.toHaveBeenCalledWith(
|
|
expect.stringContaining('Plugin attempted to retrieve a file that is in the exclusions list')
|
|
);
|
|
|
|
// Should still return results from non-excluded files
|
|
expect(results.length).toBeGreaterThan(0);
|
|
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
|
|
it('should respect allowAccessToPluginRoot parameter when true', async () => {
|
|
// Setup: Create vault with Vaultkeeper AI directory
|
|
const agentFile = createMockFile('Vaultkeeper AI/notes.md');
|
|
const normalFile = createMockFile('normal.md');
|
|
const agentFolder = createMockFolder('Vaultkeeper AI', [agentFile]);
|
|
const rootFolder = createMockFolder('/', [normalFile, agentFolder]);
|
|
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === '/' || path === '') return rootFolder;
|
|
if (path === 'Vaultkeeper AI') return agentFolder;
|
|
if (path === 'Vaultkeeper AI/notes.md') return agentFile;
|
|
if (path === 'normal.md') return normalFile;
|
|
return null;
|
|
});
|
|
|
|
mockVault.cachedRead.mockResolvedValue('searchable content');
|
|
|
|
// Search with allowAccessToPluginRoot = true
|
|
const results = await vaultService.searchVaultFiles('searchable', true);
|
|
|
|
// Should include files from Vaultkeeper AI directory
|
|
const paths = results.map(r => r.file.path);
|
|
expect(paths).toContain('Vaultkeeper AI/notes.md');
|
|
expect(paths).toContain('normal.md');
|
|
});
|
|
|
|
it('should exclude Vaultkeeper AI directory when allowAccessToPluginRoot is false', async () => {
|
|
// Setup: Create vault with Vaultkeeper AI directory
|
|
const agentFile = createMockFile('Vaultkeeper AI/notes.md');
|
|
const normalFile = createMockFile('normal.md');
|
|
const agentFolder = createMockFolder('Vaultkeeper AI', [agentFile]);
|
|
const rootFolder = createMockFolder('/', [normalFile, agentFolder]);
|
|
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === '/' || path === '') return rootFolder;
|
|
// When allowAccessToPluginRoot is false, Vaultkeeper AI should be blocked
|
|
if (path === 'Vaultkeeper AI') return null;
|
|
if (path === 'normal.md') return normalFile;
|
|
return null;
|
|
});
|
|
|
|
mockVault.cachedRead.mockResolvedValue('searchable content');
|
|
|
|
// Search with allowAccessToPluginRoot = false (default)
|
|
const results = await vaultService.searchVaultFiles('searchable', false);
|
|
|
|
// Should NOT include files from Vaultkeeper AI directory
|
|
const paths = results.map(r => r.file.path);
|
|
expect(paths).not.toContain('Vaultkeeper AI/notes.md');
|
|
expect(paths).toContain('normal.md');
|
|
});
|
|
|
|
it('should pass allowAccessToPluginRoot to listFilesInDirectory', async () => {
|
|
const file = createMockFile('test.md');
|
|
const folder = createMockFolder('/', [file]);
|
|
|
|
mockVault.getAbstractFileByPath.mockReturnValue(folder);
|
|
mockVault.cachedRead.mockResolvedValue('content');
|
|
|
|
// Create a spy on listFilesInDirectory
|
|
const listFilesSpy = vi.spyOn(vaultService, 'listFilesInDirectory');
|
|
|
|
// Call with allowAccessToPluginRoot = true
|
|
await vaultService.searchVaultFiles('test', true);
|
|
|
|
// Verify listFilesInDirectory was called with the correct parameter
|
|
expect(listFilesSpy).toHaveBeenCalledWith(Path.Root, true, true);
|
|
|
|
listFilesSpy.mockClear();
|
|
|
|
// Call with allowAccessToPluginRoot = false
|
|
await vaultService.searchVaultFiles('test', false);
|
|
|
|
// Verify listFilesInDirectory was called with the correct parameter
|
|
expect(listFilesSpy).toHaveBeenCalledWith(Path.Root, true, false);
|
|
|
|
listFilesSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('isExclusion (private method behavior)', () => {
|
|
it('should exclude exact path matches', async () => {
|
|
settingsService.settings.exclusions = ['secret.md'];
|
|
|
|
const result = await vaultService.exists('secret.md');
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should handle wildcard * (matches any non-slash)', async () => {
|
|
settingsService.settings.exclusions = ['folder/*.md'];
|
|
|
|
// Mock files to exist in vault
|
|
mockVault.adapter.exists.mockResolvedValue(true);
|
|
|
|
expect(await vaultService.exists('folder/file.md')).toBe(false);
|
|
expect(await vaultService.exists('folder/sub/file.md')).toBe(true); // * doesn't match /
|
|
});
|
|
|
|
it('should handle double wildcard ** (matches anything including slashes)', async () => {
|
|
settingsService.settings.exclusions = ['private/**'];
|
|
|
|
// Mock files to exist in vault
|
|
mockVault.adapter.exists.mockResolvedValue(true);
|
|
|
|
expect(await vaultService.exists('private/file.md')).toBe(false);
|
|
expect(await vaultService.exists('private/sub/deep/file.md')).toBe(false);
|
|
expect(await vaultService.exists('public/file.md')).toBe(true);
|
|
});
|
|
|
|
it('should handle patterns ending with / to match directory and contents', async () => {
|
|
settingsService.settings.exclusions = ['temp/'];
|
|
|
|
expect(await vaultService.exists('temp/file.md')).toBe(false);
|
|
expect(await vaultService.exists('temp/sub/file.md')).toBe(false);
|
|
});
|
|
|
|
it('should always exclude Vaultkeeper AI root by default', async () => {
|
|
const result = await vaultService.exists('Vaultkeeper AI/file.md', false);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should handle special regex characters in patterns', async () => {
|
|
settingsService.settings.exclusions = ['folder[test].md'];
|
|
|
|
// Mock files to exist in vault
|
|
mockVault.adapter.exists.mockResolvedValue(true);
|
|
|
|
// Should match literally, not as regex character class
|
|
expect(await vaultService.exists('folder[test].md')).toBe(false);
|
|
expect(await vaultService.exists('foldert.md')).toBe(true);
|
|
});
|
|
|
|
it('should handle multiple exclusion patterns', async () => {
|
|
settingsService.settings.exclusions = ['private/**', 'temp/', '*.secret'];
|
|
|
|
// Mock files to exist in vault
|
|
mockVault.adapter.exists.mockResolvedValue(true);
|
|
|
|
expect(await vaultService.exists('private/file.md')).toBe(false);
|
|
expect(await vaultService.exists('temp/file.md')).toBe(false);
|
|
expect(await vaultService.exists('data.secret')).toBe(false);
|
|
expect(await vaultService.exists('public/file.md')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('registerFileEvents', () => {
|
|
it('should register all file event handlers', () => {
|
|
const mockEventRef = { event: 'mock' };
|
|
mockVault.on.mockReturnValue(mockEventRef);
|
|
|
|
const handler = vi.fn();
|
|
vaultService.registerFileEvents(handler);
|
|
|
|
// Should register 4 events (create, modify, rename, delete)
|
|
expect(mockVault.on).toHaveBeenCalledTimes(4);
|
|
expect(mockVault.on).toHaveBeenCalledWith('create', expect.any(Function));
|
|
expect(mockVault.on).toHaveBeenCalledWith('modify', expect.any(Function));
|
|
expect(mockVault.on).toHaveBeenCalledWith('rename', expect.any(Function));
|
|
expect(mockVault.on).toHaveBeenCalledWith('delete', expect.any(Function));
|
|
expect(mockPlugin.registerEvent).toHaveBeenCalledTimes(4);
|
|
});
|
|
|
|
it('should call handler with correct parameters for create event', () => {
|
|
const handler = vi.fn();
|
|
let createCallback: any;
|
|
|
|
mockVault.on.mockImplementation((event: string, callback: any) => {
|
|
if (event === 'create') {
|
|
createCallback = callback;
|
|
}
|
|
return { event: 'mock' };
|
|
});
|
|
|
|
vaultService.registerFileEvents(handler);
|
|
|
|
const mockFile = createMockFile('test.md');
|
|
createCallback(mockFile);
|
|
|
|
expect(handler).toHaveBeenCalledWith('create', mockFile, { oldPath: '' });
|
|
});
|
|
|
|
it('should call handler with correct parameters for rename event', () => {
|
|
const handler = vi.fn();
|
|
let renameCallback: any;
|
|
|
|
mockVault.on.mockImplementation((event: string, callback: any) => {
|
|
if (event === 'rename') {
|
|
renameCallback = callback;
|
|
}
|
|
return { event: 'mock' };
|
|
});
|
|
|
|
vaultService.registerFileEvents(handler);
|
|
|
|
const mockFile = createMockFile('new.md');
|
|
renameCallback(mockFile, 'old.md');
|
|
|
|
expect(handler).toHaveBeenCalledWith('rename', mockFile, { oldPath: 'old.md' });
|
|
});
|
|
});
|
|
|
|
describe('listDirectoryContents', () => {
|
|
it('should return all files and folders when no exclusions', async () => {
|
|
const file1 = createMockFile('note1.md');
|
|
const file2 = createMockFile('note2.md');
|
|
const folder1 = createMockFolder('folder1', []);
|
|
const folder2 = createMockFolder('folder2', []);
|
|
const rootFolder = createMockFolder('/', [file1, file2, folder1, folder2]);
|
|
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === '/' || path === '') return rootFolder;
|
|
if (path === 'folder1') return folder1;
|
|
if (path === 'folder2') return folder2;
|
|
return null;
|
|
});
|
|
|
|
const result = await vaultService.listDirectoryContents(Path.Root);
|
|
|
|
expect(result).toHaveLength(4);
|
|
expect(result).toEqual(expect.arrayContaining([file1, file2, folder1, folder2]));
|
|
});
|
|
|
|
it('should filter out excluded files and folders', async () => {
|
|
const publicNote = createMockFile('public/note.md');
|
|
const agentNote = createMockFile('Vaultkeeper AI/conversation.md');
|
|
const privateNote = createMockFile('private/secret.md');
|
|
const publicFolder = createMockFolder('public', [publicNote]);
|
|
const agentFolder = createMockFolder('Vaultkeeper AI', [agentNote]);
|
|
const privateFolder = createMockFolder('private', [privateNote]);
|
|
const rootFolder = createMockFolder('/', [publicNote, agentNote, privateNote, publicFolder, agentFolder, privateFolder]);
|
|
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === '/' || path === '') return rootFolder;
|
|
if (path === 'public') return publicFolder;
|
|
if (path === 'Vaultkeeper AI') return null; // Excluded by default
|
|
if (path === 'private') return privateFolder;
|
|
return null;
|
|
});
|
|
|
|
settingsService.settings.exclusions = ['private/**'];
|
|
|
|
const result = await vaultService.listDirectoryContents(Path.Root, true, false);
|
|
|
|
// Should include: public/note.md and public folder
|
|
// Should exclude: Vaultkeeper AI folder (default exclusion), private/** content
|
|
expect(result.some((item: any) => item.path === 'public/note.md')).toBe(true);
|
|
expect(result.some((item: any) => item.path === 'public')).toBe(true);
|
|
expect(result.some((item: any) => item.path === 'Vaultkeeper AI/conversation.md')).toBe(false);
|
|
expect(result.some((item: any) => item.path === 'private/secret.md')).toBe(false);
|
|
expect(result.some((item: any) => item.path === 'private')).toBe(true); // Folder itself not excluded by 'private/**'
|
|
});
|
|
|
|
it('should exclude the Vaultkeeper AI directory itself from folder listings', async () => {
|
|
const publicNote = createMockFile('public/note.md');
|
|
const publicFolder = createMockFolder('public', [publicNote]);
|
|
const agentFolder = createMockFolder('Vaultkeeper AI', []);
|
|
const notesFolder = createMockFolder('notes', []);
|
|
const rootFolder = createMockFolder('/', [publicNote, publicFolder, agentFolder, notesFolder]);
|
|
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === '/' || path === '') return rootFolder;
|
|
if (path === 'public') return publicFolder;
|
|
if (path === 'Vaultkeeper AI') return null; // Excluded by default
|
|
if (path === 'notes') return notesFolder;
|
|
return null;
|
|
});
|
|
|
|
const result = await vaultService.listDirectoryContents(Path.Root, true, false);
|
|
|
|
// Vaultkeeper AI directory itself should be excluded
|
|
expect(result.some((item: any) => item.path === 'Vaultkeeper AI')).toBe(false);
|
|
// Other folders should be included
|
|
expect(result.some((item: any) => item.path === 'public')).toBe(true);
|
|
expect(result.some((item: any) => item.path === 'notes')).toBe(true);
|
|
});
|
|
|
|
it('should include Vaultkeeper AI directory when allowAccessToPluginRoot is true', async () => {
|
|
const note = createMockFile('note.md');
|
|
const agentNote = createMockFile('Vaultkeeper AI/conversation.md');
|
|
const agentFolder = createMockFolder('Vaultkeeper AI', [agentNote]);
|
|
const rootFolder = createMockFolder('/', [note, agentFolder]);
|
|
|
|
mockVault.getAbstractFileByPath.mockImplementation((path: string) => {
|
|
if (path === '/' || path === '') return rootFolder;
|
|
if (path === 'Vaultkeeper AI') return agentFolder; // Allowed with flag
|
|
return null;
|
|
});
|
|
|
|
const result = await vaultService.listDirectoryContents(Path.Root, true, true);
|
|
|
|
expect(result).toHaveLength(3);
|
|
expect(result.some((item: any) => item.path === 'Vaultkeeper AI/conversation.md')).toBe(true);
|
|
});
|
|
|
|
it('should return empty array when vault is empty', async () => {
|
|
const emptyRoot = createMockFolder('/', []);
|
|
mockVault.getAbstractFileByPath.mockReturnValue(emptyRoot);
|
|
|
|
const result = await vaultService.listDirectoryContents(Path.Root);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
});
|