test: rename FileSystemService methods in mocks to match interface

Update test mocks to use renamed methods: readFile → readFilePath, writeFile → writeToFilePath, patchFile → patchFileAtPath. Add activeWindow and activeDocument globals to test setup for Obsidian compatibility.
This commit is contained in:
Andrew Beal 2026-04-23 16:10:03 +01:00
parent f693933950
commit 21e237c80a
4 changed files with 75 additions and 54 deletions

View file

@ -33,9 +33,9 @@ describe('AIToolService - Integration Tests', () => {
mockFileSystemService = {
searchVaultFiles: vi.fn(),
listFilesInDirectory: vi.fn(),
readFile: vi.fn(),
writeFile: vi.fn(),
patchFile: vi.fn(),
readFilePath: vi.fn(),
writeToFilePath: vi.fn(),
patchFileAtPath: vi.fn(),
deleteFile: vi.fn(),
moveFile: vi.fn()
};
@ -192,7 +192,7 @@ describe('AIToolService - Integration Tests', () => {
describe('performAITool - ReadVaultFiles', () => {
it('should read multiple files successfully', async () => {
mockFileSystemService.readFile
mockFileSystemService.readFilePath
.mockResolvedValueOnce('Content of file 1')
.mockResolvedValueOnce('Content of file 2')
.mockResolvedValueOnce('Content of file 3');
@ -216,7 +216,7 @@ describe('AIToolService - Integration Tests', () => {
const error1 = new Error('File not found');
const error2 = new Error('File not found');
mockFileSystemService.readFile
mockFileSystemService.readFilePath
.mockResolvedValueOnce('Existing content')
.mockResolvedValueOnce(error1)
.mockResolvedValueOnce(error2);
@ -237,7 +237,7 @@ describe('AIToolService - Integration Tests', () => {
});
it('should handle mixed success and failure', async () => {
mockFileSystemService.readFile
mockFileSystemService.readFilePath
.mockResolvedValueOnce('Content A')
.mockResolvedValueOnce(new Error('File not found'))
.mockResolvedValueOnce('Content B');
@ -265,7 +265,7 @@ describe('AIToolService - Integration Tests', () => {
});
it('should handle single file read', async () => {
mockFileSystemService.readFile.mockResolvedValue('Single file content');
mockFileSystemService.readFilePath.mockResolvedValue('Single file content');
const result = await service.performAITool({
name: AITool.ReadVaultFiles,
@ -280,7 +280,7 @@ describe('AIToolService - Integration Tests', () => {
describe('performAITool - WriteVaultFile', () => {
it('should write file successfully', async () => {
mockFileSystemService.writeFile.mockResolvedValue(undefined);
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
const result = await service.performAITool({
name: AITool.WriteVaultFile,
@ -292,7 +292,7 @@ describe('AIToolService - Integration Tests', () => {
toolId: 'tool_11'
} as any);
expect(mockFileSystemService.writeFile).toHaveBeenCalledWith(
expect(mockFileSystemService.writeToFilePath).toHaveBeenCalledWith(
'notes/new-note.md',
'# New Note\n\nContent here'
);
@ -301,7 +301,7 @@ describe('AIToolService - Integration Tests', () => {
it('should handle write failure', async () => {
const error = new Error('Permission denied');
mockFileSystemService.writeFile.mockResolvedValue(error);
mockFileSystemService.writeToFilePath.mockResolvedValue(error);
const result = await service.performAITool({
name: AITool.WriteVaultFile,
@ -318,7 +318,7 @@ describe('AIToolService - Integration Tests', () => {
});
it('should normalize file path', async () => {
mockFileSystemService.writeFile.mockResolvedValue(undefined);
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
await service.performAITool({
name: AITool.WriteVaultFile,
@ -331,14 +331,14 @@ describe('AIToolService - Integration Tests', () => {
} as any);
// normalizePath should convert backslashes to forward slashes
expect(mockFileSystemService.writeFile).toHaveBeenCalledWith(
expect(mockFileSystemService.writeToFilePath).toHaveBeenCalledWith(
expect.stringContaining('/'),
'Content'
);
});
it('should handle empty content', async () => {
mockFileSystemService.writeFile.mockResolvedValue(undefined);
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
const result = await service.performAITool({
name: AITool.WriteVaultFile,
@ -350,14 +350,14 @@ describe('AIToolService - Integration Tests', () => {
toolId: 'tool_14'
} as any);
expect(mockFileSystemService.writeFile).toHaveBeenCalledWith('empty.md', '');
expect(mockFileSystemService.writeToFilePath).toHaveBeenCalledWith('empty.md', '');
expect((result.payload.response as any).success).toBe(true);
});
});
describe('performAITool - PatchVaultFile', () => {
it('should apply patch successfully', async () => {
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('notes/test.md', 'test'));
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('notes/test.md', 'test'));
const oldContent = ['old content'];
const newContent = ['new content'];
@ -373,13 +373,13 @@ describe('AIToolService - Integration Tests', () => {
toolId: 'tool_patch_1'
} as any);
expect(mockFileSystemService.patchFile).toHaveBeenCalledWith('notes/test.md', oldContent, newContent);
expect(mockFileSystemService.patchFileAtPath).toHaveBeenCalledWith('notes/test.md', oldContent, newContent);
expect(result.payload.response).toEqual({ success: true });
});
it('should handle patch failure', async () => {
const error = new Error('Content to replace was not found in the file');
mockFileSystemService.patchFile.mockResolvedValue(error);
mockFileSystemService.patchFileAtPath.mockResolvedValue(error);
const oldContent = ['old content'];
const newContent = ['new content'];
@ -400,7 +400,7 @@ describe('AIToolService - Integration Tests', () => {
});
it('should normalize file path', async () => {
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('folder/file.md', 'file'));
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('folder/file.md', 'file'));
const oldContent = ['old'];
const newContent = ['new'];
@ -417,7 +417,7 @@ describe('AIToolService - Integration Tests', () => {
} as any);
// normalizePath should convert backslashes to forward slashes
expect(mockFileSystemService.patchFile).toHaveBeenCalledWith(
expect(mockFileSystemService.patchFileAtPath).toHaveBeenCalledWith(
expect.stringContaining('/'),
oldContent,
newContent
@ -426,7 +426,7 @@ describe('AIToolService - Integration Tests', () => {
it('should handle file not found error', async () => {
const error = new Error('File does not exist: missing.md');
mockFileSystemService.patchFile.mockResolvedValue(error);
mockFileSystemService.patchFileAtPath.mockResolvedValue(error);
const oldContent = ['old'];
const newContent = ['new'];
@ -447,7 +447,7 @@ describe('AIToolService - Integration Tests', () => {
});
it('should handle complex multi-line replacement', async () => {
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('complex.md', 'complex'));
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('complex.md', 'complex'));
const oldContent = ['# Title\nOld line 1\nContext line'];
const newContent = ['# Title\nNew line 1\nContext line'];
@ -463,12 +463,12 @@ describe('AIToolService - Integration Tests', () => {
toolId: 'tool_patch_5'
} as any);
expect(mockFileSystemService.patchFile).toHaveBeenCalledWith('complex.md', oldContent, newContent);
expect(mockFileSystemService.patchFileAtPath).toHaveBeenCalledWith('complex.md', oldContent, newContent);
expect((result.payload.response as any).success).toBe(true);
});
it('should handle adding new content', async () => {
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('additions.md', 'additions'));
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('additions.md', 'additions'));
const oldContent = ['# Title\nExisting content'];
const newContent = ['# Title\nExisting content\nNew line 1\nNew line 2'];
@ -488,7 +488,7 @@ describe('AIToolService - Integration Tests', () => {
});
it('should handle removing content', async () => {
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('deletions.md', 'deletions'));
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('deletions.md', 'deletions'));
const oldContent = ['# Title\nLine to remove 1\nLine to remove 2\nRemaining content'];
const newContent = ['# Title\nRemaining content'];
@ -509,7 +509,7 @@ describe('AIToolService - Integration Tests', () => {
it('should handle permission denied error', async () => {
const error = new Error('Permission denied');
mockFileSystemService.patchFile.mockResolvedValue(error);
mockFileSystemService.patchFileAtPath.mockResolvedValue(error);
const oldContent = ['old'];
const newContent = ['new'];
@ -530,7 +530,7 @@ describe('AIToolService - Integration Tests', () => {
});
it('should return correct toolId in response', async () => {
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('test.md', 'test'));
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('test.md', 'test'));
const oldContent = 'old';
const newContent = 'new';
@ -562,7 +562,7 @@ describe('AIToolService - Integration Tests', () => {
} as any);
expect((result.payload.response as any).error).toContain('Invalid arguments for patch_vault_file');
expect(mockFileSystemService.patchFile).not.toHaveBeenCalled();
expect(mockFileSystemService.patchFileAtPath).not.toHaveBeenCalled();
});
});
@ -836,7 +836,7 @@ describe('AIToolService - Integration Tests', () => {
const foundPath = (searchResult.payload.response as any)[0].results[0].path;
// Then read
mockFileSystemService.readFile.mockResolvedValue('File content here');
mockFileSystemService.readFilePath.mockResolvedValue('File content here');
const readResult = await service.performAITool({
name: AITool.ReadVaultFiles,
@ -850,7 +850,7 @@ describe('AIToolService - Integration Tests', () => {
it('should handle write -> move workflow', async () => {
// First write
mockFileSystemService.writeFile.mockResolvedValue(undefined);
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
const writeResult = await service.performAITool({
name: AITool.WriteVaultFile,
@ -882,7 +882,7 @@ describe('AIToolService - Integration Tests', () => {
it('should handle read -> patch workflow', async () => {
// First read the file
mockFileSystemService.readFile.mockResolvedValue('# Original Title\n\nOriginal content');
mockFileSystemService.readFilePath.mockResolvedValue('# Original Title\n\nOriginal content');
const readResult = await service.performAITool({
name: AITool.ReadVaultFiles,
@ -893,7 +893,7 @@ describe('AIToolService - Integration Tests', () => {
expect((readResult.payload.response as any).results[0].contents).toContain('Original');
// Then patch it
mockFileSystemService.patchFile.mockResolvedValue(createMockFile('document.md', 'document'));
mockFileSystemService.patchFileAtPath.mockResolvedValue(createMockFile('document.md', 'document'));
const oldContent = ['# Original Title'];
const newContent = ['# Updated Title'];
@ -910,7 +910,7 @@ describe('AIToolService - Integration Tests', () => {
} as any);
expect((patchResult.payload.response as any).success).toBe(true);
expect(mockFileSystemService.patchFile).toHaveBeenCalledWith('document.md', oldContent, newContent);
expect(mockFileSystemService.patchFileAtPath).toHaveBeenCalledWith('document.md', oldContent, newContent);
});
});
});

View file

@ -12,8 +12,8 @@ describe('MemoriesService', () => {
beforeEach(() => {
mockFileSystemService = {
exists: vi.fn(),
readFile: vi.fn(),
writeFile: vi.fn()
readFilePath: vi.fn(),
writeToFilePath: vi.fn()
};
mockWorkSpaceService = {
openNoteByPath: vi.fn()
@ -30,7 +30,7 @@ describe('MemoriesService', () => {
describe('readMemories', () => {
it('should return file contents when file exists', async () => {
mockFileSystemService.readFile.mockResolvedValue('Remember: user prefers TypeScript.');
mockFileSystemService.readFilePath.mockResolvedValue('Remember: user prefers TypeScript.');
const result = await service.readMemories();
@ -38,7 +38,7 @@ describe('MemoriesService', () => {
});
it('should return MemoriesEmpty copy when file does not exist', async () => {
mockFileSystemService.readFile.mockResolvedValue(new Error('File not found'));
mockFileSystemService.readFilePath.mockResolvedValue(new Error('File not found'));
const result = await service.readMemories();
@ -46,7 +46,7 @@ describe('MemoriesService', () => {
});
it('should return MemoriesEmpty copy when read returns any error', async () => {
mockFileSystemService.readFile.mockResolvedValue(new Error('Permission denied'));
mockFileSystemService.readFilePath.mockResolvedValue(new Error('Permission denied'));
const result = await service.readMemories();
@ -56,16 +56,16 @@ describe('MemoriesService', () => {
describe('updateMemories', () => {
it('should write memories and return success message', async () => {
mockFileSystemService.writeFile.mockResolvedValue(undefined);
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
const result = await service.updateMemories('Line one\nLine two');
expect(mockFileSystemService.writeFile).toHaveBeenCalledOnce();
expect(mockFileSystemService.writeToFilePath).toHaveBeenCalledOnce();
expect(result).toBe(Copy.MemoriesUpdatedSuccess);
});
it('should write empty memories successfully', async () => {
mockFileSystemService.writeFile.mockResolvedValue(undefined);
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
const result = await service.updateMemories('');
@ -74,7 +74,7 @@ describe('MemoriesService', () => {
it('should return error when write fails', async () => {
const writeError = new Error('Disk full');
mockFileSystemService.writeFile.mockResolvedValue(writeError);
mockFileSystemService.writeToFilePath.mockResolvedValue(writeError);
const result = await service.updateMemories('some content');
@ -86,13 +86,13 @@ describe('MemoriesService', () => {
const result = await service.updateMemories(elevenLines);
expect(mockFileSystemService.writeFile).not.toHaveBeenCalled();
expect(mockFileSystemService.writeToFilePath).not.toHaveBeenCalled();
expect(result).toBeTypeOf('string');
expect(result as string).toContain('10');
});
it('should accept exactly 10 lines', async () => {
mockFileSystemService.writeFile.mockResolvedValue(undefined);
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
const tenLines = Array(10).fill('valid line').join('\n');
const result = await service.updateMemories(tenLines);
@ -105,13 +105,13 @@ describe('MemoriesService', () => {
const result = await service.updateMemories(longLine);
expect(mockFileSystemService.writeFile).not.toHaveBeenCalled();
expect(mockFileSystemService.writeToFilePath).not.toHaveBeenCalled();
expect(result).toBeTypeOf('string');
expect(result as string).toContain('200');
});
it('should accept a line of exactly 200 characters', async () => {
mockFileSystemService.writeFile.mockResolvedValue(undefined);
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
const maxLine = 'a'.repeat(200);
const result = await service.updateMemories(maxLine);
@ -120,7 +120,7 @@ describe('MemoriesService', () => {
});
it('should handle Windows-style line endings (CRLF) in line count', async () => {
mockFileSystemService.writeFile.mockResolvedValue(undefined);
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
const tenLinesCRLF = Array(10).fill('valid line').join('\r\n');
const result = await service.updateMemories(tenLinesCRLF);
@ -133,7 +133,7 @@ describe('MemoriesService', () => {
const result = await service.updateMemories(elevenLinesCRLF);
expect(mockFileSystemService.writeFile).not.toHaveBeenCalled();
expect(mockFileSystemService.writeToFilePath).not.toHaveBeenCalled();
});
});
@ -143,17 +143,17 @@ describe('MemoriesService', () => {
await service.openMemories();
expect(mockFileSystemService.writeFile).not.toHaveBeenCalled();
expect(mockFileSystemService.writeToFilePath).not.toHaveBeenCalled();
expect(mockWorkSpaceService.openNoteByPath).toHaveBeenCalledOnce();
});
it('should create memories file before opening when it does not exist', async () => {
mockFileSystemService.exists.mockResolvedValue(false);
mockFileSystemService.writeFile.mockResolvedValue(undefined);
mockFileSystemService.writeToFilePath.mockResolvedValue(undefined);
await service.openMemories();
expect(mockFileSystemService.writeFile).toHaveBeenCalledOnce();
expect(mockFileSystemService.writeToFilePath).toHaveBeenCalledOnce();
expect(mockWorkSpaceService.openNoteByPath).toHaveBeenCalledOnce();
});
});

View file

@ -41,6 +41,7 @@ describe('SettingsService', () => {
it('should merge loaded settings with defaults', () => {
const loadedSettings: Partial<IVaultkeeperAISettings> = {
firstTimeStart: false,
provider: AIProvider.Gemini,
model: AIProviderModel.GeminiFlash_2_5,
planningModel: AIProviderModel.GeminiPro_2_5,
apiKeys: {
@ -65,6 +66,7 @@ describe('SettingsService', () => {
it('should handle partially loaded settings and fill missing properties with defaults', () => {
const loadedSettings: Partial<IVaultkeeperAISettings> = {
provider: AIProvider.OpenAI,
model: AIProviderModel.GPT_5_4,
apiKeys: {
claude: '',
@ -178,8 +180,8 @@ describe('SettingsService', () => {
allowUpdatingMemories: true,
enableWebSearch: true,
enableWebViewer: false,
provider: AIProvider.Claude,
quickActionModel: AIProviderModel.ClaudeSonnet_4_6
provider: AIProvider.OpenAI,
quickActionModel: AIProviderModel.GPT_5_4_Mini
};
settingsService = new SettingsService(loadedSettings);
@ -205,8 +207,8 @@ describe('SettingsService', () => {
allowUpdatingMemories: true,
enableWebSearch: true,
enableWebViewer: false,
provider: AIProvider.Claude,
quickActionModel: AIProviderModel.ClaudeSonnet_4_6
provider: AIProvider.Gemini,
quickActionModel: AIProviderModel.GeminiFlash_2_5
};
settingsService = new SettingsService(loadedSettings);
@ -217,6 +219,7 @@ describe('SettingsService', () => {
it('should use fromModel to determine provider', () => {
// Test with various Claude models
settingsService = new SettingsService({
provider: AIProvider.Claude,
model: AIProviderModel.ClaudeOpus_4,
apiKeys: { claude: 'opus-key', openai: '', gemini: '', mistral: '' }
});
@ -224,6 +227,7 @@ describe('SettingsService', () => {
// Test with various Gemini models
settingsService = new SettingsService({
provider: AIProvider.Gemini,
model: AIProviderModel.GeminiPro_2_5,
apiKeys: { claude: '', openai: '', gemini: 'pro-key', mistral: '' }
});
@ -231,6 +235,7 @@ describe('SettingsService', () => {
// Test with various GPT models
settingsService = new SettingsService({
provider: AIProvider.OpenAI,
model: AIProviderModel.GPT_5_4,
apiKeys: { claude: '', openai: 'gpt5-key', gemini: '', mistral: '' }
});
@ -382,6 +387,7 @@ describe('SettingsService', () => {
geminiModels.forEach(model => {
settingsService = new SettingsService({
provider: AIProvider.Gemini,
model,
apiKeys: { claude: '', openai: '', gemini: 'test-gemini', mistral: '' }
});
@ -400,6 +406,7 @@ describe('SettingsService', () => {
openaiModels.forEach(model => {
settingsService = new SettingsService({
provider: AIProvider.OpenAI,
model,
apiKeys: { claude: '', openai: 'test-openai', gemini: '', mistral: '' }
});

View file

@ -9,6 +9,20 @@ if (typeof global.window === 'undefined') {
global.window = {} as any;
}
// Obsidian global aliases used in source code
if (typeof (global as any).activeWindow === 'undefined') {
(global as any).activeWindow = {
setTimeout: globalThis.setTimeout.bind(globalThis),
clearTimeout: globalThis.clearTimeout.bind(globalThis),
getSelection: () => (typeof document !== 'undefined' ? document.getSelection() : null),
window: typeof window !== 'undefined' ? window : global.window,
document: typeof document !== 'undefined' ? document : undefined,
};
}
if (typeof (global as any).activeDocument === 'undefined') {
(global as any).activeDocument = typeof document !== 'undefined' ? document : undefined;
}
// Note: Component class is now defined in __mocks__/obsidian.ts
// The vitest alias in vitest.config.ts handles mocking 'obsidian' imports