donobu 5.60.1 → 5.60.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/cli/donobu-cli.js +158 -52
  2. package/dist/envVars.d.ts +4 -0
  3. package/dist/envVars.js +12 -0
  4. package/dist/esm/cli/donobu-cli.js +158 -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 +102 -0
  10. package/dist/esm/lib/test/healRerunGate.js +228 -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/managers/DonobuStack.d.ts +19 -19
  14. package/dist/esm/reporter/buildReport.js +54 -1
  15. package/dist/esm/reporter/merge.d.ts +1 -6
  16. package/dist/esm/reporter/merge.js +57 -35
  17. package/dist/esm/reporter/model.d.ts +16 -0
  18. package/dist/esm/reporter/model.js +10 -1
  19. package/dist/esm/reporter/render.js +34 -12
  20. package/dist/esm/reporter/renderMarkdown.js +148 -93
  21. package/dist/esm/reporter/renderSlack.js +39 -28
  22. package/dist/esm/reporter/reportWalk.d.ts +16 -6
  23. package/dist/esm/reporter/reportWalk.js +63 -13
  24. package/dist/esm/utils/BrowserUtils.d.ts +4 -4
  25. package/dist/lib/page/extendPage.d.ts +6 -0
  26. package/dist/lib/page/extendPage.js +24 -1
  27. package/dist/lib/test/healRerunGate.d.ts +102 -0
  28. package/dist/lib/test/healRerunGate.js +228 -0
  29. package/dist/lib/test/testExtension.d.ts +1 -0
  30. package/dist/lib/test/testExtension.js +20 -10
  31. package/dist/managers/DonobuStack.d.ts +19 -19
  32. package/dist/reporter/buildReport.js +54 -1
  33. package/dist/reporter/merge.d.ts +1 -6
  34. package/dist/reporter/merge.js +57 -35
  35. package/dist/reporter/model.d.ts +16 -0
  36. package/dist/reporter/model.js +10 -1
  37. package/dist/reporter/render.js +34 -12
  38. package/dist/reporter/renderMarkdown.js +148 -93
  39. package/dist/reporter/renderSlack.js +39 -28
  40. package/dist/reporter/reportWalk.d.ts +16 -6
  41. package/dist/reporter/reportWalk.js +63 -13
  42. package/dist/utils/BrowserUtils.d.ts +4 -4
  43. package/package.json +4 -4
@@ -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,102 @@
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
+ * Projects the gate applies to. Tests in any other project — declared
49
+ * dependency projects (auth/setup), teardown projects — run untouched,
50
+ * exactly as Playwright schedules them. Computed by the orchestrator via
51
+ * `computeGatedProjects`; when absent, the gate falls back to the targets'
52
+ * own project names.
53
+ */
54
+ gatedProjects?: string[];
55
+ }
56
+ /** Targets indexed by absolute spec path for O(1) per-test decisions. */
57
+ export type HealRerunPlanIndex = Map<string, Set<string>>;
58
+ export declare function buildPlanIndex(plan: HealRerunPlan): HealRerunPlanIndex;
59
+ /**
60
+ * The projects the rerun gate should apply to: the heal targets' own
61
+ * projects, minus any project that is a declared (transitive) dependency of
62
+ * another target project — the dependency declaration wins, and that project
63
+ * runs in full.
64
+ */
65
+ export declare function computeGatedProjects(targetProjects: Array<string | undefined>, projectDependencies: Record<string, string[]> | undefined): string[];
66
+ /**
67
+ * Pure decision: should the test in `file` with `title` actually execute
68
+ * during the heal rerun? The plan is fully explicit — serial companions were
69
+ * already expanded into it by the orchestrator.
70
+ */
71
+ export declare function shouldRunDuringHealRerun(params: {
72
+ index: HealRerunPlanIndex;
73
+ file: string;
74
+ title: string;
75
+ }): boolean;
76
+ /**
77
+ * Expand heal targets with their `describe.serial` siblings, using the
78
+ * `serialScoped` flags the Donobu reporter recorded in the initial run's
79
+ * report. Companions live in the same file as their target by construction
80
+ * (serial scopes are intra-file), so they inherit the target's absolute file
81
+ * path; report file paths are rootDir-relative, hence the suffix match.
82
+ *
83
+ * Degrades to the unexpanded targets when the report (or the flags) are
84
+ * unavailable — serial chains then surface as honest not-reattempted
85
+ * failures instead of healing.
86
+ */
87
+ export declare function expandTargetsWithSerialCompanions(targets: HealRerunPlan['targets'], initialReport: {
88
+ suites?: unknown;
89
+ } | null): HealRerunPlan['targets'];
90
+ /** Test-only: reset the memoized plan so each test can load its own. */
91
+ export declare function resetHealRerunPlanCacheForTesting(): void;
92
+ /**
93
+ * Called from the Donobu auto fixture before any browser fixture initializes.
94
+ * Outside heal reruns this is a no-op. During a rerun, tests in gated
95
+ * projects that are outside the plan are annotated and skipped on the spot —
96
+ * no context, no page, no cost. Tests in ungated projects (declared
97
+ * dependencies of the targets, teardown projects) always run.
98
+ */
99
+ export declare function maybeSkipForHealRerun(testInfo: TestInfo, options?: {
100
+ planPath?: string;
101
+ }): void;
102
+ //# sourceMappingURL=healRerunGate.d.ts.map
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Runtime gate for auto-heal reruns.
4
+ *
5
+ * The auto-heal rerun is launched with the same project-level arguments as the
6
+ * initial run so Playwright's scheduling (declared project `dependencies`,
7
+ * workers, ordering) behaves with full fidelity. Within the targeted projects,
8
+ * however, only the tests the heal actually needs should execute. This module
9
+ * enforces that from an auto fixture that runs before any browser fixture:
10
+ * when `DONOBU_AUTO_HEAL_PLAN_PATH` is set, every test not in the rerun plan
11
+ * skips immediately — before a context or page is created — annotated with
12
+ * `donobu-heal-skip-replay` so the merge step drops the entry and leaves the
13
+ * initial run's result untouched.
14
+ *
15
+ * What runs during a heal rerun (the "declared signals only" policy):
16
+ * - Heal targets (the failed tests with actionable treatment plans).
17
+ * - For a target inside a `test.describe.serial` scope (or a file marked
18
+ * serial via `test.describe.configure({ mode: 'serial' })`): the other
19
+ * serial-scoped tests in that file. Serial mode is Playwright's declared
20
+ * intra-file ordering contract — Playwright itself re-runs whole serial
21
+ * groups on retry, and we mirror that. The orchestrator expands the plan
22
+ * with these companions before the rerun (see
23
+ * `expandTargetsWithSerialCompanions`) using the `serialScoped` flags the
24
+ * Donobu reporter recorded during the initial run — the runner process
25
+ * sees the suite tree; the worker (where this gate runs) does not.
26
+ * - Declared dependency projects, which Playwright always runs in full.
27
+ *
28
+ * Implicit ordering (checkpoint files between plain tests, cross-file state
29
+ * with `workers: 1`) is deliberately NOT honored: tests relying on it will
30
+ * skip themselves during the rerun and surface as honest failures with
31
+ * guidance to declare the dependency.
32
+ *
33
+ * The gate runs at test runtime rather than collection time so Playwright's
34
+ * test-location attribution stays untouched (a collection-time wrapper would
35
+ * become every test's reported call site, breaking the merge's file-based
36
+ * matching) and so the plan can be matched against `testInfo.file`/`title`
37
+ * exactly instead of via stack inspection.
38
+ */
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.buildPlanIndex = buildPlanIndex;
44
+ exports.computeGatedProjects = computeGatedProjects;
45
+ exports.shouldRunDuringHealRerun = shouldRunDuringHealRerun;
46
+ exports.expandTargetsWithSerialCompanions = expandTargetsWithSerialCompanions;
47
+ exports.resetHealRerunPlanCacheForTesting = resetHealRerunPlanCacheForTesting;
48
+ exports.maybeSkipForHealRerun = maybeSkipForHealRerun;
49
+ const fs_1 = __importDefault(require("fs"));
50
+ const path_1 = __importDefault(require("path"));
51
+ const envVars_1 = require("../../envVars");
52
+ const model_1 = require("../../reporter/model");
53
+ const Logger_1 = require("../../utils/Logger");
54
+ function buildPlanIndex(plan) {
55
+ const index = new Map();
56
+ for (const target of plan.targets ?? []) {
57
+ if (!target?.file || !target?.title) {
58
+ continue;
59
+ }
60
+ const file = path_1.default.resolve(target.file);
61
+ if (!index.has(file)) {
62
+ index.set(file, new Set());
63
+ }
64
+ index.get(file).add(target.title);
65
+ }
66
+ return index;
67
+ }
68
+ /**
69
+ * The projects the rerun gate should apply to: the heal targets' own
70
+ * projects, minus any project that is a declared (transitive) dependency of
71
+ * another target project — the dependency declaration wins, and that project
72
+ * runs in full.
73
+ */
74
+ function computeGatedProjects(targetProjects, projectDependencies) {
75
+ const targets = [
76
+ ...new Set(targetProjects.filter((name) => Boolean(name))),
77
+ ];
78
+ if (!projectDependencies) {
79
+ return targets;
80
+ }
81
+ // Every project reachable through any target's declared dependency chain.
82
+ const reachable = new Set();
83
+ const visit = (name) => {
84
+ for (const dependency of projectDependencies[name] ?? []) {
85
+ if (!reachable.has(dependency)) {
86
+ reachable.add(dependency);
87
+ visit(dependency);
88
+ }
89
+ }
90
+ };
91
+ targets.forEach(visit);
92
+ return targets.filter((name) => !reachable.has(name));
93
+ }
94
+ /**
95
+ * Pure decision: should the test in `file` with `title` actually execute
96
+ * during the heal rerun? The plan is fully explicit — serial companions were
97
+ * already expanded into it by the orchestrator.
98
+ */
99
+ function shouldRunDuringHealRerun(params) {
100
+ const titles = params.index.get(path_1.default.resolve(params.file));
101
+ return titles?.has(params.title) ?? false;
102
+ }
103
+ /**
104
+ * Expand heal targets with their `describe.serial` siblings, using the
105
+ * `serialScoped` flags the Donobu reporter recorded in the initial run's
106
+ * report. Companions live in the same file as their target by construction
107
+ * (serial scopes are intra-file), so they inherit the target's absolute file
108
+ * path; report file paths are rootDir-relative, hence the suffix match.
109
+ *
110
+ * Degrades to the unexpanded targets when the report (or the flags) are
111
+ * unavailable — serial chains then surface as honest not-reattempted
112
+ * failures instead of healing.
113
+ */
114
+ function expandTargetsWithSerialCompanions(targets, initialReport) {
115
+ const suites = (initialReport?.suites ?? []);
116
+ const expanded = [...targets];
117
+ const seen = new Set(targets.map((t) => `${path_1.default.resolve(t.file)}::${t.projectName}::${t.title}`));
118
+ for (const target of targets) {
119
+ if (!target.file || !target.title) {
120
+ continue;
121
+ }
122
+ const targetFile = path_1.default.resolve(target.file);
123
+ for (const suite of suites) {
124
+ const suiteFile = String(suite.file ?? '');
125
+ if (!targetFile.endsWith(path_1.default.normalize(suiteFile)) ||
126
+ suiteFile.length === 0) {
127
+ continue;
128
+ }
129
+ const specs = suite.specs ?? [];
130
+ const targetEntry = specs
131
+ .find((spec) => spec.title === target.title)
132
+ ?.tests?.find((test) => !target.projectName || test.projectName === target.projectName);
133
+ if (!targetEntry?.serialScoped) {
134
+ continue;
135
+ }
136
+ // The target is serial-scoped: every serial-scoped test in this file
137
+ // (same project) joins the plan. Bounded by the file, mirroring how
138
+ // Playwright re-runs whole serial groups on retry.
139
+ for (const spec of specs) {
140
+ for (const test of spec.tests ?? []) {
141
+ if (!test.serialScoped) {
142
+ continue;
143
+ }
144
+ if (target.projectName && test.projectName !== target.projectName) {
145
+ continue;
146
+ }
147
+ const key = `${targetFile}::${test.projectName}::${spec.title}`;
148
+ if (seen.has(key)) {
149
+ continue;
150
+ }
151
+ seen.add(key);
152
+ expanded.push({
153
+ file: target.file,
154
+ title: spec.title,
155
+ projectName: test.projectName,
156
+ });
157
+ }
158
+ }
159
+ }
160
+ }
161
+ return expanded;
162
+ }
163
+ let cachedPlan;
164
+ function loadPlan(planPathOverride) {
165
+ if (planPathOverride === undefined && cachedPlan !== undefined) {
166
+ return cachedPlan;
167
+ }
168
+ const planPath = planPathOverride ?? envVars_1.env.data.DONOBU_AUTO_HEAL_PLAN_PATH;
169
+ let loaded = null;
170
+ if (planPath) {
171
+ try {
172
+ const raw = fs_1.default.readFileSync(planPath, 'utf8');
173
+ const plan = JSON.parse(raw);
174
+ loaded = {
175
+ index: buildPlanIndex(plan),
176
+ gatedProjects: new Set(plan.gatedProjects ??
177
+ // Older plans carry no project list — gate the targets' own
178
+ // projects, leaving everything else (dependencies) untouched.
179
+ plan.targets
180
+ ?.map((target) => target.projectName)
181
+ .filter((name) => Boolean(name)) ??
182
+ []),
183
+ };
184
+ }
185
+ catch (error) {
186
+ Logger_1.appLogger.warn(`Auto-heal rerun plan at ${planPath} could not be read; running all collected tests.`, error);
187
+ loaded = null;
188
+ }
189
+ }
190
+ if (planPathOverride === undefined) {
191
+ cachedPlan = loaded;
192
+ }
193
+ return loaded;
194
+ }
195
+ /** Test-only: reset the memoized plan so each test can load its own. */
196
+ function resetHealRerunPlanCacheForTesting() {
197
+ cachedPlan = undefined;
198
+ }
199
+ /**
200
+ * Called from the Donobu auto fixture before any browser fixture initializes.
201
+ * Outside heal reruns this is a no-op. During a rerun, tests in gated
202
+ * projects that are outside the plan are annotated and skipped on the spot —
203
+ * no context, no page, no cost. Tests in ungated projects (declared
204
+ * dependencies of the targets, teardown projects) always run.
205
+ */
206
+ function maybeSkipForHealRerun(testInfo, options) {
207
+ const plan = loadPlan(options?.planPath);
208
+ if (!plan) {
209
+ return;
210
+ }
211
+ if (!plan.gatedProjects.has(testInfo.project.name)) {
212
+ return;
213
+ }
214
+ const shouldRun = shouldRunDuringHealRerun({
215
+ index: plan.index,
216
+ file: testInfo.file,
217
+ title: testInfo.title,
218
+ });
219
+ if (shouldRun) {
220
+ return;
221
+ }
222
+ testInfo.annotations.push({
223
+ type: model_1.HEAL_SKIP_REPLAY_ANNOTATION_TYPE,
224
+ description: 'Not part of the auto-heal rerun plan; the initial run result stands.',
225
+ });
226
+ testInfo.skip(true, 'Not part of the auto-heal rerun plan; the initial run result stands.');
227
+ }
228
+ //# sourceMappingURL=healRerunGate.js.map
@@ -5,6 +5,7 @@ import { FlowLogBuffer } from '../../utils/FlowLogBuffer';
5
5
  import type { DonobuExtendedPage } from '../page/DonobuExtendedPage';
6
6
  export * from '@playwright/test';
7
7
  export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
8
+ healRerunGate: void;
8
9
  flowLoggingContext: {
9
10
  flowId: string;
10
11
  logBuffer: FlowLogBuffer;
@@ -41,6 +41,7 @@ const PageLogListeners_1 = require("../../utils/PageLogListeners");
41
41
  const cacheLocator_1 = require("../ai/cache/cacheLocator");
42
42
  const extendPage_1 = require("../page/extendPage");
43
43
  const tbd_1 = require("../page/tbd");
44
+ const healRerunGate_1 = require("./healRerunGate");
44
45
  const selfHealing_1 = require("./utils/selfHealing");
45
46
  const triageTestFailure_1 = require("./utils/triageTestFailure");
46
47
  __exportStar(require("@playwright/test"), exports);
@@ -220,6 +221,20 @@ function persistVideoIfApplicable(page, testInfo, videoOption) {
220
221
  */
221
222
  const UPLOAD_DRAIN_TIMEOUT_MS = 30_000;
222
223
  exports.test = test_1.test.extend({
224
+ /**
225
+ * Auto-heal rerun gate. First in registration order so it runs before every
226
+ * other test-scoped fixture: during a heal rerun, tests outside the rerun
227
+ * plan skip here — before a browser context or page is ever created — with
228
+ * the `donobu-heal-skip-replay` annotation the merge step uses to drop
229
+ * their entries. A no-op outside heal reruns.
230
+ */
231
+ healRerunGate: [
232
+ async ({}, use, testInfo) => {
233
+ (0, healRerunGate_1.maybeSkipForHealRerun)(testInfo);
234
+ await use();
235
+ },
236
+ { scope: 'test', auto: true },
237
+ ],
223
238
  /**
224
239
  * Establish a logging scope for the entire Playwright test *before* any other
225
240
  * fixtures run. Playwright builds the fixture dependency graph eagerly, so
@@ -400,6 +415,7 @@ exports.test = test_1.test.extend({
400
415
  flowId: flowId,
401
416
  visualCueDurationMs: visualCueDurationMs,
402
417
  cacheFilepath: (0, cacheLocator_1.buildPageAiCachePath)(testInfo.file),
418
+ specFilePath: testInfo.file,
403
419
  envVars: (0, DonobuFlowsManager_1.distillAllowedEnvVariableNames)(overallObjective, testInfo.annotations
404
420
  .filter((a) => a.type === 'ENV' && a.description)
405
421
  .map((a) => a.description)),
@@ -1051,16 +1067,10 @@ async function finalizeTest(page, testInfo, logBuffer, videoOption) {
1051
1067
  }
1052
1068
  }
1053
1069
  }
1054
- else if (testInfo.status === 'passed' &&
1055
- MiscUtils_1.MiscUtils.yn(envVars_1.env.data.DONOBU_AUTO_HEAL_ACTIVE)) {
1056
- const hasSelfHealedAnnotation = testInfo.annotations.some((annotation) => annotation.type === 'self-healed');
1057
- if (!hasSelfHealedAnnotation) {
1058
- testInfo.annotations.push({
1059
- type: 'self-healed',
1060
- description: 'Automatically healed by Donobu auto-heal rerun.',
1061
- });
1062
- }
1063
- }
1070
+ // Note: passing tests in an auto-heal rerun are NOT annotated here. Whether
1071
+ // a test was actually healed (failed initially, passed on the rerun) is
1072
+ // decided by the merge step, which sees both runs — annotating every rerun
1073
+ // pass would mislabel dependency tests that never failed.
1064
1074
  // Flush any page.tbd() sessions: replace the tbd() call sites in the
1065
1075
  // source files with the generated Playwright code for the recorded
1066
1076
  // user interactions.
@@ -43,25 +43,25 @@ export type DonobuStack = {
43
43
  * environment variables.
44
44
  */
45
45
  export declare function setupDonobuStack(donobuDeploymentEnvironment: DonobuDeploymentEnvironment, controlPanelFactory: ControlPanelFactory, envPersistenceVolatile?: EnvPersistenceVolatile, environ?: import("env-struct").Env<{
46
- BASE64_GPT_CONFIG: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
47
- BROWSERBASE_API_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
48
- BROWSERBASE_PROJECT_ID: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
49
- DONOBU_API_BASE_URL: import("zod/v4").ZodDefault<import("zod/v4").ZodString>;
50
- ANTHROPIC_API_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
51
- ANTHROPIC_MODEL_NAME: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
52
- GOOGLE_GENERATIVE_AI_API_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
53
- GOOGLE_GENERATIVE_AI_MODEL_NAME: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
54
- OLLAMA_MODEL_NAME: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
55
- OLLAMA_API_URL: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
56
- OPENAI_API_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
57
- OPENAI_API_MODEL_NAME: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
58
- PERSISTENCE_PRIORITY: import("zod/v4").ZodDefault<import("zod/v4").ZodArray<import("zod/v4").ZodString>>;
59
- AWS_BEDROCK_MODEL_NAME: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
60
- AWS_ACCESS_KEY_ID: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
61
- AWS_SECRET_ACCESS_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
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>;
64
- DONOBU_UPLOADS_OWNED_BY_PARENT: import("zod/v4").ZodOptional<import("zod/v4").ZodCodec<import("zod/v4").ZodString, import("zod/v4").ZodBoolean>>;
46
+ BASE64_GPT_CONFIG: import("zod").ZodOptional<import("zod").ZodString>;
47
+ BROWSERBASE_API_KEY: import("zod").ZodOptional<import("zod").ZodString>;
48
+ BROWSERBASE_PROJECT_ID: import("zod").ZodOptional<import("zod").ZodString>;
49
+ DONOBU_API_BASE_URL: import("zod").ZodDefault<import("zod").ZodString>;
50
+ ANTHROPIC_API_KEY: import("zod").ZodOptional<import("zod").ZodString>;
51
+ ANTHROPIC_MODEL_NAME: import("zod").ZodOptional<import("zod").ZodString>;
52
+ GOOGLE_GENERATIVE_AI_API_KEY: import("zod").ZodOptional<import("zod").ZodString>;
53
+ GOOGLE_GENERATIVE_AI_MODEL_NAME: import("zod").ZodOptional<import("zod").ZodString>;
54
+ OLLAMA_MODEL_NAME: import("zod").ZodOptional<import("zod").ZodString>;
55
+ OLLAMA_API_URL: import("zod").ZodOptional<import("zod").ZodString>;
56
+ OPENAI_API_KEY: import("zod").ZodOptional<import("zod").ZodString>;
57
+ OPENAI_API_MODEL_NAME: import("zod").ZodOptional<import("zod").ZodString>;
58
+ PERSISTENCE_PRIORITY: import("zod").ZodDefault<import("zod").ZodArray<import("zod").ZodString>>;
59
+ AWS_BEDROCK_MODEL_NAME: import("zod").ZodOptional<import("zod").ZodString>;
60
+ AWS_ACCESS_KEY_ID: import("zod").ZodOptional<import("zod").ZodString>;
61
+ AWS_SECRET_ACCESS_KEY: import("zod").ZodOptional<import("zod").ZodString>;
62
+ DONOBU_API_KEY: import("zod").ZodOptional<import("zod").ZodString>;
63
+ DONOBU_PERSISTENCE_API_KEY: import("zod").ZodOptional<import("zod").ZodString>;
64
+ DONOBU_UPLOADS_OWNED_BY_PARENT: import("zod").ZodOptional<import("zod").ZodCodec<import("zod").ZodString, import("zod").ZodBoolean>>;
65
65
  }, {
66
66
  BASE64_GPT_CONFIG?: string | undefined;
67
67
  BROWSERBASE_API_KEY?: string | undefined;
@@ -41,6 +41,27 @@ function buildDonobuReport(resultsByTest, rootDir) {
41
41
  }
42
42
  byTitle.get(test.title).push(test);
43
43
  }
44
+ // Declared project dependency graph (`FullProject.dependencies`), keyed by
45
+ // project name. The auto-heal orchestrator uses it to keep the rerun gate
46
+ // away from projects that are declared dependencies of heal targets — those
47
+ // must run in full, exactly as Playwright schedules them.
48
+ const projectDependencies = {};
49
+ for (const test of resultsByTest.keys()) {
50
+ try {
51
+ let suite = test.parent;
52
+ while (suite && suite.type !== 'project') {
53
+ suite = suite.parent;
54
+ }
55
+ const project = suite?.project();
56
+ if (project?.name && !(project.name in projectDependencies)) {
57
+ projectDependencies[project.name] = [...(project.dependencies ?? [])];
58
+ }
59
+ }
60
+ catch {
61
+ // Reporter shape drift — omit the entry; the orchestrator degrades to
62
+ // gating only the heal targets' own projects.
63
+ }
64
+ }
44
65
  const suites = [];
45
66
  for (const [file, titleMap] of byFile) {
46
67
  const specs = [];
@@ -58,6 +79,14 @@ function buildDonobuReport(resultsByTest, rootDir) {
58
79
  // Signal "skipped" tests to the renderers the same way the JSON
59
80
  // reporter does.
60
81
  status: test.expectedStatus === 'skipped' ? 'skipped' : undefined,
82
+ // Consumed by `reportWalk.statusOf` to classify `test.fail()`
83
+ // specs as expected failures rather than real ones; absent from
84
+ // the legacy state-file shape.
85
+ expectedStatus: test.expectedStatus,
86
+ // Whether the test sits inside a `describe.serial` scope — declared
87
+ // intra-file ordering. The auto-heal orchestrator uses this to
88
+ // include a heal target's serial siblings in the rerun plan.
89
+ serialScoped: isSerialScoped(test),
61
90
  results: results.map((r) => ({
62
91
  status: r.status,
63
92
  duration: r.duration,
@@ -108,7 +137,31 @@ function buildDonobuReport(resultsByTest, rootDir) {
108
137
  }
109
138
  suites.push({ file, specs });
110
139
  }
111
- return { suites, metadata: {} };
140
+ return { suites, metadata: { projectDependencies } };
141
+ }
142
+ /**
143
+ * Whether any enclosing suite is in serial mode (`test.describe.serial` or
144
+ * `test.describe.configure({ mode: 'serial' })`).
145
+ *
146
+ * Reads the runner-side suite internals (`_parallelMode`) — reporters receive
147
+ * the runner's own Suite instances, which carry it. Verified against
148
+ * @playwright/test 1.58; guarded so shape drift degrades to `false` (serial
149
+ * chains then lose heal eligibility, but reporting stays honest).
150
+ */
151
+ function isSerialScoped(test) {
152
+ try {
153
+ let suite = test.parent;
154
+ while (suite) {
155
+ if (suite._parallelMode === 'serial') {
156
+ return true;
157
+ }
158
+ suite = suite.parent;
159
+ }
160
+ }
161
+ catch {
162
+ // Internal shape drifted — treat as non-serial.
163
+ }
164
+ return false;
112
165
  }
113
166
  /** Walk up the suite chain to find the enclosing project suite's title. */
114
167
  function getProjectName(test) {
@@ -11,17 +11,12 @@
11
11
  * call `mergeReports`, then persist / relocate attachments / re-render as
12
12
  * needed. Keeping the merge pure makes it trivial to test and reason about.
13
13
  */
14
- import type { DonobuReport, HealedTestDescriptor } from './model';
14
+ import type { DonobuReport } from './model';
15
15
  export interface MergeReportsParams {
16
16
  /** Pre-loaded initial report, or null when the initial run produced none. */
17
17
  initialReport: DonobuReport | null;
18
18
  /** Pre-loaded heal-run report, or null when the heal run produced none. */
19
19
  healReport: DonobuReport | null;
20
- /** Tests the orchestrator declared healed — used when the heal report
21
- * doesn't expose them under a matching key (e.g. filter rewrites). */
22
- healedTests: HealedTestDescriptor[];
23
- /** Whether the heal rerun exited cleanly. */
24
- healSucceeded: boolean;
25
20
  /** Recorded in the merged report metadata for triage auto-discovery. */
26
21
  triageRunDir?: string;
27
22
  /** Recorded in the merged report metadata purely for provenance. */