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.
- package/dist/cli/donobu-cli.js +14 -7
- package/dist/esm/cli/donobu-cli.js +14 -7
- package/dist/esm/lib/test/healRerunGate.d.ts +19 -2
- package/dist/esm/lib/test/healRerunGate.js +57 -15
- package/dist/esm/managers/DonobuStack.d.ts +19 -19
- package/dist/esm/reporter/buildReport.js +22 -1
- package/dist/esm/reporter/model.d.ts +7 -0
- package/dist/esm/utils/BrowserUtils.d.ts +4 -4
- package/dist/lib/test/healRerunGate.d.ts +19 -2
- package/dist/lib/test/healRerunGate.js +57 -15
- package/dist/managers/DonobuStack.d.ts +19 -19
- package/dist/reporter/buildReport.js +22 -1
- package/dist/reporter/model.d.ts +7 -0
- package/dist/utils/BrowserUtils.d.ts +4 -4
- package/package.json +4 -4
package/dist/cli/donobu-cli.js
CHANGED
|
@@ -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
|
|
1382
|
-
// only the heal targets (and their declared
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
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({
|
|
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
|
|
2182
|
-
//
|
|
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
|
|
1382
|
-
// only the heal targets (and their declared
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
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({
|
|
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
|
|
2182
|
-
//
|
|
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
|
|
80
|
-
* are annotated and skipped on the spot —
|
|
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
|
|
137
|
-
function
|
|
138
|
-
if (planPathOverride === undefined &&
|
|
139
|
-
return
|
|
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
|
|
169
|
+
let loaded = null;
|
|
143
170
|
if (planPath) {
|
|
144
171
|
try {
|
|
145
172
|
const raw = fs_1.default.readFileSync(planPath, 'utf8');
|
|
146
|
-
|
|
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
|
-
|
|
187
|
+
loaded = null;
|
|
151
188
|
}
|
|
152
189
|
}
|
|
153
190
|
if (planPathOverride === undefined) {
|
|
154
|
-
|
|
191
|
+
cachedPlan = loaded;
|
|
155
192
|
}
|
|
156
|
-
return
|
|
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
|
-
|
|
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
|
|
165
|
-
* are annotated and skipped on the spot —
|
|
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
|
|
169
|
-
if (!
|
|
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
|
|
47
|
-
BROWSERBASE_API_KEY: import("zod
|
|
48
|
-
BROWSERBASE_PROJECT_ID: import("zod
|
|
49
|
-
DONOBU_API_BASE_URL: import("zod
|
|
50
|
-
ANTHROPIC_API_KEY: import("zod
|
|
51
|
-
ANTHROPIC_MODEL_NAME: import("zod
|
|
52
|
-
GOOGLE_GENERATIVE_AI_API_KEY: import("zod
|
|
53
|
-
GOOGLE_GENERATIVE_AI_MODEL_NAME: import("zod
|
|
54
|
-
OLLAMA_MODEL_NAME: import("zod
|
|
55
|
-
OLLAMA_API_URL: import("zod
|
|
56
|
-
OPENAI_API_KEY: import("zod
|
|
57
|
-
OPENAI_API_MODEL_NAME: import("zod
|
|
58
|
-
PERSISTENCE_PRIORITY: import("zod
|
|
59
|
-
AWS_BEDROCK_MODEL_NAME: import("zod
|
|
60
|
-
AWS_ACCESS_KEY_ID: import("zod
|
|
61
|
-
AWS_SECRET_ACCESS_KEY: import("zod
|
|
62
|
-
DONOBU_API_KEY: import("zod
|
|
63
|
-
DONOBU_PERSISTENCE_API_KEY: import("zod
|
|
64
|
-
DONOBU_UPLOADS_OWNED_BY_PARENT: import("zod
|
|
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
|
|
75
|
-
PROXY_SERVER: import("zod
|
|
76
|
-
PROXY_USERNAME: import("zod
|
|
77
|
-
PROXY_PASSWORD: import("zod
|
|
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
|
|
80
|
-
* are annotated and skipped on the spot —
|
|
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
|
|
137
|
-
function
|
|
138
|
-
if (planPathOverride === undefined &&
|
|
139
|
-
return
|
|
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
|
|
169
|
+
let loaded = null;
|
|
143
170
|
if (planPath) {
|
|
144
171
|
try {
|
|
145
172
|
const raw = fs_1.default.readFileSync(planPath, 'utf8');
|
|
146
|
-
|
|
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
|
-
|
|
187
|
+
loaded = null;
|
|
151
188
|
}
|
|
152
189
|
}
|
|
153
190
|
if (planPathOverride === undefined) {
|
|
154
|
-
|
|
191
|
+
cachedPlan = loaded;
|
|
155
192
|
}
|
|
156
|
-
return
|
|
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
|
-
|
|
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
|
|
165
|
-
* are annotated and skipped on the spot —
|
|
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
|
|
169
|
-
if (!
|
|
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
|
|
47
|
-
BROWSERBASE_API_KEY: import("zod
|
|
48
|
-
BROWSERBASE_PROJECT_ID: import("zod
|
|
49
|
-
DONOBU_API_BASE_URL: import("zod
|
|
50
|
-
ANTHROPIC_API_KEY: import("zod
|
|
51
|
-
ANTHROPIC_MODEL_NAME: import("zod
|
|
52
|
-
GOOGLE_GENERATIVE_AI_API_KEY: import("zod
|
|
53
|
-
GOOGLE_GENERATIVE_AI_MODEL_NAME: import("zod
|
|
54
|
-
OLLAMA_MODEL_NAME: import("zod
|
|
55
|
-
OLLAMA_API_URL: import("zod
|
|
56
|
-
OPENAI_API_KEY: import("zod
|
|
57
|
-
OPENAI_API_MODEL_NAME: import("zod
|
|
58
|
-
PERSISTENCE_PRIORITY: import("zod
|
|
59
|
-
AWS_BEDROCK_MODEL_NAME: import("zod
|
|
60
|
-
AWS_ACCESS_KEY_ID: import("zod
|
|
61
|
-
AWS_SECRET_ACCESS_KEY: import("zod
|
|
62
|
-
DONOBU_API_KEY: import("zod
|
|
63
|
-
DONOBU_PERSISTENCE_API_KEY: import("zod
|
|
64
|
-
DONOBU_UPLOADS_OWNED_BY_PARENT: import("zod
|
|
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
|
package/dist/reporter/model.d.ts
CHANGED
|
@@ -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
|
|
75
|
-
PROXY_SERVER: import("zod
|
|
76
|
-
PROXY_USERNAME: import("zod
|
|
77
|
-
PROXY_PASSWORD: import("zod
|
|
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.
|
|
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.
|
|
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.
|
|
63
|
-
"playwright-core": "^1.
|
|
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"
|