donobu 5.28.0 → 5.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/cli/donobu-cli.d.ts +7 -1
  2. package/dist/cli/donobu-cli.js +195 -84
  3. package/dist/esm/cli/donobu-cli.d.ts +7 -1
  4. package/dist/esm/cli/donobu-cli.js +195 -84
  5. package/dist/esm/lib/test/testExtension.d.ts +31 -1
  6. package/dist/esm/lib/test/testExtension.js +273 -0
  7. package/dist/esm/managers/DonobuStack.js +1 -1
  8. package/dist/esm/managers/TestsManager.d.ts +2 -2
  9. package/dist/esm/managers/TestsManager.js +10 -0
  10. package/dist/esm/models/PaginatedResult.d.ts +15 -0
  11. package/dist/esm/models/PaginatedResult.js +13 -2
  12. package/dist/esm/models/TestMetadata.d.ts +630 -0
  13. package/dist/esm/models/TestMetadata.js +10 -1
  14. package/dist/esm/persistence/tests/TestsPersistence.d.ts +2 -2
  15. package/dist/esm/persistence/tests/TestsPersistenceDonobuApi.d.ts +3 -3
  16. package/dist/esm/persistence/tests/TestsPersistenceDonobuApi.js +18 -2
  17. package/dist/esm/persistence/tests/TestsPersistenceRegistry.d.ts +9 -1
  18. package/dist/esm/persistence/tests/TestsPersistenceRegistry.js +9 -2
  19. package/dist/esm/persistence/tests/TestsPersistenceSqlite.d.ts +2 -1
  20. package/dist/esm/persistence/tests/TestsPersistenceSqlite.js +65 -6
  21. package/dist/esm/persistence/tests/TestsPersistenceVolatile.d.ts +22 -3
  22. package/dist/esm/persistence/tests/TestsPersistenceVolatile.js +69 -4
  23. package/dist/esm/reporter/renderMarkdown.js +1 -1
  24. package/dist/lib/test/testExtension.d.ts +31 -1
  25. package/dist/lib/test/testExtension.js +273 -0
  26. package/dist/managers/DonobuStack.js +1 -1
  27. package/dist/managers/TestsManager.d.ts +2 -2
  28. package/dist/managers/TestsManager.js +10 -0
  29. package/dist/models/PaginatedResult.d.ts +15 -0
  30. package/dist/models/PaginatedResult.js +13 -2
  31. package/dist/models/TestMetadata.d.ts +630 -0
  32. package/dist/models/TestMetadata.js +10 -1
  33. package/dist/persistence/tests/TestsPersistence.d.ts +2 -2
  34. package/dist/persistence/tests/TestsPersistenceDonobuApi.d.ts +3 -3
  35. package/dist/persistence/tests/TestsPersistenceDonobuApi.js +18 -2
  36. package/dist/persistence/tests/TestsPersistenceRegistry.d.ts +9 -1
  37. package/dist/persistence/tests/TestsPersistenceRegistry.js +9 -2
  38. package/dist/persistence/tests/TestsPersistenceSqlite.d.ts +2 -1
  39. package/dist/persistence/tests/TestsPersistenceSqlite.js +65 -6
  40. package/dist/persistence/tests/TestsPersistenceVolatile.d.ts +22 -3
  41. package/dist/persistence/tests/TestsPersistenceVolatile.js +69 -4
  42. package/dist/reporter/renderMarkdown.js +1 -1
  43. package/package.json +1 -1
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  declare function hasReporterArg(args: string[]): boolean;
3
- declare function injectJsonReporterIntoArgs(args: string[]): boolean;
3
+ declare function injectJsonReporterIntoArgs(args: string[]): {
4
+ /** True when the args contained any `--reporter` / `-r` flag. */
5
+ reporterFlagFound: boolean;
6
+ /** True when the user's args already listed `json` — i.e. they configured
7
+ * a JSON reporter themselves rather than us force-injecting one. */
8
+ userHadJson: boolean;
9
+ };
4
10
  declare function ensureReporterValueHasJson(value: string): {
5
11
  value: string;
6
12
  changed: boolean;
@@ -309,8 +309,13 @@ const noopAsync = async () => { };
309
309
  */
310
310
  async function ensureJsonReporter(originalArgs, options = {}) {
311
311
  const args = [...originalArgs];
312
- if (injectJsonReporterIntoArgs(args)) {
313
- return { args, cleanup: noopAsync };
312
+ const argInjection = injectJsonReporterIntoArgs(args);
313
+ if (argInjection.reporterFlagFound) {
314
+ return {
315
+ args,
316
+ cleanup: noopAsync,
317
+ resolveUserConfiguredJson: async () => argInjection.userHadJson,
318
+ };
314
319
  }
315
320
  const configPath = await resolvePlaywrightConfigPath(args);
316
321
  if (!configPath) {
@@ -322,13 +327,21 @@ async function ensureJsonReporter(originalArgs, options = {}) {
322
327
  args.push('--reporter=json');
323
328
  }
324
329
  Logger_1.appLogger.debug('No Playwright config detected; falling back to CLI --reporter=json injection.');
325
- return { args, cleanup: noopAsync };
330
+ return {
331
+ args,
332
+ cleanup: noopAsync,
333
+ resolveUserConfiguredJson: async () => false,
334
+ };
326
335
  }
327
- const { configPath: wrapperPath, cleanup } = await createConfigWrapperWithJsonReporter(configPath, options);
328
- Logger_1.appLogger.debug(`Augmenting Playwright config at ${configPath} with temporary wrapper ${wrapperPath} to ensure JSON reporter.`);
336
+ const wrapper = await createConfigWrapperWithJsonReporter(configPath, options);
337
+ Logger_1.appLogger.debug(`Augmenting Playwright config at ${configPath} with temporary wrapper ${wrapper.configPath} to ensure JSON reporter.`);
329
338
  const strippedArgs = stripConfigArgs(args);
330
- const finalArgs = insertConfigArg(strippedArgs, wrapperPath);
331
- return { args: finalArgs, cleanup };
339
+ const finalArgs = insertConfigArg(strippedArgs, wrapper.configPath);
340
+ return {
341
+ args: finalArgs,
342
+ cleanup: wrapper.cleanup,
343
+ resolveUserConfiguredJson: wrapper.resolveUserConfiguredJson,
344
+ };
332
345
  }
333
346
  function hasReporterArg(args) {
334
347
  for (const arg of args) {
@@ -346,6 +359,7 @@ function hasReporterArg(args) {
346
359
  }
347
360
  function injectJsonReporterIntoArgs(args) {
348
361
  let reporterFlagFound = false;
362
+ let userHadJson = false;
349
363
  for (let i = 0; i < args.length; i += 1) {
350
364
  const arg = args[i];
351
365
  if (arg === '--') {
@@ -357,8 +371,8 @@ function injectJsonReporterIntoArgs(args) {
357
371
  if (valueIndex < args.length) {
358
372
  const { value, changed } = ensureReporterValueHasJson(args[valueIndex]);
359
373
  args[valueIndex] = value;
360
- if (changed) {
361
- reporterFlagFound = true;
374
+ if (!changed) {
375
+ userHadJson = true;
362
376
  }
363
377
  }
364
378
  i += 1;
@@ -367,11 +381,14 @@ function injectJsonReporterIntoArgs(args) {
367
381
  if (arg.startsWith('--reporter=') || arg.startsWith('-r=')) {
368
382
  reporterFlagFound = true;
369
383
  const [prefix, rawValue] = arg.split('=', 2);
370
- const { value } = ensureReporterValueHasJson(rawValue ?? '');
384
+ const { value, changed } = ensureReporterValueHasJson(rawValue ?? '');
371
385
  args[i] = `${prefix}=${value}`;
386
+ if (!changed) {
387
+ userHadJson = true;
388
+ }
372
389
  }
373
390
  }
374
- return reporterFlagFound;
391
+ return { reporterFlagFound, userHadJson };
375
392
  }
376
393
  function ensureReporterValueHasJson(value) {
377
394
  const segments = value
@@ -466,7 +483,8 @@ function insertConfigArg(args, configPath) {
466
483
  async function createConfigWrapperWithJsonReporter(originalConfigPath, options = {}) {
467
484
  const stagingDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'donobu-playwright-config-'));
468
485
  const wrapperPath = path.join(stagingDir, 'playwright.config.cjs');
469
- const content = buildConfigWrapperContent(originalConfigPath, options.jsonOutputFile);
486
+ const sentinelPath = path.join(stagingDir, '.donobu-user-had-json');
487
+ const content = buildConfigWrapperContent(originalConfigPath, sentinelPath, options.jsonOutputFile);
470
488
  await fs_1.promises.writeFile(wrapperPath, content, 'utf-8');
471
489
  const cleanup = async () => {
472
490
  try {
@@ -476,10 +494,25 @@ async function createConfigWrapperWithJsonReporter(originalConfigPath, options =
476
494
  Logger_1.appLogger.warn(`Failed to remove temporary Playwright config at ${stagingDir}.`, error);
477
495
  }
478
496
  };
479
- return { configPath: wrapperPath, cleanup };
497
+ // The wrapper writes the sentinel inside its top-level config-load code, so
498
+ // it exists by the time Playwright has loaded the config. Read it after the
499
+ // wrapped run finishes and before `cleanup` tears the staging dir down.
500
+ const resolveUserConfiguredJson = async () => {
501
+ try {
502
+ const raw = await fs_1.promises.readFile(sentinelPath, 'utf8');
503
+ return raw.trim() === '1';
504
+ }
505
+ catch {
506
+ // Sentinel missing (wrapper never loaded, or already cleaned up). Err on
507
+ // the side of "no user JSON" so we don't leave a giant artifact behind.
508
+ return false;
509
+ }
510
+ };
511
+ return { configPath: wrapperPath, cleanup, resolveUserConfiguredJson };
480
512
  }
481
- function buildConfigWrapperContent(originalConfigPath, jsonOutputFileOverride) {
513
+ function buildConfigWrapperContent(originalConfigPath, userHadJsonSentinelPath, jsonOutputFileOverride) {
482
514
  const sanitisedPath = originalConfigPath.replace(/\\/g, '\\\\');
515
+ const sentinelPath = userHadJsonSentinelPath.replace(/\\/g, '\\\\');
483
516
  const forcedJsonPath = jsonOutputFileOverride
484
517
  ? jsonOutputFileOverride.replace(/\\/g, '\\\\')
485
518
  : null;
@@ -489,6 +522,7 @@ function buildConfigWrapperContent(originalConfigPath, jsonOutputFileOverride) {
489
522
 
490
523
  const path = require('path');
491
524
  const forcedJsonOutputFile = ${forcedLiteral};
525
+ const userHadJsonSentinelPath = '${sentinelPath}';
492
526
 
493
527
  function loadBaseConfig() {
494
528
  const imported = require('${sanitisedPath}');
@@ -604,6 +638,16 @@ const hasJsonReporter = reporters.some((entry) => {
604
638
  return false;
605
639
  });
606
640
 
641
+ // Tell the parent orchestrator whether the user's own config defined a JSON
642
+ // reporter (vs us about to force-inject one). The orchestrator reads this to
643
+ // decide whether to persist the merged auto-heal report — we don't want to
644
+ // leave a multi-megabyte JSON artifact behind for users who never asked for one.
645
+ try {
646
+ require('fs').writeFileSync(userHadJsonSentinelPath, hasJsonReporter ? '1' : '0', 'utf8');
647
+ } catch (_) {
648
+ // Non-fatal — the orchestrator falls back to "no user JSON" if missing.
649
+ }
650
+
607
651
  if (!hasJsonReporter) {
608
652
  const outputFile =
609
653
  forcedJsonOutputFile ||
@@ -861,6 +905,35 @@ async function overwriteReportTargets(sourcePath, targets) {
861
905
  }
862
906
  }));
863
907
  }
908
+ /**
909
+ * Remove the JSON artifacts Donobu wrote purely for its own use:
910
+ *
911
+ * - The Donobu reporter state file (`.donobu-report-state.json`) — internal
912
+ * IPC between the HTML/Markdown/Slack reporters and this orchestrator.
913
+ * Always safe to delete once orchestration finishes; nothing else reads it.
914
+ * - The Playwright JSON report — when the user did not configure a JSON
915
+ * reporter themselves, this file is purely Donobu's by-product (we forced
916
+ * the JSON reporter on to drive treatment plans and the merge step).
917
+ *
918
+ * Best-effort: missing files are ignored. Any failure is non-fatal — these
919
+ * cleanups don't affect test exit code.
920
+ */
921
+ async function cleanupForceInjectedJsonArtifacts(params) {
922
+ const targets = [
923
+ path.join(params.playwrightOutputDir, model_1.DONOBU_REPORT_STATE_FILENAME),
924
+ ];
925
+ if (!params.userConfiguredJsonReporter && params.discoveredJsonPath) {
926
+ targets.push(params.discoveredJsonPath);
927
+ }
928
+ await Promise.all(targets.map(async (target) => {
929
+ try {
930
+ await fs_1.promises.rm(target, { force: true });
931
+ }
932
+ catch (error) {
933
+ Logger_1.appLogger.debug(`Failed to remove force-injected JSON artifact at ${target}: ${error}`);
934
+ }
935
+ }));
936
+ }
864
937
  /**
865
938
  * Donobu always wants Playwright's JSON reporter enabled so we can build
866
939
  * treatment plans. If the user did not explicitly configure it we add the
@@ -1263,11 +1336,17 @@ async function attemptAutoHealRun(params) {
1263
1336
  finally {
1264
1337
  await reporterSetup.cleanup();
1265
1338
  }
1266
- const healReportDestination = path.join(params.playwrightOutputDir, `donobu-auto-heal-report-${Date.now()}.json`);
1267
- const healReportCopy = await copyJsonReport(staging.playwrightOutputDir, healReportDestination, {
1268
- envOverrides,
1269
- additionalCandidates: [healJsonReportPath],
1270
- });
1339
+ // Persist the heal-run JSON next to the user's other reports only when
1340
+ // they actually configured a JSON reporter — otherwise the file is just
1341
+ // Donobu's by-product. The merge step below reads the report from the
1342
+ // staging dir's state file, which is still alive at this point, so we
1343
+ // don't need a persisted copy for our own use.
1344
+ const healReportCopy = params.userConfiguredJsonReporter
1345
+ ? await copyJsonReport(staging.playwrightOutputDir, path.join(params.playwrightOutputDir, `donobu-auto-heal-report-${Date.now()}.json`), {
1346
+ envOverrides,
1347
+ additionalCandidates: [healJsonReportPath],
1348
+ })
1349
+ : null;
1271
1350
  if (healTriageEnabled && healTriageContext && healExitCode !== 0) {
1272
1351
  await postProcessTriageRun(healTriageContext, healArgsForRun, healReportCopy?.destinationPath ?? undefined);
1273
1352
  const finalTriageBaseDir = params.options.triageOutputDir
@@ -1296,18 +1375,19 @@ async function attemptAutoHealRun(params) {
1296
1375
  // reporter output for configs that don't wire up the Donobu reporter.
1297
1376
  const healReport = await loadDonobuReportForMerge(staging.playwrightOutputDir, healReportCopy?.destinationPath ?? undefined);
1298
1377
  if (params.initialReport || healReport) {
1299
- // Write the merged report directly to the primary report target when
1300
- // available, avoiding a redundant intermediate file.
1378
+ // Write the merged report directly to the user's primary JSON target
1379
+ // when one exists. When the user did not configure a JSON reporter,
1380
+ // `reportTargets` is empty — skip the disk write entirely rather than
1381
+ // leaving a multi-megabyte fallback artifact behind. The HTML / Markdown
1382
+ // / Slack regeneration below uses the in-memory merged object, so they
1383
+ // don't depend on a JSON file landing on disk.
1301
1384
  const primaryTarget = params.reportTargets[0];
1302
- const mergedReportPath = primaryTarget
1303
- ? primaryTarget
1304
- : path.join(params.playwrightOutputDir, `donobu-merged-report-${Date.now()}.json`);
1305
1385
  const mergedReport = await mergePlaywrightJsonReports({
1306
1386
  initialReport: params.initialReport ?? null,
1307
1387
  initialReportSourcePath: params.initialReportSourcePath,
1308
1388
  healReport,
1309
1389
  healReportSourcePath: healReportCopy?.destinationPath ?? undefined,
1310
- mergedReportPath,
1390
+ mergedReportPath: primaryTarget,
1311
1391
  healedTests: evaluation.eligiblePlans.map((record) => ({
1312
1392
  plan: record.plan,
1313
1393
  testCase: record.evidence.failureContext.testCase,
@@ -1318,8 +1398,8 @@ async function attemptAutoHealRun(params) {
1318
1398
  });
1319
1399
  // Copy to any remaining report targets (skip the primary — already
1320
1400
  // written there).
1321
- if (mergedReport && params.reportTargets.length > 1) {
1322
- await overwriteReportTargets(mergedReportPath, params.reportTargets.slice(1));
1401
+ if (mergedReport && primaryTarget && params.reportTargets.length > 1) {
1402
+ await overwriteReportTargets(primaryTarget, params.reportTargets.slice(1));
1323
1403
  }
1324
1404
  if (mergedReport) {
1325
1405
  await regenerateDonobuReports(mergedReport);
@@ -1482,10 +1562,14 @@ async function loadDonobuReportForMerge(outputDir, fallbackPath) {
1482
1562
  }
1483
1563
  /**
1484
1564
  * Combine the reports from the original failed run and any auto-heal rerun,
1485
- * relocate any heal-run attachments to persistent storage, and write the
1486
- * merged JSON to `mergedReportPath` for downstream dashboards. Returns the
1487
- * in-memory merged report so the caller can drive HTML regeneration without
1488
- * re-reading from disk.
1565
+ * relocate any heal-run attachments to persistent storage, and optionally
1566
+ * write the merged JSON to `mergedReportPath` for downstream dashboards.
1567
+ * Returns the in-memory merged report so the caller can drive HTML
1568
+ * regeneration without re-reading from disk.
1569
+ *
1570
+ * When `mergedReportPath` is omitted, the merge happens entirely in memory —
1571
+ * useful when the user has not configured a JSON reporter and we don't want
1572
+ * to leave a giant artifact on disk just for our internal use.
1489
1573
  *
1490
1574
  * Also forwards `metadata.donobuOutputs` from whichever input recorded it
1491
1575
  * (typically the initial run's state file), so regeneration lands every
@@ -1523,9 +1607,14 @@ async function mergePlaywrightJsonReports(params) {
1523
1607
  if (params.outputDir) {
1524
1608
  await relocateTemporaryAttachments(merged, params.outputDir);
1525
1609
  }
1526
- await ensureDirectory(path.dirname(params.mergedReportPath));
1527
- await fs_1.promises.writeFile(params.mergedReportPath, JSON.stringify(merged, null, 2), 'utf8');
1528
- Logger_1.appLogger.debug(`Saved merged Playwright report to ${params.mergedReportPath}.`);
1610
+ if (params.mergedReportPath) {
1611
+ await ensureDirectory(path.dirname(params.mergedReportPath));
1612
+ // Compact JSON these reports are machine-consumed (auto-heal merge,
1613
+ // dashboards, downstream tooling), and pretty-printing inflates the file
1614
+ // by 10–15% on a multi-megabyte payload for no human benefit.
1615
+ await fs_1.promises.writeFile(params.mergedReportPath, JSON.stringify(merged), 'utf8');
1616
+ Logger_1.appLogger.debug(`Saved merged Playwright report to ${params.mergedReportPath}.`);
1617
+ }
1529
1618
  return merged;
1530
1619
  }
1531
1620
  async function readJsonIfExists(filePath) {
@@ -1640,8 +1729,12 @@ async function runTestCommand(cliArgs) {
1640
1729
  const runArgs = reporterSetup.args;
1641
1730
  Logger_1.appLogger.debug(`Initial Playwright args: ${JSON.stringify(runArgs)} with env overrides ${JSON.stringify(envOverrides)}`);
1642
1731
  let exitCode;
1732
+ let userConfiguredJsonReporter = false;
1643
1733
  try {
1644
1734
  exitCode = await runPlaywright(runArgs, envOverrides);
1735
+ // Read before cleanup, since the wrapper-sentinel lives in the staging dir.
1736
+ userConfiguredJsonReporter =
1737
+ await reporterSetup.resolveUserConfiguredJson();
1645
1738
  }
1646
1739
  finally {
1647
1740
  await reporterSetup.cleanup();
@@ -1652,68 +1745,86 @@ async function runTestCommand(cliArgs) {
1652
1745
  // can populate reportTargets regardless of whether auto-heal is needed.
1653
1746
  // Uses the same candidate-resolution order as loadJsonReport to avoid
1654
1747
  // accidentally picking up leftover donobu-*-report-*.json files.
1748
+ // Only treat the discovered report as a real "target to overwrite" when the
1749
+ // user actually configured a JSON reporter — when Donobu force-injected one
1750
+ // for its own use, leaving a multi-megabyte JSON behind is just noise.
1655
1751
  const discoveredReport = await findJsonReportPath(playwrightOutputDir, {
1656
1752
  envOverrides,
1657
1753
  additionalCandidates: [
1658
1754
  path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME),
1659
1755
  ],
1660
1756
  });
1661
- if (discoveredReport) {
1757
+ if (discoveredReport && userConfiguredJsonReporter) {
1662
1758
  reportTargets.add(discoveredReport);
1663
1759
  }
1664
- if (exitCode === 0 || !effectiveOptions.autoHeal) {
1665
- // Auto-heal wasn't attempted (either tests passed or the user disabled
1666
- // it). If a Slack reporter deferred its POST waiting for us, deliver the
1667
- // initial run's payload now nothing further will be re-rendered.
1668
- if (effectiveOptions.autoHeal) {
1760
+ try {
1761
+ if (exitCode === 0 || !effectiveOptions.autoHeal) {
1762
+ // Auto-heal wasn't attempted (either tests passed or the user disabled
1763
+ // it). If a Slack reporter deferred its POST waiting for us, deliver the
1764
+ // initial run's payload now — nothing further will be re-rendered.
1765
+ if (effectiveOptions.autoHeal) {
1766
+ await postDeferredSlackFromInitialRun(playwrightOutputDir);
1767
+ }
1768
+ return exitCode;
1769
+ }
1770
+ // Tests failed and auto-heal is enabled — load the initial report into
1771
+ // memory so we can merge it with the heal-run report later without writing
1772
+ // a redundant copy to disk.
1773
+ let loadedPlaywrightJson = null;
1774
+ let initialDonobuReport = null;
1775
+ if (triageEnabled) {
1776
+ loadedPlaywrightJson = await loadJsonReport(playwrightOutputDir, {
1777
+ envOverrides,
1778
+ additionalCandidates: [
1779
+ path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME),
1780
+ ],
1781
+ });
1782
+ if (loadedPlaywrightJson && userConfiguredJsonReporter) {
1783
+ reportTargets.add(loadedPlaywrightJson.sourcePath);
1784
+ }
1785
+ // Prefer the Donobu reporter's state file (carries the HTML output path
1786
+ // and any Donobu-specific metadata); fall back to the Playwright JSON for
1787
+ // configs that don't wire up the Donobu reporter.
1788
+ const stateReport = await readJsonIfExists(path.join(playwrightOutputDir, model_1.DONOBU_REPORT_STATE_FILENAME));
1789
+ initialDonobuReport =
1790
+ stateReport ?? loadedPlaywrightJson?.data ?? null;
1791
+ }
1792
+ if (triageEnabled && triageContext) {
1793
+ generatedPlans = await postProcessTriageRun(triageContext, playwrightArgs, loadedPlaywrightJson?.sourcePath);
1794
+ }
1795
+ const autoHealOutcome = await attemptAutoHealRun({
1796
+ options: effectiveOptions,
1797
+ playwrightArgs,
1798
+ playwrightOutputDir,
1799
+ generatedPlans,
1800
+ currentExitCode: exitCode,
1801
+ initialReport: initialDonobuReport,
1802
+ initialReportSourcePath: loadedPlaywrightJson?.sourcePath,
1803
+ triageRunDir: triageContext?.runDir,
1804
+ reportTargets: Array.from(reportTargets),
1805
+ userConfiguredJsonReporter,
1806
+ });
1807
+ // When auto-heal was eligible-checked but didn't actually run a rerun (no
1808
+ // actionable directives), nothing downstream re-renders the Slack payload —
1809
+ // deliver the pre-heal payload now so we honor the "one post per run"
1810
+ // guarantee the reporter was deferring for.
1811
+ if (!autoHealOutcome.attempted) {
1669
1812
  await postDeferredSlackFromInitialRun(playwrightOutputDir);
1670
1813
  }
1671
- return exitCode;
1814
+ return autoHealOutcome.exitCode;
1672
1815
  }
1673
- // Tests failed and auto-heal is enabled — load the initial report into
1674
- // memory so we can merge it with the heal-run report later without writing
1675
- // a redundant copy to disk.
1676
- let loadedPlaywrightJson = null;
1677
- let initialDonobuReport = null;
1678
- if (triageEnabled) {
1679
- loadedPlaywrightJson = await loadJsonReport(playwrightOutputDir, {
1680
- envOverrides,
1681
- additionalCandidates: [
1682
- path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME),
1683
- ],
1816
+ finally {
1817
+ // Clean up the JSON artifacts Donobu created for its own internal use.
1818
+ // Runs after every return path (early-exit when tests pass / auto-heal
1819
+ // disabled, and the post-heal path) so we don't leave the state file or
1820
+ // the force-injected JSON behind. Anything the user actually asked for
1821
+ // (their own JSON reporter target, HTML, Markdown, Slack) is preserved.
1822
+ await cleanupForceInjectedJsonArtifacts({
1823
+ playwrightOutputDir,
1824
+ discoveredJsonPath: discoveredReport,
1825
+ userConfiguredJsonReporter,
1684
1826
  });
1685
- if (loadedPlaywrightJson) {
1686
- reportTargets.add(loadedPlaywrightJson.sourcePath);
1687
- }
1688
- // Prefer the Donobu reporter's state file (carries the HTML output path
1689
- // and any Donobu-specific metadata); fall back to the Playwright JSON for
1690
- // configs that don't wire up the Donobu reporter.
1691
- const stateReport = await readJsonIfExists(path.join(playwrightOutputDir, model_1.DONOBU_REPORT_STATE_FILENAME));
1692
- initialDonobuReport =
1693
- stateReport ?? loadedPlaywrightJson?.data ?? null;
1694
- }
1695
- if (triageEnabled && triageContext) {
1696
- generatedPlans = await postProcessTriageRun(triageContext, playwrightArgs, loadedPlaywrightJson?.sourcePath);
1697
- }
1698
- const autoHealOutcome = await attemptAutoHealRun({
1699
- options: effectiveOptions,
1700
- playwrightArgs,
1701
- playwrightOutputDir,
1702
- generatedPlans,
1703
- currentExitCode: exitCode,
1704
- initialReport: initialDonobuReport,
1705
- initialReportSourcePath: loadedPlaywrightJson?.sourcePath,
1706
- triageRunDir: triageContext?.runDir,
1707
- reportTargets: Array.from(reportTargets),
1708
- });
1709
- // When auto-heal was eligible-checked but didn't actually run a rerun (no
1710
- // actionable directives), nothing downstream re-renders the Slack payload —
1711
- // deliver the pre-heal payload now so we honor the "one post per run"
1712
- // guarantee the reporter was deferring for.
1713
- if (!autoHealOutcome.attempted) {
1714
- await postDeferredSlackFromInitialRun(playwrightOutputDir);
1715
- }
1716
- return autoHealOutcome.exitCode;
1827
+ }
1717
1828
  }
1718
1829
  /**
1719
1830
  * When auto-heal was enabled but no rerun happened (tests passed, or no
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  declare function hasReporterArg(args: string[]): boolean;
3
- declare function injectJsonReporterIntoArgs(args: string[]): boolean;
3
+ declare function injectJsonReporterIntoArgs(args: string[]): {
4
+ /** True when the args contained any `--reporter` / `-r` flag. */
5
+ reporterFlagFound: boolean;
6
+ /** True when the user's args already listed `json` — i.e. they configured
7
+ * a JSON reporter themselves rather than us force-injecting one. */
8
+ userHadJson: boolean;
9
+ };
4
10
  declare function ensureReporterValueHasJson(value: string): {
5
11
  value: string;
6
12
  changed: boolean;