/**
* File Watcher Bug Prevention Tests
*
* Simple, focused tests that verify the specific issues that caused
* the file watcher bug cannot happen again.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestUtils } from '../helpers/test-utils.js';
import { setupDependencyInjection } from '../../src/di/setup.js';
import { SERVICE_TOKENS } from '../../src/di/interfaces.js';
import fs from 'fs/promises';
import path from 'path';
describe('File Watcher Bug Prevention', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await TestUtils.createTempDir('bug-prevention-test-');
});
afterEach(async () => {
await TestUtils.cleanupTempDir(tempDir);
});
describe('1. Async Resolution Pattern', () => {
it('should have resolveAsync method available on container', async () => {
const container = setupDependencyInjection();
// The bug was using container.resolve() instead of container.resolveAsync()
expect(typeof container.resolveAsync).toBe('function');
expect(typeof container.resolve).toBe('function');
// These methods should be different
expect(container.resolveAsync).not.toBe(container.resolve);
});
it('should register services as async-resolvable', async () => {
const container = setupDependencyInjection();
// Check that critical service tokens exist
expect(SERVICE_TOKENS.MONITORING_WORKFLOW).toBeDefined();
expect(SERVICE_TOKENS.INDEXING_WORKFLOW).toBeDefined();
// These should be symbols (which they are)
expect(typeof SERVICE_TOKENS.MONITORING_WORKFLOW).toBe('symbol');
expect(typeof SERVICE_TOKENS.INDEXING_WORKFLOW).toBe('symbol');
});
});
describe('2. Server Startup Sequence', () => {
it('should have required service tokens for startup sequence', () => {
// The bug was missing these critical service registrations
expect(SERVICE_TOKENS.MONITORING_WORKFLOW).toBeDefined();
expect(SERVICE_TOKENS.INDEXING_WORKFLOW).toBeDefined();
// Verify they are unique symbols
expect(SERVICE_TOKENS.MONITORING_WORKFLOW).not.toBe(SERVICE_TOKENS.INDEXING_WORKFLOW);
});
it('should provide container setup function', () => {
// The startup sequence requires this function
expect(typeof setupDependencyInjection).toBe('function');
const container = setupDependencyInjection();
expect(container).toBeDefined();
expect(typeof container.resolveAsync).toBe('function');
});
});
describe('3. File System Integration', () => {
it('should handle file system operations without crashing', async () => {
// Create test files to verify file system integration works
const testFile = path.join(tempDir, 'test.txt');
// These operations should not throw
await expect(fs.writeFile(testFile, 'test content')).resolves.not.toThrow();
await expect(fs.readFile(testFile, 'utf8')).resolves.toBe('test content');
await expect(fs.unlink(testFile)).resolves.not.toThrow();
});
it('should handle invalid paths gracefully', async () => {
const invalidPath = '/invalid/nonexistent/path/file.txt';
// Should handle errors gracefully, not crash
await expect(fs.writeFile(invalidPath, 'test')).rejects.toThrow();
await expect(fs.readFile(invalidPath)).rejects.toThrow();
});
});
describe('4. Configuration Validation', () => {
it('should validate file watching configuration structure', () => {
// The bug was related to improper configuration
const validConfig = {
includeFileTypes: ['.txt', '.md', '.pdf'],
excludePatterns: ['node_modules', '.git'],
debounceMs: 1000,
enableBatchProcessing: true,
batchSize: 10
};
// Configuration should have expected structure
expect(Array.isArray(validConfig.includeFileTypes)).toBe(true);
expect(Array.isArray(validConfig.excludePatterns)).toBe(true);
expect(typeof validConfig.debounceMs).toBe('number');
expect(typeof validConfig.enableBatchProcessing).toBe('boolean');
expect(typeof validConfig.batchSize).toBe('number');
// Validate configuration values
expect(validConfig.debounceMs).toBeGreaterThan(0);
expect(validConfig.batchSize).toBeGreaterThan(0);
});
it('should validate folder path requirements', () => {
// The bug was related to missing folder path validation
const validatePath = (argv: string[]) => {
const folderPath = argv[2];
if (!folderPath) {
throw new Error('Folder path is required');
}
return folderPath;
};
// Should require folder path
expect(() => validatePath(['node', 'script.js'])).toThrow('Folder path is required');
expect(() => validatePath(['node', 'script.js', ''])).toThrow('Folder path is required');
// Should accept valid path
expect(() => validatePath(['node', 'script.js', tempDir])).not.toThrow();
expect(validatePath(['node', 'script.js', tempDir])).toBe(tempDir);
});
});
describe('5. Error Handling Patterns', () => {
it('should handle async errors without crashing', async () => {
// Test the error handling pattern used in the fix
const mockAsyncOperation = async () => {
throw new Error('Simulated async error');
};
let errorCaught = false;
let operationCompleted = false;
// This is the pattern used in the server startup fix
try {
await mockAsyncOperation();
operationCompleted = true;
} catch (error) {
console.warn('Operation failed (non-critical):', error);
errorCaught = true;
// Don't fail the entire startup if this fails
}
expect(errorCaught).toBe(true);
expect(operationCompleted).toBe(false);
// Test should complete successfully despite the error
});
it('should handle service resolution failures gracefully', async () => {
// Test the pattern for handling DI resolution failures
const mockContainer = {
resolveAsync: async (token: any) => {
throw new Error(`Service not found: ${String(token)}`);
}
};
let serviceResolved = false;
let errorHandled = false;
try {
await mockContainer.resolveAsync('nonexistent-service');
serviceResolved = true;
} catch (error) {
console.warn('Service resolution failed (expected):', error);
errorHandled = true;
}
expect(serviceResolved).toBe(false);
expect(errorHandled).toBe(true);
});
});
describe('6. Critical Timing and Sequences', () => {
it('should handle timing delays properly', async () => {
// Test debouncing and timing patterns
const events: number[] = [];
const debounceMs = 100;
// Simulate file events
events.push(Date.now());
await TestUtils.sleep(50); // Less than debounce
events.push(Date.now());
await TestUtils.sleep(debounceMs + 10); // Greater than debounce
events.push(Date.now());
expect(events).toHaveLength(3);
expect(events[2]! - events[0]!).toBeGreaterThan(debounceMs);
});
it('should handle cleanup sequences properly', async () => {
// Test proper cleanup sequence
const resources: string[] = [];
const cleanup = async () => {
// Simulate stopping file watchers
resources.push('file-watcher-stopped');
// Simulate other cleanup
resources.push('transport-stopped');
resources.push('container-cleared');
};
await cleanup();
expect(resources).toEqual([
'file-watcher-stopped',
'transport-stopped',
'container-cleared'
]);
});
});
describe('7. Real File Operations', () => {
it('should detect file creation', async () => {
const testFile = path.join(tempDir, 'created-file.txt');
const content = 'File created for testing';
await fs.writeFile(testFile, content);
// Verify file exists and has correct content
const exists = await fs.access(testFile).then(() => true).catch(() => false);
expect(exists).toBe(true);
const readContent = await fs.readFile(testFile, 'utf8');
expect(readContent).toBe(content);
});
it('should detect file modifications', async () => {
const testFile = path.join(tempDir, 'modified-file.txt');
const originalContent = 'Original content';
const modifiedContent = 'Modified content';
// Create file
await fs.writeFile(testFile, originalContent);
let content = await fs.readFile(testFile, 'utf8');
expect(content).toBe(originalContent);
// Modify file
await fs.writeFile(testFile, modifiedContent);
content = await fs.readFile(testFile, 'utf8');
expect(content).toBe(modifiedContent);
});
it('should detect file deletion', async () => {
const testFile = path.join(tempDir, 'deleted-file.txt');
// Create file
await fs.writeFile(testFile, 'To be deleted');
// Verify exists
let exists = await fs.access(testFile).then(() => true).catch(() => false);
expect(exists).toBe(true);
// Delete file
await fs.unlink(testFile);
// Verify deleted
exists = await fs.access(testFile).then(() => true).catch(() => false);
expect(exists).toBe(false);
});
});
});