donobu 2.47.0 → 2.47.1

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.
@@ -64,13 +64,14 @@ const triageTestFailure_1 = require("../lib/utils/triageTestFailure");
64
64
  const Logger_1 = require("../utils/Logger");
65
65
  const FAILURE_EVIDENCE_PREFIX = 'failure-evidence-';
66
66
  const TREATMENT_PLAN_PREFIX = 'treatment-plan-';
67
+ const PLAYWRIGHT_JSON_REPORT_FILENAME = 'report.json';
67
68
  /**
68
69
  * Execute `npx playwright` while wiring Donobu-specific environment controls.
69
70
  * Streams stdout/stderr to the current process so our CLI mirrors the native
70
71
  * Playwright experience, and forwards termination signals to keep CI parity.
71
72
  */
72
73
  async function runPlaywright(args, envOverrides = {}) {
73
- Logger_1.appLogger.info(`Running Playwright with args: ${JSON.stringify(args)}`);
74
+ Logger_1.appLogger.debug(`Running Playwright with args: ${JSON.stringify(args)}`);
74
75
  if (Object.keys(envOverrides).length > 0) {
75
76
  Logger_1.appLogger.debug(`Playwright env overrides: ${JSON.stringify(envOverrides, null, 2)}`);
76
77
  }
@@ -298,20 +299,23 @@ const noopAsync = async () => { };
298
299
  * wrapper config when necessary so we can append the JSON reporter alongside
299
300
  * user-specified reporters.
300
301
  */
301
- async function ensureJsonReporter(originalArgs) {
302
+ async function ensureJsonReporter(originalArgs, options = {}) {
302
303
  const args = [...originalArgs];
303
- if (injectJsonReporterIntoArgs(args)) {
304
+ if (injectJsonReporterIntoArgs(args, options)) {
304
305
  return { args, cleanup: noopAsync };
305
306
  }
306
307
  const configPath = await resolvePlaywrightConfigPath(args);
307
308
  if (!configPath) {
308
309
  if (!hasReporterArg(args)) {
309
- args.push('--reporter=json');
310
+ const reporterValue = options.jsonOutputFile
311
+ ? `json=${options.jsonOutputFile}`
312
+ : 'json';
313
+ args.push(`--reporter=${reporterValue}`);
310
314
  }
311
315
  Logger_1.appLogger.debug('No Playwright config detected; falling back to CLI --reporter=json injection.');
312
316
  return { args, cleanup: noopAsync };
313
317
  }
314
- const { configPath: wrapperPath, cleanup } = await createConfigWrapperWithJsonReporter(configPath);
318
+ const { configPath: wrapperPath, cleanup } = await createConfigWrapperWithJsonReporter(configPath, options);
315
319
  Logger_1.appLogger.debug(`Augmenting Playwright config at ${configPath} with temporary wrapper ${wrapperPath} to ensure JSON reporter.`);
316
320
  const strippedArgs = stripConfigArgs(args);
317
321
  const finalArgs = insertConfigArg(strippedArgs, wrapperPath);
@@ -331,7 +335,7 @@ function hasReporterArg(args) {
331
335
  }
332
336
  return false;
333
337
  }
334
- function injectJsonReporterIntoArgs(args) {
338
+ function injectJsonReporterIntoArgs(args, options = {}) {
335
339
  let reporterFlagFound = false;
336
340
  for (let i = 0; i < args.length; i += 1) {
337
341
  const arg = args[i];
@@ -342,7 +346,7 @@ function injectJsonReporterIntoArgs(args) {
342
346
  reporterFlagFound = true;
343
347
  const valueIndex = i + 1;
344
348
  if (valueIndex < args.length) {
345
- const { value, changed } = ensureReporterValueHasJson(args[valueIndex]);
349
+ const { value, changed } = ensureReporterValueHasJson(args[valueIndex], options);
346
350
  args[valueIndex] = value;
347
351
  if (changed) {
348
352
  reporterFlagFound = true;
@@ -354,13 +358,13 @@ function injectJsonReporterIntoArgs(args) {
354
358
  if (arg.startsWith('--reporter=') || arg.startsWith('-r=')) {
355
359
  reporterFlagFound = true;
356
360
  const [prefix, rawValue] = arg.split('=', 2);
357
- const { value } = ensureReporterValueHasJson(rawValue ?? '');
361
+ const { value } = ensureReporterValueHasJson(rawValue ?? '', options);
358
362
  args[i] = `${prefix}=${value}`;
359
363
  }
360
364
  }
361
365
  return reporterFlagFound;
362
366
  }
363
- function ensureReporterValueHasJson(value) {
367
+ function ensureReporterValueHasJson(value, options = {}) {
364
368
  const segments = value
365
369
  .split(',')
366
370
  .map((segment) => segment.trim())
@@ -369,7 +373,10 @@ function ensureReporterValueHasJson(value) {
369
373
  if (hasJson) {
370
374
  return { value, changed: false };
371
375
  }
372
- segments.push('json');
376
+ const jsonEntry = options.jsonOutputFile
377
+ ? `json=${options.jsonOutputFile}`
378
+ : 'json';
379
+ segments.push(jsonEntry);
373
380
  return { value: segments.join(','), changed: true };
374
381
  }
375
382
  async function resolvePlaywrightConfigPath(args) {
@@ -447,10 +454,10 @@ function insertConfigArg(args, configPath) {
447
454
  ...args.slice(dashDashIndex),
448
455
  ];
449
456
  }
450
- async function createConfigWrapperWithJsonReporter(originalConfigPath) {
457
+ async function createConfigWrapperWithJsonReporter(originalConfigPath, options = {}) {
451
458
  const stagingDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'donobu-playwright-config-'));
452
459
  const wrapperPath = path.join(stagingDir, 'playwright.config.cjs');
453
- const content = buildConfigWrapperContent(originalConfigPath);
460
+ const content = buildConfigWrapperContent(originalConfigPath, options.jsonOutputFile);
454
461
  await fs_1.promises.writeFile(wrapperPath, content, 'utf-8');
455
462
  const cleanup = async () => {
456
463
  try {
@@ -462,11 +469,17 @@ async function createConfigWrapperWithJsonReporter(originalConfigPath) {
462
469
  };
463
470
  return { configPath: wrapperPath, cleanup };
464
471
  }
465
- function buildConfigWrapperContent(originalConfigPath) {
472
+ function buildConfigWrapperContent(originalConfigPath, jsonOutputFileOverride) {
466
473
  const sanitisedPath = originalConfigPath.replace(/\\/g, '\\\\');
467
- return `'use strict';
474
+ const forcedJsonPath = jsonOutputFileOverride
475
+ ? jsonOutputFileOverride.replace(/\\/g, '\\\\')
476
+ : null;
477
+ const defaultJsonName = PLAYWRIGHT_JSON_REPORT_FILENAME;
478
+ const forcedLiteral = forcedJsonPath ? `'${forcedJsonPath}'` : 'null';
479
+ return `"use strict";
468
480
 
469
481
  const path = require('path');
482
+ const forcedJsonOutputFile = ${forcedLiteral};
470
483
 
471
484
  function loadBaseConfig() {
472
485
  const imported = require('${sanitisedPath}');
@@ -544,7 +557,10 @@ if (Array.isArray(normalizedConfig.projects)) {
544
557
  if (typeof useConfig.storageState === 'string') {
545
558
  useConfig.storageState = absolutify(useConfig.storageState);
546
559
  }
547
- if (typeof useConfig.baseURL === 'string' && !/^https?:/i.test(useConfig.baseURL)) {
560
+ if (
561
+ typeof useConfig.baseURL === 'string' &&
562
+ !/^https?:/i.test(useConfig.baseURL)
563
+ ) {
548
564
  useConfig.baseURL = absolutify(useConfig.baseURL);
549
565
  }
550
566
  projectConfig.use = useConfig;
@@ -580,18 +596,46 @@ const hasJsonReporter = reporters.some((entry) => {
580
596
  });
581
597
 
582
598
  if (!hasJsonReporter) {
583
- const outputDir = process.env.PLAYWRIGHT_JSON_OUTPUT_DIR
584
- ? path.resolve(process.cwd(), process.env.PLAYWRIGHT_JSON_OUTPUT_DIR)
585
- : path.resolve(configDir, 'test-results');
586
- const outputName = process.env.PLAYWRIGHT_JSON_OUTPUT_NAME || 'report.json';
587
- const outputFile = path.isAbsolute(outputName)
588
- ? outputName
589
- : path.join(outputDir, outputName);
599
+ const outputFile =
600
+ forcedJsonOutputFile ||
601
+ (() => {
602
+ const outputDir = process.env.PLAYWRIGHT_JSON_OUTPUT_DIR
603
+ ? path.resolve(process.cwd(), process.env.PLAYWRIGHT_JSON_OUTPUT_DIR)
604
+ : path.resolve(configDir, 'test-results');
605
+ const outputName =
606
+ process.env.PLAYWRIGHT_JSON_OUTPUT_NAME || '${defaultJsonName}';
607
+ return path.isAbsolute(outputName)
608
+ ? outputName
609
+ : path.join(outputDir, outputName);
610
+ })();
590
611
  reporters.push(['json', { outputFile }]);
591
612
  }
592
613
 
593
614
  const normalisedReporters = reporters.map((entry) => {
594
- if (Array.isArray(entry) && entry.length > 1 && entry[1] && typeof entry[1] === 'object') {
615
+ if (typeof entry === 'string') {
616
+ if (!forcedJsonOutputFile) {
617
+ return entry;
618
+ }
619
+ const segments = entry
620
+ .split(',')
621
+ .map((segment) => segment.trim())
622
+ .filter((segment) => segment.length > 0);
623
+ const rewritten = segments.map((segment) => {
624
+ const [name] = segment.split('=', 2);
625
+ if (name === 'json') {
626
+ return \`json=\${forcedJsonOutputFile}\`;
627
+ }
628
+ return segment;
629
+ });
630
+ return rewritten.join(',');
631
+ }
632
+
633
+ if (
634
+ Array.isArray(entry) &&
635
+ entry.length > 1 &&
636
+ entry[1] &&
637
+ typeof entry[1] === 'object'
638
+ ) {
595
639
  const options = { ...entry[1] };
596
640
  if (options.outputFile) {
597
641
  options.outputFile = absolutify(options.outputFile);
@@ -599,8 +643,19 @@ const normalisedReporters = reporters.map((entry) => {
599
643
  if (options.outputFolder) {
600
644
  options.outputFolder = absolutify(options.outputFolder);
601
645
  }
646
+ if (entry[0] === 'json' && forcedJsonOutputFile) {
647
+ options.outputFile = forcedJsonOutputFile;
648
+ }
602
649
  return [entry[0], options];
603
650
  }
651
+
652
+ if (Array.isArray(entry) && entry.length > 0) {
653
+ const name = typeof entry[0] === 'string' ? entry[0] : '';
654
+ if (name === 'json' && forcedJsonOutputFile) {
655
+ return [entry[0], { outputFile: forcedJsonOutputFile }];
656
+ }
657
+ return entry;
658
+ }
604
659
  return entry;
605
660
  });
606
661
 
@@ -615,23 +670,113 @@ module.exports = {
615
670
  * We keep a stable snapshot because Playwright may delete the file between
616
671
  * retries or when the `--output` folder is cleaned.
617
672
  */
618
- async function copyJsonReport(outputDir, destinationPath) {
619
- const sourcePath = path.join(outputDir, 'report.json');
673
+ async function copyJsonReport(outputDir, destinationPath, options = {}) {
674
+ const candidatePaths = new Set();
675
+ const envDefinedPath = resolveEnvJsonReportPath(options.envOverrides);
676
+ if (envDefinedPath) {
677
+ candidatePaths.add(envDefinedPath);
678
+ }
679
+ candidatePaths.add(path.join(outputDir, PLAYWRIGHT_JSON_REPORT_FILENAME));
680
+ (options.additionalCandidates ?? []).forEach((candidate) => {
681
+ if (candidate) {
682
+ candidatePaths.add(candidate);
683
+ }
684
+ });
685
+ for (const sourcePath of candidatePaths) {
686
+ const copied = await tryCopyReport(sourcePath, destinationPath);
687
+ if (copied) {
688
+ return { sourcePath, destinationPath };
689
+ }
690
+ }
691
+ const fallbackSource = await findJsonReportInDir(outputDir);
692
+ if (fallbackSource) {
693
+ const copied = await tryCopyReport(fallbackSource, destinationPath);
694
+ if (copied) {
695
+ return { sourcePath: fallbackSource, destinationPath };
696
+ }
697
+ }
698
+ return null;
699
+ }
700
+ function resolveEnvJsonReportPath(envOverrides) {
701
+ if (!envOverrides) {
702
+ return null;
703
+ }
704
+ const outputDir = envOverrides.PLAYWRIGHT_JSON_OUTPUT_DIR;
705
+ const outputName = envOverrides.PLAYWRIGHT_JSON_OUTPUT_NAME;
706
+ if (!outputDir || !outputName) {
707
+ return null;
708
+ }
709
+ const resolvedDir = path.isAbsolute(outputDir)
710
+ ? outputDir
711
+ : path.resolve(process.cwd(), outputDir);
712
+ return path.isAbsolute(outputName)
713
+ ? outputName
714
+ : path.join(resolvedDir, outputName);
715
+ }
716
+ async function tryCopyReport(sourcePath, destinationPath) {
620
717
  try {
621
718
  await fs_1.promises.access(sourcePath, fs_1.constants.F_OK);
622
719
  }
623
720
  catch {
624
- return null;
721
+ return false;
625
722
  }
626
723
  await ensureDirectory(path.dirname(destinationPath));
627
724
  try {
628
725
  await fs_1.promises.copyFile(sourcePath, destinationPath);
629
- return destinationPath;
726
+ return true;
630
727
  }
631
728
  catch (error) {
632
729
  Logger_1.appLogger.warn(`Failed to copy Playwright JSON report from ${sourcePath} to ${destinationPath}.`, error);
730
+ return false;
731
+ }
732
+ }
733
+ async function findJsonReportInDir(outputDir) {
734
+ let entries;
735
+ try {
736
+ entries = await fs_1.promises.readdir(outputDir);
737
+ }
738
+ catch {
633
739
  return null;
634
740
  }
741
+ const candidates = entries
742
+ .filter((entry) => entry.endsWith('.json'))
743
+ .sort((a, b) => {
744
+ const aScore = a.includes('report') ? 0 : 1;
745
+ const bScore = b.includes('report') ? 0 : 1;
746
+ if (aScore !== bScore) {
747
+ return aScore - bScore;
748
+ }
749
+ return a.localeCompare(b);
750
+ });
751
+ for (const fileName of candidates) {
752
+ const fullPath = path.join(outputDir, fileName);
753
+ if (await isLikelyPlaywrightReport(fullPath)) {
754
+ return fullPath;
755
+ }
756
+ }
757
+ return null;
758
+ }
759
+ async function isLikelyPlaywrightReport(filePath) {
760
+ try {
761
+ const raw = await fs_1.promises.readFile(filePath, 'utf8');
762
+ const parsed = JSON.parse(raw);
763
+ return (!!parsed && typeof parsed === 'object' && Array.isArray(parsed.suites));
764
+ }
765
+ catch {
766
+ return false;
767
+ }
768
+ }
769
+ async function overwriteReportTargets(sourcePath, targets) {
770
+ const uniqueTargets = Array.from(new Set(targets)).filter((target) => target && target !== sourcePath);
771
+ await Promise.all(uniqueTargets.map(async (targetPath) => {
772
+ try {
773
+ await ensureDirectory(path.dirname(targetPath));
774
+ await fs_1.promises.copyFile(sourcePath, targetPath);
775
+ }
776
+ catch (error) {
777
+ Logger_1.appLogger.warn(`Failed to copy merged Playwright report to ${targetPath}.`, error);
778
+ }
779
+ }));
635
780
  }
636
781
  /**
637
782
  * Donobu always wants Playwright's JSON reporter enabled so we can build
@@ -643,7 +788,7 @@ function applyJsonReportEnv(env, outputDir) {
643
788
  env.PLAYWRIGHT_JSON_OUTPUT_DIR = outputDir;
644
789
  }
645
790
  if (!env.PLAYWRIGHT_JSON_OUTPUT_NAME) {
646
- env.PLAYWRIGHT_JSON_OUTPUT_NAME = 'report.json';
791
+ env.PLAYWRIGHT_JSON_OUTPUT_NAME = PLAYWRIGHT_JSON_REPORT_FILENAME;
647
792
  }
648
793
  }
649
794
  /**
@@ -987,7 +1132,10 @@ async function attemptAutoHealRun(params) {
987
1132
  // Flag downstream systems so they know this invocation came from auto-heal.
988
1133
  envOverrides.DONOBU_AUTO_HEAL_ACTIVE = '1';
989
1134
  Logger_1.appLogger.info(`Auto-heal: applying directives from ${evaluation.eligiblePlans.length} treatment plan(s) and re-running Playwright...`);
990
- const reporterSetup = await ensureJsonReporter(healArgsForRun);
1135
+ const healJsonReportPath = path.join(staging.playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
1136
+ const reporterSetup = await ensureJsonReporter(healArgsForRun, {
1137
+ jsonOutputFile: healJsonReportPath,
1138
+ });
991
1139
  try {
992
1140
  healExitCode = await runPlaywright(reporterSetup.args, envOverrides);
993
1141
  }
@@ -995,9 +1143,12 @@ async function attemptAutoHealRun(params) {
995
1143
  await reporterSetup.cleanup();
996
1144
  }
997
1145
  const healReportDestination = path.join(params.playwrightOutputDir, `donobu-auto-heal-report-${Date.now()}.json`);
998
- const healReportCopy = await copyJsonReport(staging.playwrightOutputDir, healReportDestination);
1146
+ const healReportCopy = await copyJsonReport(staging.playwrightOutputDir, healReportDestination, {
1147
+ envOverrides,
1148
+ additionalCandidates: [healJsonReportPath],
1149
+ });
999
1150
  if (healTriageEnabled && healTriageContext && healExitCode !== 0) {
1000
- await postProcessTriageRun(healTriageContext, healArgsForRun, healReportCopy ?? undefined);
1151
+ await postProcessTriageRun(healTriageContext, healArgsForRun, healReportCopy?.destinationPath ?? undefined);
1001
1152
  const finalTriageBaseDir = params.options.triageOutputDir
1002
1153
  ? path.resolve(params.options.triageOutputDir)
1003
1154
  : path.join(params.playwrightOutputDir, 'donobu-triage');
@@ -1024,7 +1175,7 @@ async function attemptAutoHealRun(params) {
1024
1175
  const mergedReportPath = path.join(params.playwrightOutputDir, `donobu-merged-report-${Date.now()}.json`);
1025
1176
  await mergePlaywrightJsonReports({
1026
1177
  initialReportPath: params.initialReportPath,
1027
- healReportPath: healReportCopy ?? undefined,
1178
+ healReportPath: healReportCopy?.destinationPath ?? undefined,
1028
1179
  mergedReportPath,
1029
1180
  healedTests: evaluation.eligiblePlans.map((record) => ({
1030
1181
  plan: record.plan,
@@ -1032,6 +1183,9 @@ async function attemptAutoHealRun(params) {
1032
1183
  })),
1033
1184
  healSucceeded: healExitCode === 0,
1034
1185
  });
1186
+ if (params.reportTargets.length > 0) {
1187
+ await overwriteReportTargets(mergedReportPath, params.reportTargets);
1188
+ }
1035
1189
  }
1036
1190
  }
1037
1191
  finally {
@@ -1058,7 +1212,8 @@ async function mergePlaywrightJsonReports(params) {
1058
1212
  const healIndex = indexReport(healReport);
1059
1213
  const healedKeys = new Set();
1060
1214
  if (healReport) {
1061
- for (const [, healEntry] of healIndex.byId) {
1215
+ const processedHealEntries = new Set();
1216
+ const processHealEntry = (healEntry) => {
1062
1217
  const key = buildTestKey(healEntry.suite.file, healEntry.test.projectName, healEntry.test.title);
1063
1218
  let combinedEntry = (healEntry.test.testId
1064
1219
  ? combinedIndex.byId.get(healEntry.test.testId)
@@ -1078,16 +1233,23 @@ async function mergePlaywrightJsonReports(params) {
1078
1233
  initialIndex.byKey.get(key) ??
1079
1234
  null;
1080
1235
  const combinedTest = combinedEntry.test;
1081
- combinedTest.results = [
1082
- ...(combinedTest.results ?? []),
1083
- ...(healEntry.test.results ?? []),
1084
- ];
1085
- combinedTest.status = healEntry.test.status;
1236
+ if (healEntry.test.results?.length) {
1237
+ combinedTest.results = [
1238
+ ...(combinedTest.results ?? []),
1239
+ ...healEntry.test.results,
1240
+ ];
1241
+ }
1242
+ if (healEntry.test.status !== undefined) {
1243
+ combinedTest.status = healEntry.test.status;
1244
+ }
1086
1245
  if (healEntry.test.outcome !== undefined) {
1087
1246
  combinedTest.outcome = healEntry.test.outcome;
1088
1247
  }
1089
- const originalStatus = originalEntry?.test?.status;
1090
- if (healEntry.test.status === 'passed' &&
1248
+ const originalStatus = originalEntry
1249
+ ? getFinalResultStatus(originalEntry.test)
1250
+ : undefined;
1251
+ const healStatus = getFinalResultStatus(healEntry.test);
1252
+ if (healStatus === 'passed' &&
1091
1253
  originalStatus &&
1092
1254
  originalStatus !== 'passed') {
1093
1255
  combinedTest.annotations = combinedTest.annotations ?? [];
@@ -1100,11 +1262,22 @@ async function mergePlaywrightJsonReports(params) {
1100
1262
  combinedTest.donobuStatus = 'healed';
1101
1263
  healedKeys.add(key);
1102
1264
  }
1103
- }
1265
+ };
1266
+ const iterateEntries = (entries) => {
1267
+ for (const [, healEntry] of entries) {
1268
+ if (processedHealEntries.has(healEntry)) {
1269
+ continue;
1270
+ }
1271
+ processedHealEntries.add(healEntry);
1272
+ processHealEntry(healEntry);
1273
+ }
1274
+ };
1275
+ iterateEntries(healIndex.byId);
1276
+ iterateEntries(healIndex.byKey);
1104
1277
  }
1105
1278
  if (params.healSucceeded && healedKeys.size === 0) {
1106
1279
  params.healedTests.forEach((descriptor) => {
1107
- const key = buildTestKey(descriptor.testCase.file, descriptor.testCase.projectName, descriptor.testCase.title);
1280
+ const key = buildTestKey(normalizeSpecPath(descriptor.testCase.file), descriptor.testCase.projectName, descriptor.testCase.title);
1108
1281
  const entry = combinedIndex.byKey.get(key);
1109
1282
  if (entry) {
1110
1283
  entry.test.annotations = entry.test.annotations ?? [];
@@ -1132,7 +1305,7 @@ async function mergePlaywrightJsonReports(params) {
1132
1305
  };
1133
1306
  await ensureDirectory(path.dirname(params.mergedReportPath));
1134
1307
  await fs_1.promises.writeFile(params.mergedReportPath, JSON.stringify(combined, null, 2), 'utf8');
1135
- Logger_1.appLogger.info(`Saved merged Playwright report to ${params.mergedReportPath}.`);
1308
+ Logger_1.appLogger.debug(`Saved merged Playwright report to ${params.mergedReportPath}.`);
1136
1309
  }
1137
1310
  // Playwright does not reliably expose stable IDs across reports; fall back to a composite key.
1138
1311
  function buildTestKey(file, projectName, title) {
@@ -1140,6 +1313,12 @@ function buildTestKey(file, projectName, title) {
1140
1313
  .map((segment) => segment.toString())
1141
1314
  .join('::');
1142
1315
  }
1316
+ function getFinalResultStatus(test) {
1317
+ if (!test) {
1318
+ return undefined;
1319
+ }
1320
+ return test.results?.at?.(-1)?.status ?? test.status;
1321
+ }
1143
1322
  /**
1144
1323
  * Build lookup tables for quickly finding test entries inside a Playwright
1145
1324
  * report. We index by both `testId` (preferred) and the composite key to handle
@@ -1319,7 +1498,7 @@ async function runTestCommand(cliArgs) {
1319
1498
  if (triageEnabled) {
1320
1499
  try {
1321
1500
  triageContext = await prepareTriageContext(playwrightOutputDir, options);
1322
- Logger_1.appLogger.info(`[donobu triage] Collecting failure evidence in ${triageContext.runDir}.`);
1501
+ Logger_1.appLogger.debug(`[donobu triage] Will collect test failure evidence in ${triageContext.runDir}.`);
1323
1502
  }
1324
1503
  catch (error) {
1325
1504
  Logger_1.appLogger.error('Failed to prepare test-failure triage directory. Continuing without triage.', error);
@@ -1348,11 +1527,27 @@ async function runTestCommand(cliArgs) {
1348
1527
  }
1349
1528
  let generatedPlans = [];
1350
1529
  let initialReportCopy = null;
1530
+ const reportTargets = new Set();
1351
1531
  if (triageEnabled) {
1352
1532
  const initialReportDestination = triageContext
1353
1533
  ? path.join(triageContext.runDir, 'initial-playwright-report.json')
1354
1534
  : path.join(playwrightOutputDir, `donobu-initial-report-${Date.now()}.json`);
1355
- initialReportCopy = await copyJsonReport(playwrightOutputDir, initialReportDestination);
1535
+ const initialReportResult = await copyJsonReport(playwrightOutputDir, initialReportDestination, {
1536
+ envOverrides,
1537
+ additionalCandidates: [
1538
+ path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME),
1539
+ ],
1540
+ });
1541
+ if (initialReportResult) {
1542
+ initialReportCopy = initialReportResult.destinationPath;
1543
+ reportTargets.add(initialReportResult.sourcePath);
1544
+ }
1545
+ }
1546
+ if (reportTargets.size === 0) {
1547
+ const discoveredReport = await findJsonReportInDir(playwrightOutputDir);
1548
+ if (discoveredReport) {
1549
+ reportTargets.add(discoveredReport);
1550
+ }
1356
1551
  }
1357
1552
  if (triageEnabled && triageContext && exitCode !== 0) {
1358
1553
  generatedPlans = await postProcessTriageRun(triageContext, playwrightArgs, initialReportCopy ?? undefined);
@@ -1367,6 +1562,7 @@ async function runTestCommand(cliArgs) {
1367
1562
  generatedPlans,
1368
1563
  currentExitCode: exitCode,
1369
1564
  initialReportPath: initialReportCopy ?? undefined,
1565
+ reportTargets: Array.from(reportTargets),
1370
1566
  });
1371
1567
  return autoHealOutcome.exitCode;
1372
1568
  }
@@ -1438,7 +1634,10 @@ async function runHealCommand(cliArgs) {
1438
1634
  // Downstream hooks check this flag to avoid recursive auto-heal loops.
1439
1635
  envOverrides.DONOBU_AUTO_HEAL_ACTIVE = '1';
1440
1636
  Logger_1.appLogger.info(`Re-running Playwright using treatment plan at ${parsed.planPath}...`);
1441
- const reporterSetup = await ensureJsonReporter(healArgsWithDirectives);
1637
+ const healJsonReportPath = path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
1638
+ const reporterSetup = await ensureJsonReporter(healArgsWithDirectives, {
1639
+ jsonOutputFile: healJsonReportPath,
1640
+ });
1442
1641
  Logger_1.appLogger.debug(`Heal command Playwright args: ${JSON.stringify(reporterSetup.args)} with env overrides ${JSON.stringify(envOverrides)}`);
1443
1642
  let exitCode;
1444
1643
  try {
@@ -1448,15 +1647,18 @@ async function runHealCommand(cliArgs) {
1448
1647
  await reporterSetup.cleanup();
1449
1648
  }
1450
1649
  const healReportDestination = path.join(path.dirname(parsed.planPath), `donobu-heal-report-${Date.now()}.json`);
1451
- const healReportCopy = await copyJsonReport(playwrightOutputDir, healReportDestination);
1650
+ const healReportCopy = await copyJsonReport(playwrightOutputDir, healReportDestination, {
1651
+ envOverrides,
1652
+ additionalCandidates: [healJsonReportPath],
1653
+ });
1452
1654
  if (triageEnabled && triageContext && exitCode !== 0) {
1453
- await postProcessTriageRun(triageContext, healArgsWithDirectives, healReportCopy ?? undefined);
1655
+ await postProcessTriageRun(triageContext, healArgsWithDirectives, healReportCopy?.destinationPath ?? undefined);
1454
1656
  }
1455
1657
  if (persisted.reportPath || healReportCopy) {
1456
1658
  const mergedReportPath = path.join(path.dirname(parsed.planPath), 'donobu-heal-merged-report.json');
1457
1659
  await mergePlaywrightJsonReports({
1458
1660
  initialReportPath: persisted.reportPath,
1459
- healReportPath: healReportCopy ?? undefined,
1661
+ healReportPath: healReportCopy?.destinationPath ?? undefined,
1460
1662
  mergedReportPath,
1461
1663
  healedTests: [
1462
1664
  {
@@ -1466,6 +1668,9 @@ async function runHealCommand(cliArgs) {
1466
1668
  ],
1467
1669
  healSucceeded: exitCode === 0,
1468
1670
  });
1671
+ if (persisted.reportPath) {
1672
+ await overwriteReportTargets(mergedReportPath, [persisted.reportPath]);
1673
+ }
1469
1674
  }
1470
1675
  return exitCode;
1471
1676
  }