donobu 5.40.0 → 5.41.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.
Files changed (49) hide show
  1. package/dist/cli/donobu-cli.js +142 -163
  2. package/dist/envVars.d.ts +2 -0
  3. package/dist/envVars.js +12 -0
  4. package/dist/esm/cli/donobu-cli.js +142 -163
  5. package/dist/esm/envVars.d.ts +2 -0
  6. package/dist/esm/envVars.js +12 -0
  7. package/dist/esm/lib/test/utils/donobuTestStack.js +1 -1
  8. package/dist/esm/main.d.ts +2 -1
  9. package/dist/esm/main.js +2 -4
  10. package/dist/esm/managers/AdminApiController.d.ts +1 -1
  11. package/dist/esm/managers/DonobuStack.d.ts +2 -0
  12. package/dist/esm/managers/DonobuStack.js +1 -1
  13. package/dist/esm/managers/TestsManager.js +5 -2
  14. package/dist/esm/persistence/DonobuApiClient.js +1 -1
  15. package/dist/esm/persistence/flows/FlowsPersistenceRegistry.d.ts +1 -1
  16. package/dist/esm/persistence/flows/FlowsPersistenceRegistry.js +7 -1
  17. package/dist/esm/persistence/suites/SuitesPersistenceRegistry.d.ts +1 -1
  18. package/dist/esm/persistence/suites/SuitesPersistenceRegistry.js +7 -1
  19. package/dist/esm/persistence/tests/TestsPersistenceDonobuApi.js +3 -0
  20. package/dist/esm/persistence/tests/TestsPersistenceRegistry.d.ts +1 -1
  21. package/dist/esm/persistence/tests/TestsPersistenceRegistry.js +7 -1
  22. package/dist/esm/persistence/tests/TestsPersistenceSqlite.js +4 -1
  23. package/dist/esm/persistence/tests/TestsPersistenceVolatile.js +4 -1
  24. package/dist/lib/test/utils/donobuTestStack.js +1 -1
  25. package/dist/main.d.ts +2 -1
  26. package/dist/main.js +2 -4
  27. package/dist/managers/AdminApiController.d.ts +1 -1
  28. package/dist/managers/DonobuStack.d.ts +2 -0
  29. package/dist/managers/DonobuStack.js +1 -1
  30. package/dist/managers/TestsManager.js +5 -2
  31. package/dist/persistence/DonobuApiClient.js +1 -1
  32. package/dist/persistence/flows/FlowsPersistenceRegistry.d.ts +1 -1
  33. package/dist/persistence/flows/FlowsPersistenceRegistry.js +7 -1
  34. package/dist/persistence/suites/SuitesPersistenceRegistry.d.ts +1 -1
  35. package/dist/persistence/suites/SuitesPersistenceRegistry.js +7 -1
  36. package/dist/persistence/tests/TestsPersistenceDonobuApi.js +3 -0
  37. package/dist/persistence/tests/TestsPersistenceRegistry.d.ts +1 -1
  38. package/dist/persistence/tests/TestsPersistenceRegistry.js +7 -1
  39. package/dist/persistence/tests/TestsPersistenceSqlite.js +4 -1
  40. package/dist/persistence/tests/TestsPersistenceVolatile.js +4 -1
  41. package/package.json +2 -8
  42. package/dist/cli/generate-site-tests.d.ts +0 -2
  43. package/dist/cli/generate-site-tests.js +0 -43
  44. package/dist/codegen/runGenerateSiteTests.d.ts +0 -69
  45. package/dist/codegen/runGenerateSiteTests.js +0 -937
  46. package/dist/esm/cli/generate-site-tests.d.ts +0 -2
  47. package/dist/esm/cli/generate-site-tests.js +0 -43
  48. package/dist/esm/codegen/runGenerateSiteTests.d.ts +0 -69
  49. package/dist/esm/codegen/runGenerateSiteTests.js +0 -937
@@ -309,14 +309,23 @@ const noopAsync = async () => { };
309
309
  * wrapper config when necessary so we can append the JSON reporter alongside
310
310
  * user-specified reporters.
311
311
  */
312
- async function ensureJsonReporter(originalArgs, options = {}) {
312
+ async function ensureJsonReporter(originalArgs, options) {
313
313
  const args = [...originalArgs];
314
+ // For branches that don't use the config wrapper, the JSON reporter has no
315
+ // explicit `outputFile` and Playwright falls back to env vars — which we set
316
+ // in applyJsonReportEnv to (playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME).
317
+ // Either honour an explicit override or reconstruct that path here.
318
+ const envDerivedJsonOutputFile = options.jsonOutputFile ??
319
+ path.join(options.playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
314
320
  const argInjection = injectJsonReporterIntoArgs(args);
315
321
  if (argInjection.reporterFlagFound) {
316
322
  return {
317
323
  args,
318
324
  cleanup: noopAsync,
319
- resolveUserConfiguredJson: async () => argInjection.userHadJson,
325
+ resolveJsonReporterInfo: async () => ({
326
+ userHadJson: argInjection.userHadJson,
327
+ jsonOutputFile: envDerivedJsonOutputFile,
328
+ }),
320
329
  };
321
330
  }
322
331
  const configPath = await resolvePlaywrightConfigPath(args);
@@ -332,17 +341,22 @@ async function ensureJsonReporter(originalArgs, options = {}) {
332
341
  return {
333
342
  args,
334
343
  cleanup: noopAsync,
335
- resolveUserConfiguredJson: async () => false,
344
+ resolveJsonReporterInfo: async () => ({
345
+ userHadJson: false,
346
+ jsonOutputFile: envDerivedJsonOutputFile,
347
+ }),
336
348
  };
337
349
  }
338
- const wrapper = await createConfigWrapperWithJsonReporter(configPath, options);
350
+ const wrapper = await createConfigWrapperWithJsonReporter(configPath, {
351
+ jsonOutputFile: options.jsonOutputFile,
352
+ });
339
353
  Logger_1.appLogger.debug(`Augmenting Playwright config at ${configPath} with temporary wrapper ${wrapper.configPath} to ensure JSON reporter.`);
340
354
  const strippedArgs = stripConfigArgs(args);
341
355
  const finalArgs = insertConfigArg(strippedArgs, wrapper.configPath);
342
356
  return {
343
357
  args: finalArgs,
344
358
  cleanup: wrapper.cleanup,
345
- resolveUserConfiguredJson: wrapper.resolveUserConfiguredJson,
359
+ resolveJsonReporterInfo: wrapper.resolveJsonReporterInfo,
346
360
  };
347
361
  }
348
362
  function hasReporterArg(args) {
@@ -499,18 +513,26 @@ async function createConfigWrapperWithJsonReporter(originalConfigPath, options =
499
513
  // The wrapper writes the sentinel inside its top-level config-load code, so
500
514
  // it exists by the time Playwright has loaded the config. Read it after the
501
515
  // wrapped run finishes and before `cleanup` tears the staging dir down.
502
- const resolveUserConfiguredJson = async () => {
516
+ const resolveJsonReporterInfo = async () => {
503
517
  try {
504
518
  const raw = await fs_1.promises.readFile(sentinelPath, 'utf8');
505
- return raw.trim() === '1';
519
+ const parsed = JSON.parse(raw);
520
+ return {
521
+ userHadJson: parsed?.userHadJson === true,
522
+ jsonOutputFile: typeof parsed?.jsonOutputFile === 'string' &&
523
+ parsed.jsonOutputFile.length > 0
524
+ ? parsed.jsonOutputFile
525
+ : null,
526
+ };
506
527
  }
507
528
  catch {
508
- // Sentinel missing (wrapper never loaded, or already cleaned up). Err on
509
- // the side of "no user JSON" so we don't leave a giant artifact behind.
510
- return false;
529
+ // Sentinel missing or malformed (wrapper never loaded, or already
530
+ // cleaned up). Err on the side of "no user JSON" so we don't leave a
531
+ // giant artifact behind.
532
+ return { userHadJson: false, jsonOutputFile: null };
511
533
  }
512
534
  };
513
- return { configPath: wrapperPath, cleanup, resolveUserConfiguredJson };
535
+ return { configPath: wrapperPath, cleanup, resolveJsonReporterInfo };
514
536
  }
515
537
  function buildConfigWrapperContent(originalConfigPath, userHadJsonSentinelPath, jsonOutputFileOverride) {
516
538
  const sanitisedPath = originalConfigPath.replace(/\\/g, '\\\\');
@@ -622,6 +644,17 @@ const reporters = Array.isArray(normalizedConfig.reporter)
622
644
  ? [normalizedConfig.reporter]
623
645
  : [];
624
646
 
647
+ function computeEnvDerivedJsonOutputFile() {
648
+ const outputDir = process.env.PLAYWRIGHT_JSON_OUTPUT_DIR
649
+ ? path.resolve(process.cwd(), process.env.PLAYWRIGHT_JSON_OUTPUT_DIR)
650
+ : path.resolve(configDir, 'test-results');
651
+ const outputName =
652
+ process.env.PLAYWRIGHT_JSON_OUTPUT_NAME || '${defaultJsonName}';
653
+ return path.isAbsolute(outputName)
654
+ ? outputName
655
+ : path.join(outputDir, outputName);
656
+ }
657
+
625
658
  const hasJsonReporter = reporters.some((entry) => {
626
659
  if (!entry) {
627
660
  return false;
@@ -640,30 +673,11 @@ const hasJsonReporter = reporters.some((entry) => {
640
673
  return false;
641
674
  });
642
675
 
643
- // Tell the parent orchestrator whether the user's own config defined a JSON
644
- // reporter (vs us about to force-inject one). The orchestrator reads this to
645
- // decide whether to persist the merged auto-heal report — we don't want to
646
- // leave a multi-megabyte JSON artifact behind for users who never asked for one.
647
- try {
648
- require('fs').writeFileSync(userHadJsonSentinelPath, hasJsonReporter ? '1' : '0', 'utf8');
649
- } catch (_) {
650
- // Non-fatal — the orchestrator falls back to "no user JSON" if missing.
651
- }
652
-
653
676
  if (!hasJsonReporter) {
654
- const outputFile =
655
- forcedJsonOutputFile ||
656
- (() => {
657
- const outputDir = process.env.PLAYWRIGHT_JSON_OUTPUT_DIR
658
- ? path.resolve(process.cwd(), process.env.PLAYWRIGHT_JSON_OUTPUT_DIR)
659
- : path.resolve(configDir, 'test-results');
660
- const outputName =
661
- process.env.PLAYWRIGHT_JSON_OUTPUT_NAME || '${defaultJsonName}';
662
- return path.isAbsolute(outputName)
663
- ? outputName
664
- : path.join(outputDir, outputName);
665
- })();
666
- reporters.push(['json', { outputFile }]);
677
+ reporters.push([
678
+ 'json',
679
+ { outputFile: forcedJsonOutputFile || computeEnvDerivedJsonOutputFile() },
680
+ ]);
667
681
  }
668
682
 
669
683
  // Resolve a reporter name to an absolute path so that Playwright can find it
@@ -729,6 +743,57 @@ const normalisedReporters = reporters.map((entry) => {
729
743
  return entry;
730
744
  });
731
745
 
746
+ // Walk the final reporter list to surface the JSON output path the orchestrator
747
+ // should treat as the merge target — Playwright's JSON reporter accepts the
748
+ // path via either reporter options or env var, so checking both keeps us in
749
+ // sync with whatever Playwright will actually do.
750
+ function extractJsonOutputFile(rep) {
751
+ for (const entry of rep) {
752
+ if (Array.isArray(entry) && entry[0] === 'json') {
753
+ if (
754
+ entry[1] &&
755
+ typeof entry[1] === 'object' &&
756
+ typeof entry[1].outputFile === 'string'
757
+ ) {
758
+ return entry[1].outputFile;
759
+ }
760
+ return computeEnvDerivedJsonOutputFile();
761
+ }
762
+ if (typeof entry === 'string') {
763
+ const segments = entry
764
+ .split(',')
765
+ .map((s) => s.trim())
766
+ .filter((s) => s.length > 0);
767
+ for (const segment of segments) {
768
+ const eqIndex = segment.indexOf('=');
769
+ const name = eqIndex === -1 ? segment : segment.slice(0, eqIndex);
770
+ const value = eqIndex === -1 ? '' : segment.slice(eqIndex + 1);
771
+ if (name === 'json') {
772
+ return value ? absolutify(value) : computeEnvDerivedJsonOutputFile();
773
+ }
774
+ }
775
+ }
776
+ }
777
+ return null;
778
+ }
779
+
780
+ // Tell the parent orchestrator (a) whether the user's own config defined a
781
+ // JSON reporter and (b) the exact path that JSON will land at. The path lets
782
+ // the orchestrator merge auto-heal results onto the user's file directly,
783
+ // without filesystem-scanning heuristics that can pick the wrong file.
784
+ try {
785
+ require('fs').writeFileSync(
786
+ userHadJsonSentinelPath,
787
+ JSON.stringify({
788
+ userHadJson: hasJsonReporter,
789
+ jsonOutputFile: extractJsonOutputFile(normalisedReporters),
790
+ }),
791
+ 'utf8',
792
+ );
793
+ } catch (_) {
794
+ // Non-fatal — the orchestrator falls back to "no user JSON" if missing.
795
+ }
796
+
732
797
  module.exports = {
733
798
  ...normalizedConfig,
734
799
  reporter: normalisedReporters,
@@ -767,65 +832,6 @@ async function copyJsonReport(outputDir, destinationPath, options = {}) {
767
832
  }
768
833
  return null;
769
834
  }
770
- /**
771
- * Locate the canonical Playwright JSON report and read it into memory.
772
- * Uses the same candidate-resolution logic as {@link copyJsonReport} but
773
- * avoids writing a duplicate file to disk.
774
- */
775
- async function loadJsonReport(outputDir, options = {}) {
776
- const candidatePaths = new Set();
777
- const envDefinedPath = resolveEnvJsonReportPath(options.envOverrides);
778
- if (envDefinedPath) {
779
- candidatePaths.add(envDefinedPath);
780
- }
781
- candidatePaths.add(path.join(outputDir, PLAYWRIGHT_JSON_REPORT_FILENAME));
782
- (options.additionalCandidates ?? []).forEach((candidate) => {
783
- if (candidate) {
784
- candidatePaths.add(candidate);
785
- }
786
- });
787
- for (const sourcePath of candidatePaths) {
788
- const data = await readJsonIfExists(sourcePath);
789
- if (data) {
790
- return { sourcePath, data };
791
- }
792
- }
793
- const fallbackSource = await findJsonReportInDir(outputDir);
794
- if (fallbackSource) {
795
- const data = await readJsonIfExists(fallbackSource);
796
- if (data) {
797
- return { sourcePath: fallbackSource, data };
798
- }
799
- }
800
- return null;
801
- }
802
- /**
803
- * Resolve the canonical Playwright JSON report path without reading or
804
- * copying. Uses the same candidate-resolution order as {@link loadJsonReport}.
805
- */
806
- async function findJsonReportPath(outputDir, options = {}) {
807
- const candidatePaths = new Set();
808
- const envDefinedPath = resolveEnvJsonReportPath(options.envOverrides);
809
- if (envDefinedPath) {
810
- candidatePaths.add(envDefinedPath);
811
- }
812
- candidatePaths.add(path.join(outputDir, PLAYWRIGHT_JSON_REPORT_FILENAME));
813
- (options.additionalCandidates ?? []).forEach((candidate) => {
814
- if (candidate) {
815
- candidatePaths.add(candidate);
816
- }
817
- });
818
- for (const candidate of candidatePaths) {
819
- try {
820
- await fs_1.promises.access(candidate, fs_1.constants.F_OK);
821
- return candidate;
822
- }
823
- catch {
824
- // not found, try next
825
- }
826
- }
827
- return findJsonReportInDir(outputDir);
828
- }
829
835
  function resolveEnvJsonReportPath(envOverrides) {
830
836
  if (!envOverrides) {
831
837
  return null;
@@ -859,6 +865,16 @@ async function tryCopyReport(sourcePath, destinationPath) {
859
865
  return false;
860
866
  }
861
867
  }
868
+ /** Files we write into the Playwright output dir ourselves. They happen to
869
+ * share `suites`-shaped JSON with Playwright's own report, so the scan
870
+ * fallback must exclude them or it can pick our own state file (or a
871
+ * previous heal-run copy) as the "user's JSON report". */
872
+ function isDonobuInternalJsonFile(fileName) {
873
+ return (fileName === model_1.DONOBU_REPORT_STATE_FILENAME ||
874
+ fileName.startsWith('donobu-auto-heal-report-') ||
875
+ fileName.startsWith('donobu-heal-report-') ||
876
+ fileName === 'donobu-heal-merged-report.json');
877
+ }
862
878
  async function findJsonReportInDir(outputDir) {
863
879
  let entries;
864
880
  try {
@@ -869,6 +885,7 @@ async function findJsonReportInDir(outputDir) {
869
885
  }
870
886
  const candidates = entries
871
887
  .filter((entry) => entry.endsWith('.json'))
888
+ .filter((entry) => !isDonobuInternalJsonFile(entry))
872
889
  .sort((a, b) => {
873
890
  const aScore = a.includes('report') ? 0 : 1;
874
891
  const bScore = b.includes('report') ? 0 : 1;
@@ -895,18 +912,6 @@ async function isLikelyPlaywrightReport(filePath) {
895
912
  return false;
896
913
  }
897
914
  }
898
- async function overwriteReportTargets(sourcePath, targets) {
899
- const uniqueTargets = Array.from(new Set(targets)).filter((target) => target && target !== sourcePath);
900
- await Promise.all(uniqueTargets.map(async (targetPath) => {
901
- try {
902
- await ensureDirectory(path.dirname(targetPath));
903
- await fs_1.promises.copyFile(sourcePath, targetPath);
904
- }
905
- catch (error) {
906
- Logger_1.appLogger.warn(`Failed to copy merged Playwright report to ${targetPath}.`, error);
907
- }
908
- }));
909
- }
910
915
  /**
911
916
  * Remove the JSON artifacts Donobu wrote purely for its own use:
912
917
  *
@@ -924,8 +929,8 @@ async function cleanupForceInjectedJsonArtifacts(params) {
924
929
  const targets = [
925
930
  path.join(params.playwrightOutputDir, model_1.DONOBU_REPORT_STATE_FILENAME),
926
931
  ];
927
- if (!params.userConfiguredJsonReporter && params.discoveredJsonPath) {
928
- targets.push(params.discoveredJsonPath);
932
+ if (!params.userHadJson && params.jsonOutputFile) {
933
+ targets.push(params.jsonOutputFile);
929
934
  }
930
935
  await Promise.all(targets.map(async (target) => {
931
936
  try {
@@ -1331,6 +1336,7 @@ async function attemptAutoHealRun(params) {
1331
1336
  const healJsonReportPath = path.join(staging.playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
1332
1337
  const reporterSetup = await ensureJsonReporter(healArgsForRun, {
1333
1338
  jsonOutputFile: healJsonReportPath,
1339
+ playwrightOutputDir: staging.playwrightOutputDir,
1334
1340
  });
1335
1341
  try {
1336
1342
  healExitCode = await runPlaywright(reporterSetup.args, envOverrides);
@@ -1343,7 +1349,7 @@ async function attemptAutoHealRun(params) {
1343
1349
  // Donobu's by-product. The merge step below reads the report from the
1344
1350
  // staging dir's state file, which is still alive at this point, so we
1345
1351
  // don't need a persisted copy for our own use.
1346
- const healReportCopy = params.userConfiguredJsonReporter
1352
+ const healReportCopy = params.userJsonOutputFile
1347
1353
  ? await copyJsonReport(staging.playwrightOutputDir, path.join(params.playwrightOutputDir, `donobu-auto-heal-report-${Date.now()}.json`), {
1348
1354
  envOverrides,
1349
1355
  additionalCandidates: [healJsonReportPath],
@@ -1377,19 +1383,18 @@ async function attemptAutoHealRun(params) {
1377
1383
  // reporter output for configs that don't wire up the Donobu reporter.
1378
1384
  const healReport = await loadDonobuReportForMerge(staging.playwrightOutputDir, healReportCopy?.destinationPath ?? undefined);
1379
1385
  if (params.initialReport || healReport) {
1380
- // Write the merged report directly to the user's primary JSON target
1386
+ // Write the merged report directly to the user's JSON reporter target
1381
1387
  // when one exists. When the user did not configure a JSON reporter,
1382
- // `reportTargets` is empty — skip the disk write entirely rather than
1383
- // leaving a multi-megabyte fallback artifact behind. The HTML / Markdown
1384
- // / Slack regeneration below uses the in-memory merged object, so they
1385
- // don't depend on a JSON file landing on disk.
1386
- const primaryTarget = params.reportTargets[0];
1388
+ // `userJsonOutputFile` is null — skip the disk write entirely rather
1389
+ // than leaving a multi-megabyte fallback artifact behind. The HTML /
1390
+ // Markdown / Slack regeneration below uses the in-memory merged object,
1391
+ // so they don't depend on a JSON file landing on disk.
1387
1392
  const mergedReport = await mergePlaywrightJsonReports({
1388
1393
  initialReport: params.initialReport ?? null,
1389
1394
  initialReportSourcePath: params.initialReportSourcePath,
1390
1395
  healReport,
1391
1396
  healReportSourcePath: healReportCopy?.destinationPath ?? undefined,
1392
- mergedReportPath: primaryTarget,
1397
+ mergedReportPath: params.userJsonOutputFile ?? undefined,
1393
1398
  healedTests: evaluation.eligiblePlans.map((record) => ({
1394
1399
  plan: record.plan,
1395
1400
  testCase: record.evidence.failureContext.testCase,
@@ -1398,11 +1403,6 @@ async function attemptAutoHealRun(params) {
1398
1403
  outputDir: params.playwrightOutputDir,
1399
1404
  triageRunDir: params.triageRunDir,
1400
1405
  });
1401
- // Copy to any remaining report targets (skip the primary — already
1402
- // written there).
1403
- if (mergedReport && primaryTarget && params.reportTargets.length > 1) {
1404
- await overwriteReportTargets(primaryTarget, params.reportTargets.slice(1));
1405
- }
1406
1406
  if (mergedReport) {
1407
1407
  await regenerateDonobuReports(mergedReport);
1408
1408
  await writeAutoHealPullRequestBody(mergedReport, params.playwrightOutputDir);
@@ -1772,38 +1772,26 @@ async function runTestCommand(cliArgs) {
1772
1772
  if (effectiveOptions.autoHeal) {
1773
1773
  envOverrides.DONOBU_AUTO_HEAL_ORCHESTRATED = '1';
1774
1774
  }
1775
- const reporterSetup = await ensureJsonReporter(playwrightArgs);
1775
+ const reporterSetup = await ensureJsonReporter(playwrightArgs, {
1776
+ playwrightOutputDir,
1777
+ });
1776
1778
  const runArgs = reporterSetup.args;
1777
1779
  Logger_1.appLogger.debug(`Initial Playwright args: ${JSON.stringify(runArgs)} with env overrides ${JSON.stringify(envOverrides)}`);
1778
1780
  let exitCode;
1779
- let userConfiguredJsonReporter = false;
1781
+ let jsonReporterInfo = {
1782
+ userHadJson: false,
1783
+ jsonOutputFile: null,
1784
+ };
1780
1785
  try {
1781
1786
  exitCode = await runPlaywright(runArgs, envOverrides);
1782
1787
  // Read before cleanup, since the wrapper-sentinel lives in the staging dir.
1783
- userConfiguredJsonReporter =
1784
- await reporterSetup.resolveUserConfiguredJson();
1788
+ jsonReporterInfo = await reporterSetup.resolveJsonReporterInfo();
1785
1789
  }
1786
1790
  finally {
1787
1791
  await reporterSetup.cleanup();
1788
1792
  }
1793
+ const { userHadJson, jsonOutputFile } = jsonReporterInfo;
1789
1794
  let generatedPlans = [];
1790
- const reportTargets = new Set();
1791
- // Discover the canonical report path eagerly (cheap — no file read) so we
1792
- // can populate reportTargets regardless of whether auto-heal is needed.
1793
- // Uses the same candidate-resolution order as loadJsonReport to avoid
1794
- // accidentally picking up leftover donobu-*-report-*.json files.
1795
- // Only treat the discovered report as a real "target to overwrite" when the
1796
- // user actually configured a JSON reporter — when Donobu force-injected one
1797
- // for its own use, leaving a multi-megabyte JSON behind is just noise.
1798
- const discoveredReport = await findJsonReportPath(playwrightOutputDir, {
1799
- envOverrides,
1800
- additionalCandidates: [
1801
- path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME),
1802
- ],
1803
- });
1804
- if (discoveredReport && userConfiguredJsonReporter) {
1805
- reportTargets.add(discoveredReport);
1806
- }
1807
1795
  try {
1808
1796
  if (exitCode === 0 || !effectiveOptions.autoHeal) {
1809
1797
  // Auto-heal wasn't attempted (either tests passed or the user disabled
@@ -1815,29 +1803,20 @@ async function runTestCommand(cliArgs) {
1815
1803
  return exitCode;
1816
1804
  }
1817
1805
  // Tests failed and auto-heal is enabled — load the initial report into
1818
- // memory so we can merge it with the heal-run report later without writing
1819
- // a redundant copy to disk.
1820
- let loadedPlaywrightJson = null;
1806
+ // memory so we can merge it with the heal-run report later. Prefer the
1807
+ // Donobu reporter's state file (carries Donobu-specific metadata like the
1808
+ // HTML output path); fall back to reading the user's JSON file directly.
1821
1809
  let initialDonobuReport = null;
1822
1810
  if (triageEnabled) {
1823
- loadedPlaywrightJson = await loadJsonReport(playwrightOutputDir, {
1824
- envOverrides,
1825
- additionalCandidates: [
1826
- path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME),
1827
- ],
1828
- });
1829
- if (loadedPlaywrightJson && userConfiguredJsonReporter) {
1830
- reportTargets.add(loadedPlaywrightJson.sourcePath);
1831
- }
1832
- // Prefer the Donobu reporter's state file (carries the HTML output path
1833
- // and any Donobu-specific metadata); fall back to the Playwright JSON for
1834
- // configs that don't wire up the Donobu reporter.
1835
1811
  const stateReport = await readJsonIfExists(path.join(playwrightOutputDir, model_1.DONOBU_REPORT_STATE_FILENAME));
1836
1812
  initialDonobuReport =
1837
- stateReport ?? loadedPlaywrightJson?.data ?? null;
1813
+ stateReport ??
1814
+ (jsonOutputFile
1815
+ ? await readJsonIfExists(jsonOutputFile)
1816
+ : null);
1838
1817
  }
1839
1818
  if (triageEnabled && triageContext) {
1840
- generatedPlans = await postProcessTriageRun(triageContext, playwrightArgs, loadedPlaywrightJson?.sourcePath);
1819
+ generatedPlans = await postProcessTriageRun(triageContext, playwrightArgs, jsonOutputFile ?? undefined);
1841
1820
  }
1842
1821
  const autoHealOutcome = await attemptAutoHealRun({
1843
1822
  options: effectiveOptions,
@@ -1846,10 +1825,9 @@ async function runTestCommand(cliArgs) {
1846
1825
  generatedPlans,
1847
1826
  currentExitCode: exitCode,
1848
1827
  initialReport: initialDonobuReport,
1849
- initialReportSourcePath: loadedPlaywrightJson?.sourcePath,
1828
+ initialReportSourcePath: jsonOutputFile ?? undefined,
1850
1829
  triageRunDir: triageContext?.runDir,
1851
- reportTargets: Array.from(reportTargets),
1852
- userConfiguredJsonReporter,
1830
+ userJsonOutputFile: userHadJson ? jsonOutputFile : null,
1853
1831
  });
1854
1832
  // When auto-heal was eligible-checked but didn't actually run a rerun (no
1855
1833
  // actionable directives), nothing downstream re-renders the Slack payload —
@@ -1868,8 +1846,8 @@ async function runTestCommand(cliArgs) {
1868
1846
  // (their own JSON reporter target, HTML, Markdown, Slack) is preserved.
1869
1847
  await cleanupForceInjectedJsonArtifacts({
1870
1848
  playwrightOutputDir,
1871
- discoveredJsonPath: discoveredReport,
1872
- userConfiguredJsonReporter,
1849
+ userHadJson,
1850
+ jsonOutputFile,
1873
1851
  });
1874
1852
  }
1875
1853
  }
@@ -1971,6 +1949,7 @@ async function runHealCommand(cliArgs) {
1971
1949
  const healJsonReportPath = path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
1972
1950
  const reporterSetup = await ensureJsonReporter(healArgsWithDirectives, {
1973
1951
  jsonOutputFile: healJsonReportPath,
1952
+ playwrightOutputDir,
1974
1953
  });
1975
1954
  Logger_1.appLogger.debug(`Heal command Playwright args: ${JSON.stringify(reporterSetup.args)} with env overrides ${JSON.stringify(envOverrides)}`);
1976
1955
  let exitCode;
@@ -74,6 +74,7 @@ export declare const env: Env<{
74
74
  AWS_SECRET_ACCESS_KEY: z.ZodOptional<z.ZodString>;
75
75
  EXPERIMENTAL_FEATURES_ENABLED: z.ZodDefault<z.ZodBoolean>;
76
76
  DONOBU_API_KEY: z.ZodOptional<z.ZodString>;
77
+ DONOBU_PERSISTENCE_API_KEY: z.ZodOptional<z.ZodString>;
77
78
  GOOGLE_CLOUD_STORAGE_BUCKET: z.ZodOptional<z.ZodString>;
78
79
  SCREENSHOT_TIMEOUT_MS: z.ZodDefault<z.ZodNumber>;
79
80
  SELF_HEAL_TESTS_ENABLED: z.ZodOptional<z.ZodString>;
@@ -127,6 +128,7 @@ export declare const env: Env<{
127
128
  AWS_ACCESS_KEY_ID?: string | undefined;
128
129
  AWS_SECRET_ACCESS_KEY?: string | undefined;
129
130
  DONOBU_API_KEY?: string | undefined;
131
+ DONOBU_PERSISTENCE_API_KEY?: string | undefined;
130
132
  GOOGLE_CLOUD_STORAGE_BUCKET?: string | undefined;
131
133
  SELF_HEAL_TESTS_ENABLED?: string | undefined;
132
134
  PROXY_SERVER?: string | undefined;
@@ -188,6 +188,18 @@ flows. If specified, AWS credentials must also be present.`),
188
188
  .optional()
189
189
  .describe(`The API key for the Donobu API. Used to create GPT configurations backed
190
190
  by the Donobu API, and to persist flows via the Donobu API when set.`),
191
+ DONOBU_PERSISTENCE_API_KEY: v4_1.z
192
+ .string()
193
+ .optional()
194
+ .describe(`Persistence-only fallback for the Donobu API key. When set, the DONOBU
195
+ persistence layer will instantiate using this key if DONOBU_API_KEY is
196
+ absent — but unlike DONOBU_API_KEY, this value is NOT consulted by
197
+ DonobuFlowsManager.createGptClient's env-var fallback chain.
198
+
199
+ Intended for hosts (e.g. the Donobu Studio desktop app) that drive AI
200
+ inference through an explicit gpt-config / flow-runner agent and want
201
+ to enable Donobu Cloud persistence without their persistence credential
202
+ silently overriding the user's flow-runner pick.`),
191
203
  GOOGLE_CLOUD_STORAGE_BUCKET: v4_1.z.string().optional()
192
204
  .describe(`Directs Donobu flows to be persisted using this Google Cloud Storage
193
205
  bucket.`),
@@ -20,7 +20,7 @@ function getEnvSnapshot() {
20
20
  }
21
21
  async function getOrCreateDonobuStack() {
22
22
  if (!donobuStack) {
23
- donobuStack = await (0, DonobuStack_1.setupDonobuStack)('LOCAL', ControlPanel_1.NoOpControlPanelFactory, new EnvPersistenceVolatile_1.EnvPersistenceVolatile(getEnvSnapshot()), envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY'));
23
+ donobuStack = await (0, DonobuStack_1.setupDonobuStack)('LOCAL', ControlPanel_1.NoOpControlPanelFactory, new EnvPersistenceVolatile_1.EnvPersistenceVolatile(getEnvSnapshot()), envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'DONOBU_PERSISTENCE_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY'));
24
24
  return donobuStack;
25
25
  }
26
26
  else {
@@ -6,7 +6,6 @@ export { GptClient } from './clients/GptClient';
6
6
  export { type GptClientPlugin, GptClientPluginRegistry, } from './clients/GptClientPlugin';
7
7
  export { OpenAiGptClient } from './clients/OpenAiGptClient';
8
8
  export { VercelAiGptClient } from './clients/VercelAiGptClient';
9
- export { runGenerateSiteTests } from './codegen/runGenerateSiteTests';
10
9
  export { env } from './envVars';
11
10
  export * from './exceptions/DonobuException';
12
11
  export * from './exceptions/FlowIdCollisionException';
@@ -105,6 +104,7 @@ export declare function startDonobuServer({ port, controlPanelHost, environ, }?:
105
104
  AWS_ACCESS_KEY_ID: z.ZodOptional<z.ZodString>;
106
105
  AWS_SECRET_ACCESS_KEY: z.ZodOptional<z.ZodString>;
107
106
  DONOBU_API_KEY: z.ZodOptional<z.ZodString>;
107
+ DONOBU_PERSISTENCE_API_KEY: z.ZodOptional<z.ZodString>;
108
108
  }, {
109
109
  BASE64_GPT_CONFIG?: string | undefined;
110
110
  BROWSERBASE_API_KEY?: string | undefined;
@@ -124,6 +124,7 @@ export declare function startDonobuServer({ port, controlPanelHost, environ, }?:
124
124
  AWS_ACCESS_KEY_ID?: string | undefined;
125
125
  AWS_SECRET_ACCESS_KEY?: string | undefined;
126
126
  DONOBU_API_KEY?: string | undefined;
127
+ DONOBU_PERSISTENCE_API_KEY?: string | undefined;
127
128
  }> | undefined;
128
129
  }): Promise<void>;
129
130
  //# sourceMappingURL=main.d.ts.map
package/dist/esm/main.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.TargetRuntimePluginRegistry = exports.PersistencePluginRegistry = exports.shutdownFileUploadWorkers = exports.getFileUploadAggregateStatus = exports.createDefaultToolRegistry = exports.ToolManager = exports.PluginLoader = exports.InteractionVisualizer = exports.setupDonobuStack = exports.prepareToolCallsForRerun = exports.DonobuFlowsManager = exports.distillAllowedEnvVariableNames = exports.DonobuFlow = exports.AdminApiController = exports.env = exports.runGenerateSiteTests = exports.VercelAiGptClient = exports.OpenAiGptClient = exports.GptClientPluginRegistry = exports.GptClient = exports.GoogleGenerativeAiGptClient = exports.fixAssertFields = exports.DonobuGptClient = exports.AnthropicGptClient = void 0;
17
+ exports.TargetRuntimePluginRegistry = exports.PersistencePluginRegistry = exports.shutdownFileUploadWorkers = exports.getFileUploadAggregateStatus = exports.createDefaultToolRegistry = exports.ToolManager = exports.PluginLoader = exports.InteractionVisualizer = exports.setupDonobuStack = exports.prepareToolCallsForRerun = exports.DonobuFlowsManager = exports.distillAllowedEnvVariableNames = exports.DonobuFlow = exports.AdminApiController = exports.env = exports.VercelAiGptClient = exports.OpenAiGptClient = exports.GptClientPluginRegistry = exports.GptClient = exports.GoogleGenerativeAiGptClient = exports.fixAssertFields = exports.DonobuGptClient = exports.AnthropicGptClient = void 0;
18
18
  exports.startDonobuServer = startDonobuServer;
19
19
  const commander_1 = require("commander");
20
20
  const v4_1 = require("zod/v4");
@@ -39,8 +39,6 @@ var OpenAiGptClient_1 = require("./clients/OpenAiGptClient");
39
39
  Object.defineProperty(exports, "OpenAiGptClient", { enumerable: true, get: function () { return OpenAiGptClient_1.OpenAiGptClient; } });
40
40
  var VercelAiGptClient_1 = require("./clients/VercelAiGptClient");
41
41
  Object.defineProperty(exports, "VercelAiGptClient", { enumerable: true, get: function () { return VercelAiGptClient_1.VercelAiGptClient; } });
42
- var runGenerateSiteTests_1 = require("./codegen/runGenerateSiteTests");
43
- Object.defineProperty(exports, "runGenerateSiteTests", { enumerable: true, get: function () { return runGenerateSiteTests_1.runGenerateSiteTests; } });
44
42
  var envVars_2 = require("./envVars");
45
43
  Object.defineProperty(exports, "env", { enumerable: true, get: function () { return envVars_2.env; } });
46
44
  __exportStar(require("./exceptions/DonobuException"), exports);
@@ -113,7 +111,7 @@ const DEFAULT_PORT = 31000;
113
111
  * Starts a Donobu API server at the given port. The server assumes that the
114
112
  * Playwright browsers have been installed.
115
113
  */
116
- async function startDonobuServer({ port = DEFAULT_PORT, controlPanelHost = ControlPanel_1.NoOpControlPanelFactory, environ = envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_DEPLOYMENT_ENVIRONMENT', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY'), } = {}) {
114
+ async function startDonobuServer({ port = DEFAULT_PORT, controlPanelHost = ControlPanel_1.NoOpControlPanelFactory, environ = envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_DEPLOYMENT_ENVIRONMENT', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'DONOBU_PERSISTENCE_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY'), } = {}) {
117
115
  try {
118
116
  const adminController = await AdminApiController_1.AdminApiController.create(environ.data.DONOBU_DEPLOYMENT_ENVIRONMENT ?? 'LOCAL', controlPanelHost, environ);
119
117
  await adminController.start(port);
@@ -59,7 +59,7 @@ export declare class AdminApiController {
59
59
  * - no checking for flow ownership is performed, as all flows are considered owned by the
60
60
  * local environment.
61
61
  */
62
- static create(donobuDeploymentEnvironment: DonobuDeploymentEnvironment, controlPanelFactory: ControlPanelFactory, environ: EnvPick<typeof env, 'ANTHROPIC_API_KEY' | 'ANTHROPIC_MODEL_NAME' | 'AWS_ACCESS_KEY_ID' | 'AWS_BEDROCK_MODEL_NAME' | 'AWS_SECRET_ACCESS_KEY' | 'BASE64_GPT_CONFIG' | 'BROWSERBASE_API_KEY' | 'BROWSERBASE_PROJECT_ID' | 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'GOOGLE_GENERATIVE_AI_API_KEY' | 'GOOGLE_GENERATIVE_AI_MODEL_NAME' | 'OLLAMA_API_URL' | 'OLLAMA_MODEL_NAME' | 'OPENAI_API_KEY' | 'OPENAI_API_MODEL_NAME' | 'PERSISTENCE_PRIORITY'>): Promise<AdminApiController>;
62
+ static create(donobuDeploymentEnvironment: DonobuDeploymentEnvironment, controlPanelFactory: ControlPanelFactory, environ: EnvPick<typeof env, 'ANTHROPIC_API_KEY' | 'ANTHROPIC_MODEL_NAME' | 'AWS_ACCESS_KEY_ID' | 'AWS_BEDROCK_MODEL_NAME' | 'AWS_SECRET_ACCESS_KEY' | 'BASE64_GPT_CONFIG' | 'BROWSERBASE_API_KEY' | 'BROWSERBASE_PROJECT_ID' | 'DONOBU_API_BASE_URL' | 'DONOBU_API_KEY' | 'DONOBU_PERSISTENCE_API_KEY' | 'GOOGLE_GENERATIVE_AI_API_KEY' | 'GOOGLE_GENERATIVE_AI_MODEL_NAME' | 'OLLAMA_API_URL' | 'OLLAMA_MODEL_NAME' | 'OPENAI_API_KEY' | 'OPENAI_API_MODEL_NAME' | 'PERSISTENCE_PRIORITY'>): Promise<AdminApiController>;
63
63
  private constructor();
64
64
  /**
65
65
  * Binds the API/web-asset server to `port` and resolves once the socket is
@@ -60,6 +60,7 @@ export declare function setupDonobuStack(donobuDeploymentEnvironment: DonobuDepl
60
60
  AWS_ACCESS_KEY_ID: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
61
61
  AWS_SECRET_ACCESS_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
62
62
  DONOBU_API_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
63
+ DONOBU_PERSISTENCE_API_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
63
64
  }, {
64
65
  BASE64_GPT_CONFIG?: string | undefined;
65
66
  BROWSERBASE_API_KEY?: string | undefined;
@@ -78,5 +79,6 @@ export declare function setupDonobuStack(donobuDeploymentEnvironment: DonobuDepl
78
79
  AWS_ACCESS_KEY_ID?: string | undefined;
79
80
  AWS_SECRET_ACCESS_KEY?: string | undefined;
80
81
  DONOBU_API_KEY?: string | undefined;
82
+ DONOBU_PERSISTENCE_API_KEY?: string | undefined;
81
83
  }>): Promise<DonobuStack>;
82
84
  //# sourceMappingURL=DonobuStack.d.ts.map
@@ -33,7 +33,7 @@ const ToolRegistry_1 = require("./ToolRegistry");
33
33
  * then having this snapshot is relevant so that tests can use normal
34
34
  * environment variables.
35
35
  */
36
- async function setupDonobuStack(donobuDeploymentEnvironment, controlPanelFactory, envPersistenceVolatile, environ = envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY')) {
36
+ async function setupDonobuStack(donobuDeploymentEnvironment, controlPanelFactory, envPersistenceVolatile, environ = envVars_1.env.pick('ANTHROPIC_API_KEY', 'ANTHROPIC_MODEL_NAME', 'AWS_ACCESS_KEY_ID', 'AWS_BEDROCK_MODEL_NAME', 'AWS_SECRET_ACCESS_KEY', 'BASE64_GPT_CONFIG', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'DONOBU_API_BASE_URL', 'DONOBU_API_KEY', 'DONOBU_PERSISTENCE_API_KEY', 'GOOGLE_GENERATIVE_AI_API_KEY', 'GOOGLE_GENERATIVE_AI_MODEL_NAME', 'OLLAMA_API_URL', 'OLLAMA_MODEL_NAME', 'OPENAI_API_KEY', 'OPENAI_API_MODEL_NAME', 'PERSISTENCE_PRIORITY')) {
37
37
  const loadedPlugins = await loadDefaultPlugins();
38
38
  const resolvedToolRegistry = await (0, ToolRegistry_1.createDefaultToolRegistry)(loadedPlugins.tools);
39
39
  const persistencePlugins = new PersistencePlugin_1.PersistencePluginRegistry(loadedPlugins.persistencePlugins);
@@ -174,8 +174,11 @@ class TestsManager {
174
174
  try {
175
175
  await persistence.deleteTest(testId);
176
176
  }
177
- catch {
178
- // Ignore errors from layers that don't have this test.
177
+ catch (e) {
178
+ // Ignore TestNotFoundException errors from layers that don't have this test.
179
+ if (!(e instanceof TestNotFoundException_1.TestNotFoundException)) {
180
+ throw e;
181
+ }
179
182
  }
180
183
  }
181
184
  // Cascade-delete flows belonging to this test. Paginate through all flows
@@ -24,7 +24,7 @@ class DonobuApiClient {
24
24
  async jsonRequest(path, method, body) {
25
25
  return this.request(path, {
26
26
  method,
27
- headers: { 'Content-Type': 'application/json' },
27
+ headers: body !== undefined ? { 'Content-Type': 'application/json' } : {},
28
28
  body: body !== undefined ? JSON.stringify(body) : undefined,
29
29
  });
30
30
  }