UNPKG

@debugg-ai/cli

Version:
959 lines (949 loc) 46.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestManager = void 0; const client_1 = require("../backend/cli/client"); const git_analyzer_1 = require("./git-analyzer"); const tunnel_manager_1 = require("./tunnel-manager"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const chalk_1 = __importDefault(require("chalk")); const system_logger_1 = require("../util/system-logger"); const telemetry_1 = require("../services/telemetry"); /** * Manages the complete test lifecycle from commit analysis to result reporting */ class TestManager { constructor(options) { this.options = { testOutputDir: 'tests/debugg-ai', serverTimeout: 30000, // 30 seconds maxTestWaitTime: 600000, // 10 minutes downloadArtifacts: false, // Default to NOT downloading artifacts for CI/CD environments ...options }; this.client = new client_1.CLIBackendClient({ apiKey: options.apiKey, baseUrl: options.baseUrl || 'https://api.debugg.ai', repoPath: options.repoPath, timeout: this.options.serverTimeout || 30000 }); this.gitAnalyzer = new git_analyzer_1.GitAnalyzer({ repoPath: options.repoPath }); // Initialize tunnel manager if tunnel creation is requested if (this.options.createTunnel) { this.tunnelManager = new tunnel_manager_1.TunnelManager({ baseDomain: 'ngrok.debugg.ai' }); } } /** * Create TestManager with tunnel URL support */ static withTunnel(options, tunnelUrl, tunnelMetadata) { return new TestManager({ ...options, tunnelUrl, tunnelMetadata }); } /** * Create TestManager that will create an ngrok tunnel after backend provides tunnelKey * This is the correct flow: Backend creates commit suite -> provides tunnelKey -> create tunnel */ static withAutoTunnel(options, endpointUuid, tunnelPort = 3000) { return new TestManager({ ...options, tunnelKey: endpointUuid, createTunnel: true, tunnelPort }); } /** * Run tests for the current commit or working changes */ async runCommitTests() { const testStartTime = Date.now(); system_logger_1.systemLogger.info('Starting test analysis and generation', { category: 'test' }); try { // Step 1: Validate git repository const isValidRepo = await this.gitAnalyzer.validateGitRepo(); if (!isValidRepo) { throw new Error('Not a valid git repository'); } // Step 2: Initialize the CLI client (includes connection test) system_logger_1.systemLogger.info('Initializing backend client', { category: 'test' }); await this.client.initialize(); // Step 3: Test authentication system_logger_1.systemLogger.info('Validating API key', { category: 'api' }); const authTest = await this.client.testAuthentication(); if (!authTest.success) { throw new Error(`Authentication failed: ${authTest.error}`); } system_logger_1.systemLogger.api.auth(true, authTest.user?.email || authTest.user?.id); // Step 3.4: Check if GitHub App PR testing is enabled if (this.options.pr) { system_logger_1.systemLogger.info(`GitHub App PR testing enabled - PR #${this.options.pr}`, { category: 'test' }); return await this.runGitHubAppPRTest(); } // Step 3.5: Check if PR sequence testing is enabled if (this.options.prSequence) { system_logger_1.systemLogger.info('PR sequence testing enabled - analyzing commit sequence', { category: 'test' }); return await this.runPRCommitSequenceTests(); } // Step 4: Validate tunnel URL if provided (simplified for now) if (this.options.tunnelUrl) { system_logger_1.systemLogger.info('Using tunnel URL', { category: 'tunnel' }); // Note: Tunnel validation can be added later if needed system_logger_1.systemLogger.info(`Using tunnel URL: ${this.options.tunnelUrl}`); } // Step 5: Analyze git changes system_logger_1.systemLogger.info('Analyzing git changes', { category: 'git' }); const changes = await this.analyzeChanges(); if (changes.changes.length === 0) { system_logger_1.systemLogger.success('No changes detected - skipping test generation'); return { success: true, testFiles: [] }; } system_logger_1.systemLogger.info(`Found ${changes.changes.length} changed files`, { category: 'git' }); // Track test execution start const executionType = this.options.pr ? 'pr' : this.options.prSequence ? 'pr-sequence' : this.options.commit ? 'commit' : 'working'; telemetry_1.telemetry.trackTestStart(executionType, { filesChanged: changes.changes.length, branch: changes.branchInfo.branch, hasCommit: !!this.options.commit, hasCommitRange: !!this.options.commitRange, hasSince: !!this.options.since, hasLast: !!this.options.last }); // Step 7: Submit test request system_logger_1.systemLogger.info('Creating test suite', { category: 'test' }); // Create test description for the changes const testDescription = await this.createTestDescription(changes); // Get PR number if available const prNumber = this.gitAnalyzer.getPRNumber(); const testRequest = { repoName: this.gitAnalyzer.getRepoName(), repoPath: this.options.repoPath, branchName: changes.branchInfo.branch, commitHash: changes.branchInfo.commitHash, workingChanges: changes.changes, testDescription, ...(prNumber && { prNumber }) }; // Add tunnel key (UUID) for custom endpoints (e.g., <uuid>.debugg.ai) // This tells the backend which subdomain to expect the tunnel on if (this.options.tunnelKey) { testRequest.key = this.options.tunnelKey; system_logger_1.systemLogger.debug('Sending tunnel key to backend', { category: 'tunnel', details: { key: this.options.tunnelKey.substring(0, 8) + '...' } }); } // if (this.options.tunnelUrl) { // testRequest.publicUrl = this.options.tunnelUrl; // testRequest.testEnvironment = { // url: this.options.tunnelUrl, // type: 'ngrok_tunnel' as const, // metadata: this.options.tunnelMetadata // }; // } const response = await this.client.createCommitTestSuite(testRequest); if (!response.success || !response.testSuiteUuid) { throw new Error(`Failed to create test suite: ${response.error}`); } system_logger_1.systemLogger.info(`Test suite created: ${response.testSuiteUuid}`, { category: 'test' }); // Step 7.5: Create tunnel if requested and backend provided tunnelKey let tunnelInfo; system_logger_1.systemLogger.debug('Tunnel setup', { category: 'tunnel', details: { createTunnel: this.options.createTunnel, tunnelKey: this.options.tunnelKey, backendTunnelKey: response.tunnelKey } }); if (response.tunnelKey && this.options.tunnelKey) { system_logger_1.systemLogger.info('Setting up ngrok tunnel', { category: 'tunnel' }); system_logger_1.systemLogger.info('TUNNEL SETUP'); system_logger_1.systemLogger.info(`Endpoint UUID: ${this.options.tunnelKey}`); system_logger_1.systemLogger.info(`Expected URL: https://${this.options.tunnelKey}.ngrok.debugg.ai`); system_logger_1.systemLogger.info(`Local port: ${this.options.tunnelPort || 3000}`); system_logger_1.systemLogger.info(`Backend provided tunnelKey: ${response.tunnelKey ? '✓ YES' : '✗ NO'}`); if (!this.tunnelManager) { throw new Error('Tunnel manager not initialized. This should not happen.'); } const tunnelPort = this.options.tunnelPort || 3000; try { system_logger_1.systemLogger.info('Starting ngrok tunnel'); tunnelInfo = await this.tunnelManager.createTunnelWithBackendKey(tunnelPort, this.options.tunnelKey, // UUID for endpoint response.tunnelKey // ngrok auth token from backend ); // Store tunnel info for later use this.tunnelInfo = tunnelInfo; system_logger_1.systemLogger.tunnel.connected(tunnelInfo.url); system_logger_1.systemLogger.success(`TUNNEL ACTIVE: ${tunnelInfo.url} -> localhost:${tunnelPort}`); // Track successful tunnel creation telemetry_1.telemetry.trackTunnelCreation(true, tunnelPort); } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; system_logger_1.systemLogger.error(`TUNNEL FAILED: ${errorMsg}`); system_logger_1.systemLogger.warn('Tests will proceed without tunnel - ensure your server is accessible at the expected URL'); system_logger_1.systemLogger.warn(`Expected backend URL: https://${this.options.tunnelKey}.ngrok.debugg.ai`); // Track tunnel creation failure telemetry_1.telemetry.trackTunnelCreation(false, tunnelPort, errorMsg); } } else { // Log why tunnel wasn't created if (this.options.createTunnel) { system_logger_1.systemLogger.info('TUNNEL SETUP SKIPPED'); system_logger_1.systemLogger.debug(`createTunnel: ${this.options.createTunnel ? '✓' : '✗'}`); system_logger_1.systemLogger.debug(`tunnelKey provided: ${this.options.tunnelKey ? '✓' : '✗'}`); system_logger_1.systemLogger.debug(`backend tunnelKey: ${response.tunnelKey ? '✓' : '✗'}`); } } // Step 8: Wait for tests to complete system_logger_1.systemLogger.info('Waiting for tests to complete', { category: 'test' }); const completedSuite = await this.client.waitForCommitTestSuiteCompletion(response.testSuiteUuid, { maxWaitTime: this.options.maxTestWaitTime || 600000, pollInterval: 5000, onProgress: (suite) => { const testCount = suite.tests?.length || 0; const completedTests = suite.tests?.filter((t) => t.curRun?.status === 'completed' || t.curRun?.status === 'failed').length || 0; system_logger_1.systemLogger.info(`Running tests... (${completedTests}/${testCount} completed)`, { category: 'test' }); } }); if (!completedSuite) { throw new Error('Test suite timed out or failed to complete'); } // Step 9: Download and save test artifacts (only if enabled) let testFiles = []; if (this.options.downloadArtifacts) { system_logger_1.systemLogger.info('Downloading test artifacts', { category: 'test' }); testFiles = await this.saveTestArtifacts(completedSuite); } else { system_logger_1.systemLogger.debug('Skipping artifact download - downloadArtifacts is disabled', { category: 'test' }); } // Step 10: Report results this.reportResults(completedSuite); if (this.options.downloadArtifacts) { system_logger_1.systemLogger.success(`Tests completed successfully! Generated ${testFiles.length} test files`); telemetry_1.telemetry.trackArtifactDownload('test_files', true, testFiles.length); } else { system_logger_1.systemLogger.success('Tests completed successfully! (artifacts not downloaded - use --download-artifacts to save test files)'); } // Track test completion const testsGenerated = completedSuite.tests?.length || 0; const testExecutionType = this.options.pr ? 'pr' : this.options.prSequence ? 'pr-sequence' : this.options.commit ? 'commit' : 'working'; telemetry_1.telemetry.trackTestComplete({ suiteUuid: response.testSuiteUuid, duration: Date.now() - testStartTime, filesChanged: changes.changes.length, testsGenerated, success: true, executionType: testExecutionType }); const result = { success: true, suiteUuid: response.testSuiteUuid, suite: completedSuite, testFiles }; // Add optional fields only if they have values if (response.tunnelKey) { result.tunnelKey = response.tunnelKey; } if (tunnelInfo) { result.tunnelInfo = tunnelInfo; } return result; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; system_logger_1.systemLogger.error(`Test run failed: ${errorMsg}`); // Track test failure const failureExecutionType = this.options.pr ? 'pr' : this.options.prSequence ? 'pr-sequence' : this.options.commit ? 'commit' : 'working'; telemetry_1.telemetry.trackTestComplete({ suiteUuid: '', duration: Date.now() - testStartTime, filesChanged: 0, testsGenerated: 0, success: false, error: errorMsg, executionType: failureExecutionType }); return { success: false, error: errorMsg }; } finally { // Cleanup tunnel if it was created if (this.tunnelManager) { try { await this.tunnelManager.disconnectAll(); system_logger_1.systemLogger.info('✓ Tunnels cleaned up'); } catch (error) { system_logger_1.systemLogger.warn('⚠ Failed to cleanup tunnels: ' + error); } } } } /** * Run GitHub App-based PR test - sends single request with PR number * Backend handles all git analysis via GitHub App integration */ async runGitHubAppPRTest() { try { // Get current branch name const branchInfo = await this.gitAnalyzer.getCurrentBranchInfo(); system_logger_1.systemLogger.info(`Submitting PR #${this.options.pr} for GitHub App-based testing`, { category: 'test' }); system_logger_1.systemLogger.info(`Branch: ${branchInfo.branch}`, { category: 'git' }); // Create test request for GitHub App PR testing const testRequest = { type: 'pull_request', repoName: this.gitAnalyzer.getRepoName(), repoPath: this.options.repoPath, branch: branchInfo.branch, pr_number: this.options.pr, commitHash: branchInfo.commitHash, testDescription: `Automated E2E tests for PR #${this.options.pr}` }; // Add tunnel configuration if applicable if (this.options.tunnelUrl) { testRequest.tunnelUrl = this.options.tunnelUrl; } if (this.options.tunnelMetadata) { testRequest.tunnelMetadata = this.options.tunnelMetadata; } if (this.options.tunnelKey) { testRequest.tunnelKey = this.options.tunnelKey; } // Submit test request system_logger_1.systemLogger.info('Submitting PR test request to backend', { category: 'api' }); const createResult = await this.client.createCommitTestSuite(testRequest); if (!createResult.success || !createResult.testSuiteUuid) { throw new Error(createResult.error || 'Failed to create test suite'); } const suiteUuid = createResult.testSuiteUuid; system_logger_1.systemLogger.info(`Test suite created: ${suiteUuid}`, { category: 'test' }); // Wait for test completion system_logger_1.systemLogger.info('Waiting for test execution', { category: 'test' }); const suite = await this.client.waitForCommitTestSuiteCompletion(suiteUuid, { maxWaitTime: this.options.maxTestWaitTime || 600000, pollInterval: 5000 }); if (!suite) { throw new Error('Test suite failed or timed out'); } // Download artifacts if requested let downloadedFiles = []; if (this.options.downloadArtifacts && suite.status === 'completed') { system_logger_1.systemLogger.info('Downloading test artifacts', { category: 'test' }); downloadedFiles = await this.saveTestArtifacts(suite); } system_logger_1.systemLogger.success(`GitHub App PR test completed for PR #${this.options.pr}`); const result = { success: true, suiteUuid, suite, testFiles: downloadedFiles }; // Add optional fields only if they have values if (this.options.tunnelKey) { result.tunnelKey = this.options.tunnelKey; } if (this.tunnelInfo) { result.tunnelInfo = this.tunnelInfo; } return result; } catch (error) { const errorMsg = error.message || 'Unknown error occurred'; system_logger_1.systemLogger.error(`GitHub App PR test failed: ${errorMsg}`); return { success: false, error: errorMsg }; } } /** * Run PR commit sequence tests - sends individual test requests for each commit */ async runPRCommitSequenceTests() { try { // Step 1: Analyze PR commit sequence const prSequence = await this.gitAnalyzer.analyzePRCommitSequence(this.options.baseBranch, this.options.headBranch); if (!prSequence || prSequence.commits.length === 0) { system_logger_1.systemLogger.warn('No PR commits found to test'); return { success: true, testFiles: [], prSequenceResults: [], totalCommitsTested: 0 }; } system_logger_1.systemLogger.info(`Found ${prSequence.totalCommits} commits to test sequentially`, { category: 'test' }); system_logger_1.systemLogger.info(`PR: ${prSequence.baseBranch} <- ${prSequence.headBranch}`, { category: 'git' }); const sequenceResults = []; const allTestFiles = []; let anyFailed = false; // Process each commit individually for (const commit of prSequence.commits) { system_logger_1.systemLogger.info(`\n--- Testing Commit ${commit.order}/${prSequence.totalCommits} ---`, { category: 'test' }); system_logger_1.systemLogger.info(`Commit: ${commit.hash.substring(0, 8)} - ${commit.message}`, { category: 'git' }); system_logger_1.systemLogger.info(`Author: ${commit.author}`, { category: 'git' }); system_logger_1.systemLogger.info(`Changes: ${commit.changes.length} files`, { category: 'git' }); try { // Create test request for this specific commit const result = await this.createCommitTestSuite(commit, prSequence); if (result.success && result.suiteUuid) { // Wait for completion const completedSuite = await this.client.waitForCommitTestSuiteCompletion(result.suiteUuid, { maxWaitTime: this.options.maxTestWaitTime || 600000 }); if (completedSuite) { // Download artifacts if enabled let testFiles = []; if (this.options.downloadArtifacts) { testFiles = await this.saveTestArtifacts(completedSuite); allTestFiles.push(...testFiles); } sequenceResults.push({ commitHash: commit.hash, commitMessage: commit.message, commitOrder: commit.order, suiteUuid: result.suiteUuid, success: true, testFiles }); system_logger_1.systemLogger.success(`✓ Commit ${commit.order} tests completed`); } else { // Test suite timed out sequenceResults.push({ commitHash: commit.hash, commitMessage: commit.message, commitOrder: commit.order, suiteUuid: result.suiteUuid, success: false, error: 'Test suite timed out' }); anyFailed = true; system_logger_1.systemLogger.error(`✗ Commit ${commit.order} tests timed out`); } } else { // Test suite creation failed sequenceResults.push({ commitHash: commit.hash, commitMessage: commit.message, commitOrder: commit.order, suiteUuid: '', success: false, error: result.error || 'Failed to create test suite' }); anyFailed = true; system_logger_1.systemLogger.error(`✗ Commit ${commit.order} test creation failed: ${result.error}`); } } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; sequenceResults.push({ commitHash: commit.hash, commitMessage: commit.message, commitOrder: commit.order, suiteUuid: '', success: false, error: errorMsg }); anyFailed = true; system_logger_1.systemLogger.error(`✗ Commit ${commit.order} failed: ${errorMsg}`); } } // Report overall results const successCount = sequenceResults.filter(r => r.success).length; system_logger_1.systemLogger.info(`\n=== PR Commit Sequence Results ===`, { category: 'test' }); system_logger_1.systemLogger.info(`Total commits tested: ${prSequence.totalCommits}`, { category: 'test' }); system_logger_1.systemLogger.info(`Successful: ${successCount}`, { category: 'test' }); system_logger_1.systemLogger.info(`Failed: ${prSequence.totalCommits - successCount}`, { category: 'test' }); if (this.options.downloadArtifacts && allTestFiles.length > 0) { system_logger_1.systemLogger.success(`Generated ${allTestFiles.length} total test files across all commits`); } const firstSuiteUuid = sequenceResults.length > 0 && sequenceResults[0] ? sequenceResults[0].suiteUuid : undefined; return { success: !anyFailed, testFiles: allTestFiles, prSequenceResults: sequenceResults, totalCommitsTested: prSequence.totalCommits, suiteUuid: firstSuiteUuid }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; system_logger_1.systemLogger.error(`PR sequence testing failed: ${errorMsg}`); return { success: false, error: errorMsg, prSequenceResults: [], totalCommitsTested: 0 }; } } /** * Create a test suite for a specific commit in a PR sequence */ async createCommitTestSuite(commit, prSequence) { // Get repository information const repoName = this.gitAnalyzer.getRepoName(); // Create test description specific to this commit const testDescription = await this.createCommitTestDescription(commit, prSequence); const testRequest = { repoName, repoPath: this.options.repoPath, branchName: prSequence.headBranch, commitHash: commit.hash, workingChanges: commit.changes.map((change) => ({ status: change.status, file: change.file, diff: change.diff })), testDescription, ...(prSequence.prNumber && { prNumber: prSequence.prNumber }), // Add PR context metadata prContext: { baseBranch: prSequence.baseBranch, headBranch: prSequence.headBranch, commitOrder: commit.order, totalCommits: prSequence.totalCommits, isSequentialTest: true } }; // Add tunnel configuration if available if (this.options.tunnelKey) { testRequest.tunnelKey = this.options.tunnelKey; } if (this.options.createTunnel) { testRequest.createTunnel = true; testRequest.tunnelPort = this.options.tunnelPort || 3000; } return await this.client.createCommitTestSuite(testRequest); } /** * Create a test description for a specific commit in a PR sequence */ async createCommitTestDescription(commit, prSequence) { const changeTypes = this.analyzeFileTypes(commit.changes); // Count changes by type const componentCount = changeTypes.filter(t => t.type === 'component').reduce((sum, t) => sum + t.count, 0); const routingCount = changeTypes.filter(t => t.type === 'routing').reduce((sum, t) => sum + t.count, 0); const configCount = changeTypes.filter(t => t.type === 'configuration').reduce((sum, t) => sum + t.count, 0); const stylingCount = changeTypes.filter(t => t.type === 'styling').reduce((sum, t) => sum + t.count, 0); const otherCount = changeTypes.filter(t => !['component', 'routing', 'configuration', 'styling'].includes(t.type)).reduce((sum, t) => sum + t.count, 0); return `Sequential PR Test - Commit ${commit.order}/${prSequence.totalCommits} Commit: ${commit.hash.substring(0, 8)} - ${commit.message} Author: ${commit.author} Branch: ${prSequence.baseBranch} <- ${prSequence.headBranch} Changes in this commit: ${commit.changes.map((c) => `- [${c.status}] ${c.file}`).join('\n')} Change Summary: - ${commit.changes.length} file${commit.changes.length !== 1 ? 's' : ''} modified - Components: ${componentCount} - Routing: ${routingCount} - Configuration: ${configCount} - Styling: ${stylingCount} - Other: ${otherCount} Focus: Test the specific functionality changes introduced by this individual commit in the sequence.`; } /** * Wait for the local development server to be ready */ async waitForServer(port = 3000, maxWaitTime = 60000) { const startTime = Date.now(); const pollInterval = 2000; system_logger_1.systemLogger.info(`Waiting for server on port ${port}`, { category: 'server' }); while (Date.now() - startTime < maxWaitTime) { try { // Simple HTTP check to see if server is responding const response = await fetch(`http://localhost:${port}`, { method: 'GET', signal: AbortSignal.timeout(5000) }); if (response.ok || response.status === 404) { system_logger_1.systemLogger.success(`Server is ready on port ${port}`); return true; } } catch (error) { // Server not ready yet, continue waiting } await new Promise(resolve => setTimeout(resolve, pollInterval)); } system_logger_1.systemLogger.error(`Server on port ${port} did not start within ${maxWaitTime}ms`); return false; } /** * Analyze git changes (working changes, specific commit, or commit range) */ async analyzeChanges() { // Priority order: explicit options > environment variables > working changes // 1. Check for explicit commit hash option if (this.options.commit) { return await this.gitAnalyzer.getCommitChanges(this.options.commit); } // 2. Check for commit range option if (this.options.commitRange) { const commitHashes = await this.gitAnalyzer.getCommitsFromRange(this.options.commitRange); return await this.gitAnalyzer.getCombinedCommitChanges(commitHashes); } // 3. Check for since date option if (this.options.since) { const commitHashes = await this.gitAnalyzer.getCommitsSince(this.options.since); return await this.gitAnalyzer.getCombinedCommitChanges(commitHashes); } // 4. Check for last N commits option if (this.options.last) { const commitHashes = await this.gitAnalyzer.getLastCommits(this.options.last); return await this.gitAnalyzer.getCombinedCommitChanges(commitHashes); } // 5. In CI/CD, check for environment variable (GitHub Actions) const envCommitHash = process.env.GITHUB_SHA; if (envCommitHash) { // Analyze specific commit (typical for push events) return await this.gitAnalyzer.getCommitChanges(envCommitHash); } // 6. Default: analyze working changes (for local development) return await this.gitAnalyzer.getWorkingChanges(); } /** * Create a comprehensive test description based on changes */ async createTestDescription(changes) { const commitHash = changes.branchInfo.commitHash; const branch = changes.branchInfo.branch; const fileCount = changes.changes.length; // Use enhanced context analysis inspired by backend architecture const contextAnalysis = await this.gitAnalyzer.analyzeChangesWithContext(changes.changes); // Determine the source of changes for better description let sourceDescription; if (this.options.commit) { sourceDescription = `specific commit ${this.options.commit.substring(0, 8)}`; } else if (this.options.commitRange) { sourceDescription = `commit range ${this.options.commitRange}`; } else if (this.options.since) { sourceDescription = `commits since ${this.options.since}`; } else if (this.options.last) { sourceDescription = `last ${this.options.last} commit${this.options.last > 1 ? 's' : ''}`; } else if (process.env.GITHUB_SHA) { sourceDescription = `CI commit ${commitHash.substring(0, 8)}`; } else { sourceDescription = `working changes`; } // Build focused description based on analysis let description = `Generate comprehensive E2E tests for the ${sourceDescription} on branch ${branch}. Change Analysis: - Total Files: ${fileCount} - Complexity: ${contextAnalysis.changeComplexity.toUpperCase()} - Languages: ${contextAnalysis.affectedLanguages.join(', ')}`; // Add specific areas of focus based on changes if (contextAnalysis.suggestedFocusAreas.length > 0) { description += ` Focus Areas: ${contextAnalysis.suggestedFocusAreas.map(area => `- ${area}`).join('\n')}`; } // Add component-specific context if (contextAnalysis.componentChanges.length > 0) { description += ` Components Changed: ${contextAnalysis.componentChanges.slice(0, 5).map(file => `- ${file}`).join('\n')}${contextAnalysis.componentChanges.length > 5 ? '\n- ...' : ''}`; } // Add routing context if (contextAnalysis.routingChanges.length > 0) { description += ` Routing Changes: ${contextAnalysis.routingChanges.map(file => `- ${file}`).join('\n')}`; } // Add configuration context if (contextAnalysis.configChanges.length > 0) { description += ` Configuration Changes: ${contextAnalysis.configChanges.map(file => `- ${file}`).join('\n')}`; } description += ` Test Requirements: 1. Generate Playwright tests focused on the identified change areas 2. Test both positive and negative scenarios for modified functionality 3. Include edge cases and error handling for ${contextAnalysis.changeComplexity} complexity changes 4. Focus testing on: ${contextAnalysis.suggestedFocusAreas.slice(0, 3).join(', ')} 5. Ensure tests cover the interaction between changed ${contextAnalysis.affectedLanguages.join(' and ')} components`; return description; } /** * Analyze file types in the changes */ analyzeFileTypes(files) { const typeMap = new Map(); for (const file of files) { const ext = path.extname(file).toLowerCase(); let type; switch (ext) { case '.ts': case '.tsx': type = 'TypeScript'; break; case '.js': case '.jsx': type = 'JavaScript'; break; case '.py': type = 'Python'; break; case '.java': type = 'Java'; break; case '.css': case '.scss': case '.sass': type = 'Stylesheets'; break; case '.html': type = 'HTML'; break; case '.json': type = 'Configuration'; break; case '.md': type = 'Documentation'; break; default: if (file.includes('test') || file.includes('spec')) { type = 'Tests'; } else if (file.includes('config') || file.includes('package')) { type = 'Configuration'; } else { type = 'Other'; } } if (!typeMap.has(type)) { typeMap.set(type, []); } typeMap.get(type).push(file); } return Array.from(typeMap.entries()).map(([type, files]) => ({ type, count: files.length, files })); } /** * Save test artifacts (scripts, recordings, etc.) to local directory */ async saveTestArtifacts(suite) { const savedFiles = []; if (!suite.tests || suite.tests.length === 0) { system_logger_1.systemLogger.debug('No tests found in suite for artifact saving', { category: 'artifact' }); return savedFiles; } system_logger_1.systemLogger.debug(`Starting to save artifacts for ${suite.tests.length} tests`, { category: 'artifact' }); // Ensure test output directory exists if (!this.options.testOutputDir) { throw new Error('testOutputDir is undefined. This should not happen - please file a bug report.'); } const outputDir = path.join(this.options.repoPath, this.options.testOutputDir); await fs.ensureDir(outputDir); for (const test of suite.tests) { if (!test.curRun) { system_logger_1.systemLogger.debug(`Skipping test ${test.name || test.uuid} - no curRun data`, { category: 'artifact' }); continue; } const testName = test.name || `test-${test.uuid?.substring(0, 8)}`; const testDir = path.join(outputDir, testName); await fs.ensureDir(testDir); system_logger_1.systemLogger.debug(`Processing test: ${testName}`, { category: 'artifact', details: { hasScript: !!test.curRun.runScript, hasGif: !!test.curRun.runGif, hasJson: !!test.curRun.runJson, testDir: path.relative(this.options.repoPath, testDir) } }); // Save test script if (test.curRun.runScript) { try { const scriptPath = path.join(testDir, `${testName}.spec.js`); // For scripts, we need to replace tunnel URLs with localhost - use originalBaseUrl like recordingHandler // Fallback to port 3000 if tunnelPort is not set const port = this.options.tunnelPort || 3000; const originalBaseUrl = `http://localhost:${port}`; system_logger_1.systemLogger.debug(`Downloading script for ${testName}`, { category: 'artifact', details: { url: test.curRun.runScript, targetPath: path.relative(this.options.repoPath, scriptPath), originalBaseUrl } }); const success = await this.client.downloadArtifactToFile(test.curRun.runScript, scriptPath, originalBaseUrl); system_logger_1.systemLogger.debug(`Script download result for ${testName}: ${success}`, { category: 'artifact' }); if (success) { savedFiles.push(scriptPath); system_logger_1.systemLogger.info(`✓ Saved test script: ${path.relative(this.options.repoPath, scriptPath)}`); } else { system_logger_1.systemLogger.warn(`⚠ Script download failed for ${testName} - no file saved`); } } catch (error) { system_logger_1.systemLogger.warn(`⚠ Failed to download script for ${testName}: ${error}`); } } // Save test recording (GIF) if (test.curRun.runGif) { try { const gifPath = path.join(testDir, `${testName}-recording.gif`); system_logger_1.systemLogger.debug(`Downloading GIF for ${testName}`, { category: 'artifact', details: { url: test.curRun.runGif, targetPath: path.relative(this.options.repoPath, gifPath) } }); const success = await this.client.downloadArtifactToFile(test.curRun.runGif, gifPath); system_logger_1.systemLogger.debug(`GIF download result for ${testName}: ${success}`, { category: 'artifact' }); if (success) { savedFiles.push(gifPath); system_logger_1.systemLogger.info(`✓ Saved test recording: ${path.relative(this.options.repoPath, gifPath)}`); } else { system_logger_1.systemLogger.warn(`⚠ GIF download failed for ${testName} - no file saved`); } } catch (error) { system_logger_1.systemLogger.warn(`⚠ Failed to download recording for ${testName}: ${error}`); } } // Save test details (JSON) if (test.curRun.runJson) { try { const jsonPath = path.join(testDir, `${testName}-details.json`); system_logger_1.systemLogger.debug(`Downloading JSON for ${testName}`, { category: 'artifact', details: { url: test.curRun.runJson, targetPath: path.relative(this.options.repoPath, jsonPath) } }); const success = await this.client.downloadArtifactToFile(test.curRun.runJson, jsonPath); system_logger_1.systemLogger.debug(`JSON download result for ${testName}: ${success}`, { category: 'artifact' }); if (success) { savedFiles.push(jsonPath); system_logger_1.systemLogger.info(`✓ Saved test details: ${path.relative(this.options.repoPath, jsonPath)}`); } else { system_logger_1.systemLogger.warn(`⚠ JSON download failed for ${testName} - no file saved`); } } catch (error) { system_logger_1.systemLogger.warn(`⚠ Failed to download details for ${testName}: ${error}`); } } } system_logger_1.systemLogger.debug(`Artifact saving completed. Total files saved: ${savedFiles.length}`, { category: 'artifact', details: { savedFiles: savedFiles.map(f => path.relative(this.options.repoPath, f)) } }); return savedFiles; } /** * Report test results to console */ reportResults(suite) { // Use systemLogger's displayResults which handles both dev and user modes system_logger_1.systemLogger.displayResults(suite); // Set exit code for CI/CD based on test outcomes if (suite.tests && suite.tests.length > 0) { const failed = suite.tests.filter((t) => t.curRun?.outcome === 'fail').length; if (failed > 0) { process.exitCode = 1; // Set non-zero exit code for CI/CD } } } /** * Get colored status text */ getStatusColor(status) { switch (status) { case 'completed': return chalk_1.default.green('✓ PASSED'); case 'failed': return chalk_1.default.red('✗ FAILED'); case 'running': return chalk_1.default.yellow('⏳ RUNNING'); case 'pending': return chalk_1.default.blue('⏸ PENDING'); default: return chalk_1.default.gray('❓ UNKNOWN'); } } } exports.TestManager = TestManager; //# sourceMappingURL=test-manager.js.map