/** * 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'; /** * 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; constructor(onProgress?: (result: TestResult) => void, delayBetweenTests: number = 6000) { this.onProgress = onProgress; this.delayBetweenTests = delayBetweenTests; // Default 6 seconds to prevent rate limiting } /** * 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++) { await this.runSuite(suites[i]); if (this.shouldStop) { break; } // Add delay between suites with adaptive timing if (i < suites.length - 1 && this.delayBetweenTests > 0) { // Longer delay after submission-heavy suites const isHeavySuite = submissionHeavySuites.includes(suites[i].name); const delayMs = isHeavySuite ? this.delayBetweenTests * 2 // 12s delay after heavy suites : this.delayBetweenTests; // 6s delay after others 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' } }); } } } 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; } { 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 }; } /** * Check if runner is currently running */ getIsRunning(): boolean { return this.isRunning; } /** * Reset runner state */ reset(): void { this.results = []; this.isRunning = false; this.shouldStop = false; } }