donobu 5.60.2 → 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.
@@ -1378,12 +1378,16 @@ async function attemptAutoHealRun(params) {
1378
1378
  envOverrides.DONOBU_PAGE_AI_CLEAR_CACHE_FILES =
1379
1379
  JSON.stringify(clearCacheFiles);
1380
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 fullPlaywright's semantics.
1381
+ // The rerun plan drives the runtime gate in the test fixture: within the
1382
+ // gated projects, only the heal targets (and their declared
1383
+ // `describe.serial` siblings) execute. Projects outside the gate
1384
+ // declared dependencies of target projects, teardown projects run in
1385
+ // full, exactly as Playwright schedules them.
1385
1386
  const healPlanPath = path.join(staging.rootDir, 'heal-rerun-plan.json');
1386
- await fs_1.promises.writeFile(healPlanPath, JSON.stringify({ targets: healTargets }), 'utf8');
1387
+ await fs_1.promises.writeFile(healPlanPath, JSON.stringify({
1388
+ targets: healTargets,
1389
+ gatedProjects: (0, healRerunGate_1.computeGatedProjects)(healTargets.map((target) => target.projectName), params.initialReport?.metadata?.projectDependencies),
1390
+ }), 'utf8');
1387
1391
  envOverrides.DONOBU_AUTO_HEAL_PLAN_PATH = healPlanPath;
1388
1392
  Logger_1.appLogger.info(`Auto-heal: re-running ${healTargets.length} targeted test(s) from ${evaluation.eligiblePlans.length} treatment plan(s)...`);
1389
1393
  const healJsonReportPath = path.join(staging.playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
@@ -2178,8 +2182,10 @@ async function runHealCommand(cliArgs) {
2178
2182
  applyJsonReportEnv(envOverrides, playwrightOutputDir);
2179
2183
  // Downstream hooks check this flag to avoid recursive auto-heal loops.
2180
2184
  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.
2185
+ // Same runtime gating as the automatic rerun: only the plan's test (plus
2186
+ // declared `describe.serial` siblings) executes within its own project;
2187
+ // dependency projects run in full. Without an initial report there is no
2188
+ // dependency graph, so the gate scopes to just the target's project.
2183
2189
  const healPlanPath = path.join(os.tmpdir(), `donobu-heal-rerun-plan-${Date.now()}.json`);
2184
2190
  await fs_1.promises.writeFile(healPlanPath, JSON.stringify({
2185
2191
  targets: [
@@ -2189,6 +2195,7 @@ async function runHealCommand(cliArgs) {
2189
2195
  projectName: persisted.failure.testCase.projectName,
2190
2196
  },
2191
2197
  ],
2198
+ gatedProjects: (0, healRerunGate_1.computeGatedProjects)([persisted.failure.testCase.projectName], undefined),
2192
2199
  }), 'utf8');
2193
2200
  envOverrides.DONOBU_AUTO_HEAL_PLAN_PATH = healPlanPath;
2194
2201
  Logger_1.appLogger.info(`Re-running Playwright using treatment plan at ${parsed.planPath}...`);
@@ -1378,12 +1378,16 @@ async function attemptAutoHealRun(params) {
1378
1378
  envOverrides.DONOBU_PAGE_AI_CLEAR_CACHE_FILES =
1379
1379
  JSON.stringify(clearCacheFiles);
1380
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 fullPlaywright's semantics.
1381
+ // The rerun plan drives the runtime gate in the test fixture: within the
1382
+ // gated projects, only the heal targets (and their declared
1383
+ // `describe.serial` siblings) execute. Projects outside the gate
1384
+ // declared dependencies of target projects, teardown projects run in
1385
+ // full, exactly as Playwright schedules them.
1385
1386
  const healPlanPath = path.join(staging.rootDir, 'heal-rerun-plan.json');
1386
- await fs_1.promises.writeFile(healPlanPath, JSON.stringify({ targets: healTargets }), 'utf8');
1387
+ await fs_1.promises.writeFile(healPlanPath, JSON.stringify({
1388
+ targets: healTargets,
1389
+ gatedProjects: (0, healRerunGate_1.computeGatedProjects)(healTargets.map((target) => target.projectName), params.initialReport?.metadata?.projectDependencies),
1390
+ }), 'utf8');
1387
1391
  envOverrides.DONOBU_AUTO_HEAL_PLAN_PATH = healPlanPath;
1388
1392
  Logger_1.appLogger.info(`Auto-heal: re-running ${healTargets.length} targeted test(s) from ${evaluation.eligiblePlans.length} treatment plan(s)...`);
1389
1393
  const healJsonReportPath = path.join(staging.playwrightOutputDir, PLAYWRIGHT_JSON_REPORT_FILENAME);
@@ -2178,8 +2182,10 @@ async function runHealCommand(cliArgs) {
2178
2182
  applyJsonReportEnv(envOverrides, playwrightOutputDir);
2179
2183
  // Downstream hooks check this flag to avoid recursive auto-heal loops.
2180
2184
  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.
2185
+ // Same runtime gating as the automatic rerun: only the plan's test (plus
2186
+ // declared `describe.serial` siblings) executes within its own project;
2187
+ // dependency projects run in full. Without an initial report there is no
2188
+ // dependency graph, so the gate scopes to just the target's project.
2183
2189
  const healPlanPath = path.join(os.tmpdir(), `donobu-heal-rerun-plan-${Date.now()}.json`);
2184
2190
  await fs_1.promises.writeFile(healPlanPath, JSON.stringify({
2185
2191
  targets: [
@@ -2189,6 +2195,7 @@ async function runHealCommand(cliArgs) {
2189
2195
  projectName: persisted.failure.testCase.projectName,
2190
2196
  },
2191
2197
  ],
2198
+ gatedProjects: (0, healRerunGate_1.computeGatedProjects)([persisted.failure.testCase.projectName], undefined),
2192
2199
  }), 'utf8');
2193
2200
  envOverrides.DONOBU_AUTO_HEAL_PLAN_PATH = healPlanPath;
2194
2201
  Logger_1.appLogger.info(`Re-running Playwright using treatment plan at ${parsed.planPath}...`);
@@ -44,10 +44,25 @@ export interface HealRerunPlan {
44
44
  title: string;
45
45
  projectName?: string;
46
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[];
47
55
  }
48
56
  /** Targets indexed by absolute spec path for O(1) per-test decisions. */
49
57
  export type HealRerunPlanIndex = Map<string, Set<string>>;
50
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[];
51
66
  /**
52
67
  * Pure decision: should the test in `file` with `title` actually execute
53
68
  * during the heal rerun? The plan is fully explicit — serial companions were
@@ -76,8 +91,10 @@ export declare function expandTargetsWithSerialCompanions(targets: HealRerunPlan
76
91
  export declare function resetHealRerunPlanCacheForTesting(): void;
77
92
  /**
78
93
  * 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.
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.
81
98
  */
82
99
  export declare function maybeSkipForHealRerun(testInfo: TestInfo, options?: {
83
100
  planPath?: string;
@@ -41,6 +41,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
41
41
  };
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.buildPlanIndex = buildPlanIndex;
44
+ exports.computeGatedProjects = computeGatedProjects;
44
45
  exports.shouldRunDuringHealRerun = shouldRunDuringHealRerun;
45
46
  exports.expandTargetsWithSerialCompanions = expandTargetsWithSerialCompanions;
46
47
  exports.resetHealRerunPlanCacheForTesting = resetHealRerunPlanCacheForTesting;
@@ -64,6 +65,32 @@ function buildPlanIndex(plan) {
64
65
  }
65
66
  return index;
66
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
+ }
67
94
  /**
68
95
  * Pure decision: should the test in `file` with `title` actually execute
69
96
  * during the heal rerun? The plan is fully explicit — serial companions were
@@ -133,44 +160,59 @@ function expandTargetsWithSerialCompanions(targets, initialReport) {
133
160
  }
134
161
  return expanded;
135
162
  }
136
- let cachedPlanIndex;
137
- function getPlanIndex(planPathOverride) {
138
- if (planPathOverride === undefined && cachedPlanIndex !== undefined) {
139
- return cachedPlanIndex;
163
+ let cachedPlan;
164
+ function loadPlan(planPathOverride) {
165
+ if (planPathOverride === undefined && cachedPlan !== undefined) {
166
+ return cachedPlan;
140
167
  }
141
168
  const planPath = planPathOverride ?? envVars_1.env.data.DONOBU_AUTO_HEAL_PLAN_PATH;
142
- let index = null;
169
+ let loaded = null;
143
170
  if (planPath) {
144
171
  try {
145
172
  const raw = fs_1.default.readFileSync(planPath, 'utf8');
146
- index = buildPlanIndex(JSON.parse(raw));
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
+ };
147
184
  }
148
185
  catch (error) {
149
186
  Logger_1.appLogger.warn(`Auto-heal rerun plan at ${planPath} could not be read; running all collected tests.`, error);
150
- index = null;
187
+ loaded = null;
151
188
  }
152
189
  }
153
190
  if (planPathOverride === undefined) {
154
- cachedPlanIndex = index;
191
+ cachedPlan = loaded;
155
192
  }
156
- return index;
193
+ return loaded;
157
194
  }
158
195
  /** Test-only: reset the memoized plan so each test can load its own. */
159
196
  function resetHealRerunPlanCacheForTesting() {
160
- cachedPlanIndex = undefined;
197
+ cachedPlan = undefined;
161
198
  }
162
199
  /**
163
200
  * Called from the Donobu auto fixture before any browser fixture initializes.
164
- * Outside heal reruns this is a no-op. During a rerun, tests outside the plan
165
- * are annotated and skipped on the spot — no context, no page, no cost.
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.
166
205
  */
167
206
  function maybeSkipForHealRerun(testInfo, options) {
168
- const index = getPlanIndex(options?.planPath);
169
- if (!index) {
207
+ const plan = loadPlan(options?.planPath);
208
+ if (!plan) {
209
+ return;
210
+ }
211
+ if (!plan.gatedProjects.has(testInfo.project.name)) {
170
212
  return;
171
213
  }
172
214
  const shouldRun = shouldRunDuringHealRerun({
173
- index,
215
+ index: plan.index,
174
216
  file: testInfo.file,
175
217
  title: testInfo.title,
176
218
  });
@@ -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 = [];
@@ -116,7 +137,7 @@ function buildDonobuReport(resultsByTest, rootDir) {
116
137
  }
117
138
  suites.push({ file, specs });
118
139
  }
119
- return { suites, metadata: {} };
140
+ return { suites, metadata: { projectDependencies } };
120
141
  }
121
142
  /**
122
143
  * Whether any enclosing suite is in serial mode (`test.describe.serial` or
@@ -57,6 +57,13 @@ export interface DonobuReportMetadata {
57
57
  /** True on reports that are the result of merging an initial + heal run. */
58
58
  donobuMergedReport?: boolean;
59
59
  mergedAtIso?: string;
60
+ /**
61
+ * Declared Playwright project dependency graph (`FullProject.dependencies`)
62
+ * keyed by project name, recorded by the reporter. The auto-heal
63
+ * orchestrator uses it to exclude declared dependencies of heal targets
64
+ * from the rerun gate — they must run in full.
65
+ */
66
+ projectDependencies?: Record<string, string[]>;
60
67
  sources?: {
61
68
  initial?: string | null;
62
69
  autoHeal?: string | null;
@@ -71,10 +71,10 @@ export declare class BrowserUtils {
71
71
  * @throws {InvalidParamValueException} When an invalid browser type is specified.
72
72
  */
73
73
  static create(browserConfig: BrowserConfig, videoDir?: string, storageState?: BrowserStorageState, environ?: import("env-struct").Env<{
74
- BROWSERBASE_API_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
75
- PROXY_SERVER: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
76
- PROXY_USERNAME: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
77
- PROXY_PASSWORD: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
74
+ BROWSERBASE_API_KEY: import("zod").ZodOptional<import("zod").ZodString>;
75
+ PROXY_SERVER: import("zod").ZodOptional<import("zod").ZodString>;
76
+ PROXY_USERNAME: import("zod").ZodOptional<import("zod").ZodString>;
77
+ PROXY_PASSWORD: import("zod").ZodOptional<import("zod").ZodString>;
78
78
  }, {
79
79
  BROWSERBASE_API_KEY?: string | undefined;
80
80
  PROXY_SERVER?: string | undefined;
@@ -44,10 +44,25 @@ export interface HealRerunPlan {
44
44
  title: string;
45
45
  projectName?: string;
46
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[];
47
55
  }
48
56
  /** Targets indexed by absolute spec path for O(1) per-test decisions. */
49
57
  export type HealRerunPlanIndex = Map<string, Set<string>>;
50
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[];
51
66
  /**
52
67
  * Pure decision: should the test in `file` with `title` actually execute
53
68
  * during the heal rerun? The plan is fully explicit — serial companions were
@@ -76,8 +91,10 @@ export declare function expandTargetsWithSerialCompanions(targets: HealRerunPlan
76
91
  export declare function resetHealRerunPlanCacheForTesting(): void;
77
92
  /**
78
93
  * 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.
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.
81
98
  */
82
99
  export declare function maybeSkipForHealRerun(testInfo: TestInfo, options?: {
83
100
  planPath?: string;
@@ -41,6 +41,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
41
41
  };
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.buildPlanIndex = buildPlanIndex;
44
+ exports.computeGatedProjects = computeGatedProjects;
44
45
  exports.shouldRunDuringHealRerun = shouldRunDuringHealRerun;
45
46
  exports.expandTargetsWithSerialCompanions = expandTargetsWithSerialCompanions;
46
47
  exports.resetHealRerunPlanCacheForTesting = resetHealRerunPlanCacheForTesting;
@@ -64,6 +65,32 @@ function buildPlanIndex(plan) {
64
65
  }
65
66
  return index;
66
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
+ }
67
94
  /**
68
95
  * Pure decision: should the test in `file` with `title` actually execute
69
96
  * during the heal rerun? The plan is fully explicit — serial companions were
@@ -133,44 +160,59 @@ function expandTargetsWithSerialCompanions(targets, initialReport) {
133
160
  }
134
161
  return expanded;
135
162
  }
136
- let cachedPlanIndex;
137
- function getPlanIndex(planPathOverride) {
138
- if (planPathOverride === undefined && cachedPlanIndex !== undefined) {
139
- return cachedPlanIndex;
163
+ let cachedPlan;
164
+ function loadPlan(planPathOverride) {
165
+ if (planPathOverride === undefined && cachedPlan !== undefined) {
166
+ return cachedPlan;
140
167
  }
141
168
  const planPath = planPathOverride ?? envVars_1.env.data.DONOBU_AUTO_HEAL_PLAN_PATH;
142
- let index = null;
169
+ let loaded = null;
143
170
  if (planPath) {
144
171
  try {
145
172
  const raw = fs_1.default.readFileSync(planPath, 'utf8');
146
- index = buildPlanIndex(JSON.parse(raw));
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
+ };
147
184
  }
148
185
  catch (error) {
149
186
  Logger_1.appLogger.warn(`Auto-heal rerun plan at ${planPath} could not be read; running all collected tests.`, error);
150
- index = null;
187
+ loaded = null;
151
188
  }
152
189
  }
153
190
  if (planPathOverride === undefined) {
154
- cachedPlanIndex = index;
191
+ cachedPlan = loaded;
155
192
  }
156
- return index;
193
+ return loaded;
157
194
  }
158
195
  /** Test-only: reset the memoized plan so each test can load its own. */
159
196
  function resetHealRerunPlanCacheForTesting() {
160
- cachedPlanIndex = undefined;
197
+ cachedPlan = undefined;
161
198
  }
162
199
  /**
163
200
  * Called from the Donobu auto fixture before any browser fixture initializes.
164
- * Outside heal reruns this is a no-op. During a rerun, tests outside the plan
165
- * are annotated and skipped on the spot — no context, no page, no cost.
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.
166
205
  */
167
206
  function maybeSkipForHealRerun(testInfo, options) {
168
- const index = getPlanIndex(options?.planPath);
169
- if (!index) {
207
+ const plan = loadPlan(options?.planPath);
208
+ if (!plan) {
209
+ return;
210
+ }
211
+ if (!plan.gatedProjects.has(testInfo.project.name)) {
170
212
  return;
171
213
  }
172
214
  const shouldRun = shouldRunDuringHealRerun({
173
- index,
215
+ index: plan.index,
174
216
  file: testInfo.file,
175
217
  title: testInfo.title,
176
218
  });
@@ -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 = [];
@@ -116,7 +137,7 @@ function buildDonobuReport(resultsByTest, rootDir) {
116
137
  }
117
138
  suites.push({ file, specs });
118
139
  }
119
- return { suites, metadata: {} };
140
+ return { suites, metadata: { projectDependencies } };
120
141
  }
121
142
  /**
122
143
  * Whether any enclosing suite is in serial mode (`test.describe.serial` or
@@ -57,6 +57,13 @@ export interface DonobuReportMetadata {
57
57
  /** True on reports that are the result of merging an initial + heal run. */
58
58
  donobuMergedReport?: boolean;
59
59
  mergedAtIso?: string;
60
+ /**
61
+ * Declared Playwright project dependency graph (`FullProject.dependencies`)
62
+ * keyed by project name, recorded by the reporter. The auto-heal
63
+ * orchestrator uses it to exclude declared dependencies of heal targets
64
+ * from the rerun gate — they must run in full.
65
+ */
66
+ projectDependencies?: Record<string, string[]>;
60
67
  sources?: {
61
68
  initial?: string | null;
62
69
  autoHeal?: string | null;
@@ -71,10 +71,10 @@ export declare class BrowserUtils {
71
71
  * @throws {InvalidParamValueException} When an invalid browser type is specified.
72
72
  */
73
73
  static create(browserConfig: BrowserConfig, videoDir?: string, storageState?: BrowserStorageState, environ?: import("env-struct").Env<{
74
- BROWSERBASE_API_KEY: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
75
- PROXY_SERVER: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
76
- PROXY_USERNAME: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
77
- PROXY_PASSWORD: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
74
+ BROWSERBASE_API_KEY: import("zod").ZodOptional<import("zod").ZodString>;
75
+ PROXY_SERVER: import("zod").ZodOptional<import("zod").ZodString>;
76
+ PROXY_USERNAME: import("zod").ZodOptional<import("zod").ZodString>;
77
+ PROXY_PASSWORD: import("zod").ZodOptional<import("zod").ZodString>;
78
78
  }, {
79
79
  BROWSERBASE_API_KEY?: string | undefined;
80
80
  PROXY_SERVER?: string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "donobu",
3
- "version": "5.60.2",
3
+ "version": "5.60.3",
4
4
  "description": "Create browser automations with an LLM agent and replay them as Playwright scripts.",
5
5
  "main": "dist/main.js",
6
6
  "module": "dist/esm/main.js",
@@ -46,7 +46,7 @@
46
46
  "license": "UNLICENSED",
47
47
  "devDependencies": {
48
48
  "@eslint/js": "^10.0.1",
49
- "@playwright/test": "^1.57.0",
49
+ "@playwright/test": "^1.60.0",
50
50
  "@types/better-sqlite3": "^7.6.13",
51
51
  "@types/express": "^5.0.6",
52
52
  "@types/node": "^22.10.5",
@@ -59,8 +59,8 @@
59
59
  "eslint-plugin-perfectionist": "^5.9.0",
60
60
  "eslint-plugin-simple-import-sort": "^12.1.1",
61
61
  "globals": "^16.0.0",
62
- "playwright": "^1.57.0",
63
- "playwright-core": "^1.57.0",
62
+ "playwright": "^1.60.0",
63
+ "playwright-core": "^1.60.0",
64
64
  "typescript-eslint": "^8.47.0",
65
65
  "vitest": "^4.0.17",
66
66
  "winston-transport": "^4.9.0"