donobu 5.60.1 → 5.60.2

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 (39) hide show
  1. package/dist/cli/donobu-cli.js +151 -52
  2. package/dist/envVars.d.ts +4 -0
  3. package/dist/envVars.js +12 -0
  4. package/dist/esm/cli/donobu-cli.js +151 -52
  5. package/dist/esm/envVars.d.ts +4 -0
  6. package/dist/esm/envVars.js +12 -0
  7. package/dist/esm/lib/page/extendPage.d.ts +6 -0
  8. package/dist/esm/lib/page/extendPage.js +24 -1
  9. package/dist/esm/lib/test/healRerunGate.d.ts +85 -0
  10. package/dist/esm/lib/test/healRerunGate.js +186 -0
  11. package/dist/esm/lib/test/testExtension.d.ts +1 -0
  12. package/dist/esm/lib/test/testExtension.js +20 -10
  13. package/dist/esm/reporter/buildReport.js +32 -0
  14. package/dist/esm/reporter/merge.d.ts +1 -6
  15. package/dist/esm/reporter/merge.js +57 -35
  16. package/dist/esm/reporter/model.d.ts +9 -0
  17. package/dist/esm/reporter/model.js +10 -1
  18. package/dist/esm/reporter/render.js +34 -12
  19. package/dist/esm/reporter/renderMarkdown.js +148 -93
  20. package/dist/esm/reporter/renderSlack.js +39 -28
  21. package/dist/esm/reporter/reportWalk.d.ts +16 -6
  22. package/dist/esm/reporter/reportWalk.js +63 -13
  23. package/dist/lib/page/extendPage.d.ts +6 -0
  24. package/dist/lib/page/extendPage.js +24 -1
  25. package/dist/lib/test/healRerunGate.d.ts +85 -0
  26. package/dist/lib/test/healRerunGate.js +186 -0
  27. package/dist/lib/test/testExtension.d.ts +1 -0
  28. package/dist/lib/test/testExtension.js +20 -10
  29. package/dist/reporter/buildReport.js +32 -0
  30. package/dist/reporter/merge.d.ts +1 -6
  31. package/dist/reporter/merge.js +57 -35
  32. package/dist/reporter/model.d.ts +9 -0
  33. package/dist/reporter/model.js +10 -1
  34. package/dist/reporter/render.js +34 -12
  35. package/dist/reporter/renderMarkdown.js +148 -93
  36. package/dist/reporter/renderSlack.js +39 -28
  37. package/dist/reporter/reportWalk.d.ts +16 -6
  38. package/dist/reporter/reportWalk.js +63 -13
  39. package/package.json +1 -1
@@ -62,6 +62,7 @@ const path = __importStar(require("path"));
62
62
  const v4_1 = require("zod/v4");
63
63
  const envVars_1 = require("../envVars");
64
64
  const gptClients_1 = require("../lib/test/fixtures/gptClients");
65
+ const healRerunGate_1 = require("../lib/test/healRerunGate");
65
66
  const donobuTestStack_1 = require("../lib/test/utils/donobuTestStack");
66
67
  const triageTestFailure_1 = require("../lib/test/utils/triageTestFailure");
67
68
  const fileUploadWorkerRegistry_1 = require("../persistence/files/fileUploadWorkerRegistry");
@@ -73,6 +74,7 @@ const render_1 = require("../reporter/render");
73
74
  const renderMarkdown_1 = require("../reporter/renderMarkdown");
74
75
  const renderPullRequestBody_1 = require("../reporter/renderPullRequestBody");
75
76
  const renderSlack_1 = require("../reporter/renderSlack");
77
+ const reportWalk_1 = require("../reporter/reportWalk");
76
78
  const slack_1 = require("../reporter/slack");
77
79
  const Logger_1 = require("../utils/Logger");
78
80
  const FAILURE_EVIDENCE_PREFIX = 'failure-evidence-';
@@ -988,12 +990,21 @@ function evaluateAutoHealEligibility(plans) {
988
990
  /**
989
991
  * Coalesce directives from one or more treatment plans into a single Playwright
990
992
  * invocation. Multiple failed tests can be healed in a single rerun, so we
991
- * gather all relevant files/projects/titles here.
993
+ * gather all relevant projects (or files, as a fallback) here.
994
+ *
995
+ * The rerun deliberately targets whole Playwright *projects*, not individual
996
+ * failed tests. Suites commonly chain ordered tests — checkpoint guards,
997
+ * serial files, cross-file prerequisites within a project — and a rerun that
998
+ * filters down to just the failed titles can never heal such a test: its
999
+ * prerequisite never runs, the test skips itself, and no fix is exercised.
1000
+ * The project is the unit where Playwright encodes that ordering (testMatch
1001
+ * order, workers, dependencies), so it is the narrowest scope that is always
1002
+ * safe to re-run. File narrowing is kept only as a fallback when no project
1003
+ * name could be resolved.
992
1004
  */
993
1005
  function derivePlaywrightDirectiveArgs(descriptors) {
994
1006
  const targetFiles = new Set();
995
1007
  const targetProjects = new Set();
996
- const targetTitles = new Set();
997
1008
  const additionalArgs = [];
998
1009
  for (const descriptor of descriptors) {
999
1010
  const directives = descriptor.plan.automationDirectives;
@@ -1008,9 +1019,6 @@ function derivePlaywrightDirectiveArgs(descriptors) {
1008
1019
  if (projectCandidate && !looksLikePath(projectCandidate)) {
1009
1020
  targetProjects.add(projectCandidate);
1010
1021
  }
1011
- if (descriptor.testCase.title) {
1012
- targetTitles.add(descriptor.testCase.title);
1013
- }
1014
1022
  if (directives.additionalPlaywrightArgs) {
1015
1023
  directives.additionalPlaywrightArgs.forEach((arg) => {
1016
1024
  additionalArgs.push(arg);
@@ -1018,20 +1026,11 @@ function derivePlaywrightDirectiveArgs(descriptors) {
1018
1026
  }
1019
1027
  }
1020
1028
  return {
1021
- files: Array.from(targetFiles),
1029
+ files: targetProjects.size > 0 ? [] : Array.from(targetFiles),
1022
1030
  projects: Array.from(targetProjects),
1023
- grepPattern: targetTitles.size > 0
1024
- ? Array.from(targetTitles)
1025
- .map((title) => escapeRegex(title))
1026
- .join('|')
1027
- : undefined,
1028
1031
  extras: additionalArgs,
1029
1032
  };
1030
1033
  }
1031
- // We match test titles via `--grep`, so ensure literal characters are escaped.
1032
- function escapeRegex(value) {
1033
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1034
- }
1035
1034
  // Some teams name projects after directories (e.g. `projects/mobile`); treat those as file paths.
1036
1035
  function looksLikePath(value) {
1037
1036
  return value.includes('/') || value.includes('\\');
@@ -1057,21 +1056,34 @@ function extractOriginalFiles(args) {
1057
1056
  }
1058
1057
  /**
1059
1058
  * Preserve most user-provided Playwright flags (e.g. `--config`, `--workers`).
1060
- * We only strip flags we know we're going to replace (projects, grep, reporter).
1059
+ * We only strip flags we know we're going to replace (projects, reporter).
1060
+ * The user's own `--grep` IS preserved: it scoped the initial run, so the heal
1061
+ * rerun must stay inside it (the rerun no longer adds a grep of its own).
1061
1062
  */
1062
1063
  function extractPreservedOptions(args) {
1063
- return args.slice(1).filter((arg) => {
1064
+ const preserved = [];
1065
+ const rest = args.slice(1);
1066
+ for (let i = 0; i < rest.length; i += 1) {
1067
+ const arg = rest[i];
1064
1068
  if (!arg.startsWith('--')) {
1065
- return false;
1069
+ continue;
1066
1070
  }
1067
- const optionName = arg.startsWith('--') ? arg.split('=')[0] : arg;
1068
- return (optionName !== '--project' &&
1069
- !optionName.startsWith('--project=') &&
1070
- optionName !== '--grep' &&
1071
- optionName !== '--reporter' &&
1072
- !optionName.startsWith('--grep=') &&
1073
- !optionName.startsWith('--reporter='));
1074
- });
1071
+ const optionName = arg.split('=')[0];
1072
+ if (optionName === '--project' || optionName === '--reporter') {
1073
+ continue;
1074
+ }
1075
+ preserved.push(arg);
1076
+ // Space-separated flag values (`--grep pattern`) would otherwise be
1077
+ // dropped by the `--`-prefix filter above; carry the grep value along.
1078
+ if ((optionName === '--grep' || optionName === '--grep-invert') &&
1079
+ arg === optionName &&
1080
+ i + 1 < rest.length &&
1081
+ !rest[i + 1].startsWith('--')) {
1082
+ preserved.push(rest[i + 1]);
1083
+ i += 1;
1084
+ }
1085
+ }
1086
+ return preserved;
1075
1087
  }
1076
1088
  /**
1077
1089
  * Merge the user's original Playwright command with the automation directives
@@ -1084,14 +1096,14 @@ function buildPlaywrightArgsWithDirectives(originalArgs, directives) {
1084
1096
  : extractOriginalFiles(originalArgs);
1085
1097
  const preservedOptions = extractPreservedOptions(originalArgs);
1086
1098
  const projectArgs = directives.projects.map((project) => `--project=${project}`);
1087
- const grepArgs = directives.grepPattern
1088
- ? ['--grep', directives.grepPattern]
1089
- : [];
1099
+ // When the rerun targets whole projects, file filters from the original
1100
+ // invocation would shrink the scope below the project's testMatch and can
1101
+ // exclude prerequisite tests — drop them; `--project` already narrows.
1102
+ const fileArgs = directives.projects.length > 0 ? [] : files;
1090
1103
  const finalArgs = [
1091
1104
  'test',
1092
- ...files,
1105
+ ...fileArgs,
1093
1106
  ...projectArgs,
1094
- ...grepArgs,
1095
1107
  ...directives.extras,
1096
1108
  ...preservedOptions,
1097
1109
  ];
@@ -1317,12 +1329,28 @@ async function attemptAutoHealRun(params) {
1317
1329
  let healExitCode = params.currentExitCode;
1318
1330
  const healOptions = {
1319
1331
  ...params.options,
1320
- clearAiCache: params.options.clearAiCache || evaluation.clearPageAiCache === true,
1321
1332
  autoHeal: false,
1322
1333
  triageOutputDir: staging.triageBaseDir,
1323
1334
  };
1324
- if (evaluation.clearPageAiCache && !params.options.clearAiCache) {
1325
- Logger_1.appLogger.info('Auto-heal: clearing Page.AI cache as recommended by the treatment plan.');
1335
+ // Heal targets, expanded with their declared `describe.serial` siblings so
1336
+ // the rerun executes the whole declared group (Playwright's own retry
1337
+ // semantics for serial chains). The serial flags come from the initial
1338
+ // run's Donobu report; without it, targets run unexpanded.
1339
+ const healTargets = (0, healRerunGate_1.expandTargetsWithSerialCompanions)(evaluation.eligiblePlans.map((record) => ({
1340
+ file: record.evidence.failureContext.testCase.file ?? '',
1341
+ title: record.evidence.failureContext.testCase.title,
1342
+ projectName: record.evidence.failureContext.testCase.projectName,
1343
+ })), params.initialReport ?? null);
1344
+ // Cache invalidation is scoped to the heal targets' spec files: serial
1345
+ // prerequisites and any other re-running test keep their fast cache replay.
1346
+ // The user's own `--clear-ai-cache` flag stays run-wide.
1347
+ const clearCacheFiles = evaluation.clearPageAiCache && !params.options.clearAiCache
1348
+ ? Array.from(new Set(healTargets
1349
+ .map((target) => target.file)
1350
+ .filter((file) => Boolean(file))))
1351
+ : [];
1352
+ if (clearCacheFiles.length > 0) {
1353
+ Logger_1.appLogger.info(`Auto-heal: clearing Page.AI cache for ${clearCacheFiles.length} target spec file(s) as recommended by the treatment plan.`);
1326
1354
  }
1327
1355
  const healArgsWithDirectives = buildPlaywrightArgsWithDirectives(params.playwrightArgs, evaluation.directives);
1328
1356
  const healArgsForRun = overrideOutputDir(healArgsWithDirectives, staging.playwrightOutputDir);
@@ -1346,7 +1374,18 @@ async function attemptAutoHealRun(params) {
1346
1374
  applyJsonReportEnv(envOverrides, staging.playwrightOutputDir);
1347
1375
  // Flag downstream systems so they know this invocation came from auto-heal.
1348
1376
  envOverrides.DONOBU_AUTO_HEAL_ACTIVE = '1';
1349
- Logger_1.appLogger.info(`Auto-heal: applying directives from ${evaluation.eligiblePlans.length} treatment plan(s) and re-running Playwright...`);
1377
+ if (clearCacheFiles.length > 0) {
1378
+ envOverrides.DONOBU_PAGE_AI_CLEAR_CACHE_FILES =
1379
+ JSON.stringify(clearCacheFiles);
1380
+ }
1381
+ // The rerun plan drives the collection-time gate in the test wrapper:
1382
+ // only the heal targets (and their declared `describe.serial` siblings)
1383
+ // execute; every other collected test statically skips. Declared
1384
+ // dependency projects still run in full — Playwright's semantics.
1385
+ const healPlanPath = path.join(staging.rootDir, 'heal-rerun-plan.json');
1386
+ await fs_1.promises.writeFile(healPlanPath, JSON.stringify({ targets: healTargets }), 'utf8');
1387
+ envOverrides.DONOBU_AUTO_HEAL_PLAN_PATH = healPlanPath;
1388
+ Logger_1.appLogger.info(`Auto-heal: re-running ${healTargets.length} targeted test(s) from ${evaluation.eligiblePlans.length} treatment plan(s)...`);
1350
1389
  const healJsonReportPath = path.join(staging.playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
1351
1390
  const reporterSetup = await ensureJsonReporter(healArgsForRun, {
1352
1391
  jsonOutputFile: healJsonReportPath,
@@ -1396,6 +1435,30 @@ async function attemptAutoHealRun(params) {
1396
1435
  // Prefer the heal reporter's state file; fall back to Playwright's JSON
1397
1436
  // reporter output for configs that don't wire up the Donobu reporter.
1398
1437
  const healReport = await loadDonobuReportForMerge(staging.playwrightOutputDir, healReportCopy?.destinationPath ?? undefined);
1438
+ // A heal rerun "succeeds" only when every test it set out to heal
1439
+ // actually passed in the rerun. The rerun's exit code alone can't tell:
1440
+ // a target that skipped itself (e.g. a precondition guard fired) exits
1441
+ // zero, and the project-scope rerun includes unrelated tests. Matched on
1442
+ // title + project — file paths differ in shape between treatment plans
1443
+ // (absolute) and reports (rootDir-relative).
1444
+ if (healExitCode === 0) {
1445
+ const unhealedTargets = evaluation.eligiblePlans.filter((record) => {
1446
+ const testCase = record.evidence.failureContext.testCase;
1447
+ const healTest = findTestInReport(healReport, testCase.title, testCase.projectName);
1448
+ if (!healTest) {
1449
+ return true;
1450
+ }
1451
+ const status = (0, reportWalk_1.statusOf)(healTest);
1452
+ return status !== 'passed' && status !== 'flaky' && status !== 'healed';
1453
+ });
1454
+ if (unhealedTargets.length > 0) {
1455
+ const titles = unhealedTargets
1456
+ .map((record) => record.evidence.failureContext.testCase.title)
1457
+ .join('", "');
1458
+ Logger_1.appLogger.warn(`Auto-heal rerun did not fix ${unhealedTargets.length} targeted test(s): "${titles}" (failed again, or never re-attempted because a precondition was missing). Keeping failing status.`);
1459
+ healExitCode = 1;
1460
+ }
1461
+ }
1399
1462
  if (params.initialReport || healReport) {
1400
1463
  // Write the merged report directly to the user's JSON reporter target
1401
1464
  // when one exists. When the user did not configure a JSON reporter,
@@ -1409,20 +1472,15 @@ async function attemptAutoHealRun(params) {
1409
1472
  healReport,
1410
1473
  healReportSourcePath: healReportCopy?.destinationPath ?? undefined,
1411
1474
  mergedReportPath: params.userJsonOutputFile ?? undefined,
1412
- healedTests: evaluation.eligiblePlans.map((record) => ({
1413
- plan: record.plan,
1414
- testCase: record.evidence.failureContext.testCase,
1415
- })),
1416
- healSucceeded: healExitCode === 0,
1417
1475
  outputDir: params.playwrightOutputDir,
1418
1476
  triageRunDir: params.triageRunDir,
1419
1477
  });
1420
1478
  if (mergedReport) {
1421
1479
  await regenerateDonobuReports(mergedReport);
1422
1480
  await writeAutoHealPullRequestBody(mergedReport, params.playwrightOutputDir);
1423
- // The heal rerun only re-runs the grep-filtered subset of healable
1424
- // tests, so `healExitCode` is blind to failures triage declined to
1425
- // retry (e.g. real product bugs). Re-derive the status from the merged
1481
+ // The heal rerun only re-runs the projects containing healable tests,
1482
+ // so `healExitCode` is blind to failures triage declined to retry
1483
+ // (e.g. real product bugs). Re-derive the status from the merged
1426
1484
  // report so those remaining failures still fail CI. We only escalate
1427
1485
  // (never downgrade a non-zero heal exit) to preserve infra/crash codes.
1428
1486
  const remainingFailures = countUnexpectedTests(mergedReport);
@@ -1450,6 +1508,43 @@ function countUnexpectedTests(report) {
1450
1508
  const unexpected = stats?.unexpected;
1451
1509
  return typeof unexpected === 'number' ? unexpected : undefined;
1452
1510
  }
1511
+ /**
1512
+ * Find a test entry in a report by title + project name. File paths are
1513
+ * deliberately not part of the lookup: treatment plans carry absolute paths
1514
+ * while reports carry rootDir-relative ones, and title + project is already
1515
+ * how Playwright disambiguates tests within a run.
1516
+ */
1517
+ function findTestInReport(report, title, projectName) {
1518
+ if (!report?.suites || !title) {
1519
+ return null;
1520
+ }
1521
+ const visitSuite = (suite) => {
1522
+ for (const spec of suite.specs ?? []) {
1523
+ if (spec.title !== title) {
1524
+ continue;
1525
+ }
1526
+ for (const test of spec.tests ?? []) {
1527
+ if (!projectName || (test.projectName ?? '') === projectName) {
1528
+ return test;
1529
+ }
1530
+ }
1531
+ }
1532
+ for (const child of suite.suites ?? []) {
1533
+ const found = visitSuite(child);
1534
+ if (found) {
1535
+ return found;
1536
+ }
1537
+ }
1538
+ return null;
1539
+ };
1540
+ for (const suite of report.suites) {
1541
+ const found = visitSuite(suite);
1542
+ if (found) {
1543
+ return found;
1544
+ }
1545
+ }
1546
+ return null;
1547
+ }
1453
1548
  /**
1454
1549
  * Filename of the auto-heal PR body artifact. Lands in the Playwright output
1455
1550
  * directory alongside the merged JSON / HTML / Markdown reports so the
@@ -1667,8 +1762,6 @@ async function mergePlaywrightJsonReports(params) {
1667
1762
  const merged = (0, merge_1.mergeReports)({
1668
1763
  initialReport: params.initialReport,
1669
1764
  healReport: params.healReport,
1670
- healedTests: params.healedTests,
1671
- healSucceeded: params.healSucceeded,
1672
1765
  triageRunDir: params.triageRunDir,
1673
1766
  initialReportSourcePath: params.initialReportSourcePath,
1674
1767
  healReportSourcePath: params.healReportSourcePath,
@@ -2085,6 +2178,19 @@ async function runHealCommand(cliArgs) {
2085
2178
  applyJsonReportEnv(envOverrides, playwrightOutputDir);
2086
2179
  // Downstream hooks check this flag to avoid recursive auto-heal loops.
2087
2180
  envOverrides.DONOBU_AUTO_HEAL_ACTIVE = '1';
2181
+ // Same collection-time gating as the automatic rerun: only the plan's test
2182
+ // (plus declared `describe.serial` siblings) executes.
2183
+ const healPlanPath = path.join(os.tmpdir(), `donobu-heal-rerun-plan-${Date.now()}.json`);
2184
+ await fs_1.promises.writeFile(healPlanPath, JSON.stringify({
2185
+ targets: [
2186
+ {
2187
+ file: persisted.failure.testCase.file,
2188
+ title: persisted.failure.testCase.title,
2189
+ projectName: persisted.failure.testCase.projectName,
2190
+ },
2191
+ ],
2192
+ }), 'utf8');
2193
+ envOverrides.DONOBU_AUTO_HEAL_PLAN_PATH = healPlanPath;
2088
2194
  Logger_1.appLogger.info(`Re-running Playwright using treatment plan at ${parsed.planPath}...`);
2089
2195
  const healJsonReportPath = path.join(playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
2090
2196
  const reporterSetup = await ensureJsonReporter(healArgsWithDirectives, {
@@ -2123,13 +2229,6 @@ async function runHealCommand(cliArgs) {
2123
2229
  healReport,
2124
2230
  healReportSourcePath: healReportCopy?.destinationPath ?? undefined,
2125
2231
  mergedReportPath,
2126
- healedTests: [
2127
- {
2128
- plan: persisted.plan,
2129
- testCase: persisted.failure.testCase,
2130
- },
2131
- ],
2132
- healSucceeded: exitCode === 0,
2133
2232
  outputDir: path.dirname(mergedReportPath),
2134
2233
  });
2135
2234
  if (mergedReport) {
@@ -59,6 +59,8 @@ export declare const env: Env<{
59
59
  true: "true";
60
60
  false: "false";
61
61
  }>>;
62
+ DONOBU_PAGE_AI_CLEAR_CACHE_FILES: z.ZodOptional<z.ZodArray<z.ZodString>>;
63
+ DONOBU_AUTO_HEAL_PLAN_PATH: z.ZodOptional<z.ZodString>;
62
64
  GOOGLE_GENERATIVE_AI_API_KEY: z.ZodOptional<z.ZodString>;
63
65
  GOOGLE_GENERATIVE_AI_MODEL_NAME: z.ZodOptional<z.ZodString>;
64
66
  OLLAMA_MODEL_NAME: z.ZodOptional<z.ZodString>;
@@ -116,6 +118,8 @@ export declare const env: Env<{
116
118
  CI_COMMIT_REF_NAME?: string | undefined;
117
119
  PLAYWRIGHT_JSON_OUTPUT_DIR?: string | undefined;
118
120
  DONOBU_PAGE_AI_CLEAR_CACHE?: "0" | "1" | "true" | "false" | undefined;
121
+ DONOBU_PAGE_AI_CLEAR_CACHE_FILES?: string[] | undefined;
122
+ DONOBU_AUTO_HEAL_PLAN_PATH?: string | undefined;
119
123
  GOOGLE_GENERATIVE_AI_API_KEY?: string | undefined;
120
124
  GOOGLE_GENERATIVE_AI_MODEL_NAME?: string | undefined;
121
125
  OLLAMA_MODEL_NAME?: string | undefined;
@@ -128,6 +128,18 @@ re-render reports from merged data).`),
128
128
  bypass and invalidate Page.AI cache entries for the current run. The Donobu
129
129
  CLI sets this automatically when invoked with \`--clear-ai-cache\` so a retry
130
130
  always regenerates selectors from scratch.`),
131
+ DONOBU_PAGE_AI_CLEAR_CACHE_FILES: v4_1.z.array(v4_1.z.string()).optional()
132
+ .describe(`Spec file paths whose Page.AI cache entries should be bypassed and
133
+ invalidated for the current run (JSON-encoded on the wire). Set by the
134
+ auto-heal orchestrator so the rerun regenerates selectors only for the spec
135
+ files that contain heal targets, while every other re-running test keeps its
136
+ fast deterministic cache replay.`),
137
+ DONOBU_AUTO_HEAL_PLAN_PATH: v4_1.z.string().optional()
138
+ .describe(`Path to the auto-heal rerun plan (JSON) written by the Donobu CLI before the
139
+ heal rerun. When set, a Donobu auto fixture skips every test that is not part
140
+ of the plan (heal targets and, for targets inside a \`describe.serial\` group,
141
+ their serial siblings) before any browser fixture initializes, so the rerun
142
+ only pays for what the heal actually needs.`),
131
143
  GOOGLE_GENERATIVE_AI_API_KEY: v4_1.z.string().optional()
132
144
  .describe(`Automatically create GPT configurations for Google Gemini using this API key.
133
145
  For convenience, the created configuration names will reflect the
@@ -20,6 +20,12 @@ export declare function extendPage(page: Page, options?: {
20
20
  flowId?: string;
21
21
  visualCueDurationMs?: number;
22
22
  cacheFilepath?: string;
23
+ /**
24
+ * Spec file this page is serving. Used to decide whether the file-scoped
25
+ * Page.AI cache invalidation (`DONOBU_PAGE_AI_CLEAR_CACHE_FILES`) applies
26
+ * to this context.
27
+ */
28
+ specFilePath?: string;
23
29
  envVars?: string[];
24
30
  gptClient?: GptClient;
25
31
  headless?: boolean;
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.extendPage = extendPage;
4
7
  const crypto_1 = require("crypto");
8
+ const path_1 = __importDefault(require("path"));
5
9
  const v4_1 = require("zod/v4");
6
10
  const GptClient_1 = require("../../clients/GptClient");
7
11
  const VercelAiGptClient_1 = require("../../clients/VercelAiGptClient");
@@ -60,6 +64,25 @@ function resolveBaseUrl(page, url) {
60
64
  // Donobu page extension helpers: decorate Playwright pages with Donobu behaviors and keep one
61
65
  // coherent flow (and persistence record) per browser context so new tabs share state safely.
62
66
  const PLACEHOLDER_FLOW_URL = 'https://example.com';
67
+ /**
68
+ * Whether Page.AI cache entries should be bypassed and invalidated for this
69
+ * context. Two knobs:
70
+ * - `DONOBU_PAGE_AI_CLEAR_CACHE` — run-wide, set by `--clear-ai-cache`.
71
+ * - `DONOBU_PAGE_AI_CLEAR_CACHE_FILES` — JSON array of spec paths, set by the
72
+ * auto-heal rerun so only heal-target spec files regenerate selectors while
73
+ * other re-running tests (serial prerequisites) keep their cache replay.
74
+ */
75
+ function shouldClearPageAiCache(specFilePath) {
76
+ if (MiscUtils_1.MiscUtils.yn(envVars_1.env.data.DONOBU_PAGE_AI_CLEAR_CACHE)) {
77
+ return true;
78
+ }
79
+ const files = envVars_1.env.data.DONOBU_PAGE_AI_CLEAR_CACHE_FILES;
80
+ if (!files?.length || !specFilePath) {
81
+ return false;
82
+ }
83
+ const resolved = path_1.default.resolve(specFilePath);
84
+ return files.some((file) => path_1.default.resolve(file) === resolved);
85
+ }
63
86
  // Cache the shared Donobu state per browser context so every tab in that context reuses the same
64
87
  // flow metadata, persistence, GPT client, and visualizer. WeakMap ensures cleanup when contexts die.
65
88
  const contextSharedState = new WeakMap();
@@ -137,7 +160,7 @@ async function extendPage(page, options) {
137
160
  gptClient: resolvedGptClient,
138
161
  controlPanelFactory: options?.controlPanelFactory,
139
162
  runtimeDirectives: {
140
- clearPageAiCache: MiscUtils_1.MiscUtils.yn(envVars_1.env.data.DONOBU_PAGE_AI_CLEAR_CACHE),
163
+ clearPageAiCache: shouldClearPageAiCache(options?.specFilePath),
141
164
  },
142
165
  tbdSessions: [],
143
166
  aiInvocations: [],
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @fileoverview Runtime gate for auto-heal reruns.
3
+ *
4
+ * The auto-heal rerun is launched with the same project-level arguments as the
5
+ * initial run so Playwright's scheduling (declared project `dependencies`,
6
+ * workers, ordering) behaves with full fidelity. Within the targeted projects,
7
+ * however, only the tests the heal actually needs should execute. This module
8
+ * enforces that from an auto fixture that runs before any browser fixture:
9
+ * when `DONOBU_AUTO_HEAL_PLAN_PATH` is set, every test not in the rerun plan
10
+ * skips immediately — before a context or page is created — annotated with
11
+ * `donobu-heal-skip-replay` so the merge step drops the entry and leaves the
12
+ * initial run's result untouched.
13
+ *
14
+ * What runs during a heal rerun (the "declared signals only" policy):
15
+ * - Heal targets (the failed tests with actionable treatment plans).
16
+ * - For a target inside a `test.describe.serial` scope (or a file marked
17
+ * serial via `test.describe.configure({ mode: 'serial' })`): the other
18
+ * serial-scoped tests in that file. Serial mode is Playwright's declared
19
+ * intra-file ordering contract — Playwright itself re-runs whole serial
20
+ * groups on retry, and we mirror that. The orchestrator expands the plan
21
+ * with these companions before the rerun (see
22
+ * `expandTargetsWithSerialCompanions`) using the `serialScoped` flags the
23
+ * Donobu reporter recorded during the initial run — the runner process
24
+ * sees the suite tree; the worker (where this gate runs) does not.
25
+ * - Declared dependency projects, which Playwright always runs in full.
26
+ *
27
+ * Implicit ordering (checkpoint files between plain tests, cross-file state
28
+ * with `workers: 1`) is deliberately NOT honored: tests relying on it will
29
+ * skip themselves during the rerun and surface as honest failures with
30
+ * guidance to declare the dependency.
31
+ *
32
+ * The gate runs at test runtime rather than collection time so Playwright's
33
+ * test-location attribution stays untouched (a collection-time wrapper would
34
+ * become every test's reported call site, breaking the merge's file-based
35
+ * matching) and so the plan can be matched against `testInfo.file`/`title`
36
+ * exactly instead of via stack inspection.
37
+ */
38
+ import type { TestInfo } from '@playwright/test';
39
+ /** Shape of the plan file the auto-heal orchestrator writes before the rerun. */
40
+ export interface HealRerunPlan {
41
+ targets: Array<{
42
+ /** Spec file path; absolute, or relative to the rerun's CWD. */
43
+ file: string;
44
+ title: string;
45
+ projectName?: string;
46
+ }>;
47
+ }
48
+ /** Targets indexed by absolute spec path for O(1) per-test decisions. */
49
+ export type HealRerunPlanIndex = Map<string, Set<string>>;
50
+ export declare function buildPlanIndex(plan: HealRerunPlan): HealRerunPlanIndex;
51
+ /**
52
+ * Pure decision: should the test in `file` with `title` actually execute
53
+ * during the heal rerun? The plan is fully explicit — serial companions were
54
+ * already expanded into it by the orchestrator.
55
+ */
56
+ export declare function shouldRunDuringHealRerun(params: {
57
+ index: HealRerunPlanIndex;
58
+ file: string;
59
+ title: string;
60
+ }): boolean;
61
+ /**
62
+ * Expand heal targets with their `describe.serial` siblings, using the
63
+ * `serialScoped` flags the Donobu reporter recorded in the initial run's
64
+ * report. Companions live in the same file as their target by construction
65
+ * (serial scopes are intra-file), so they inherit the target's absolute file
66
+ * path; report file paths are rootDir-relative, hence the suffix match.
67
+ *
68
+ * Degrades to the unexpanded targets when the report (or the flags) are
69
+ * unavailable — serial chains then surface as honest not-reattempted
70
+ * failures instead of healing.
71
+ */
72
+ export declare function expandTargetsWithSerialCompanions(targets: HealRerunPlan['targets'], initialReport: {
73
+ suites?: unknown;
74
+ } | null): HealRerunPlan['targets'];
75
+ /** Test-only: reset the memoized plan so each test can load its own. */
76
+ export declare function resetHealRerunPlanCacheForTesting(): void;
77
+ /**
78
+ * Called from the Donobu auto fixture before any browser fixture initializes.
79
+ * Outside heal reruns this is a no-op. During a rerun, tests outside the plan
80
+ * are annotated and skipped on the spot — no context, no page, no cost.
81
+ */
82
+ export declare function maybeSkipForHealRerun(testInfo: TestInfo, options?: {
83
+ planPath?: string;
84
+ }): void;
85
+ //# sourceMappingURL=healRerunGate.d.ts.map