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.
- package/dist/cli/donobu-cli.d.ts +7 -1
- package/dist/cli/donobu-cli.js +195 -84
- package/dist/esm/cli/donobu-cli.d.ts +7 -1
- package/dist/esm/cli/donobu-cli.js +195 -84
- package/dist/esm/lib/test/testExtension.d.ts +31 -1
- package/dist/esm/lib/test/testExtension.js +273 -0
- package/dist/esm/managers/DonobuStack.js +1 -1
- package/dist/esm/managers/TestsManager.d.ts +2 -2
- package/dist/esm/managers/TestsManager.js +10 -0
- package/dist/esm/models/PaginatedResult.d.ts +15 -0
- package/dist/esm/models/PaginatedResult.js +13 -2
- package/dist/esm/models/TestMetadata.d.ts +630 -0
- package/dist/esm/models/TestMetadata.js +10 -1
- package/dist/esm/persistence/tests/TestsPersistence.d.ts +2 -2
- package/dist/esm/persistence/tests/TestsPersistenceDonobuApi.d.ts +3 -3
- package/dist/esm/persistence/tests/TestsPersistenceDonobuApi.js +18 -2
- package/dist/esm/persistence/tests/TestsPersistenceRegistry.d.ts +9 -1
- package/dist/esm/persistence/tests/TestsPersistenceRegistry.js +9 -2
- package/dist/esm/persistence/tests/TestsPersistenceSqlite.d.ts +2 -1
- package/dist/esm/persistence/tests/TestsPersistenceSqlite.js +65 -6
- package/dist/esm/persistence/tests/TestsPersistenceVolatile.d.ts +22 -3
- package/dist/esm/persistence/tests/TestsPersistenceVolatile.js +69 -4
- package/dist/esm/reporter/renderMarkdown.js +1 -1
- package/dist/lib/test/testExtension.d.ts +31 -1
- package/dist/lib/test/testExtension.js +273 -0
- package/dist/managers/DonobuStack.js +1 -1
- package/dist/managers/TestsManager.d.ts +2 -2
- package/dist/managers/TestsManager.js +10 -0
- package/dist/models/PaginatedResult.d.ts +15 -0
- package/dist/models/PaginatedResult.js +13 -2
- package/dist/models/TestMetadata.d.ts +630 -0
- package/dist/models/TestMetadata.js +10 -1
- package/dist/persistence/tests/TestsPersistence.d.ts +2 -2
- package/dist/persistence/tests/TestsPersistenceDonobuApi.d.ts +3 -3
- package/dist/persistence/tests/TestsPersistenceDonobuApi.js +18 -2
- package/dist/persistence/tests/TestsPersistenceRegistry.d.ts +9 -1
- package/dist/persistence/tests/TestsPersistenceRegistry.js +9 -2
- package/dist/persistence/tests/TestsPersistenceSqlite.d.ts +2 -1
- package/dist/persistence/tests/TestsPersistenceSqlite.js +65 -6
- package/dist/persistence/tests/TestsPersistenceVolatile.d.ts +22 -3
- package/dist/persistence/tests/TestsPersistenceVolatile.js +69 -4
- package/dist/reporter/renderMarkdown.js +1 -1
- package/package.json +1 -1
package/dist/cli/donobu-cli.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
declare function hasReporterArg(args: string[]): boolean;
|
|
3
|
-
declare function injectJsonReporterIntoArgs(args: string[]):
|
|
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;
|
package/dist/cli/donobu-cli.js
CHANGED
|
@@ -309,8 +309,13 @@ const noopAsync = async () => { };
|
|
|
309
309
|
*/
|
|
310
310
|
async function ensureJsonReporter(originalArgs, options = {}) {
|
|
311
311
|
const args = [...originalArgs];
|
|
312
|
-
|
|
313
|
-
|
|
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 {
|
|
330
|
+
return {
|
|
331
|
+
args,
|
|
332
|
+
cleanup: noopAsync,
|
|
333
|
+
resolveUserConfiguredJson: async () => false,
|
|
334
|
+
};
|
|
326
335
|
}
|
|
327
|
-
const
|
|
328
|
-
Logger_1.appLogger.debug(`Augmenting Playwright config at ${configPath} with temporary wrapper ${
|
|
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,
|
|
331
|
-
return {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
|
1300
|
-
//
|
|
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(
|
|
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
|
|
1486
|
-
* merged JSON to `mergedReportPath` for downstream dashboards.
|
|
1487
|
-
* in-memory merged report so the caller can drive HTML
|
|
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
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
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
|
-
|
|
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[]):
|
|
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;
|