/** * cli:test-report — execute.ts * Runs test suites and produces a structured report. */ import { execSync } from 'node:child_process' import { writeFileSync, mkdirSync } from 'node:fs' import { resolve, join } from 'node:path' import type { TestReportInput, TestReportResult, LayerResult, TestFailure } from './types.js' const BACKEND_CATEGORIES = ['Seed', 'Domain', 'Business', 'Integration'] export function execute(input: TestReportInput): { report: TestReportResult; reportPath: string } { const projectPath = resolve(input.projectPath) const start = Date.now() const byLayer: Record = {} const failures: TestFailure[] = [] // Run backend tests by category if (input.includeBackend) { for (const category of BACKEND_CATEGORIES) { const result = runDotnetTests(projectPath, category) byLayer[category.toLowerCase()] = result.layerResult failures.push(...result.failures) } } // Run frontend tests if (input.includeFrontend) { const result = runFrontendTests(projectPath, input.module) byLayer['frontend'] = result.layerResult failures.push(...result.failures) } // Aggregate summary const summary = { total: Object.values(byLayer).reduce((s, l) => s + l.total, 0), passed: Object.values(byLayer).reduce((s, l) => s + l.passed, 0), failed: Object.values(byLayer).reduce((s, l) => s + l.failed, 0), skipped: Object.values(byLayer).reduce((s, l) => s + l.skipped, 0), } const report: TestReportResult = { module: input.module, timestamp: new Date().toISOString(), duration: Date.now() - start, summary, byLayer, failures, coverage: { line: 0, branch: 0 }, // TODO: integrate with coverage tools } // Write report const reportsDir = join(projectPath, '.smartstack', 'reports') mkdirSync(reportsDir, { recursive: true }) const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) const reportPath = join(reportsDir, `TEST-${input.module}-${ts}.json`) writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8') return { report, reportPath } } function runDotnetTests(projectPath: string, category: string): { layerResult: LayerResult; failures: TestFailure[] } { try { const output = execSync( `dotnet test --no-build --filter Category=${category} --verbosity normal --logger "trx;LogFileName=${category}.trx"`, { cwd: projectPath, encoding: 'utf-8', timeout: 300_000, stdio: ['pipe', 'pipe', 'pipe'] }, ) // Parse test counts from output const passed = parseInt(output.match(/Passed:\s+(\d+)/)?.[1] ?? '0') const failed = parseInt(output.match(/Failed:\s+(\d+)/)?.[1] ?? '0') const skipped = parseInt(output.match(/Skipped:\s+(\d+)/)?.[1] ?? '0') const total = passed + failed + skipped return { layerResult: { total, passed, failed, skipped }, failures: [], } } catch (err: any) { const output = (err.stdout ?? '') + (err.stderr ?? '') const passed = parseInt(output.match(/Passed:\s+(\d+)/)?.[1] ?? '0') const failed = parseInt(output.match(/Failed:\s+(\d+)/)?.[1] ?? '0') const skipped = parseInt(output.match(/Skipped:\s+(\d+)/)?.[1] ?? '0') const total = passed + failed + skipped // Extract failure details const failureMatches = output.matchAll(/Failed\s+(\S+)\s+\[.*?\]\s*\n\s*(.+)/g) const failures: TestFailure[] = [] for (const match of failureMatches) { failures.push({ test: match[1], layer: category.toLowerCase(), error: match[2]?.trim() ?? 'Unknown error', file: '', }) } return { layerResult: { total: Math.max(total, 1), passed, failed: Math.max(failed, 1), skipped }, failures, } } } function runFrontendTests(projectPath: string, module: string): { layerResult: LayerResult; failures: TestFailure[] } { try { const output = execSync( `npm test -- --reporter=json --filter ${module}`, { cwd: projectPath, encoding: 'utf-8', timeout: 300_000, stdio: ['pipe', 'pipe', 'pipe'] }, ) // Attempt JSON parse of vitest output try { const json = JSON.parse(output) const passed = json.numPassedTests ?? 0 const failed = json.numFailedTests ?? 0 const total = json.numTotalTests ?? passed + failed return { layerResult: { total, passed, failed, skipped: 0 }, failures: (json.testResults ?? []) .filter((t: any) => t.status === 'failed') .map((t: any) => ({ test: t.name ?? 'unknown', layer: 'frontend', error: t.message ?? 'Test failed', file: t.file ?? '', })), } } catch { // Fallback: assume pass if exit 0 return { layerResult: { total: 1, passed: 1, failed: 0, skipped: 0 }, failures: [] } } } catch (err: any) { return { layerResult: { total: 1, passed: 0, failed: 1, skipped: 0 }, failures: [{ test: 'frontend-suite', layer: 'frontend', error: (err.stderr ?? 'Test suite failed').slice(0, 500), file: '' }], } } }