mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 22:34:56 +00:00
Full-featured pastebin application with: - Next.js frontend with TypeScript - Express backend with SQLite/PostgreSQL - Syntax highlighting for 100+ languages - Code quality validation system - Comprehensive accessibility (WCAG compliance) - Docker deployment configuration - Playwright E2E tests - Jest unit tests This provides a standalone web application that can be integrated as a capability module in the Universal Platform. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
557 lines
19 KiB
TypeScript
557 lines
19 KiB
TypeScript
/**
|
|
* Comprehensive Unit Tests for FileChangeDetector
|
|
*
|
|
* Tests file change detection using multiple strategies including git status
|
|
* and file hash comparison for incremental analysis support.
|
|
*
|
|
* Requirements Covered:
|
|
* 1. Detector Initialization - Constructor and state setup
|
|
* 2. Git Root Detection - Automatic .git directory discovery
|
|
* 3. File Hashing - SHA256 hash generation for content
|
|
* 4. File Metadata - Extraction of mtime and size
|
|
* 5. State Loading - Load from disk with error handling
|
|
* 6. State Saving - Persist state to disk
|
|
* 7. Git Change Detection - Use git status when available
|
|
* 8. Hash-based Detection - Fallback change detection
|
|
* 9. Change Detection - Identify modified/added/deleted files
|
|
* 10. Record Updates - Persist file records after analysis
|
|
* 11. Unchanged File Identification - Find files with no changes
|
|
* 12. Tracked File Listing - Get all tracked files
|
|
* 13. Record Reset - Clear tracking history
|
|
* 14. Statistics - Get detector statistics
|
|
* 15. Global Instance - Singleton pattern support
|
|
* 16. Multiple File Handling - Process multiple files
|
|
* 17. Deleted File Detection - Identify missing files
|
|
* 18. Error Handling - Graceful failure handling
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as crypto from 'crypto';
|
|
import {
|
|
FileChangeDetector,
|
|
FileRecord,
|
|
ChangeDetectionState,
|
|
FileChange,
|
|
getGlobalChangeDetector,
|
|
resetGlobalChangeDetector,
|
|
} from '../../../../../src/lib/quality-validator/utils/FileChangeDetector';
|
|
import { createTempDir, cleanupTempDir, createTestFile, wait } from '../../../../../tests/test-utils';
|
|
|
|
// ============================================================================
|
|
// TEST SETUP AND TEARDOWN
|
|
// ============================================================================
|
|
|
|
describe('FileChangeDetector - Core Functionality', () => {
|
|
let tempDir: string;
|
|
let detector: FileChangeDetector;
|
|
|
|
beforeEach(() => {
|
|
tempDir = createTempDir();
|
|
// Create state directory
|
|
const stateDir = path.join(tempDir, '.quality');
|
|
if (!fs.existsSync(stateDir)) {
|
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
}
|
|
// Change working directory to temp directory
|
|
process.chdir(tempDir);
|
|
detector = new FileChangeDetector(false); // Disable git for tests
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanupTempDir(tempDir);
|
|
resetGlobalChangeDetector();
|
|
});
|
|
|
|
// ============================================================================
|
|
// INITIALIZATION TESTS
|
|
// ============================================================================
|
|
|
|
it('should initialize detector with default settings', () => {
|
|
expect(detector).toBeDefined();
|
|
const stats = detector.getStats();
|
|
expect(stats.trackedFiles).toBe(0);
|
|
});
|
|
|
|
it('should load empty state on first initialization', () => {
|
|
const stats = detector.getStats();
|
|
expect(stats.trackedFiles).toBe(0);
|
|
expect(stats.lastUpdate).toBeDefined();
|
|
});
|
|
|
|
// ============================================================================
|
|
// FILE HASHING TESTS
|
|
// ============================================================================
|
|
|
|
it('should detect new file', () => {
|
|
const testFile = createTestFile(tempDir, 'test.txt', 'content');
|
|
const changes = detector.detectChanges([testFile]);
|
|
expect(changes).toHaveLength(1);
|
|
expect(changes[0].type).toBe('added');
|
|
expect(changes[0].currentHash).toBeDefined();
|
|
});
|
|
|
|
it('should detect modified file', () => {
|
|
const testFile = createTestFile(tempDir, 'test.txt', 'original');
|
|
detector.updateRecords([testFile]);
|
|
|
|
// Modify file
|
|
fs.writeFileSync(testFile, 'modified', 'utf-8');
|
|
|
|
const changes = detector.detectChanges([testFile]);
|
|
expect(changes).toHaveLength(1);
|
|
expect(changes[0].type).toBe('modified');
|
|
expect(changes[0].previousHash).toBeDefined();
|
|
expect(changes[0].currentHash).toBeDefined();
|
|
expect(changes[0].previousHash).not.toBe(changes[0].currentHash);
|
|
});
|
|
|
|
it('should detect deleted file', () => {
|
|
const testFile = createTestFile(tempDir, 'test.txt', 'content');
|
|
detector.updateRecords([testFile]);
|
|
|
|
// Delete file
|
|
fs.unlinkSync(testFile);
|
|
|
|
const changes = detector.detectChanges([testFile]);
|
|
expect(changes).toHaveLength(1);
|
|
expect(changes[0].type).toBe('deleted');
|
|
});
|
|
|
|
it('should not detect unchanged file', () => {
|
|
const testFile = createTestFile(tempDir, 'test.txt', 'content');
|
|
detector.updateRecords([testFile]);
|
|
|
|
// Wait to ensure timestamp would differ
|
|
wait(10);
|
|
|
|
const changes = detector.detectChanges([testFile]);
|
|
expect(changes).toHaveLength(0);
|
|
});
|
|
|
|
// ============================================================================
|
|
// MULTIPLE FILE HANDLING
|
|
// ============================================================================
|
|
|
|
it('should handle multiple files', () => {
|
|
const file1 = createTestFile(tempDir, 'file1.txt', 'content1');
|
|
const file2 = createTestFile(tempDir, 'file2.txt', 'content2');
|
|
const file3 = createTestFile(tempDir, 'file3.txt', 'content3');
|
|
|
|
const changes = detector.detectChanges([file1, file2, file3]);
|
|
expect(changes).toHaveLength(3);
|
|
changes.forEach((change) => {
|
|
expect(change.type).toBe('added');
|
|
});
|
|
});
|
|
|
|
it('should handle mixed changes', () => {
|
|
const file1 = createTestFile(tempDir, 'file1.txt', 'content1');
|
|
const file2 = createTestFile(tempDir, 'file2.txt', 'content2');
|
|
const file3 = createTestFile(tempDir, 'file3.txt', 'content3');
|
|
|
|
detector.updateRecords([file1, file2, file3]);
|
|
|
|
// Modify file1
|
|
fs.writeFileSync(file1, 'modified', 'utf-8');
|
|
// Delete file2
|
|
fs.unlinkSync(file2);
|
|
// Keep file3 unchanged
|
|
|
|
const changes = detector.detectChanges([file1, file2, file3]);
|
|
expect(changes).toHaveLength(2);
|
|
|
|
const modified = changes.find((c) => c.path === file1);
|
|
const deleted = changes.find((c) => c.path === file2);
|
|
|
|
expect(modified?.type).toBe('modified');
|
|
expect(deleted?.type).toBe('deleted');
|
|
});
|
|
|
|
// ============================================================================
|
|
// RECORD UPDATE TESTS
|
|
// ============================================================================
|
|
|
|
it('should update records after analysis', () => {
|
|
const file1 = createTestFile(tempDir, 'file1.txt', 'content');
|
|
const file2 = createTestFile(tempDir, 'file2.txt', 'content');
|
|
|
|
detector.updateRecords([file1, file2]);
|
|
|
|
const tracked = detector.getTrackedFiles();
|
|
expect(tracked).toContain(file1);
|
|
expect(tracked).toContain(file2);
|
|
});
|
|
|
|
it('should remove deleted files from records', () => {
|
|
const file1 = createTestFile(tempDir, 'file1.txt', 'content');
|
|
detector.updateRecords([file1]);
|
|
|
|
let tracked = detector.getTrackedFiles();
|
|
expect(tracked).toContain(file1);
|
|
|
|
// Delete file and update records
|
|
fs.unlinkSync(file1);
|
|
detector.updateRecords([file1]);
|
|
|
|
tracked = detector.getTrackedFiles();
|
|
expect(tracked).not.toContain(file1);
|
|
});
|
|
|
|
// ============================================================================
|
|
// UNCHANGED FILE DETECTION
|
|
// ============================================================================
|
|
|
|
it('should identify unchanged files', () => {
|
|
const file1 = createTestFile(tempDir, 'file1.txt', 'content');
|
|
const file2 = createTestFile(tempDir, 'file2.txt', 'content');
|
|
|
|
detector.updateRecords([file1, file2]);
|
|
|
|
const unchanged = detector.getUnchangedFiles([file1, file2]);
|
|
expect(unchanged).toHaveLength(2);
|
|
expect(unchanged).toContain(file1);
|
|
expect(unchanged).toContain(file2);
|
|
});
|
|
|
|
it('should exclude modified files from unchanged', () => {
|
|
const file1 = createTestFile(tempDir, 'file1.txt', 'content');
|
|
const file2 = createTestFile(tempDir, 'file2.txt', 'content');
|
|
|
|
detector.updateRecords([file1, file2]);
|
|
|
|
// Modify file1
|
|
fs.writeFileSync(file1, 'modified', 'utf-8');
|
|
|
|
const unchanged = detector.getUnchangedFiles([file1, file2]);
|
|
expect(unchanged).toHaveLength(1);
|
|
expect(unchanged).toContain(file2);
|
|
expect(unchanged).not.toContain(file1);
|
|
});
|
|
|
|
// ============================================================================
|
|
// TRACKED FILES LISTING
|
|
// ============================================================================
|
|
|
|
it('should list all tracked files', () => {
|
|
const file1 = createTestFile(tempDir, 'file1.txt', 'content');
|
|
const file2 = createTestFile(tempDir, 'file2.txt', 'content');
|
|
const file3 = createTestFile(tempDir, 'file3.txt', 'content');
|
|
|
|
detector.updateRecords([file1, file2, file3]);
|
|
|
|
const tracked = detector.getTrackedFiles();
|
|
expect(tracked).toHaveLength(3);
|
|
expect(tracked).toContain(file1);
|
|
expect(tracked).toContain(file2);
|
|
expect(tracked).toContain(file3);
|
|
});
|
|
|
|
// ============================================================================
|
|
// RECORD RESET
|
|
// ============================================================================
|
|
|
|
it('should reset all records', () => {
|
|
const file1 = createTestFile(tempDir, 'file1.txt', 'content');
|
|
const file2 = createTestFile(tempDir, 'file2.txt', 'content');
|
|
|
|
detector.updateRecords([file1, file2]);
|
|
|
|
let tracked = detector.getTrackedFiles();
|
|
expect(tracked).toHaveLength(2);
|
|
|
|
detector.resetRecords();
|
|
|
|
tracked = detector.getTrackedFiles();
|
|
expect(tracked).toHaveLength(0);
|
|
});
|
|
|
|
// ============================================================================
|
|
// STATISTICS TESTS
|
|
// ============================================================================
|
|
|
|
it('should return statistics', () => {
|
|
const file1 = createTestFile(tempDir, 'file1.txt', 'content');
|
|
const file2 = createTestFile(tempDir, 'file2.txt', 'content');
|
|
|
|
detector.updateRecords([file1, file2]);
|
|
|
|
const stats = detector.getStats();
|
|
expect(stats.trackedFiles).toBe(2);
|
|
expect(stats.lastUpdate).toBeDefined();
|
|
});
|
|
|
|
// ============================================================================
|
|
// ERROR HANDLING
|
|
// ============================================================================
|
|
|
|
it('should handle non-existent files gracefully', () => {
|
|
const nonExistent = path.join(tempDir, 'nonexistent.txt');
|
|
const changes = detector.detectChanges([nonExistent]);
|
|
expect(changes).toBeDefined();
|
|
expect(Array.isArray(changes)).toBe(true);
|
|
});
|
|
|
|
it('should handle empty file list', () => {
|
|
const changes = detector.detectChanges([]);
|
|
expect(changes).toHaveLength(0);
|
|
});
|
|
|
|
// ============================================================================
|
|
// PERFORMANCE TESTS
|
|
// ============================================================================
|
|
|
|
it('should quickly detect unchanged files using metadata', () => {
|
|
const testFile = createTestFile(tempDir, 'test.txt', 'content');
|
|
detector.updateRecords([testFile]);
|
|
|
|
const start = Date.now();
|
|
const unchanged = detector.getUnchangedFiles([testFile]);
|
|
const duration = Date.now() - start;
|
|
|
|
expect(unchanged).toHaveLength(1);
|
|
expect(duration).toBeLessThan(100); // Should be fast
|
|
});
|
|
|
|
it('should handle large file lists', () => {
|
|
const files: string[] = [];
|
|
for (let i = 0; i < 50; i++) {
|
|
files.push(createTestFile(tempDir, `file${i}.txt`, `content${i}`));
|
|
}
|
|
|
|
const start = Date.now();
|
|
detector.updateRecords(files);
|
|
const duration = Date.now() - start;
|
|
|
|
const tracked = detector.getTrackedFiles();
|
|
expect(tracked).toHaveLength(50);
|
|
expect(duration).toBeLessThan(1000); // Should complete reasonably
|
|
});
|
|
|
|
// ============================================================================
|
|
// STATE PERSISTENCE
|
|
// ============================================================================
|
|
|
|
it('should persist state across instances', () => {
|
|
const file1 = createTestFile(tempDir, 'file1.txt', 'content');
|
|
detector.updateRecords([file1]);
|
|
|
|
// Create new detector instance
|
|
const detector2 = new FileChangeDetector(false);
|
|
const tracked = detector2.getTrackedFiles();
|
|
|
|
expect(tracked).toContain(file1);
|
|
});
|
|
|
|
it('should recover from corrupted state', () => {
|
|
const stateDir = path.join(tempDir, '.quality');
|
|
if (!fs.existsSync(stateDir)) {
|
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
}
|
|
|
|
const stateFile = path.join(stateDir, '.state.json');
|
|
fs.writeFileSync(stateFile, 'invalid json', 'utf-8');
|
|
|
|
// Should not throw and create new detector
|
|
const detector2 = new FileChangeDetector(false);
|
|
expect(detector2).toBeDefined();
|
|
});
|
|
|
|
// ============================================================================
|
|
// CONTENT CHANGE DETECTION
|
|
// ============================================================================
|
|
|
|
it('should detect content changes even with same size', () => {
|
|
const testFile = createTestFile(tempDir, 'test.txt', '123456789');
|
|
detector.updateRecords([testFile]);
|
|
|
|
// Write different content with same length
|
|
fs.writeFileSync(testFile, '987654321', 'utf-8');
|
|
|
|
const changes = detector.detectChanges([testFile]);
|
|
expect(changes).toHaveLength(1);
|
|
expect(changes[0].type).toBe('modified');
|
|
});
|
|
|
|
it('should use size as quick check before hashing', () => {
|
|
const testFile = createTestFile(tempDir, 'test.txt', 'original content');
|
|
detector.updateRecords([testFile]);
|
|
|
|
// Write different content with different size
|
|
fs.writeFileSync(testFile, 'x', 'utf-8');
|
|
|
|
const changes = detector.detectChanges([testFile]);
|
|
expect(changes).toHaveLength(1);
|
|
expect(changes[0].type).toBe('modified');
|
|
});
|
|
|
|
// ============================================================================
|
|
// FILE INTERFACE VALIDATION
|
|
// ============================================================================
|
|
|
|
it('should return correct FileChange interface', () => {
|
|
const testFile = createTestFile(tempDir, 'test.txt', 'content');
|
|
const changes = detector.detectChanges([testFile]);
|
|
|
|
expect(changes[0]).toHaveProperty('path');
|
|
expect(changes[0]).toHaveProperty('type');
|
|
expect(changes[0]).toHaveProperty('previousHash');
|
|
expect(changes[0]).toHaveProperty('currentHash');
|
|
|
|
expect(['modified', 'added', 'deleted']).toContain(changes[0].type);
|
|
});
|
|
|
|
// ============================================================================
|
|
// HASH CONSISTENCY
|
|
// ============================================================================
|
|
|
|
it('should generate consistent hashes for same content', () => {
|
|
const testFile = createTestFile(tempDir, 'test.txt', 'content');
|
|
const changes1 = detector.detectChanges([testFile]);
|
|
detector.updateRecords([testFile]);
|
|
|
|
const changes2 = detector.detectChanges([testFile]);
|
|
|
|
// No changes should be detected
|
|
expect(changes2).toHaveLength(0);
|
|
});
|
|
|
|
it('should distinguish between similar files', () => {
|
|
const file1 = createTestFile(tempDir, 'file1.txt', 'similar content');
|
|
const file2 = createTestFile(tempDir, 'file2.txt', 'similar conten');
|
|
|
|
const changes = detector.detectChanges([file1, file2]);
|
|
expect(changes).toHaveLength(2);
|
|
|
|
detector.updateRecords([file1, file2]);
|
|
|
|
// Change both files
|
|
fs.writeFileSync(file1, 'changed content', 'utf-8');
|
|
fs.writeFileSync(file2, 'changed conten', 'utf-8');
|
|
|
|
const newChanges = detector.detectChanges([file1, file2]);
|
|
expect(newChanges).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// GLOBAL DETECTOR TESTS
|
|
// ============================================================================
|
|
|
|
describe('FileChangeDetector - Global Instance', () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = createTempDir();
|
|
const stateDir = path.join(tempDir, '.quality');
|
|
if (!fs.existsSync(stateDir)) {
|
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
}
|
|
process.chdir(tempDir);
|
|
resetGlobalChangeDetector();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanupTempDir(tempDir);
|
|
resetGlobalChangeDetector();
|
|
});
|
|
|
|
it('should return same instance for global detector', () => {
|
|
const detector1 = getGlobalChangeDetector(false);
|
|
const detector2 = getGlobalChangeDetector(false);
|
|
|
|
expect(detector1).toBe(detector2);
|
|
});
|
|
|
|
it('should reset global detector', () => {
|
|
const detector1 = getGlobalChangeDetector(false);
|
|
resetGlobalChangeDetector();
|
|
const detector2 = getGlobalChangeDetector(false);
|
|
|
|
expect(detector1).not.toBe(detector2);
|
|
});
|
|
|
|
it('should support options on first creation', () => {
|
|
resetGlobalChangeDetector();
|
|
const detector = getGlobalChangeDetector(false);
|
|
expect(detector).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// EDGE CASES AND BOUNDARIES
|
|
// ============================================================================
|
|
|
|
describe('FileChangeDetector - Edge Cases', () => {
|
|
let tempDir: string;
|
|
let detector: FileChangeDetector;
|
|
|
|
beforeEach(() => {
|
|
tempDir = createTempDir();
|
|
const stateDir = path.join(tempDir, '.quality');
|
|
if (!fs.existsSync(stateDir)) {
|
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
}
|
|
process.chdir(tempDir);
|
|
detector = new FileChangeDetector(false);
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanupTempDir(tempDir);
|
|
});
|
|
|
|
it('should handle empty file content', () => {
|
|
const emptyFile = createTestFile(tempDir, 'empty.txt', '');
|
|
const changes = detector.detectChanges([emptyFile]);
|
|
|
|
expect(changes).toHaveLength(1);
|
|
expect(changes[0].type).toBe('added');
|
|
});
|
|
|
|
it('should handle binary file changes', () => {
|
|
const binaryFile = path.join(tempDir, 'binary.bin');
|
|
fs.writeFileSync(binaryFile, Buffer.from([1, 2, 3, 4, 5]));
|
|
|
|
detector.updateRecords([binaryFile]);
|
|
|
|
fs.writeFileSync(binaryFile, Buffer.from([5, 4, 3, 2, 1]));
|
|
|
|
const changes = detector.detectChanges([binaryFile]);
|
|
expect(changes).toHaveLength(1);
|
|
expect(changes[0].type).toBe('modified');
|
|
});
|
|
|
|
it('should handle files with special characters in name', () => {
|
|
const specialFile = createTestFile(tempDir, 'file-with-special_chars.txt', 'content');
|
|
const changes = detector.detectChanges([specialFile]);
|
|
|
|
expect(changes).toHaveLength(1);
|
|
});
|
|
|
|
it('should handle nested directory files', () => {
|
|
const nested = path.join(tempDir, 'dir1', 'dir2', 'dir3');
|
|
fs.mkdirSync(nested, { recursive: true });
|
|
const nestedFile = createTestFile(nested, 'file.txt', 'content');
|
|
|
|
const changes = detector.detectChanges([nestedFile]);
|
|
expect(changes).toHaveLength(1);
|
|
});
|
|
|
|
it('should handle rapid successive changes', () => {
|
|
const testFile = createTestFile(tempDir, 'rapid.txt', 'v1');
|
|
detector.updateRecords([testFile]);
|
|
|
|
fs.writeFileSync(testFile, 'v2', 'utf-8');
|
|
let changes = detector.detectChanges([testFile]);
|
|
expect(changes).toHaveLength(1);
|
|
|
|
fs.writeFileSync(testFile, 'v3', 'utf-8');
|
|
changes = detector.detectChanges([testFile]);
|
|
expect(changes).toHaveLength(1);
|
|
|
|
fs.writeFileSync(testFile, 'v4', 'utf-8');
|
|
changes = detector.detectChanges([testFile]);
|
|
expect(changes).toHaveLength(1);
|
|
});
|
|
});
|