import * as fs from "node:fs"; import * as path from "node:path"; import { BaseAgent, AgentContext, AgentResult } from "./base.js"; import { execSync } from "node:child_process"; export interface TesterResult { success: boolean; integrationTestsFound: number; integrationTestsPassed: number; e2eTestsFound: number; e2eTestsPassed: number; overallPassed: boolean; error?: string; } export class TesterAgent extends BaseAgent { readonly name = "tester"; readonly description = "Runs integration, e2e, functional tests. Validates non-unit test coverage."; readonly workflow = "test"; async execute(context: AgentContext): Promise { const start = Date.now(); this.log("Running automated tests..."); if (context.backend) { const result = await this.executeViaBackend( context, `Run integration, e2e, and functional tests for phase ${context.phase}. Specification: ${context.specification}. Detect *.integration.test.ts, *.e2e.test.ts, *.functional.test.ts files. Run npm test. Parse output for pass/fail counts per category. Report structured TesterResult. Do NOT write any test files — only detect and run existing ones.` ); return { ...result, duration_ms: Date.now() - start }; } const result = await this.runMechanicalTests(context); const output = JSON.stringify(result, null, 2); return { success: result.success, output, artifacts_created: [], decisions: 0, escalations: result.overallPassed ? 0 : 1, duration_ms: Date.now() - start, error: result.error, }; } private async runMechanicalTests(context: AgentContext): Promise { try { const srcDir = path.join(context.project_path, "src"); const integrationFiles = fs.existsSync(srcDir) ? this.findTestFiles(srcDir, /\.integration\.test\.ts$/) : []; const e2eFiles = fs.existsSync(srcDir) ? this.findTestFiles(srcDir, /\.e2e\.test\.ts$/) : []; const functionalFiles = fs.existsSync(srcDir) ? this.findTestFiles(srcDir, /\.functional\.test\.ts$/) : []; const integrationTestsFound = integrationFiles.length; const e2eTestsFound = e2eFiles.length + functionalFiles.length; let overallPassed = false; let integrationTestsPassed = 0; let e2eTestsPassed = 0; try { const testOutput = execSync("npm test 2>&1", { cwd: context.project_path, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 120000, }); overallPassed = true; const passCounts = this.parseTestOutput(testOutput); integrationTestsPassed = integrationTestsFound > 0 ? integrationTestsFound : 0; e2eTestsPassed = e2eTestsFound > 0 ? e2eTestsFound : 0; if (integrationTestsFound > 0) { integrationTestsPassed = this.estimateCategoryPassed(testOutput, "integration"); } if (e2eTestsFound > 0) { e2eTestsPassed = this.estimateCategoryPassed(testOutput, "e2e"); } } catch (err) { const output = err instanceof Error && "stdout" in err ? (err as unknown as { stdout: string }).stdout || "" : ""; const stderr = err instanceof Error && "stderr" in err ? (err as unknown as { stderr: string }).stderr || "" : ""; const combined = `${output}\n${stderr}`; overallPassed = false; const passCounts = this.parseTestOutput(combined); if (integrationTestsFound > 0) { integrationTestsPassed = this.estimateCategoryPassed(combined, "integration"); } if (e2eTestsFound > 0) { e2eTestsPassed = this.estimateCategoryPassed(combined, "e2e"); } return { success: false, integrationTestsFound, integrationTestsPassed, e2eTestsFound, e2eTestsPassed, overallPassed: false, error: `npm test failed: ${err instanceof Error ? err.message : String(err)}`, }; } return { success: overallPassed, integrationTestsFound, integrationTestsPassed, e2eTestsFound, e2eTestsPassed, overallPassed, }; } catch (err) { return { success: false, integrationTestsFound: 0, integrationTestsPassed: 0, e2eTestsFound: 0, e2eTestsPassed: 0, overallPassed: false, error: `Test execution failed: ${err instanceof Error ? err.message : String(err)}`, }; } } private findTestFiles(dir: string, pattern: RegExp): string[] { const files: string[] = []; if (!fs.existsSync(dir)) return files; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && entry.name !== "node_modules") { files.push(...this.findTestFiles(fullPath, pattern)); } else if (pattern.test(entry.name)) { files.push(fullPath); } } return files; } private parseTestOutput(output: string): { total: number; passed: number; failed: number } { const jestSummary = output.match(/Tests:\s+(\d+)\s+passed(?:,\s+(\d+)\s+failed)?/); if (jestSummary) { const passed = parseInt(jestSummary[1], 10) || 0; const failed = parseInt(jestSummary[2], 10) || 0; return { total: passed + failed, passed, failed }; } const jestAlt = output.match(/(\d+)\s+passing/); const jestAltFail = output.match(/(\d+)\s+failing/); if (jestAlt) { const passed = parseInt(jestAlt[1], 10) || 0; const failed = jestAltFail ? parseInt(jestAltFail[1], 10) || 0 : 0; return { total: passed + failed, passed, failed }; } return { total: 0, passed: 0, failed: 0 }; } private estimateCategoryPassed(output: string, category: string): number { const categoryPattern = category === "integration" ? /\.integration\.test\.ts/g : /\.e2e\.test\.ts|\.functional\.test\.ts/g; const mentions = (output.match(categoryPattern) || []).length; if (mentions > 0) { const failPattern = /FAIL|failed|error/i; const lines = output.split("\n").filter(l => categoryPattern.test(l)); const failed = lines.filter(l => failPattern.test(l)).length; return Math.max(mentions - failed, 0); } return 0; } }