mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
test: add cleanup to prevent memory leaks in test suites
Add DeregisterAllServices() calls in afterEach hooks across all test files to clear singleton registry between tests. Export DeregisterAllServices from DependencyService. Fix StatusBarService test timing issues with animation frames.
This commit is contained in:
parent
3852194f2d
commit
f2d5c01970
15 changed files with 128 additions and 59 deletions
|
|
@ -21,4 +21,8 @@ export function Resolve<T>(type: symbol): T {
|
|||
|
||||
// It's a singleton, return the existing instance
|
||||
return service as T;
|
||||
}
|
||||
|
||||
export function DeregisterAllServices(): void {
|
||||
services.clear();
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { Claude } from '../../AIClasses/Claude/Claude';
|
||||
import { RegisterSingleton, Resolve } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, Resolve, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { StreamingService } from '../../Services/StreamingService';
|
||||
import type { IPrompt } from '../../AIClasses/IPrompt';
|
||||
|
|
@ -18,8 +18,6 @@ describe('Claude', () => {
|
|||
let mockFunctionDefinitions: any;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
|
||||
// Mock IPrompt
|
||||
mockPrompt = {
|
||||
systemInstruction: vi.fn().mockReturnValue('System instruction'),
|
||||
|
|
@ -62,6 +60,11 @@ describe('Claude', () => {
|
|||
claude = new Claude();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
});
|
||||
|
||||
describe('Constructor and Dependencies', () => {
|
||||
it('should initialize with dependencies from DependencyService', () => {
|
||||
expect(claude).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { ClaudeConversationNamingService } from '../../AIClasses/Claude/ClaudeConversationNamingService';
|
||||
import { RegisterSingleton, ClearAll } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { AIProviderModel } from '../../Enums/ApiProvider';
|
||||
import { Role } from '../../Enums/Role';
|
||||
|
|
@ -11,8 +11,6 @@ describe('ClaudeConversationNamingService', () => {
|
|||
let fetchMock: any;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
|
||||
mockPlugin = {
|
||||
settings: {
|
||||
apiKey: 'test-claude-key'
|
||||
|
|
@ -28,6 +26,8 @@ describe('ClaudeConversationNamingService', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { Gemini } from '../../AIClasses/Gemini/Gemini';
|
||||
import { RegisterSingleton, Resolve } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, Resolve, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { StreamingService } from '../../Services/StreamingService';
|
||||
import type { IPrompt } from '../../AIClasses/IPrompt';
|
||||
|
|
@ -18,8 +18,6 @@ describe('Gemini', () => {
|
|||
let mockFunctionDefinitions: any;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
|
||||
// Mock IPrompt
|
||||
mockPrompt = {
|
||||
systemInstruction: vi.fn().mockReturnValue('System instruction'),
|
||||
|
|
@ -62,6 +60,11 @@ describe('Gemini', () => {
|
|||
gemini = new Gemini();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
});
|
||||
|
||||
describe('Constructor and Dependencies', () => {
|
||||
it('should initialize with dependencies from DependencyService', () => {
|
||||
expect(gemini).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { GeminiConversationNamingService } from '../../AIClasses/Gemini/GeminiConversationNamingService';
|
||||
import { RegisterSingleton, ClearAll } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { AIProviderModel } from '../../Enums/ApiProvider';
|
||||
import { Role } from '../../Enums/Role';
|
||||
|
|
@ -11,8 +11,6 @@ describe('GeminiConversationNamingService', () => {
|
|||
let fetchMock: any;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
|
||||
mockPlugin = {
|
||||
settings: {
|
||||
apiKey: 'test-gemini-key'
|
||||
|
|
@ -28,6 +26,8 @@ describe('GeminiConversationNamingService', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { OpenAI } from '../../AIClasses/OpenAI/OpenAI';
|
||||
import { RegisterSingleton, Resolve } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, Resolve, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { StreamingService } from '../../Services/StreamingService';
|
||||
import type { IPrompt } from '../../AIClasses/IPrompt';
|
||||
|
|
@ -18,8 +18,6 @@ describe('OpenAI', () => {
|
|||
let mockFunctionDefinitions: any;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
|
||||
// Mock IPrompt
|
||||
mockPrompt = {
|
||||
systemInstruction: vi.fn().mockReturnValue('System instruction'),
|
||||
|
|
@ -62,6 +60,11 @@ describe('OpenAI', () => {
|
|||
openai = new OpenAI();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
});
|
||||
|
||||
describe('Constructor and Dependencies', () => {
|
||||
it('should initialize with dependencies from DependencyService', () => {
|
||||
expect(openai).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { OpenAIConversationNamingService } from '../../AIClasses/OpenAI/OpenAIConversationNamingService';
|
||||
import { RegisterSingleton, ClearAll } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { AIProviderModel } from '../../Enums/ApiProvider';
|
||||
import { Role } from '../../Enums/Role';
|
||||
|
|
@ -11,8 +11,6 @@ describe('OpenAIConversationNamingService', () => {
|
|||
let fetchMock: any;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
|
||||
mockPlugin = {
|
||||
settings: {
|
||||
apiKey: 'test-openai-key'
|
||||
|
|
@ -28,6 +26,8 @@ describe('OpenAIConversationNamingService', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { AIFunctionService } from '../../Services/AIFunctionService';
|
||||
import { FileSystemService } from '../../Services/FileSystemService';
|
||||
import { RegisterSingleton } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { AIFunction } from '../../Enums/AIFunction';
|
||||
import { TFile, TFolder } from 'obsidian';
|
||||
|
|
@ -44,6 +44,8 @@ describe('AIFunctionService - Integration Tests', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { ChatService } from '../../Services/ChatService';
|
||||
import { RegisterSingleton } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { Conversation } from '../../Conversations/Conversation';
|
||||
import { ConversationContent } from '../../Conversations/ConversationContent';
|
||||
|
|
@ -64,6 +64,8 @@ describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { ConversationFileSystemService } from '../../Services/ConversationFileSystemService';
|
||||
import { FileSystemService } from '../../Services/FileSystemService';
|
||||
import { RegisterSingleton } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { Conversation } from '../../Conversations/Conversation';
|
||||
import { ConversationContent } from '../../Conversations/ConversationContent';
|
||||
|
|
@ -45,6 +45,8 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { ConversationNamingService } from '../../Services/ConversationNamingService';
|
||||
import { RegisterSingleton } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { Conversation } from '../../Conversations/Conversation';
|
||||
import { Path } from '../../Enums/Path';
|
||||
|
|
@ -12,7 +12,6 @@ describe('ConversationNamingService', () => {
|
|||
let mockVaultService: any;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
// Mock IConversationNamingService
|
||||
mockNamingProvider = {
|
||||
generateName: vi.fn().mockResolvedValue('Generated Title')
|
||||
|
|
@ -35,6 +34,11 @@ describe('ConversationNamingService', () => {
|
|||
service = new ConversationNamingService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
});
|
||||
|
||||
describe('Constructor and Dependencies', () => {
|
||||
it('should initialize with dependencies', () => {
|
||||
expect(service).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { StatusBarService } from '../../Services/StatusBarService';
|
||||
import { RegisterSingleton } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
|
||||
describe('StatusBarService', () => {
|
||||
|
|
@ -49,6 +49,16 @@ describe('StatusBarService', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up service resources (cancel animations, remove DOM elements)
|
||||
service.removeStatusBarMessage();
|
||||
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
|
||||
// Clear RAF callback references
|
||||
rafCallbacks = [];
|
||||
|
||||
// Restore all mocks
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
|
@ -77,7 +87,8 @@ describe('StatusBarService', () => {
|
|||
|
||||
// Should only create once
|
||||
expect(mockPlugin.addStatusBarItem).toHaveBeenCalledTimes(1);
|
||||
expect(mockStatusBarItem.empty).toHaveBeenCalledTimes(2);
|
||||
// empty() is called: createStatusBarMessage(1) + setStatusBarMessage first call(2) + second call(3) = 3 times
|
||||
expect(mockStatusBarItem.empty).toHaveBeenCalledTimes(3);
|
||||
expect(mockStatusBarItem.createEl).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
|
|
@ -156,19 +167,25 @@ describe('StatusBarService', () => {
|
|||
it('should complete animation after 1000ms', () => {
|
||||
service.animateTokens(100, 200);
|
||||
|
||||
// Advance through animation
|
||||
// Simulate animation frames until completion
|
||||
currentTime = 0;
|
||||
let frameIndex = 0;
|
||||
while (frameIndex < rafCallbacks.length && currentTime <= 1000) {
|
||||
rafCallbacks[frameIndex](currentTime);
|
||||
frameIndex++;
|
||||
let iterations = 0;
|
||||
const maxIterations = 100; // Safety limit
|
||||
|
||||
while (currentTime < 1000 && iterations < maxIterations) {
|
||||
const callbackIndex = rafCallbacks.length - 1; // Get most recent callback
|
||||
if (callbackIndex >= 0) {
|
||||
rafCallbacks[callbackIndex](currentTime);
|
||||
}
|
||||
currentTime += 16; // ~60fps
|
||||
iterations++;
|
||||
}
|
||||
|
||||
// Run final frame at exactly 1000ms
|
||||
// Run final frame at exactly 1000ms to ensure completion
|
||||
currentTime = 1000;
|
||||
if (frameIndex < rafCallbacks.length) {
|
||||
rafCallbacks[frameIndex](currentTime);
|
||||
const finalCallbackIndex = rafCallbacks.length - 1;
|
||||
if (finalCallbackIndex >= 0) {
|
||||
rafCallbacks[finalCallbackIndex](currentTime);
|
||||
}
|
||||
|
||||
// Should reach target values
|
||||
|
|
@ -272,15 +289,11 @@ describe('StatusBarService', () => {
|
|||
// Should cancel previous animations
|
||||
expect(global.cancelAnimationFrame).toHaveBeenCalled();
|
||||
|
||||
// Should have latest animation running
|
||||
// Run the latest animation to completion
|
||||
currentTime = 1000;
|
||||
const lastIndex = rafCallbacks.length - 1;
|
||||
let frameIndex = 0;
|
||||
while (frameIndex <= lastIndex && currentTime >= 0) {
|
||||
if (rafCallbacks[frameIndex]) {
|
||||
rafCallbacks[frameIndex](currentTime);
|
||||
}
|
||||
frameIndex++;
|
||||
const latestCallback = rafCallbacks[rafCallbacks.length - 1];
|
||||
if (latestCallback) {
|
||||
latestCallback(currentTime);
|
||||
}
|
||||
|
||||
const lastCall = mockStatusBarItem.createEl.mock.calls[mockStatusBarItem.createEl.mock.calls.length - 1];
|
||||
|
|
@ -289,24 +302,51 @@ describe('StatusBarService', () => {
|
|||
});
|
||||
|
||||
it('should maintain state between animations', () => {
|
||||
// First animation
|
||||
// First animation - run to completion
|
||||
service.animateTokens(100, 200);
|
||||
currentTime = 1000;
|
||||
let frameIndex = 0;
|
||||
while (frameIndex < rafCallbacks.length) {
|
||||
rafCallbacks[frameIndex](currentTime);
|
||||
frameIndex++;
|
||||
|
||||
// Simulate animation to completion
|
||||
currentTime = 0;
|
||||
const startTime = currentTime;
|
||||
let iterations = 0;
|
||||
while (currentTime - startTime < 1000 && iterations < 100) {
|
||||
const callbackIndex = rafCallbacks.length - 1;
|
||||
if (callbackIndex >= 0) {
|
||||
rafCallbacks[callbackIndex](currentTime);
|
||||
}
|
||||
currentTime += 16;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
// Final frame at exactly 1000ms
|
||||
currentTime = startTime + 1000;
|
||||
const finalIndex = rafCallbacks.length - 1;
|
||||
if (finalIndex >= 0) {
|
||||
rafCallbacks[finalIndex](currentTime);
|
||||
}
|
||||
|
||||
// Second animation should start from previous end values
|
||||
rafCallbacks = []; // Reset RAF tracking
|
||||
service.animateTokens(200, 400);
|
||||
|
||||
currentTime = 1000;
|
||||
frameIndex = 0;
|
||||
while (frameIndex < rafCallbacks.length) {
|
||||
rafCallbacks[frameIndex](currentTime);
|
||||
frameIndex++;
|
||||
// Simulate second animation to completion
|
||||
currentTime = performance.now();
|
||||
const secondStartTime = currentTime;
|
||||
iterations = 0;
|
||||
while (currentTime - secondStartTime < 1000 && iterations < 100) {
|
||||
const callbackIndex = rafCallbacks.length - 1;
|
||||
if (callbackIndex >= 0) {
|
||||
rafCallbacks[callbackIndex](currentTime);
|
||||
}
|
||||
currentTime += 16;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
// Final frame at exactly 1000ms
|
||||
currentTime = secondStartTime + 1000;
|
||||
const secondFinalIndex = rafCallbacks.length - 1;
|
||||
if (secondFinalIndex >= 0) {
|
||||
rafCallbacks[secondFinalIndex](currentTime);
|
||||
}
|
||||
|
||||
const lastCall = mockStatusBarItem.createEl.mock.calls[mockStatusBarItem.createEl.mock.calls.length - 1];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { UserInputService } from '../../Services/UserInputService';
|
||||
import { SearchTrigger } from '../../Enums/SearchTrigger';
|
||||
import { RegisterSingleton } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { writable, get } from 'svelte/store';
|
||||
import type { ISearchState } from '../../Stores/SearchStateStore';
|
||||
|
|
@ -53,6 +53,8 @@ describe('UserInputService', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|||
import { VaultCacheService } from '../../Services/VaultCacheService';
|
||||
import { VaultService } from '../../Services/VaultService';
|
||||
import { TFile, TFolder, MetadataCache } from 'obsidian';
|
||||
import { RegisterSingleton } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { FileEvent } from '../../Enums/FileEvent';
|
||||
|
||||
|
|
@ -108,6 +108,8 @@ describe('VaultCacheService - Integration Tests', () => {
|
|||
let fileEventHandler: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ 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 } from '../../Services/DependencyService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
import { SanitiserService } from '../../Services/SanitiserService';
|
||||
|
||||
|
|
@ -99,6 +99,8 @@ describe('VaultService - Integration Tests', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue