/** * Integration Test Runner * * Core infrastructure for running comprehensive integration tests. * Tests run against real database functions, edge functions, and API endpoints. */ import { moderationTestSuite } from './suites/moderationTests'; import { moderationLockTestSuite } from './suites/moderationLockTests'; import { moderationDependencyTestSuite } from './suites/moderationDependencyTests'; import { approvalPipelineTestSuite } from './suites/approvalPipelineTests'; import { cleanupTestData, type CleanupSummary } from './testCleanup'; /** * Registry of all available test suites */ export const ALL_TEST_SUITES = [ moderationTestSuite, moderationLockTestSuite, moderationDependencyTestSuite, approvalPipelineTestSuite, ]; export interface TestResult { id: string; name: string; suite: string; status: 'pass' | 'fail' | 'skip' | 'running'; duration: number; // milliseconds error?: string; details?: any; timestamp: string; stack?: string; } export interface Test { id: string; name: string; description: string; run: () => Promise; } export interface TestSuite { id: string; name: string; description: string; tests: Test[]; } export class IntegrationTestRunner { private results: TestResult[] = []; private isRunning = false; private shouldStop = false; private onProgress?: (result: TestResult) => void; private delayBetweenTests: number; private cleanupEnabled: boolean; private cleanupSummary?: CleanupSummary; constructor( onProgress?: (result: TestResult) => void, delayBetweenTests: number = 8000, cleanupEnabled: boolean = true ) { this.onProgress = onProgress; this.delayBetweenTests = delayBetweenTests; // Default 8 seconds to prevent rate limiting this.cleanupEnabled = cleanupEnabled; } /** * Wait for specified milliseconds (for rate limiting prevention) */ private async delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Run a single test with error handling */ async runTest(test: Test, suiteName: string): Promise { if (this.shouldStop) { return { id: test.id, name: test.name, suite: suiteName, status: 'skip', duration: 0, timestamp: new Date().toISOString(), details: { reason: 'Test run stopped by user' } }; } // Mark as running const runningResult: TestResult = { id: test.id, name: test.name, suite: suiteName, status: 'running', duration: 0, timestamp: new Date().toISOString(), }; if (this.onProgress) { this.onProgress(runningResult); } try { const result = await test.run(); this.results.push(result); if (this.onProgress) { this.onProgress(result); } return result; } catch (error) { const failResult: TestResult = { id: test.id, name: test.name, suite: suiteName, status: 'fail', duration: 0, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, timestamp: new Date().toISOString(), }; this.results.push(failResult); if (this.onProgress) { this.onProgress(failResult); } return failResult; } } /** * Run all tests in a suite */ async runSuite(suite: TestSuite): Promise { const suiteResults: TestResult[] = []; for (let i = 0; i < suite.tests.length; i++) { const test = suite.tests[i]; const result = await this.runTest(test, suite.name); suiteResults.push(result); if (this.shouldStop) { break; } // Add delay between tests to prevent rate limiting (except after the last test) if (i < suite.tests.length - 1 && this.delayBetweenTests > 0) { // Report delay status with countdown const delaySeconds = this.delayBetweenTests / 1000; const delayResult: TestResult = { id: `delay-${Date.now()}`, name: `⏳ Rate limit delay: ${delaySeconds}s`, suite: suite.name, status: 'running', duration: 0, timestamp: new Date().toISOString(), details: { reason: 'Pausing to prevent rate limiting', delayMs: this.delayBetweenTests } }; if (this.onProgress) { this.onProgress(delayResult); } await this.delay(this.delayBetweenTests); // Mark delay as complete const delayCompleteResult: TestResult = { ...delayResult, status: 'skip', duration: this.delayBetweenTests, details: { reason: 'Rate limit delay completed' } }; if (this.onProgress) { this.onProgress(delayCompleteResult); } } } return suiteResults; } /** * Run all suites sequentially */ async runAllSuites(suites: TestSuite[]): Promise { this.results = []; this.isRunning = true; this.shouldStop = false; // Track submission-heavy suites for adaptive delays const submissionHeavySuites = [ 'Entity Submission & Validation', 'Approval Pipeline', 'Unit Conversion Tests', 'Performance & Scalability' ]; for (let i = 0; i < suites.length; i++) { const isHeavySuite = submissionHeavySuites.includes(suites[i].name); // PREEMPTIVE delay BEFORE heavy suites start (prevents rate limit buildup) if (isHeavySuite && i > 0) { const preemptiveDelayMs = 8000; // 8s "cooldown" before heavy suite const delaySeconds = preemptiveDelayMs / 1000; const delayResult: TestResult = { id: `preemptive-delay-${Date.now()}`, name: `⏳ Pre-suite cooldown: ${delaySeconds}s (preparing for ${suites[i].name})`, suite: 'System', status: 'running', duration: 0, timestamp: new Date().toISOString(), details: { reason: 'Preemptive rate limit prevention before submission-heavy suite', nextSuite: suites[i].name } }; if (this.onProgress) { this.onProgress(delayResult); } await this.delay(preemptiveDelayMs); if (this.onProgress) { this.onProgress({ ...delayResult, status: 'skip', duration: preemptiveDelayMs, details: { reason: 'Cooldown completed' } }); } } await this.runSuite(suites[i]); if (this.shouldStop) { break; } // REACTIVE delay AFTER suites complete if (i < suites.length - 1 && this.delayBetweenTests > 0) { // Longer delay after submission-heavy suites const delayMs = isHeavySuite ? this.delayBetweenTests * 2.25 // 18s delay after heavy suites (increased from 12s) : this.delayBetweenTests; // 8s delay after others (increased from 6s) const delaySeconds = delayMs / 1000; const delayResult: TestResult = { id: `suite-delay-${Date.now()}`, name: `⏳ Suite completion delay: ${delaySeconds}s${isHeavySuite ? ' (submission-heavy)' : ''}`, suite: 'System', status: 'running', duration: 0, timestamp: new Date().toISOString(), details: { reason: 'Pausing between suites to prevent rate limiting', isSubmissionHeavy: isHeavySuite } }; if (this.onProgress) { this.onProgress(delayResult); } await this.delay(delayMs); if (this.onProgress) { this.onProgress({ ...delayResult, status: 'skip', duration: delayMs, details: { reason: 'Suite delay completed' } }); } } } // Run cleanup after all tests complete (if enabled) if (this.cleanupEnabled && !this.shouldStop) { const cleanupStartResult: TestResult = { id: `cleanup-start-${Date.now()}`, name: '🧹 Starting test data cleanup...', suite: 'System', status: 'running', duration: 0, timestamp: new Date().toISOString(), details: { reason: 'Removing test fixtures to prevent database bloat' } }; if (this.onProgress) { this.onProgress(cleanupStartResult); } try { this.cleanupSummary = await cleanupTestData(); const cleanupCompleteResult: TestResult = { id: `cleanup-complete-${Date.now()}`, name: `✅ Cleanup complete: ${this.cleanupSummary.totalDeleted} records deleted`, suite: 'System', status: this.cleanupSummary.success ? 'pass' : 'fail', duration: this.cleanupSummary.totalDuration, timestamp: new Date().toISOString(), details: { totalDeleted: this.cleanupSummary.totalDeleted, results: this.cleanupSummary.results, success: this.cleanupSummary.success } }; if (this.onProgress) { this.onProgress(cleanupCompleteResult); } } catch (error) { const cleanupErrorResult: TestResult = { id: `cleanup-error-${Date.now()}`, name: '❌ Cleanup failed', suite: 'System', status: 'fail', duration: 0, timestamp: new Date().toISOString(), error: error instanceof Error ? error.message : String(error) }; if (this.onProgress) { this.onProgress(cleanupErrorResult); } } } this.isRunning = false; return this.results; } /** * Stop the test run */ stop(): void { this.shouldStop = true; } /** * Get all results */ getResults(): TestResult[] { return this.results; } /** * Get summary statistics */ getSummary(): { total: number; passed: number; failed: number; skipped: number; running: number; totalDuration: number; cleanup?: CleanupSummary; } { const total = this.results.length; const passed = this.results.filter(r => r.status === 'pass').length; const failed = this.results.filter(r => r.status === 'fail').length; const skipped = this.results.filter(r => r.status === 'skip').length; const running = this.results.filter(r => r.status === 'running').length; const totalDuration = this.results.reduce((sum, r) => sum + r.duration, 0); return { total, passed, failed, skipped, running, totalDuration, cleanup: this.cleanupSummary }; } /** * Check if runner is currently running */ getIsRunning(): boolean { return this.isRunning; } /** * Reset runner state */ reset(): void { this.results = []; this.isRunning = false; this.shouldStop = false; this.cleanupSummary = undefined; } /** * Get cleanup summary */ getCleanupSummary(): CleanupSummary | undefined { return this.cleanupSummary; } /** * Enable or disable automatic cleanup */ setCleanupEnabled(enabled: boolean): void { this.cleanupEnabled = enabled; } }