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.
- package/dist/cli/donobu-cli.js +158 -52
- package/dist/envVars.d.ts +4 -0
- package/dist/envVars.js +12 -0
- package/dist/esm/cli/donobu-cli.js +158 -52
- package/dist/esm/envVars.d.ts +4 -0
- package/dist/esm/envVars.js +12 -0
- package/dist/esm/lib/page/extendPage.d.ts +6 -0
- package/dist/esm/lib/page/extendPage.js +24 -1
- package/dist/esm/lib/test/healRerunGate.d.ts +102 -0
- package/dist/esm/lib/test/healRerunGate.js +228 -0
- package/dist/esm/lib/test/testExtension.d.ts +1 -0
- package/dist/esm/lib/test/testExtension.js +20 -10
- package/dist/esm/managers/DonobuStack.d.ts +19 -19
- package/dist/esm/reporter/buildReport.js +54 -1
- package/dist/esm/reporter/merge.d.ts +1 -6
- package/dist/esm/reporter/merge.js +57 -35
- package/dist/esm/reporter/model.d.ts +16 -0
- package/dist/esm/reporter/model.js +10 -1
- package/dist/esm/reporter/render.js +34 -12
- package/dist/esm/reporter/renderMarkdown.js +148 -93
- package/dist/esm/reporter/renderSlack.js +39 -28
- package/dist/esm/reporter/reportWalk.d.ts +16 -6
- package/dist/esm/reporter/reportWalk.js +63 -13
- package/dist/esm/utils/BrowserUtils.d.ts +4 -4
- package/dist/lib/page/extendPage.d.ts +6 -0
- package/dist/lib/page/extendPage.js +24 -1
- package/dist/lib/test/healRerunGate.d.ts +102 -0
- package/dist/lib/test/healRerunGate.js +228 -0
- package/dist/lib/test/testExtension.d.ts +1 -0
- package/dist/lib/test/testExtension.js +20 -10
- package/dist/managers/DonobuStack.d.ts +19 -19
- package/dist/reporter/buildReport.js +54 -1
- package/dist/reporter/merge.d.ts +1 -6
- package/dist/reporter/merge.js +57 -35
- package/dist/reporter/model.d.ts +16 -0
- package/dist/reporter/model.js +10 -1
- package/dist/reporter/render.js +34 -12
- package/dist/reporter/renderMarkdown.js +148 -93
- package/dist/reporter/renderSlack.js +39 -28
- package/dist/reporter/reportWalk.d.ts +16 -6
- package/dist/reporter/reportWalk.js +63 -13
- package/dist/utils/BrowserUtils.d.ts +4 -4
- package/package.json +4 -4
|
@@ -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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
|
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 = [];
|
|
@@ -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
|
|
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. */
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.mergeReports = mergeReports;
|
|
17
17
|
exports.buildTestKey = buildTestKey;
|
|
18
|
+
const model_1 = require("./model");
|
|
19
|
+
const reportWalk_1 = require("./reportWalk");
|
|
18
20
|
function mergeReports(params) {
|
|
19
21
|
const { initialReport, healReport } = params;
|
|
20
22
|
if (!initialReport && !healReport) {
|
|
@@ -29,7 +31,17 @@ function mergeReports(params) {
|
|
|
29
31
|
if (healReport) {
|
|
30
32
|
const processedHealEntries = new Set();
|
|
31
33
|
const processHealEntry = (healEntry) => {
|
|
32
|
-
|
|
34
|
+
// Tests the rerun's collection gate statically skipped carry zero
|
|
35
|
+
// information — the initial run's result for them stands untouched, so
|
|
36
|
+
// their heal entries are dropped wholesale (no appended attempts, no
|
|
37
|
+
// status churn, no skip noise in the merged report).
|
|
38
|
+
if (healEntry.test.annotations?.some((annotation) => annotation.type === model_1.HEAL_SKIP_REPLAY_ANNOTATION_TYPE)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Titles live on the spec in both report shapes (the Donobu state file
|
|
42
|
+
// and Playwright's native JSON); `test.title` is never set. Without the
|
|
43
|
+
// spec fallback all tests in a file/project collapse to one key.
|
|
44
|
+
const key = buildTestKey(healEntry.suite.file, healEntry.test.projectName, healEntry.test.title ?? healEntry.spec.title);
|
|
33
45
|
let combinedEntry = (healEntry.test.testId
|
|
34
46
|
? combinedIndex.byId.get(healEntry.test.testId)
|
|
35
47
|
: undefined) ??
|
|
@@ -54,16 +66,47 @@ function mergeReports(params) {
|
|
|
54
66
|
...healEntry.test.results,
|
|
55
67
|
];
|
|
56
68
|
}
|
|
57
|
-
if (healEntry.test.status !== undefined) {
|
|
58
|
-
combinedTest.status = healEntry.test.status;
|
|
59
|
-
}
|
|
60
|
-
if (healEntry.test.outcome !== undefined) {
|
|
61
|
-
combinedTest.outcome = healEntry.test.outcome;
|
|
62
|
-
}
|
|
63
69
|
const originalStatus = originalEntry
|
|
64
70
|
? getFinalResultStatus(originalEntry.test)
|
|
65
71
|
: undefined;
|
|
66
72
|
const healStatus = getFinalResultStatus(healEntry.test);
|
|
73
|
+
const originalFailed = originalStatus !== undefined &&
|
|
74
|
+
originalStatus !== 'passed' &&
|
|
75
|
+
originalStatus !== 'skipped';
|
|
76
|
+
// The heal rerun may only improve a test's outcome:
|
|
77
|
+
// - It must not relabel a genuine failure as "skipped" (a rerun in
|
|
78
|
+
// which the test skipped itself — e.g. a precondition guard fired —
|
|
79
|
+
// used to turn red runs green).
|
|
80
|
+
// - It must not flip an initially-passing test red: rerun failures of
|
|
81
|
+
// tests that tagged along (serial siblings, dependency projects) are
|
|
82
|
+
// advisory, recorded in the attempt history and an annotation.
|
|
83
|
+
const adoptHealStatus = healStatus === 'passed' ||
|
|
84
|
+
(originalStatus !== 'passed' && healStatus !== 'skipped');
|
|
85
|
+
if (adoptHealStatus) {
|
|
86
|
+
if (healEntry.test.status !== undefined) {
|
|
87
|
+
combinedTest.status = healEntry.test.status;
|
|
88
|
+
}
|
|
89
|
+
if (healEntry.test.outcome !== undefined) {
|
|
90
|
+
combinedTest.outcome = healEntry.test.outcome;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (originalFailed && healStatus === 'skipped') {
|
|
94
|
+
combinedTest.annotations = combinedTest.annotations ?? [];
|
|
95
|
+
combinedTest.annotations.push({
|
|
96
|
+
type: 'auto-heal-not-reattempted',
|
|
97
|
+
description: 'The auto-heal rerun could not re-attempt this test: it skipped itself during the rerun because a precondition set up by another test was missing. The original failure stands. To make this test heal-eligible, declare the ordering explicitly — via Playwright project `dependencies` or `test.describe.serial` — so the rerun can execute the prerequisites.',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else if (originalStatus === 'passed' &&
|
|
101
|
+
healStatus !== undefined &&
|
|
102
|
+
healStatus !== 'passed' &&
|
|
103
|
+
healStatus !== 'skipped') {
|
|
104
|
+
combinedTest.annotations = combinedTest.annotations ?? [];
|
|
105
|
+
combinedTest.annotations.push({
|
|
106
|
+
type: 'auto-heal-rerun-failed',
|
|
107
|
+
description: 'This test passed in the initial run but failed when re-run during the auto-heal pass. The initial result stands; the failed rerun attempt is preserved in the attempt history.',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
67
110
|
if (healStatus === 'passed' &&
|
|
68
111
|
originalStatus &&
|
|
69
112
|
originalStatus !== 'passed') {
|
|
@@ -90,26 +133,6 @@ function mergeReports(params) {
|
|
|
90
133
|
iterateEntries(healIndex.byId);
|
|
91
134
|
iterateEntries(healIndex.byKey);
|
|
92
135
|
}
|
|
93
|
-
if (params.healSucceeded && healedKeys.size === 0) {
|
|
94
|
-
params.healedTests.forEach((descriptor) => {
|
|
95
|
-
// Descriptors must arrive with `testCase.file` already normalized to the
|
|
96
|
-
// same form that suite.file has in the reports — the caller owns path
|
|
97
|
-
// normalization because it has access to the CWD the run was launched in.
|
|
98
|
-
const key = buildTestKey(descriptor.testCase.file, descriptor.testCase.projectName, descriptor.testCase.title);
|
|
99
|
-
const entry = combinedIndex.byKey.get(key);
|
|
100
|
-
if (entry) {
|
|
101
|
-
entry.test.annotations = entry.test.annotations ?? [];
|
|
102
|
-
if (!entry.test.annotations.some((annotation) => annotation.type === 'self-healed')) {
|
|
103
|
-
entry.test.annotations.push({
|
|
104
|
-
type: 'self-healed',
|
|
105
|
-
description: 'Automatically healed by Donobu auto-heal rerun after applying treatment plan.',
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
entry.test.donobuStatus = 'healed';
|
|
109
|
-
healedKeys.add(key);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
136
|
combined.stats = computeReportStats(combined);
|
|
114
137
|
const mergedMetadata = {
|
|
115
138
|
...(combined.metadata ?? {}),
|
|
@@ -152,7 +175,7 @@ function indexReport(report) {
|
|
|
152
175
|
if (test.testId) {
|
|
153
176
|
byId.set(test.testId, entry);
|
|
154
177
|
}
|
|
155
|
-
const key = buildTestKey(suite.file, test.projectName, test.title);
|
|
178
|
+
const key = buildTestKey(suite.file, test.projectName, test.title ?? spec.title);
|
|
156
179
|
byKey.set(key, entry);
|
|
157
180
|
});
|
|
158
181
|
});
|
|
@@ -195,9 +218,13 @@ function computeReportStats(report) {
|
|
|
195
218
|
if (finalResult?.duration) {
|
|
196
219
|
duration += finalResult.duration;
|
|
197
220
|
}
|
|
198
|
-
|
|
199
|
-
|
|
221
|
+
// Classify from the full attempt history so a heal-rerun skip can't
|
|
222
|
+
// hide an earlier genuine failure, retry recoveries count as flaky,
|
|
223
|
+
// and expected failures (`test.fail()`) count as expected.
|
|
224
|
+
switch ((0, reportWalk_1.statusOf)(test)) {
|
|
200
225
|
case 'passed':
|
|
226
|
+
case 'expectedFailure':
|
|
227
|
+
case 'healed':
|
|
201
228
|
expected += 1;
|
|
202
229
|
break;
|
|
203
230
|
case 'skipped':
|
|
@@ -206,11 +233,6 @@ function computeReportStats(report) {
|
|
|
206
233
|
case 'flaky':
|
|
207
234
|
flaky += 1;
|
|
208
235
|
break;
|
|
209
|
-
case 'failed':
|
|
210
|
-
case 'timedOut':
|
|
211
|
-
case 'interrupted':
|
|
212
|
-
unexpected += 1;
|
|
213
|
-
break;
|
|
214
236
|
default:
|
|
215
237
|
unexpected += 1;
|
|
216
238
|
}
|
|
@@ -21,6 +21,15 @@
|
|
|
21
21
|
* stable across Donobu versions.
|
|
22
22
|
*/
|
|
23
23
|
export declare const DONOBU_REPORT_STATE_FILENAME = ".donobu-report-state.json";
|
|
24
|
+
/**
|
|
25
|
+
* Annotation type stamped on tests the auto-heal rerun statically skipped
|
|
26
|
+
* because they were not part of the rerun plan (they passed the initial run
|
|
27
|
+
* and were not prerequisites of any heal target). The merge step drops these
|
|
28
|
+
* heal-run entries entirely: they carry no information, and the initial run's
|
|
29
|
+
* result for those tests stands untouched. Defined here (rather than in the
|
|
30
|
+
* test-side gate) so the pure reporter modules don't import test runtime code.
|
|
31
|
+
*/
|
|
32
|
+
export declare const HEAL_SKIP_REPLAY_ANNOTATION_TYPE = "donobu-heal-skip-replay";
|
|
24
33
|
/**
|
|
25
34
|
* Per-format output record written by each Donobu reporter into the shared
|
|
26
35
|
* state file. The auto-heal orchestrator reads this map after merging two
|
|
@@ -48,6 +57,13 @@ export interface DonobuReportMetadata {
|
|
|
48
57
|
/** True on reports that are the result of merging an initial + heal run. */
|
|
49
58
|
donobuMergedReport?: boolean;
|
|
50
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[]>;
|
|
51
67
|
sources?: {
|
|
52
68
|
initial?: string | null;
|
|
53
69
|
autoHeal?: string | null;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* retype the whole Playwright reporter surface.
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.DONOBU_REPORT_STATE_FILENAME = void 0;
|
|
17
|
+
exports.HEAL_SKIP_REPLAY_ANNOTATION_TYPE = exports.DONOBU_REPORT_STATE_FILENAME = void 0;
|
|
18
18
|
/**
|
|
19
19
|
* Filename the reporter writes into each run's Playwright output directory.
|
|
20
20
|
* The auto-heal orchestrator looks for this file (in both the initial run's
|
|
@@ -24,4 +24,13 @@ exports.DONOBU_REPORT_STATE_FILENAME = void 0;
|
|
|
24
24
|
* stable across Donobu versions.
|
|
25
25
|
*/
|
|
26
26
|
exports.DONOBU_REPORT_STATE_FILENAME = '.donobu-report-state.json';
|
|
27
|
+
/**
|
|
28
|
+
* Annotation type stamped on tests the auto-heal rerun statically skipped
|
|
29
|
+
* because they were not part of the rerun plan (they passed the initial run
|
|
30
|
+
* and were not prerequisites of any heal target). The merge step drops these
|
|
31
|
+
* heal-run entries entirely: they carry no information, and the initial run's
|
|
32
|
+
* result for those tests stands untouched. Defined here (rather than in the
|
|
33
|
+
* test-side gate) so the pure reporter modules don't import test runtime code.
|
|
34
|
+
*/
|
|
35
|
+
exports.HEAL_SKIP_REPLAY_ANNOTATION_TYPE = 'donobu-heal-skip-replay';
|
|
27
36
|
//# sourceMappingURL=model.js.map
|