UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

229 lines 9.05 kB
"use strict"; /** * @fileoverview Pure in-memory merge of two `DonobuReport`s. * * Combines the initial Playwright run and an auto-heal rerun into a single * report that carries both attempts and annotates any test whose outcome * flipped from failing → passing as `self-healed`. The result is destined for * the HTML renderer (and, for back-compat, the on-disk Playwright JSON that * downstream dashboards consume). * * This module intentionally owns zero I/O. Callers load the reports off disk, * call `mergeReports`, then persist / relocate attachments / re-render as * needed. Keeping the merge pure makes it trivial to test and reason about. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.mergeReports = mergeReports; exports.buildTestKey = buildTestKey; function mergeReports(params) { const { initialReport, healReport } = params; if (!initialReport && !healReport) { return null; } // Clone so we never mutate either input. const combined = JSON.parse(JSON.stringify(initialReport ?? healReport)); const initialIndex = indexReport(initialReport); const combinedIndex = indexReport(combined); const healIndex = indexReport(healReport); const healedKeys = new Set(); if (healReport) { const processedHealEntries = new Set(); const processHealEntry = (healEntry) => { const key = buildTestKey(healEntry.suite.file, healEntry.test.projectName, healEntry.test.title); let combinedEntry = (healEntry.test.testId ? combinedIndex.byId.get(healEntry.test.testId) : undefined) ?? combinedIndex.byKey.get(key) ?? null; if (!combinedEntry) { combinedEntry = insertTestIntoReport(combined, healEntry); if (healEntry.test.testId) { combinedIndex.byId.set(healEntry.test.testId, combinedEntry); } combinedIndex.byKey.set(key, combinedEntry); } const originalEntry = (healEntry.test.testId ? initialIndex.byId.get(healEntry.test.testId) : undefined) ?? initialIndex.byKey.get(key) ?? null; const combinedTest = combinedEntry.test; if (healEntry.test.results?.length) { combinedTest.results = [ ...(combinedTest.results ?? []), ...healEntry.test.results, ]; } if (healEntry.test.status !== undefined) { combinedTest.status = healEntry.test.status; } if (healEntry.test.outcome !== undefined) { combinedTest.outcome = healEntry.test.outcome; } const originalStatus = originalEntry ? getFinalResultStatus(originalEntry.test) : undefined; const healStatus = getFinalResultStatus(healEntry.test); if (healStatus === 'passed' && originalStatus && originalStatus !== 'passed') { combinedTest.annotations = combinedTest.annotations ?? []; if (!combinedTest.annotations.some((annotation) => annotation.type === 'self-healed')) { combinedTest.annotations.push({ type: 'self-healed', description: 'Automatically healed by Donobu auto-heal rerun after applying treatment plan.', }); } combinedTest.donobuStatus = 'healed'; healedKeys.add(key); } }; const iterateEntries = (entries) => { for (const [, healEntry] of entries) { if (processedHealEntries.has(healEntry)) { continue; } processedHealEntries.add(healEntry); processHealEntry(healEntry); } }; iterateEntries(healIndex.byId); iterateEntries(healIndex.byKey); } if (params.healSucceeded && healedKeys.size === 0) { params.healedTests.forEach((descriptor) => { // Descriptors must arrive with `testCase.file` already normalized to the // same form that suite.file has in the reports — the caller owns path // normalization because it has access to the CWD the run was launched in. const key = buildTestKey(descriptor.testCase.file, descriptor.testCase.projectName, descriptor.testCase.title); const entry = combinedIndex.byKey.get(key); if (entry) { entry.test.annotations = entry.test.annotations ?? []; if (!entry.test.annotations.some((annotation) => annotation.type === 'self-healed')) { entry.test.annotations.push({ type: 'self-healed', description: 'Automatically healed by Donobu auto-heal rerun after applying treatment plan.', }); } entry.test.donobuStatus = 'healed'; healedKeys.add(key); } }); } combined.stats = computeReportStats(combined); const mergedMetadata = { ...(combined.metadata ?? {}), donobuMergedReport: true, mergedAtIso: new Date().toISOString(), sources: { initial: params.initialReportSourcePath ?? null, autoHeal: params.healReportSourcePath ?? null, }, ...(params.triageRunDir ? { triageRunDir: params.triageRunDir } : {}), donobuHealedTests: Array.from(healedKeys.values()), }; combined.metadata = mergedMetadata; return combined; } // Playwright does not reliably expose stable IDs across reports; fall back to // a composite key. Exported so the orchestrator can build matching keys for // heal descriptors when needed. function buildTestKey(file, projectName, title) { return [file ?? 'unknown-file', projectName ?? 'default', title ?? ''] .map((segment) => segment.toString()) .join('::'); } function getFinalResultStatus(test) { if (!test) { return undefined; } return test.results?.at?.(-1)?.status ?? test.status; } function indexReport(report) { const byId = new Map(); const byKey = new Map(); if (!report?.suites) { return { byId, byKey }; } report.suites.forEach((suite) => { suite.specs?.forEach((spec) => { spec.tests?.forEach((test) => { const entry = { suite, spec, test }; if (test.testId) { byId.set(test.testId, entry); } const key = buildTestKey(suite.file, test.projectName, test.title); byKey.set(key, entry); }); }); }); return { byId, byKey }; } function insertTestIntoReport(report, entry) { const suites = report.suites ?? []; let suite = suites.find((candidate) => candidate.file === entry.suite.file); if (!suite) { suite = JSON.parse(JSON.stringify(entry.suite)); suite.specs = []; report.suites = [...suites, suite]; } let spec = suite.specs.find((candidate) => candidate.title === entry.spec.title); if (!spec) { spec = JSON.parse(JSON.stringify(entry.spec)); spec.tests = []; suite.specs.push(spec); } const testClone = JSON.parse(JSON.stringify(entry.test)); spec.tests.push(testClone); return { suite, spec, test: testClone }; } function computeReportStats(report) { let expected = 0; let unexpected = 0; let skipped = 0; let flaky = 0; let total = 0; let duration = 0; if (!report?.suites) { return report?.stats ?? {}; } report.suites.forEach((suite) => { suite.specs?.forEach((spec) => { spec.tests?.forEach((test) => { total += 1; const finalResult = test.results?.at(-1); if (finalResult?.duration) { duration += finalResult.duration; } const status = finalResult?.status ?? test.status; switch (status) { case 'passed': expected += 1; break; case 'skipped': skipped += 1; break; case 'flaky': flaky += 1; break; case 'failed': case 'timedOut': case 'interrupted': unexpected += 1; break; default: unexpected += 1; } }); }); }); return { expected, unexpected, flaky, skipped, duration, total, }; } //# sourceMappingURL=merge.js.map