Skip to main content
Glama
HooksService.real-logic.test.ts13.5 kB
/** * Integration test to verify real business logic execution after dependency injection refactoring * This test demonstrates that service logic is actually running, not being intercepted by mocks */ import { HooksService } from '../services/HooksService'; import { HookEvent, HookRequest } from '../types/hooks.types'; import { FileSystemAdapter, PathAdapter, WatcherFactory, WatcherInstance } from '../interfaces/dependencies'; // Controlled test doubles that verify real logic execution class ControlledFileSystem implements FileSystemAdapter { public calls: { method: string; args: any[] }[] = []; existsSync(path: string): boolean { this.calls.push({ method: 'existsSync', args: [path] }); return true; // Allow operation to continue } writeFileSync(path: string, data: string): void { this.calls.push({ method: 'writeFileSync', args: [path, data] }); } appendFileSync(path: string, data: string): void { this.calls.push({ method: 'appendFileSync', args: [path, data] }); } mkdirSync(path: string, options?: { recursive?: boolean }): void { this.calls.push({ method: 'mkdirSync', args: [path, options] }); } chmodSync(path: string, mode: number): void { this.calls.push({ method: 'chmodSync', args: [path, mode] }); } reset(): void { this.calls = []; } } class ControlledPathAdapter implements PathAdapter { public calls: { method: string; args: any[] }[] = []; join(...paths: string[]): string { this.calls.push({ method: 'join', args: paths }); return paths.join('/'); } resolve(...paths: string[]): string { this.calls.push({ method: 'resolve', args: paths }); return paths.join('/'); } dirname(filePath: string): string { this.calls.push({ method: 'dirname', args: [filePath] }); return filePath.split('/').slice(0, -1).join('/'); } reset(): void { this.calls = []; } } class ControlledWatcher implements WatcherInstance { public handlers: Map<string, Function[]> = new Map(); on(event: string, listener: (...args: any[]) => void): this { if (!this.handlers.has(event)) { this.handlers.set(event, []); } this.handlers.get(event)!.push(listener); return this; } async close(): Promise<void> { // Cleanup logic would go here } // Test helper to trigger events trigger(event: string, ...args: any[]): void { const listeners = this.handlers.get(event) || []; listeners.forEach(listener => listener(...args)); } } class ControlledWatcherFactory implements WatcherFactory { public lastWatcher: ControlledWatcher | null = null; public calls: { method: string; args: any[] }[] = []; watch(patterns: string | string[], options?: any): WatcherInstance { this.calls.push({ method: 'watch', args: [patterns, options] }); this.lastWatcher = new ControlledWatcher(); return this.lastWatcher; } reset(): void { this.calls = []; this.lastWatcher = null; } } describe('HooksService Real Logic Execution', () => { let service: HooksService; let fileSystem: ControlledFileSystem; let pathAdapter: ControlledPathAdapter; let watcherFactory: ControlledWatcherFactory; beforeEach(() => { fileSystem = new ControlledFileSystem(); pathAdapter = new ControlledPathAdapter(); watcherFactory = new ControlledWatcherFactory(); service = new HooksService('/test/project', { fileSystem, pathAdapter, watcherFactory }); }); afterEach(() => { fileSystem.reset(); pathAdapter.reset(); watcherFactory.reset(); }); describe('Real Business Logic Verification', () => { it('should execute real recommendation generation logic for pre-work events', async () => { const request: HookRequest = { event: { type: 'pre-work', data: { files: ['src/api/users.ts', 'src/components/Button.test.tsx'], context: 'Adding security features' }, timestamp: new Date().toISOString() } }; const response = await service.processHookRequest(request); // Verify real business logic executed - recommendations generated based on file patterns expect(response.success).toBe(true); expect(response.data?.recommendations).toEqual( expect.arrayContaining([ 'Consider reviewing TypeScript best practices documentation', 'Review testing guidelines and coverage requirements', 'Check API documentation and security guidelines', 'Review security documentation and run security checks' ]) ); // Verify this is real logic, not mocked - specific recommendations based on file patterns and context const tsRecommendation = response.data?.recommendations.find((r: string) => r.includes('TypeScript best practices') ); const testRecommendation = response.data?.recommendations.find((r: string) => r.includes('testing guidelines') ); const apiRecommendation = response.data?.recommendations.find((r: string) => r.includes('API documentation') ); const securityRecommendation = response.data?.recommendations.find((r: string) => r.includes('security documentation') ); expect(tsRecommendation).toBeDefined(); // Because file contains .ts expect(testRecommendation).toBeDefined(); // Because file contains 'test' expect(apiRecommendation).toBeDefined(); // Because file contains 'api' expect(securityRecommendation).toBeDefined(); // Because context contains 'security' }); it('should execute real validation logic for post-work events', async () => { const request: HookRequest = { event: { type: 'post-work', data: { files: ['src/utils/helpers.ts', 'src/utils/helpers.test.ts', 'docs/README.md'], changes: 'Added utility functions with comprehensive tests', context: 'Utility enhancement' }, timestamp: new Date().toISOString() } }; const response = await service.processHookRequest(request); // Verify real validation logic executed expect(response.success).toBe(true); expect(response.data?.validations).toBeDefined(); const validations = response.data?.validations; // Real logic should generate specific validations based on file types const lintValidation = validations.find((v: any) => v.type === 'lint'); const testValidation = validations.find((v: any) => v.type === 'test'); const docValidation = validations.find((v: any) => v.type === 'documentation'); expect(lintValidation).toBeDefined(); // Because TypeScript files are present expect(lintValidation.message).toContain('TypeScript files need linting'); expect(testValidation).toBeDefined(); // Because test files are present expect(testValidation.message).toContain('test files modified'); expect(docValidation).toBeDefined(); // Because .md files are present expect(docValidation.status).toBe('completed'); }); it('should execute real significance analysis for file-change events', async () => { const testCases = [ { filePath: 'package.json', expected: true, reason: 'configuration file' }, { filePath: 'src/api/routes.ts', expected: true, reason: 'API file' }, { filePath: 'prisma/schema.prisma', expected: true, reason: 'database schema' }, { filePath: 'docs/CHANGELOG.md', expected: false, reason: 'documentation file' } ]; for (const testCase of testCases) { const request: HookRequest = { event: { type: 'file-change', data: { filePath: testCase.filePath, changeType: 'change' }, timestamp: new Date().toISOString() } }; const response = await service.processHookRequest(request); expect(response.success).toBe(true); expect(response.data?.significant).toBe(testCase.expected); // Verify this is real business logic - significance determined by actual file patterns console.log(`File: ${testCase.filePath}, Significant: ${response.data?.significant}, Reason: ${testCase.reason}`); } }); it('should execute real session summary generation logic', async () => { const startTime = new Date(Date.now() - 120000); // 2 minutes ago const request: HookRequest = { event: { type: 'session-end', data: { sessionId: 'test-session-123', startTime: startTime.toISOString(), filesModified: ['file1.ts', 'file2.js', 'file3.md'] }, timestamp: new Date().toISOString() } }; const response = await service.processHookRequest(request); expect(response.success).toBe(true); expect(response.data?.summary).toBeDefined(); const summary = response.data?.summary; // Verify real calculation logic executed expect(summary.duration).toBeGreaterThanOrEqual(1); // Should be ~2 minutes expect(summary.duration).toBeLessThanOrEqual(3); // Allow for test execution time expect(summary.filesModified).toBe(3); // Actual count from data expect(summary.eventsProcessed).toBeGreaterThanOrEqual(0); // Real event counting }); it('should execute real git hook generation logic', async () => { // Reset file system to capture calls fileSystem.reset(); const result = await service.setupGitHooks(); expect(result).toBe(true); // Verify real business logic executed - actual file operations called const writeFilesSyncCalls = fileSystem.calls.filter(call => call.method === 'writeFileSync'); expect(writeFilesSyncCalls).toHaveLength(2); // Verify real git hook content generation const preCommitCall = writeFilesSyncCalls.find(call => call.args[0].includes('pre-commit') ); const postCommitCall = writeFilesSyncCalls.find(call => call.args[0].includes('post-commit') ); expect(preCommitCall).toBeDefined(); expect(postCommitCall).toBeDefined(); // Verify actual hook content is generated (not mocked) expect(preCommitCall?.args[1]).toContain('CastPlan Automation Pre-commit Hook'); expect(preCommitCall?.args[1]).toContain('changed_files=$(git diff --cached --name-only)'); expect(postCommitCall?.args[1]).toContain('CastPlan Automation Post-commit Hook'); expect(postCommitCall?.args[1]).toContain('changed_files=$(git diff-tree --no-commit-id --name-only -r HEAD)'); }); it('should execute real file watching setup with dependency injection', async () => { watcherFactory.reset(); await service.startFileWatching(['**/*.custom']); // Verify real watcher factory was called with real configuration expect(watcherFactory.calls).toHaveLength(1); expect(watcherFactory.calls[0].method).toBe('watch'); expect(watcherFactory.calls[0].args[0]).toEqual(['**/*.custom']); const watchOptions = watcherFactory.calls[0].args[1]; expect(watchOptions.persistent).toBe(true); expect(watchOptions.ignoreInitial).toBe(true); expect(watchOptions.cwd).toBe('/test/project'); // Verify real event handlers are registered const watcher = watcherFactory.lastWatcher!; expect(watcher.handlers.has('add')).toBe(true); expect(watcher.handlers.has('change')).toBe(true); expect(watcher.handlers.has('unlink')).toBe(true); expect(watcher.handlers.has('error')).toBe(true); // Verify real event handling by triggering file system event const emitSpy = jest.spyOn(service, 'emit'); // Trigger a file system event through the real handler watcher.trigger('add', 'src/new-file.ts'); // Verify real event processing occurred expect(emitSpy).toHaveBeenCalledWith('file-change', expect.objectContaining({ filePath: '/test/project/src/new-file.ts', // Real path resolution changeType: 'add' })); }); }); describe('Coverage Verification', () => { it('should execute file notification logic with real path operations', async () => { pathAdapter.reset(); fileSystem.reset(); const request: HookRequest = { event: { type: 'pre-work', data: { files: ['test.ts'], context: 'test' }, timestamp: new Date().toISOString() }, config: { notifications: { enabled: true, channels: ['file' as 'file'], logFile: '/custom/logs/hooks.log' } } }; await service.processHookRequest(request); // Verify real path operations executed const dirnameCalls = pathAdapter.calls.filter(call => call.method === 'dirname'); expect(dirnameCalls.length).toBeGreaterThan(0); // Verify real file operations executed const appendCalls = fileSystem.calls.filter(call => call.method === 'appendFileSync'); expect(appendCalls).toHaveLength(1); expect(appendCalls[0].args[0]).toBe('/custom/logs/hooks.log'); expect(appendCalls[0].args[1]).toContain('[PRE-WORK]'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ghostseller/CastPlan_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server