donobu 2.46.6 → 2.47.0
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/assets/generated/version +1 -1
- package/dist/cli/donobu-cli.d.ts +3 -0
- package/dist/cli/donobu-cli.d.ts.map +1 -0
- package/dist/cli/donobu-cli.js +1493 -0
- package/dist/cli/donobu-cli.js.map +1 -0
- package/dist/cli/playwright-json-to-markdown.js +43 -22
- package/dist/cli/playwright-json-to-markdown.js.map +1 -1
- package/dist/envVars.d.ts +23 -0
- package/dist/envVars.d.ts.map +1 -1
- package/dist/envVars.js +13 -0
- package/dist/envVars.js.map +1 -1
- package/dist/esm/assets/generated/version +1 -1
- package/dist/esm/cli/donobu-cli.d.ts +3 -0
- package/dist/esm/cli/donobu-cli.d.ts.map +1 -0
- package/dist/esm/cli/donobu-cli.js +1493 -0
- package/dist/esm/cli/donobu-cli.js.map +1 -0
- package/dist/esm/cli/playwright-json-to-markdown.js +43 -22
- package/dist/esm/cli/playwright-json-to-markdown.js.map +1 -1
- package/dist/esm/envVars.d.ts +23 -0
- package/dist/esm/envVars.d.ts.map +1 -1
- package/dist/esm/envVars.js +13 -0
- package/dist/esm/envVars.js.map +1 -1
- package/dist/esm/lib/DonobuExtendedPage.d.ts +3 -0
- package/dist/esm/lib/DonobuExtendedPage.d.ts.map +1 -1
- package/dist/esm/lib/PageAi.d.ts.map +1 -1
- package/dist/esm/lib/PageAi.js +7 -1
- package/dist/esm/lib/PageAi.js.map +1 -1
- package/dist/esm/lib/testExtension.d.ts.map +1 -1
- package/dist/esm/lib/testExtension.js +53 -9
- package/dist/esm/lib/testExtension.js.map +1 -1
- package/dist/esm/lib/utils/triageTestFailure.d.ts +231 -0
- package/dist/esm/lib/utils/triageTestFailure.d.ts.map +1 -0
- package/dist/esm/lib/utils/triageTestFailure.js +1267 -0
- package/dist/esm/lib/utils/triageTestFailure.js.map +1 -0
- package/dist/lib/DonobuExtendedPage.d.ts +3 -0
- package/dist/lib/DonobuExtendedPage.d.ts.map +1 -1
- package/dist/lib/PageAi.d.ts.map +1 -1
- package/dist/lib/PageAi.js +7 -1
- package/dist/lib/PageAi.js.map +1 -1
- package/dist/lib/testExtension.d.ts.map +1 -1
- package/dist/lib/testExtension.js +53 -9
- package/dist/lib/testExtension.js.map +1 -1
- package/dist/lib/utils/triageTestFailure.d.ts +231 -0
- package/dist/lib/utils/triageTestFailure.d.ts.map +1 -0
- package/dist/lib/utils/triageTestFailure.js +1267 -0
- package/dist/lib/utils/triageTestFailure.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.TreatmentPlan = exports.AdditionalDataRequestSchema = exports.RemediationStepSchema = exports.FailureReasonSchema = void 0;
|
|
40
|
+
exports.buildTreatmentPlanFromHeuristics = buildTreatmentPlanFromHeuristics;
|
|
41
|
+
exports.gatherTestFailureEvidence = gatherTestFailureEvidence;
|
|
42
|
+
exports.generateTreatmentPlanFromEvidence = generateTreatmentPlanFromEvidence;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const crypto_1 = require("crypto");
|
|
46
|
+
const Logger_1 = require("../../utils/Logger");
|
|
47
|
+
const AnalyzePageTextTool_1 = require("../../tools/AnalyzePageTextTool");
|
|
48
|
+
const MarkObjectiveCompleteTool_1 = require("../../tools/MarkObjectiveCompleteTool");
|
|
49
|
+
const MarkObjectiveNotCompletableTool_1 = require("../../tools/MarkObjectiveNotCompletableTool");
|
|
50
|
+
const SummarizeLearningsTool_1 = require("../../tools/SummarizeLearningsTool");
|
|
51
|
+
const v4_1 = require("zod/v4");
|
|
52
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
53
|
+
/**
|
|
54
|
+
* Utilities for transforming a Playwright test failure into a structured treatment plan
|
|
55
|
+
* that Donobu Studio can surface to engineers or automation. The flow pivots between:
|
|
56
|
+
* 1. Gathering heuristics from the failing run (errors, attachments, Donobu AI history).
|
|
57
|
+
* 2. Asking the GPT-based triage agent for a canonical plan compliant with the schema.
|
|
58
|
+
* 3. Falling back to heuristics when the LLM cannot respond, so automation still receives
|
|
59
|
+
* next steps. The helper functions below exist to keep these stages composable.
|
|
60
|
+
*/
|
|
61
|
+
exports.FailureReasonSchema = v4_1.z
|
|
62
|
+
.enum([
|
|
63
|
+
'UNKNOWN',
|
|
64
|
+
'AUTOMATION_SCRIPT_ISSUE',
|
|
65
|
+
'SELECTOR_REGRESSION',
|
|
66
|
+
'TIMING_OR_SYNCHRONISATION',
|
|
67
|
+
'ASSERTION_DRIFT',
|
|
68
|
+
'APPLICATION_DEFECT',
|
|
69
|
+
'AUTHENTICATION_FAILURE',
|
|
70
|
+
'ENVIRONMENT_CONFIGURATION',
|
|
71
|
+
'TEST_DATA_UNAVAILABLE',
|
|
72
|
+
'NETWORK_OR_DEPENDENCY',
|
|
73
|
+
])
|
|
74
|
+
.describe(`UNKNOWN: Triggered when no concrete root cause can be inferred.
|
|
75
|
+
AUTOMATION_SCRIPT_ISSUE: The scripted steps are incorrect or incomplete.
|
|
76
|
+
SELECTOR_REGRESSION: UI element locators have changed and need updates.
|
|
77
|
+
TIMING_OR_SYNCHRONISATION: Wait conditions or timing assumptions failed.
|
|
78
|
+
ASSERTION_DRIFT: The expected outcomes in assertions no longer match reality.
|
|
79
|
+
APPLICATION_DEFECT: The product behaviour is broken and must be fixed upstream.
|
|
80
|
+
AUTHENTICATION_FAILURE: Login, MFA, or session preconditions were not met.
|
|
81
|
+
ENVIRONMENT_CONFIGURATION: Test infra or env vars are misconfigured.
|
|
82
|
+
TEST_DATA_UNAVAILABLE: Seed data or fixtures are missing or expired.
|
|
83
|
+
NETWORK_OR_DEPENDENCY: External services or network connectivity failed.`);
|
|
84
|
+
const RemediationCategorySchema = v4_1.z
|
|
85
|
+
.enum([
|
|
86
|
+
'RETRY_AUTOMATION',
|
|
87
|
+
'UPDATE_TEST_LOGIC',
|
|
88
|
+
'UPDATE_SELECTORS',
|
|
89
|
+
'ADJUST_TIMING',
|
|
90
|
+
'REFINE_ASSERTIONS',
|
|
91
|
+
'FIX_APPLICATION',
|
|
92
|
+
'VALIDATE_AUTHENTICATION',
|
|
93
|
+
'CHECK_ENVIRONMENT',
|
|
94
|
+
'REFRESH_TEST_DATA',
|
|
95
|
+
'STABILIZE_DEPENDENCIES',
|
|
96
|
+
'ESCALATE_MANUAL_REVIEW',
|
|
97
|
+
'UNKNOWN',
|
|
98
|
+
])
|
|
99
|
+
.describe('Categorises the type of remediation that should be attempted so that downstream systems can pick appropriate playbooks.');
|
|
100
|
+
exports.RemediationStepSchema = v4_1.z.object({
|
|
101
|
+
category: RemediationCategorySchema,
|
|
102
|
+
summary: v4_1.z
|
|
103
|
+
.string()
|
|
104
|
+
.describe('Short actionable label describing what must happen next.'),
|
|
105
|
+
details: v4_1.z
|
|
106
|
+
.string()
|
|
107
|
+
.describe('Specific guidance for performing the remediation step.'),
|
|
108
|
+
});
|
|
109
|
+
exports.AdditionalDataRequestSchema = v4_1.z.object({
|
|
110
|
+
description: v4_1.z
|
|
111
|
+
.string()
|
|
112
|
+
.describe('Information that would materially help confirm the root cause.'),
|
|
113
|
+
suggestedSources: v4_1.z
|
|
114
|
+
.array(v4_1.z.string())
|
|
115
|
+
.describe('Where to look for the requested data.')
|
|
116
|
+
.default([]),
|
|
117
|
+
});
|
|
118
|
+
const AutomationDirectivesSchema = v4_1.z
|
|
119
|
+
.object({
|
|
120
|
+
clearPageAiCache: v4_1.z
|
|
121
|
+
.boolean()
|
|
122
|
+
.describe('When true, clear cached Page.AI selectors before attempting an automated retry.')
|
|
123
|
+
.optional(),
|
|
124
|
+
targetTestFile: v4_1.z
|
|
125
|
+
.string()
|
|
126
|
+
.describe('Relative path to the Playwright spec that should be re-run when applying this plan.')
|
|
127
|
+
.optional(),
|
|
128
|
+
targetProject: v4_1.z
|
|
129
|
+
.string()
|
|
130
|
+
.describe('Playwright project name that should be used when re-running automation for this failure.')
|
|
131
|
+
.optional(),
|
|
132
|
+
additionalPlaywrightArgs: v4_1.z
|
|
133
|
+
.array(v4_1.z.string())
|
|
134
|
+
.describe('Extra Playwright CLI arguments the orchestrator should append when applying this plan.')
|
|
135
|
+
.optional(),
|
|
136
|
+
})
|
|
137
|
+
.partial();
|
|
138
|
+
exports.TreatmentPlan = v4_1.z.object({
|
|
139
|
+
failureSummary: v4_1.z
|
|
140
|
+
.string()
|
|
141
|
+
.describe('A summary of the nature of the test failure'),
|
|
142
|
+
failureReason: exports.FailureReasonSchema,
|
|
143
|
+
confidence: v4_1.z
|
|
144
|
+
.number()
|
|
145
|
+
.min(0)
|
|
146
|
+
.max(1)
|
|
147
|
+
.describe('Confidence score between 0 and 1 estimating how likely the failureReason classification is correct.'),
|
|
148
|
+
observedIndicators: v4_1.z
|
|
149
|
+
.array(v4_1.z.string())
|
|
150
|
+
.describe('Signals and observations that led to the triage conclusion.')
|
|
151
|
+
.default([]),
|
|
152
|
+
remediationSteps: v4_1.z
|
|
153
|
+
.array(exports.RemediationStepSchema)
|
|
154
|
+
.describe('Ordered remediation steps to attempt.')
|
|
155
|
+
.default([]),
|
|
156
|
+
additionalDataRequests: v4_1.z
|
|
157
|
+
.array(exports.AdditionalDataRequestSchema)
|
|
158
|
+
.describe('Extra context that would help if remediation stalls.')
|
|
159
|
+
.default([]),
|
|
160
|
+
shouldRetryAutomation: v4_1.z
|
|
161
|
+
.boolean()
|
|
162
|
+
.describe('True if the automation framework should attempt another run.'),
|
|
163
|
+
requiresCodeChange: v4_1.z
|
|
164
|
+
.boolean()
|
|
165
|
+
.describe('True when the Playwright test code likely needs updates.'),
|
|
166
|
+
requiresProductFix: v4_1.z
|
|
167
|
+
.boolean()
|
|
168
|
+
.describe('True when an application-level defect is suspected.'),
|
|
169
|
+
notes: v4_1.z
|
|
170
|
+
.string()
|
|
171
|
+
.describe('Optional free-form notes that do not fit the structured fields.')
|
|
172
|
+
.optional(),
|
|
173
|
+
automationDirectives: AutomationDirectivesSchema.optional(),
|
|
174
|
+
});
|
|
175
|
+
const MAX_SERIALIZED_STRING_LENGTH = 10000;
|
|
176
|
+
const MAX_TOOL_CALLS_TO_INCLUDE = 12;
|
|
177
|
+
const TRIAGE_EVIDENCE_SCHEMA_VERSION = 1;
|
|
178
|
+
/**
|
|
179
|
+
* Ensures text blobs captured from tool calls or error messages fit within storage
|
|
180
|
+
* limits without losing useful context by padding or truncation artifacts.
|
|
181
|
+
*/
|
|
182
|
+
function truncateString(value, maxLength) {
|
|
183
|
+
if (value.length <= maxLength) {
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
if (maxLength <= 3) {
|
|
187
|
+
return value.slice(0, maxLength);
|
|
188
|
+
}
|
|
189
|
+
return `${value.slice(0, maxLength - 3)}...`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Serialises arbitrary values while constraining nested strings and the overall
|
|
193
|
+
* payload so that GPT prompts and persistence never exceed downstream quotas.
|
|
194
|
+
*/
|
|
195
|
+
function safeStringify(value, maxLength, stringMaxLength = 500) {
|
|
196
|
+
try {
|
|
197
|
+
const serialized = JSON.stringify(value, (_, innerValue) => {
|
|
198
|
+
if (typeof innerValue === 'string') {
|
|
199
|
+
return truncateString(innerValue, stringMaxLength);
|
|
200
|
+
}
|
|
201
|
+
return innerValue;
|
|
202
|
+
}, 2);
|
|
203
|
+
return truncateString(serialized, maxLength);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const err = error;
|
|
207
|
+
return `Failed to stringify: ${err.name}: ${err.message}`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Strips sensitive or oversized fields from the persisted flow metadata so the
|
|
212
|
+
* triage agent receives only the contextual attributes it can safely reason over.
|
|
213
|
+
*/
|
|
214
|
+
function sanitizeFlowMetadata(metadata) {
|
|
215
|
+
if (!metadata) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
id: metadata.id,
|
|
220
|
+
name: metadata.name,
|
|
221
|
+
runMode: metadata.runMode,
|
|
222
|
+
state: metadata.state,
|
|
223
|
+
targetWebsite: metadata.targetWebsite,
|
|
224
|
+
overallObjective: metadata.overallObjective,
|
|
225
|
+
allowedTools: metadata.allowedTools,
|
|
226
|
+
envVars: metadata.envVars,
|
|
227
|
+
startedAt: metadata.startedAt,
|
|
228
|
+
completedAt: metadata.completedAt,
|
|
229
|
+
maxToolCalls: metadata.maxToolCalls,
|
|
230
|
+
gptConfigName: metadata.gptConfigName,
|
|
231
|
+
defaultMessageDuration: metadata.defaultMessageDuration,
|
|
232
|
+
resultSummary: metadata.result ? JSON.stringify(metadata.result) : null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Condenses the Donobu tool invocation history so the most recent calls and their
|
|
237
|
+
* outcomes can influence the triage decision without overwhelming the prompt.
|
|
238
|
+
*/
|
|
239
|
+
function summarizeToolCalls(toolCalls) {
|
|
240
|
+
return toolCalls.slice(-MAX_TOOL_CALLS_TO_INCLUDE).map((toolCall) => ({
|
|
241
|
+
id: toolCall.id,
|
|
242
|
+
toolName: toolCall.toolName,
|
|
243
|
+
success: toolCall.outcome.isSuccessful,
|
|
244
|
+
outcomeSummary: toolCall.outcome.forLlm,
|
|
245
|
+
durationMs: toolCall.completedAt - toolCall.startedAt,
|
|
246
|
+
page: toolCall.page,
|
|
247
|
+
startedAtIso: new Date(toolCall.startedAt).toISOString(),
|
|
248
|
+
completedAtIso: new Date(toolCall.completedAt).toISOString(),
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Loads the failing Playwright test file and extracts the statement block that
|
|
253
|
+
* defines the target test case so the triage agent can corroborate expectations.
|
|
254
|
+
*/
|
|
255
|
+
async function extractTestCaseSnippet(testFilePath, testName) {
|
|
256
|
+
if (!testFilePath) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const sourceCode = await fs.promises.readFile(testFilePath, 'utf8');
|
|
261
|
+
const sourceFile = typescript_1.default.createSourceFile(testFilePath, sourceCode, typescript_1.default.ScriptTarget.Latest, true);
|
|
262
|
+
let snippet = null;
|
|
263
|
+
const visit = (node) => {
|
|
264
|
+
if (snippet) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (typescript_1.default.isExpressionStatement(node) &&
|
|
268
|
+
typescript_1.default.isCallExpression(node.expression)) {
|
|
269
|
+
const expression = node.expression.expression;
|
|
270
|
+
if ((typescript_1.default.isIdentifier(expression) &&
|
|
271
|
+
(expression.text === 'test' || expression.text === 'it')) ||
|
|
272
|
+
(typescript_1.default.isPropertyAccessExpression(expression) &&
|
|
273
|
+
typescript_1.default.isIdentifier(expression.expression) &&
|
|
274
|
+
expression.expression.text === 'test')) {
|
|
275
|
+
const args = node.expression.arguments;
|
|
276
|
+
if (args.length > 0 && typescript_1.default.isStringLiteral(args[0])) {
|
|
277
|
+
const title = args[0].text;
|
|
278
|
+
if (title === testName ||
|
|
279
|
+
testName.includes(title) ||
|
|
280
|
+
title.includes(testName)) {
|
|
281
|
+
snippet = sourceCode.substring(node.pos, node.end).trim();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
typescript_1.default.forEachChild(node, visit);
|
|
288
|
+
};
|
|
289
|
+
visit(sourceFile);
|
|
290
|
+
if (!snippet) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
return truncateString(snippet, MAX_SERIALIZED_STRING_LENGTH);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
Logger_1.appLogger.warn(`Failed to extract test case snippet from ${testFilePath}`, error);
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Normalises the heterogeneous error structures Playwright can emit into
|
|
302
|
+
* concise summaries that the LLM can rank and cross-reference with history.
|
|
303
|
+
*/
|
|
304
|
+
function buildErrorSummaries(testInfo) {
|
|
305
|
+
const rawErrors = (testInfo.errors ?? []).length > 0
|
|
306
|
+
? testInfo.errors
|
|
307
|
+
: testInfo.error
|
|
308
|
+
? [testInfo.error]
|
|
309
|
+
: [];
|
|
310
|
+
return rawErrors.map((err) => {
|
|
311
|
+
const summary = {};
|
|
312
|
+
if (typeof err?.message === 'string') {
|
|
313
|
+
summary.message = truncateString(err.message, 2000);
|
|
314
|
+
}
|
|
315
|
+
if (typeof err?.stack === 'string') {
|
|
316
|
+
summary.stack = truncateString(err.stack, 2000);
|
|
317
|
+
}
|
|
318
|
+
if (err?.name) {
|
|
319
|
+
summary.name = String(err.name);
|
|
320
|
+
}
|
|
321
|
+
if (err?.value !== undefined) {
|
|
322
|
+
summary.value = safeStringify(err.value, 2000);
|
|
323
|
+
}
|
|
324
|
+
if (err?.actual !== undefined) {
|
|
325
|
+
summary.actual = safeStringify(err.actual, 1000);
|
|
326
|
+
}
|
|
327
|
+
if (err?.expected !== undefined) {
|
|
328
|
+
summary.expected = safeStringify(err.expected, 1000);
|
|
329
|
+
}
|
|
330
|
+
if (err?.location) {
|
|
331
|
+
summary.location = safeStringify(err.location, 500);
|
|
332
|
+
}
|
|
333
|
+
if (typeof err?.snippet === 'string') {
|
|
334
|
+
summary.snippet = truncateString(err.snippet, 1000);
|
|
335
|
+
}
|
|
336
|
+
return summary;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Translates an inferred failure reason into a sequenced set of remediation
|
|
341
|
+
* actions so downstream automation and humans receive concrete next steps.
|
|
342
|
+
*/
|
|
343
|
+
function remediationStepsForReason(reason, context = {}) {
|
|
344
|
+
switch (reason) {
|
|
345
|
+
case 'AUTOMATION_SCRIPT_ISSUE':
|
|
346
|
+
return [
|
|
347
|
+
{
|
|
348
|
+
category: 'UPDATE_TEST_LOGIC',
|
|
349
|
+
summary: 'Inspect the failing automation logic.',
|
|
350
|
+
details: `Review the Playwright test and any Donobu tool invocations around the failure.
|
|
351
|
+
Align the scripted steps with the intended business flow.`,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
category: 'RETRY_AUTOMATION',
|
|
355
|
+
summary: 'Retry after updating the automation.',
|
|
356
|
+
details: 'Re-run the test or Donobu flow once the script adjustments are in place to validate the fix.',
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
case 'SELECTOR_REGRESSION':
|
|
360
|
+
if (context.occurredDuringPageAi) {
|
|
361
|
+
return [
|
|
362
|
+
{
|
|
363
|
+
category: 'RETRY_AUTOMATION',
|
|
364
|
+
summary: 'Delete the test cache and retry the test.',
|
|
365
|
+
details: `Delete the cached donobu.json entry for this test so page.ai recalculates selectors against the live DOM,
|
|
366
|
+
then rerun the automation to verify recovery.`,
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
category: 'UPDATE_SELECTORS',
|
|
370
|
+
summary: 'Update selectors if the autonomous retry still fails.',
|
|
371
|
+
details: 'If cache invalidation and autonomous retry still fail, fall back to manually adjusting the selector strategy.',
|
|
372
|
+
},
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
return [
|
|
377
|
+
{
|
|
378
|
+
category: 'UPDATE_SELECTORS',
|
|
379
|
+
summary: 'Refresh selectors for the affected elements.',
|
|
380
|
+
details: 'Use page.find failovers or Playwright locators to update the targeting strategy for the broken element.',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
category: 'RETRY_AUTOMATION',
|
|
384
|
+
summary: 'Validate selectors by re-running the test.',
|
|
385
|
+
details: 'Execute the test or self-healing run to confirm the new selectors resolve the regression.',
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
}
|
|
389
|
+
case 'TIMING_OR_SYNCHRONISATION':
|
|
390
|
+
return [
|
|
391
|
+
{
|
|
392
|
+
category: 'ADJUST_TIMING',
|
|
393
|
+
summary: 'Stabilise async waits and retry logic.',
|
|
394
|
+
details: 'Add explicit waits, polling, or guard conditions so the automation aligns with the application response times.',
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
category: 'RETRY_AUTOMATION',
|
|
398
|
+
summary: 'Run the test after timing adjustments.',
|
|
399
|
+
details: 'Execute an automation retry to ensure the timing changes eliminate the flake.',
|
|
400
|
+
},
|
|
401
|
+
];
|
|
402
|
+
case 'ASSERTION_DRIFT':
|
|
403
|
+
return [
|
|
404
|
+
{
|
|
405
|
+
category: 'REFINE_ASSERTIONS',
|
|
406
|
+
summary: 'Revisit expected outcomes and test assertions.',
|
|
407
|
+
details: 'Cross-check the assertion expectations against the latest product behaviour and update the checks accordingly.',
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
category: 'RETRY_AUTOMATION',
|
|
411
|
+
summary: 'Confirm updated assertions.',
|
|
412
|
+
details: 'Execute the test again once assertions have been updated to verify alignment with the application.',
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
case 'APPLICATION_DEFECT':
|
|
416
|
+
return [
|
|
417
|
+
{
|
|
418
|
+
category: 'FIX_APPLICATION',
|
|
419
|
+
summary: 'Log and prioritise the suspected product defect.',
|
|
420
|
+
details: 'Capture reproduction steps using the failing automation and escalate to the owning development team.',
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
category: 'ESCALATE_MANUAL_REVIEW',
|
|
424
|
+
summary: 'Coordinate QA verification of the fix.',
|
|
425
|
+
details: 'Have QA validate the defect manually and confirm once the product change is deployed.',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
category: 'RETRY_AUTOMATION',
|
|
429
|
+
summary: 'Re-run automation after the product fix.',
|
|
430
|
+
details: 'Execute the test to confirm the application change resolves the failure.',
|
|
431
|
+
},
|
|
432
|
+
];
|
|
433
|
+
case 'AUTHENTICATION_FAILURE':
|
|
434
|
+
return [
|
|
435
|
+
{
|
|
436
|
+
category: 'VALIDATE_AUTHENTICATION',
|
|
437
|
+
summary: 'Verify credentials and auth flows.',
|
|
438
|
+
details: 'Check login secrets, MFA configuration, and session state preconditions for the test environment.',
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
category: 'RETRY_AUTOMATION',
|
|
442
|
+
summary: 'Run after auth prerequisites are restored.',
|
|
443
|
+
details: 'Execute the test once authentication is confirmed to be working.',
|
|
444
|
+
},
|
|
445
|
+
];
|
|
446
|
+
case 'ENVIRONMENT_CONFIGURATION':
|
|
447
|
+
return [
|
|
448
|
+
{
|
|
449
|
+
category: 'CHECK_ENVIRONMENT',
|
|
450
|
+
summary: 'Inspect environment and configuration.',
|
|
451
|
+
details: 'Validate environment variables, feature flags, and infrastructure dependencies referenced by the test.',
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
category: 'RETRY_AUTOMATION',
|
|
455
|
+
summary: 'Re-run once environment is stable.',
|
|
456
|
+
details: 'Execute automation after configuration corrections to confirm stability.',
|
|
457
|
+
},
|
|
458
|
+
];
|
|
459
|
+
case 'TEST_DATA_UNAVAILABLE':
|
|
460
|
+
return [
|
|
461
|
+
{
|
|
462
|
+
category: 'REFRESH_TEST_DATA',
|
|
463
|
+
summary: 'Restore or seed required test data.',
|
|
464
|
+
details: 'Populate fixtures, reset accounts, or refresh records relied upon by the test.',
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
category: 'RETRY_AUTOMATION',
|
|
468
|
+
summary: 'Run after data restoration.',
|
|
469
|
+
details: 'Execute automation with the refreshed data to ensure the flow passes.',
|
|
470
|
+
},
|
|
471
|
+
];
|
|
472
|
+
case 'NETWORK_OR_DEPENDENCY':
|
|
473
|
+
return [
|
|
474
|
+
{
|
|
475
|
+
category: 'STABILIZE_DEPENDENCIES',
|
|
476
|
+
summary: 'Check external services or network health.',
|
|
477
|
+
details: 'Verify the availability and latency of downstream services, APIs, or network connections.',
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
category: 'RETRY_AUTOMATION',
|
|
481
|
+
summary: 'Retry once dependencies recover.',
|
|
482
|
+
details: 'Re-run the test when network conditions or dependency status return to normal.',
|
|
483
|
+
},
|
|
484
|
+
];
|
|
485
|
+
case 'UNKNOWN':
|
|
486
|
+
default:
|
|
487
|
+
return [
|
|
488
|
+
{
|
|
489
|
+
category: 'ESCALATE_MANUAL_REVIEW',
|
|
490
|
+
summary: 'Perform deeper manual triage.',
|
|
491
|
+
details: 'Inspect Playwright traces, Donobu tool history, and application logs to narrow down the root cause.',
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
category: 'RETRY_AUTOMATION',
|
|
495
|
+
summary: 'Retry once additional context is gathered.',
|
|
496
|
+
details: 'After manual analysis, attempt another automation run to see if the issue reproduces consistently.',
|
|
497
|
+
},
|
|
498
|
+
];
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Specifies follow-up context the triage agent should ask for when evidence is
|
|
503
|
+
* thin, keeping human responders focused on the data that unblocks a fix fastest.
|
|
504
|
+
*/
|
|
505
|
+
function additionalDataRequestsForReason(reason, _context = {}) {
|
|
506
|
+
switch (reason) {
|
|
507
|
+
case 'SELECTOR_REGRESSION':
|
|
508
|
+
return [
|
|
509
|
+
{
|
|
510
|
+
description: 'Collect DOM snapshots or screenshots around the failing selector.',
|
|
511
|
+
suggestedSources: [
|
|
512
|
+
'Playwright trace viewer',
|
|
513
|
+
'Donobu tool call screenshots',
|
|
514
|
+
],
|
|
515
|
+
},
|
|
516
|
+
];
|
|
517
|
+
case 'TIMING_OR_SYNCHRONISATION':
|
|
518
|
+
return [
|
|
519
|
+
{
|
|
520
|
+
description: 'Gather network and performance timings for the affected actions.',
|
|
521
|
+
suggestedSources: [
|
|
522
|
+
'Browser devtools performance logs',
|
|
523
|
+
'Backend request metrics',
|
|
524
|
+
],
|
|
525
|
+
},
|
|
526
|
+
];
|
|
527
|
+
case 'APPLICATION_DEFECT':
|
|
528
|
+
return [
|
|
529
|
+
{
|
|
530
|
+
description: 'Capture backend logs or Sentry events around the failure window.',
|
|
531
|
+
suggestedSources: ['Application logging platform', 'APM traces'],
|
|
532
|
+
},
|
|
533
|
+
];
|
|
534
|
+
case 'AUTHENTICATION_FAILURE':
|
|
535
|
+
return [
|
|
536
|
+
{
|
|
537
|
+
description: 'Validate authentication tokens and secrets used by the test.',
|
|
538
|
+
suggestedSources: ['Secret manager', 'Identity provider logs'],
|
|
539
|
+
},
|
|
540
|
+
];
|
|
541
|
+
case 'ENVIRONMENT_CONFIGURATION':
|
|
542
|
+
return [
|
|
543
|
+
{
|
|
544
|
+
description: 'Review environment variable values and feature flag states.',
|
|
545
|
+
suggestedSources: ['Deployment configuration', 'Infra dashboards'],
|
|
546
|
+
},
|
|
547
|
+
];
|
|
548
|
+
case 'TEST_DATA_UNAVAILABLE':
|
|
549
|
+
return [
|
|
550
|
+
{
|
|
551
|
+
description: 'Check the lifecycle of the test accounts or fixtures.',
|
|
552
|
+
suggestedSources: [
|
|
553
|
+
'Test data management system',
|
|
554
|
+
'Database snapshots',
|
|
555
|
+
],
|
|
556
|
+
},
|
|
557
|
+
];
|
|
558
|
+
case 'NETWORK_OR_DEPENDENCY':
|
|
559
|
+
return [
|
|
560
|
+
{
|
|
561
|
+
description: 'Inspect dependency uptime and recent incidents.',
|
|
562
|
+
suggestedSources: ['Status pages', 'Network monitoring dashboards'],
|
|
563
|
+
},
|
|
564
|
+
];
|
|
565
|
+
default:
|
|
566
|
+
return [
|
|
567
|
+
{
|
|
568
|
+
description: 'Review Playwright trace, Donobu flow metadata, and browser console logs.',
|
|
569
|
+
suggestedSources: [
|
|
570
|
+
'Playwright trace viewer',
|
|
571
|
+
'Donobu persistence layer',
|
|
572
|
+
],
|
|
573
|
+
},
|
|
574
|
+
];
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Applies lightweight heuristics across Playwright errors and Donobu tool logs
|
|
579
|
+
* to produce a first-pass failure classification and supporting evidence trail.
|
|
580
|
+
*/
|
|
581
|
+
function inferFailureReason(errorSummaries, toolCalls) {
|
|
582
|
+
const combinedText = [
|
|
583
|
+
...errorSummaries.map((err) => err.message ?? ''),
|
|
584
|
+
...errorSummaries.map((err) => err.stack ?? ''),
|
|
585
|
+
...toolCalls.map((tc) => tc.outcomeSummary),
|
|
586
|
+
]
|
|
587
|
+
.filter(Boolean)
|
|
588
|
+
.join('\n')
|
|
589
|
+
.toLowerCase();
|
|
590
|
+
const evidence = [];
|
|
591
|
+
const matches = (pattern) => pattern.test(combinedText);
|
|
592
|
+
if (matches(/(selector|locator|element|node).*(not found|failed|undefined)/i)) {
|
|
593
|
+
evidence.push('Automation reported a missing selector or locator.');
|
|
594
|
+
return {
|
|
595
|
+
reason: 'SELECTOR_REGRESSION',
|
|
596
|
+
evidence,
|
|
597
|
+
confidence: 0.65,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
if (matches(/timed out|timeout|wait.*exceeded|waiting for/i) ||
|
|
601
|
+
matches(/promise.*did not resolve/i)) {
|
|
602
|
+
evidence.push('Timeout or waiting condition was detected in the failure.');
|
|
603
|
+
return {
|
|
604
|
+
reason: 'TIMING_OR_SYNCHRONISATION',
|
|
605
|
+
evidence,
|
|
606
|
+
confidence: 0.6,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
if (matches(/expect(ed)?|AssertionError|toEqual|toBe|received|expected/i) &&
|
|
610
|
+
!matches(/network|timeout/)) {
|
|
611
|
+
evidence.push('Assertion mismatch detected in error details.');
|
|
612
|
+
return {
|
|
613
|
+
reason: 'ASSERTION_DRIFT',
|
|
614
|
+
evidence,
|
|
615
|
+
confidence: 0.55,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
if (matches(/401|403|unauthori[sz]ed|forbidden|login|credential|token/i)) {
|
|
619
|
+
evidence.push('Authentication-related error message detected.');
|
|
620
|
+
return {
|
|
621
|
+
reason: 'AUTHENTICATION_FAILURE',
|
|
622
|
+
evidence,
|
|
623
|
+
confidence: 0.6,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (matches(/env(var|iron)|environment variable|configuration|config/i) ||
|
|
627
|
+
matches(/missing .*config|misconfig/i)) {
|
|
628
|
+
evidence.push('Environment configuration issue referenced in failure text.');
|
|
629
|
+
return {
|
|
630
|
+
reason: 'ENVIRONMENT_CONFIGURATION',
|
|
631
|
+
evidence,
|
|
632
|
+
confidence: 0.55,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (matches(/test data|fixture|seed data|record not found|no data/i) ||
|
|
636
|
+
matches(/entity.*not found/)) {
|
|
637
|
+
evidence.push('Missing or stale test data referenced.');
|
|
638
|
+
return {
|
|
639
|
+
reason: 'TEST_DATA_UNAVAILABLE',
|
|
640
|
+
evidence,
|
|
641
|
+
confidence: 0.55,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (matches(/ECONN|ENOTFOUND|EAI_AGAIN|network|socket hang up|connection/i) ||
|
|
645
|
+
matches(/502|503|504|gateway|dns/i)) {
|
|
646
|
+
evidence.push('Network or dependency outage detected.');
|
|
647
|
+
return {
|
|
648
|
+
reason: 'NETWORK_OR_DEPENDENCY',
|
|
649
|
+
evidence,
|
|
650
|
+
confidence: 0.6,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (matches(/500|internal server error|TypeError|ReferenceError|Unhandled/i)) {
|
|
654
|
+
evidence.push('Application-side error or exception detected.');
|
|
655
|
+
return {
|
|
656
|
+
reason: 'APPLICATION_DEFECT',
|
|
657
|
+
evidence,
|
|
658
|
+
confidence: 0.6,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (combinedText.trim().length > 0) {
|
|
662
|
+
evidence.push('Falling back to automation script issue from generic error content.');
|
|
663
|
+
return {
|
|
664
|
+
reason: 'AUTOMATION_SCRIPT_ISSUE',
|
|
665
|
+
evidence,
|
|
666
|
+
confidence: 0.4,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
evidence.push('No diagnostic text available, marking as unknown.');
|
|
670
|
+
return {
|
|
671
|
+
reason: 'UNKNOWN',
|
|
672
|
+
evidence,
|
|
673
|
+
confidence: 0.2,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
const PAGE_AI_STACK_MARKERS = [
|
|
677
|
+
'page.ai',
|
|
678
|
+
'pageairunner',
|
|
679
|
+
'pageaiexception',
|
|
680
|
+
'donobuflow',
|
|
681
|
+
'donobuextendedpage.ai',
|
|
682
|
+
];
|
|
683
|
+
const PAGE_AI_TOOL_MARKERS = new Set([
|
|
684
|
+
AnalyzePageTextTool_1.AnalyzePageTextTool.NAME,
|
|
685
|
+
SummarizeLearningsTool_1.SummarizeLearningsTool.NAME,
|
|
686
|
+
MarkObjectiveCompleteTool_1.MarkObjectiveCompleteTool.NAME,
|
|
687
|
+
MarkObjectiveNotCompletableTool_1.MarkObjectiveNotCompletableTool.NAME,
|
|
688
|
+
]);
|
|
689
|
+
/**
|
|
690
|
+
* Detects whether the failure manifested during Donobu's autonomous page.ai
|
|
691
|
+
* routines, signalling that cached selectors or AI-driven steps may need resets.
|
|
692
|
+
*/
|
|
693
|
+
function didFailureOccurDuringPageAi(errorSummaries, toolCalls) {
|
|
694
|
+
const stackIndicator = errorSummaries.some((err) => {
|
|
695
|
+
const blob = `${err.message ?? ''}\n${err.stack ?? ''}`.toLowerCase();
|
|
696
|
+
return PAGE_AI_STACK_MARKERS.some((marker) => blob.includes(marker));
|
|
697
|
+
});
|
|
698
|
+
if (stackIndicator) {
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
return toolCalls.some((tc) => PAGE_AI_TOOL_MARKERS.has(tc.toolName));
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Analyzes multiple signals to determine if the failure is likely caused by stale
|
|
705
|
+
* page.ai instruction cache versus a legitimate test failure. This nuanced detection
|
|
706
|
+
* helps differentiate between:
|
|
707
|
+
* - Stale cache: cached actions succeeded but were semantically wrong (clicked wrong elements)
|
|
708
|
+
* - Legitimate failure: cache was correct, but assertions reveal real issues
|
|
709
|
+
*
|
|
710
|
+
* The hardest case: page.ai uses stale cache, actions succeed (selectors still exist),
|
|
711
|
+
* but the page was redesigned so we're interacting with wrong elements. This manifests
|
|
712
|
+
* as successful page.ai execution followed by assertion failures about unexpected state.
|
|
713
|
+
*/
|
|
714
|
+
function analyzeStaleCacheIndicators(testInfo, errorSummaries, toolCalls, flowMetadata) {
|
|
715
|
+
// Check if the flow ran in DETERMINISTIC mode (meaning cache was used)
|
|
716
|
+
const usedDeterministicMode = flowMetadata?.runMode === 'DETERMINISTIC';
|
|
717
|
+
// Check if this is a retry attempt (cache would have been invalidated)
|
|
718
|
+
const isRetryAttempt = testInfo.retry > 0;
|
|
719
|
+
// Check if selector issues occurred during page.ai tool execution
|
|
720
|
+
const selectorFailedDuringPageAi = didFailureOccurDuringPageAi(errorSummaries, toolCalls) &&
|
|
721
|
+
toolCalls.some((tc) => {
|
|
722
|
+
const isPageAiTool = PAGE_AI_TOOL_MARKERS.has(tc.toolName);
|
|
723
|
+
const hasSelectorIssue = !tc.success &&
|
|
724
|
+
/(selector|locator|element|node).*(not found|failed|undefined)/i.test(tc.outcomeSummary);
|
|
725
|
+
return isPageAiTool && hasSelectorIssue;
|
|
726
|
+
});
|
|
727
|
+
// Check if tool calls show selector issues (more broadly)
|
|
728
|
+
const toolCallsShowSelectorIssues = toolCalls.some((tc) => !tc.success &&
|
|
729
|
+
/(selector|locator|element|node).*(not found|failed|undefined|visible|attached)/i.test(tc.outcomeSummary));
|
|
730
|
+
// Check for quick failure pattern (DETERMINISTIC mode failures are typically fast)
|
|
731
|
+
// When cache is stale, the first cached action often fails quickly
|
|
732
|
+
const quickFailurePattern = usedDeterministicMode &&
|
|
733
|
+
testInfo.duration < 5000 && // Failed in less than 5 seconds
|
|
734
|
+
toolCalls.length > 0 &&
|
|
735
|
+
toolCalls.length < 5; // Few tool calls before failure
|
|
736
|
+
// Check if page.ai completed successfully but subsequent assertions failed
|
|
737
|
+
const pageAiToolCalls = toolCalls.filter((tc) => PAGE_AI_TOOL_MARKERS.has(tc.toolName));
|
|
738
|
+
const hasPageAiCalls = pageAiToolCalls.length > 0;
|
|
739
|
+
const allPageAiCallsSucceeded = hasPageAiCalls && pageAiToolCalls.every((tc) => tc.success);
|
|
740
|
+
const hasPostPageAiFailure = errorSummaries.length > 0 &&
|
|
741
|
+
errorSummaries.some((err) => {
|
|
742
|
+
const blob = `${err.message ?? ''}\n${err.stack ?? ''}`.toLowerCase();
|
|
743
|
+
return (/expect(ed)?|assertion/i.test(blob) &&
|
|
744
|
+
!PAGE_AI_STACK_MARKERS.some((marker) => blob.includes(marker)));
|
|
745
|
+
});
|
|
746
|
+
const assertionsFailedAfterSuccessfulPageAi = allPageAiCallsSucceeded && hasPostPageAiFailure;
|
|
747
|
+
// Check if failure occurred after page.ai completed (not during)
|
|
748
|
+
const failedAfterPageAiCompleted = hasPageAiCalls &&
|
|
749
|
+
!didFailureOccurDuringPageAi(errorSummaries, toolCalls) &&
|
|
750
|
+
allPageAiCallsSucceeded;
|
|
751
|
+
return {
|
|
752
|
+
usedDeterministicMode,
|
|
753
|
+
selectorFailedDuringPageAi,
|
|
754
|
+
failedAfterPageAiCompleted,
|
|
755
|
+
isRetryAttempt,
|
|
756
|
+
quickFailurePattern,
|
|
757
|
+
toolCallsShowSelectorIssues,
|
|
758
|
+
assertionsFailedAfterSuccessfulPageAi,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Maps each failure reason to downstream orchestration attributes and adapts
|
|
763
|
+
* them when the failure happened during page.ai execution.
|
|
764
|
+
*/
|
|
765
|
+
function reasonAttributesFor(reason, context) {
|
|
766
|
+
const { occurredDuringPageAi } = context;
|
|
767
|
+
const base = {
|
|
768
|
+
UNKNOWN: {
|
|
769
|
+
shouldRetry: true,
|
|
770
|
+
requiresCodeChange: false,
|
|
771
|
+
requiresProductFix: false,
|
|
772
|
+
},
|
|
773
|
+
AUTOMATION_SCRIPT_ISSUE: {
|
|
774
|
+
shouldRetry: false,
|
|
775
|
+
requiresCodeChange: true,
|
|
776
|
+
requiresProductFix: false,
|
|
777
|
+
},
|
|
778
|
+
SELECTOR_REGRESSION: {
|
|
779
|
+
shouldRetry: false,
|
|
780
|
+
requiresCodeChange: true,
|
|
781
|
+
requiresProductFix: false,
|
|
782
|
+
},
|
|
783
|
+
TIMING_OR_SYNCHRONISATION: {
|
|
784
|
+
shouldRetry: false,
|
|
785
|
+
requiresCodeChange: true,
|
|
786
|
+
requiresProductFix: false,
|
|
787
|
+
},
|
|
788
|
+
ASSERTION_DRIFT: {
|
|
789
|
+
shouldRetry: false,
|
|
790
|
+
requiresCodeChange: true,
|
|
791
|
+
requiresProductFix: false,
|
|
792
|
+
},
|
|
793
|
+
APPLICATION_DEFECT: {
|
|
794
|
+
shouldRetry: false,
|
|
795
|
+
requiresCodeChange: false,
|
|
796
|
+
requiresProductFix: true,
|
|
797
|
+
},
|
|
798
|
+
AUTHENTICATION_FAILURE: {
|
|
799
|
+
shouldRetry: false,
|
|
800
|
+
requiresCodeChange: false,
|
|
801
|
+
requiresProductFix: false,
|
|
802
|
+
},
|
|
803
|
+
ENVIRONMENT_CONFIGURATION: {
|
|
804
|
+
shouldRetry: false,
|
|
805
|
+
requiresCodeChange: false,
|
|
806
|
+
requiresProductFix: false,
|
|
807
|
+
},
|
|
808
|
+
TEST_DATA_UNAVAILABLE: {
|
|
809
|
+
shouldRetry: false,
|
|
810
|
+
requiresCodeChange: false,
|
|
811
|
+
requiresProductFix: false,
|
|
812
|
+
},
|
|
813
|
+
NETWORK_OR_DEPENDENCY: {
|
|
814
|
+
shouldRetry: true,
|
|
815
|
+
requiresCodeChange: false,
|
|
816
|
+
requiresProductFix: false,
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
const attributes = { ...base[reason] };
|
|
820
|
+
if (reason === 'SELECTOR_REGRESSION' && occurredDuringPageAi) {
|
|
821
|
+
attributes.shouldRetry = true;
|
|
822
|
+
attributes.requiresCodeChange = false;
|
|
823
|
+
}
|
|
824
|
+
return attributes;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Builds the heuristic triage assessment by combining rule-based inference,
|
|
828
|
+
* contextual flags, and derived remediation guidance ahead of GPT enrichment.
|
|
829
|
+
*/
|
|
830
|
+
function deriveHeuristicAssessment(testInfo, errorSummaries, toolCalls, flowMetadata) {
|
|
831
|
+
const inference = inferFailureReason(errorSummaries, toolCalls);
|
|
832
|
+
const occurredDuringPageAi = didFailureOccurDuringPageAi(errorSummaries, toolCalls);
|
|
833
|
+
const staleCacheIndicators = analyzeStaleCacheIndicators(testInfo, errorSummaries, toolCalls, flowMetadata);
|
|
834
|
+
const primaryErrorMessage = errorSummaries.find((err) => err.message)?.message ??
|
|
835
|
+
`Test "${testInfo.title}" failed without an explicit error message.`;
|
|
836
|
+
// Compute a stale cache likelihood score based on multiple indicators
|
|
837
|
+
const staleCacheScore = computeStaleCacheScore(staleCacheIndicators);
|
|
838
|
+
// Adjust the initial inference based on stale cache analysis
|
|
839
|
+
let finalReason = inference.reason;
|
|
840
|
+
let finalConfidence = inference.confidence;
|
|
841
|
+
const evidence = [...inference.evidence];
|
|
842
|
+
const notes = [];
|
|
843
|
+
// If we have strong evidence of stale cache, upgrade to SELECTOR_REGRESSION
|
|
844
|
+
// with higher confidence, or adjust existing SELECTOR_REGRESSION assessment
|
|
845
|
+
if (staleCacheScore.isLikelyStaleCache &&
|
|
846
|
+
inference.reason === 'SELECTOR_REGRESSION') {
|
|
847
|
+
// Boost confidence when multiple stale cache indicators align
|
|
848
|
+
finalConfidence = Math.min(0.85, inference.confidence + 0.2);
|
|
849
|
+
evidence.push(`Strong stale cache indicators: ${staleCacheScore.supportingEvidence.join(', ')}`);
|
|
850
|
+
notes.push(`High confidence stale cache scenario detected (score: ${staleCacheScore.score.toFixed(2)}). Cache invalidation + autonomous retry strongly recommended.`);
|
|
851
|
+
}
|
|
852
|
+
else if (staleCacheScore.isLikelyStaleCache &&
|
|
853
|
+
staleCacheIndicators.toolCallsShowSelectorIssues) {
|
|
854
|
+
// Even if not initially classified as SELECTOR_REGRESSION, upgrade if stale cache is likely
|
|
855
|
+
finalReason = 'SELECTOR_REGRESSION';
|
|
856
|
+
finalConfidence = 0.75;
|
|
857
|
+
evidence.push('Reclassified as SELECTOR_REGRESSION based on stale cache analysis.');
|
|
858
|
+
evidence.push(...staleCacheScore.supportingEvidence);
|
|
859
|
+
notes.push(`Stale cache detected (score: ${staleCacheScore.score.toFixed(2)}). The cached page.ai instructions are likely outdated.`);
|
|
860
|
+
}
|
|
861
|
+
else if (staleCacheScore.isLikelyLegitimateFailure) {
|
|
862
|
+
// We have strong evidence this is NOT a stale cache issue
|
|
863
|
+
evidence.push(`Stale cache unlikely: ${staleCacheScore.contradictingEvidence.join(', ')}`);
|
|
864
|
+
if (inference.reason === 'SELECTOR_REGRESSION' && occurredDuringPageAi) {
|
|
865
|
+
// Downgrade confidence if we think cache is NOT the issue
|
|
866
|
+
finalConfidence = Math.max(0.4, inference.confidence - 0.15);
|
|
867
|
+
notes.push('Selector regression detected, but stale cache is unlikely. This may be a legitimate test failure requiring manual review.');
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (testInfo.status === 'timedOut') {
|
|
871
|
+
notes.push('Playwright marked the test as timed out.');
|
|
872
|
+
}
|
|
873
|
+
// Legacy note for backward compatibility
|
|
874
|
+
if (inference.reason === 'SELECTOR_REGRESSION' &&
|
|
875
|
+
occurredDuringPageAi &&
|
|
876
|
+
!staleCacheScore.isLikelyLegitimateFailure) {
|
|
877
|
+
notes.push('Selector failure occurred while executing page.ai steps. Deleting the cached tool calls enables a fresh autonomous attempt.');
|
|
878
|
+
evidence.push('Stack trace or tool history indicates selector failure inside a page.ai call.');
|
|
879
|
+
}
|
|
880
|
+
const attributes = reasonAttributesFor(finalReason, {
|
|
881
|
+
occurredDuringPageAi,
|
|
882
|
+
});
|
|
883
|
+
return {
|
|
884
|
+
failureReason: finalReason,
|
|
885
|
+
evidence,
|
|
886
|
+
confidence: finalConfidence,
|
|
887
|
+
failureSummary: primaryErrorMessage,
|
|
888
|
+
shouldRetryAutomation: attributes.shouldRetry,
|
|
889
|
+
requiresCodeChange: attributes.requiresCodeChange,
|
|
890
|
+
requiresProductFix: attributes.requiresProductFix,
|
|
891
|
+
remediationSteps: remediationStepsForReason(finalReason, {
|
|
892
|
+
flowMetadata,
|
|
893
|
+
occurredDuringPageAi,
|
|
894
|
+
}),
|
|
895
|
+
additionalDataRequests: additionalDataRequestsForReason(finalReason, {
|
|
896
|
+
flowMetadata,
|
|
897
|
+
occurredDuringPageAi,
|
|
898
|
+
}),
|
|
899
|
+
notes: notes.length > 0 ? notes.join(' ') : undefined,
|
|
900
|
+
occurredDuringPageAi,
|
|
901
|
+
staleCacheIndicators,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Computes a composite score indicating the likelihood that a failure is due to
|
|
906
|
+
* stale cache versus a legitimate test issue. Returns both the score and supporting
|
|
907
|
+
* evidence for transparency.
|
|
908
|
+
*
|
|
909
|
+
* KEY INSIGHT: The hardest case to detect is when stale cache causes semantic errors:
|
|
910
|
+
* - Cached selectors still exist (no selector errors thrown)
|
|
911
|
+
* - Actions execute "successfully" (clicks, inputs work)
|
|
912
|
+
* - BUT we're interacting with WRONG elements due to page redesign
|
|
913
|
+
* - This manifests as: page.ai succeeds + assertions fail with unexpected state
|
|
914
|
+
*
|
|
915
|
+
* This is ambiguous! Could be stale cache OR legitimate test failure. We use additional
|
|
916
|
+
* context like retry status and DETERMINISTIC mode to increase confidence.
|
|
917
|
+
*/
|
|
918
|
+
function computeStaleCacheScore(indicators) {
|
|
919
|
+
let score = 0;
|
|
920
|
+
const supportingEvidence = [];
|
|
921
|
+
const contradictingEvidence = [];
|
|
922
|
+
// STRONG positive indicator: Used DETERMINISTIC mode
|
|
923
|
+
// This is a prerequisite - if no cache was used, can't be stale cache issue
|
|
924
|
+
if (indicators.usedDeterministicMode) {
|
|
925
|
+
score += 0.4;
|
|
926
|
+
supportingEvidence.push('Used DETERMINISTIC mode (cache was active and could be stale)');
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
// Strong negative - if no cache used, cannot be stale cache issue
|
|
930
|
+
score -= 0.5;
|
|
931
|
+
contradictingEvidence.push('Did not use DETERMINISTIC mode (no cache was active)');
|
|
932
|
+
}
|
|
933
|
+
// Obvious stale cache: selector fails during execution
|
|
934
|
+
if (indicators.selectorFailedDuringPageAi) {
|
|
935
|
+
score += 0.3;
|
|
936
|
+
supportingEvidence.push('Selector failed during page.ai execution (cached selector no longer exists)');
|
|
937
|
+
}
|
|
938
|
+
if (indicators.quickFailurePattern) {
|
|
939
|
+
score += 0.1;
|
|
940
|
+
supportingEvidence.push('Quick failure pattern (typical of deterministic replay issues)');
|
|
941
|
+
}
|
|
942
|
+
if (indicators.toolCallsShowSelectorIssues && !indicators.isRetryAttempt) {
|
|
943
|
+
score += 0.15;
|
|
944
|
+
supportingEvidence.push('Tool calls show selector issues on first attempt');
|
|
945
|
+
}
|
|
946
|
+
// THE CRITICAL CASE: page.ai succeeded but assertions failed
|
|
947
|
+
// This is AMBIGUOUS - could be stale cache OR legitimate failure
|
|
948
|
+
// We treat it as MILD evidence of stale cache when combined with DETERMINISTIC mode
|
|
949
|
+
if (indicators.assertionsFailedAfterSuccessfulPageAi &&
|
|
950
|
+
indicators.usedDeterministicMode &&
|
|
951
|
+
!indicators.isRetryAttempt) {
|
|
952
|
+
score += 0.25;
|
|
953
|
+
supportingEvidence.push('CRITICAL: Page.ai succeeded but assertions failed (cached actions may have interacted with wrong elements due to page redesign)');
|
|
954
|
+
supportingEvidence.push('This is ambiguous - could be stale cache OR legitimate test failure. Retry with fresh cache is low-cost way to rule out staleness.');
|
|
955
|
+
}
|
|
956
|
+
// STRONG negative indicator: Already retried (cache was invalidated)
|
|
957
|
+
// If we already ran with fresh cache and still failed, NOT a stale cache issue
|
|
958
|
+
if (indicators.isRetryAttempt) {
|
|
959
|
+
score -= 0.8;
|
|
960
|
+
contradictingEvidence.push('STRONG: Already on retry attempt (cache was invalidated, this is NOT a stale cache issue)');
|
|
961
|
+
}
|
|
962
|
+
// Normalize score to 0-1 range
|
|
963
|
+
const normalizedScore = Math.max(0, Math.min(1, score));
|
|
964
|
+
// Adjust thresholds based on the ambiguous nature of some scenarios
|
|
965
|
+
// We want to be conservative - if uncertain, recommend retry (low cost, high value)
|
|
966
|
+
return {
|
|
967
|
+
score: normalizedScore,
|
|
968
|
+
isLikelyStaleCache: normalizedScore >= 0.5, // Lower threshold for retry recommendation
|
|
969
|
+
isLikelyLegitimateFailure: normalizedScore <= 0.2, // High threshold for ruling out cache
|
|
970
|
+
supportingEvidence,
|
|
971
|
+
contradictingEvidence,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Provides a canonical retry instruction that flushes page.ai caches before
|
|
976
|
+
* automation proceeds, ensuring we do not stack manual work on stale selectors.
|
|
977
|
+
*/
|
|
978
|
+
function buildPageAiSelectorRetryStep() {
|
|
979
|
+
return {
|
|
980
|
+
category: 'RETRY_AUTOMATION',
|
|
981
|
+
summary: 'Delete the test cache and retry the test.',
|
|
982
|
+
details: `Remove the cached donobu.json entry so page.ai regenerates selectors against the live DOM,
|
|
983
|
+
then rerun the automation to confirm recovery before escalating.`,
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function deriveAutomationDirectives(base, heuristics, resolvedFailureReason) {
|
|
987
|
+
const directives = {
|
|
988
|
+
...(base ?? {}),
|
|
989
|
+
};
|
|
990
|
+
const staleSelectorRegression = heuristics.occurredDuringPageAi &&
|
|
991
|
+
(resolvedFailureReason === 'SELECTOR_REGRESSION' ||
|
|
992
|
+
heuristics.failureReason === 'SELECTOR_REGRESSION');
|
|
993
|
+
if (staleSelectorRegression) {
|
|
994
|
+
directives.clearPageAiCache = true;
|
|
995
|
+
}
|
|
996
|
+
return Object.keys(directives).length > 0 ? directives : undefined;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Aligns the GPT-authored treatment plan with heuristic safeguards, especially
|
|
1000
|
+
* for page.ai regressions where we prefer automated retries over manual toil.
|
|
1001
|
+
*/
|
|
1002
|
+
function reconcileTreatmentPlan(plan, heuristics) {
|
|
1003
|
+
const adjusted = {
|
|
1004
|
+
...plan,
|
|
1005
|
+
remediationSteps: [...plan.remediationSteps],
|
|
1006
|
+
additionalDataRequests: [...plan.additionalDataRequests],
|
|
1007
|
+
observedIndicators: [...plan.observedIndicators],
|
|
1008
|
+
};
|
|
1009
|
+
if (heuristics.occurredDuringPageAi &&
|
|
1010
|
+
plan.failureReason === 'SELECTOR_REGRESSION') {
|
|
1011
|
+
adjusted.shouldRetryAutomation = true;
|
|
1012
|
+
adjusted.requiresCodeChange = false;
|
|
1013
|
+
const retryStep = buildPageAiSelectorRetryStep();
|
|
1014
|
+
const hasRetryStep = adjusted.remediationSteps.some((step) => step.summary.toLowerCase() === retryStep.summary.toLowerCase() ||
|
|
1015
|
+
step.details.toLowerCase().includes('donobu') ||
|
|
1016
|
+
step.details.toLowerCase().includes('page.ai'));
|
|
1017
|
+
if (!hasRetryStep) {
|
|
1018
|
+
adjusted.remediationSteps = [retryStep, ...adjusted.remediationSteps];
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
adjusted.remediationSteps = adjusted.remediationSteps.map((step) => step.summary.toLowerCase() === retryStep.summary.toLowerCase()
|
|
1022
|
+
? { ...retryStep }
|
|
1023
|
+
: step);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
adjusted.automationDirectives = deriveAutomationDirectives(plan.automationDirectives, heuristics, adjusted.failureReason);
|
|
1027
|
+
return adjusted;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Ensures callers always receive a complete treatment plan even when the GPT
|
|
1031
|
+
* orchestration fails, leaning entirely on the heuristic assessment data.
|
|
1032
|
+
*/
|
|
1033
|
+
function buildFallbackTreatmentPlan(heuristics) {
|
|
1034
|
+
return {
|
|
1035
|
+
failureSummary: heuristics.failureSummary,
|
|
1036
|
+
failureReason: heuristics.failureReason,
|
|
1037
|
+
confidence: heuristics.confidence,
|
|
1038
|
+
observedIndicators: heuristics.evidence,
|
|
1039
|
+
remediationSteps: heuristics.remediationSteps,
|
|
1040
|
+
additionalDataRequests: heuristics.additionalDataRequests,
|
|
1041
|
+
shouldRetryAutomation: heuristics.shouldRetryAutomation,
|
|
1042
|
+
requiresCodeChange: heuristics.requiresCodeChange,
|
|
1043
|
+
requiresProductFix: heuristics.requiresProductFix,
|
|
1044
|
+
notes: heuristics.notes,
|
|
1045
|
+
automationDirectives: deriveAutomationDirectives(undefined, heuristics, heuristics.failureReason),
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function sanitizeFilenameSegment(value, maxLength = 48) {
|
|
1049
|
+
const sanitized = value
|
|
1050
|
+
.toLowerCase()
|
|
1051
|
+
.replace(/[^a-z0-9]+/gi, '-')
|
|
1052
|
+
.replace(/-+/g, '-')
|
|
1053
|
+
.replace(/^-|-$/g, '')
|
|
1054
|
+
.slice(0, maxLength);
|
|
1055
|
+
return sanitized.length > 0 ? sanitized : 'test';
|
|
1056
|
+
}
|
|
1057
|
+
function resolveRunDirectory(testInfo, options) {
|
|
1058
|
+
if (options?.runDirectory) {
|
|
1059
|
+
return options.runDirectory;
|
|
1060
|
+
}
|
|
1061
|
+
const envRunDir = process.env.DONOBU_TRIAGE_RUN_DIR;
|
|
1062
|
+
if (envRunDir && envRunDir.trim().length > 0) {
|
|
1063
|
+
return envRunDir;
|
|
1064
|
+
}
|
|
1065
|
+
const envBaseDir = process.env.DONOBU_TRIAGE_OUTPUT_BASE_DIR;
|
|
1066
|
+
if (envBaseDir && envBaseDir.trim().length > 0) {
|
|
1067
|
+
const runId = process.env.DONOBU_TRIAGE_RUN_ID ?? 'adhoc-run';
|
|
1068
|
+
return path.join(envBaseDir, runId);
|
|
1069
|
+
}
|
|
1070
|
+
const configOutputDir = typeof testInfo.config?.outputDir === 'string'
|
|
1071
|
+
? testInfo.config.outputDir
|
|
1072
|
+
: undefined;
|
|
1073
|
+
const fallbackBase = configOutputDir
|
|
1074
|
+
? path.resolve(configOutputDir)
|
|
1075
|
+
: path.resolve(process.cwd(), 'test-results');
|
|
1076
|
+
return path.join(fallbackBase, 'donobu-triage');
|
|
1077
|
+
}
|
|
1078
|
+
async function collectFailureContext(testInfo, page) {
|
|
1079
|
+
const attachments = testInfo.attachments?.map((attachment) => ({
|
|
1080
|
+
name: attachment.name,
|
|
1081
|
+
contentType: attachment.contentType,
|
|
1082
|
+
path: 'path' in attachment ? (attachment.path ?? null) : null,
|
|
1083
|
+
})) ?? [];
|
|
1084
|
+
const errorSummaries = buildErrorSummaries(testInfo);
|
|
1085
|
+
let flowMetadata = null;
|
|
1086
|
+
let recentToolCalls = [];
|
|
1087
|
+
const flowId = page._dnb?.donobuFlowMetadata?.id;
|
|
1088
|
+
const persistence = page._dnb?.persistence;
|
|
1089
|
+
if (flowId && persistence) {
|
|
1090
|
+
try {
|
|
1091
|
+
flowMetadata = await persistence.getFlowMetadataById(flowId);
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
Logger_1.appLogger.warn(`Failed to load persisted flow metadata for ${flowId}, using in-memory snapshot.`, error);
|
|
1095
|
+
flowMetadata = page._dnb?.donobuFlowMetadata ?? null;
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
const toolCalls = await persistence.getToolCalls(flowId);
|
|
1099
|
+
recentToolCalls = summarizeToolCalls(toolCalls);
|
|
1100
|
+
}
|
|
1101
|
+
catch (error) {
|
|
1102
|
+
Logger_1.appLogger.warn(`Failed to fetch tool call history for flow ${flowId}.`, error);
|
|
1103
|
+
recentToolCalls = [];
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
flowMetadata = page._dnb?.donobuFlowMetadata ?? null;
|
|
1108
|
+
}
|
|
1109
|
+
const sanitizedMetadata = sanitizeFlowMetadata(flowMetadata);
|
|
1110
|
+
const heuristics = deriveHeuristicAssessment(testInfo, errorSummaries, recentToolCalls, sanitizedMetadata);
|
|
1111
|
+
const testSnippet = await extractTestCaseSnippet(testInfo.file, testInfo.title);
|
|
1112
|
+
return {
|
|
1113
|
+
testCase: {
|
|
1114
|
+
title: testInfo.title,
|
|
1115
|
+
file: testInfo.file,
|
|
1116
|
+
projectName: testInfo.project.name,
|
|
1117
|
+
status: testInfo.status,
|
|
1118
|
+
expectedStatus: testInfo.expectedStatus,
|
|
1119
|
+
retry: testInfo.retry,
|
|
1120
|
+
repeatEachIndex: testInfo.repeatEachIndex,
|
|
1121
|
+
workerIndex: testInfo.workerIndex,
|
|
1122
|
+
timeout: testInfo.timeout,
|
|
1123
|
+
duration: testInfo.duration,
|
|
1124
|
+
annotations: testInfo.annotations,
|
|
1125
|
+
},
|
|
1126
|
+
failure: {
|
|
1127
|
+
errors: errorSummaries,
|
|
1128
|
+
attachments: attachments,
|
|
1129
|
+
},
|
|
1130
|
+
donobuFlow: {
|
|
1131
|
+
metadata: sanitizedMetadata,
|
|
1132
|
+
recentToolCalls: recentToolCalls,
|
|
1133
|
+
},
|
|
1134
|
+
testSnippet,
|
|
1135
|
+
heuristics,
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function createEvidenceFilePath(testInfo, runDirectory, evidenceId) {
|
|
1139
|
+
const projectSegment = sanitizeFilenameSegment(testInfo.project.name);
|
|
1140
|
+
const titleSegment = sanitizeFilenameSegment(testInfo.title);
|
|
1141
|
+
const workerSegment = `w${testInfo.workerIndex}-r${testInfo.retry}-e${testInfo.repeatEachIndex}`;
|
|
1142
|
+
const filename = `failure-evidence-${projectSegment}-${titleSegment}-${workerSegment}-${evidenceId}.json`;
|
|
1143
|
+
return path.join(runDirectory, filename);
|
|
1144
|
+
}
|
|
1145
|
+
function buildTreatmentPlanFromHeuristics(evidence) {
|
|
1146
|
+
return buildFallbackTreatmentPlan(evidence.failureContext.heuristics);
|
|
1147
|
+
}
|
|
1148
|
+
async function gatherTestFailureEvidence(testInfo, page, options = {}) {
|
|
1149
|
+
if (!options.force && process.env.DONOBU_TRIAGE_DISABLED === '1') {
|
|
1150
|
+
Logger_1.appLogger.debug('Skipping Donobu triage evidence gathering because DONOBU_TRIAGE_DISABLED=1.');
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
const failureContext = await collectFailureContext(testInfo, page);
|
|
1154
|
+
const runDirectory = resolveRunDirectory(testInfo, options);
|
|
1155
|
+
const evidenceId = (0, crypto_1.randomUUID)();
|
|
1156
|
+
const evidence = {
|
|
1157
|
+
schemaVersion: TRIAGE_EVIDENCE_SCHEMA_VERSION,
|
|
1158
|
+
evidenceId,
|
|
1159
|
+
runId: process.env.DONOBU_TRIAGE_RUN_ID ?? null,
|
|
1160
|
+
runDirectory,
|
|
1161
|
+
collectedAtIso: new Date().toISOString(),
|
|
1162
|
+
failureContext,
|
|
1163
|
+
};
|
|
1164
|
+
const persistToDisk = options.persistToDisk ?? true;
|
|
1165
|
+
let filePath = null;
|
|
1166
|
+
if (persistToDisk) {
|
|
1167
|
+
try {
|
|
1168
|
+
await fs.promises.mkdir(runDirectory, { recursive: true });
|
|
1169
|
+
filePath = createEvidenceFilePath(testInfo, runDirectory, evidenceId);
|
|
1170
|
+
await fs.promises.writeFile(filePath, JSON.stringify(evidence, null, 2), 'utf8');
|
|
1171
|
+
try {
|
|
1172
|
+
await testInfo.attach('donobu-triage-evidence', {
|
|
1173
|
+
path: filePath,
|
|
1174
|
+
contentType: 'application/json',
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
catch (attachError) {
|
|
1178
|
+
Logger_1.appLogger.debug('Failed to attach Donobu triage evidence to Playwright report.', attachError);
|
|
1179
|
+
}
|
|
1180
|
+
Logger_1.appLogger.debug(`Persisted Donobu triage evidence for "${testInfo.title}" to ${filePath}.`);
|
|
1181
|
+
}
|
|
1182
|
+
catch (error) {
|
|
1183
|
+
Logger_1.appLogger.error(`Failed to persist Donobu triage evidence for "${testInfo.title}" to ${runDirectory}.`, error);
|
|
1184
|
+
filePath = null;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return {
|
|
1188
|
+
evidence,
|
|
1189
|
+
filePath,
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
async function generateTreatmentPlanFromEvidence(gptClient, evidence) {
|
|
1193
|
+
const { failureContext } = evidence;
|
|
1194
|
+
const heuristics = failureContext.heuristics;
|
|
1195
|
+
const systemPrompt = `You are a specialist for triaging website test failures.
|
|
1196
|
+
Analyse Playwright test failures that may mix deterministic code and autonomous web test agents.
|
|
1197
|
+
Use the provided heuristics as hints, but override them when the evidence indicates a better diagnosis.
|
|
1198
|
+
Produce a rigorous treatment plan following the schema.
|
|
1199
|
+
|
|
1200
|
+
Failure reason definitions:
|
|
1201
|
+
${exports.FailureReasonSchema.description}
|
|
1202
|
+
|
|
1203
|
+
Instructions:
|
|
1204
|
+
- Return a JSON object that satisfies the TreatmentPlan schema.
|
|
1205
|
+
- Select failureReason from the enumerated options ONLY.
|
|
1206
|
+
- Populate remediationSteps in priority order, keeping descriptions concise but actionable.
|
|
1207
|
+
- If evidence is thin, lower confidence rather than guessing.
|
|
1208
|
+
- Set shouldRetryAutomation to true only when a retry is likely to succeed without code or environment changes.
|
|
1209
|
+
- requiresCodeChange should capture updates needed to Playwright or Donobu automation.
|
|
1210
|
+
- requiresProductFix should be true only when the application itself is broken.
|
|
1211
|
+
- Use additionalDataRequests to call out missing data that would materially help.
|
|
1212
|
+
|
|
1213
|
+
CRITICAL - Stale Cache Detection:
|
|
1214
|
+
The heuristics include staleCacheIndicators that help distinguish between:
|
|
1215
|
+
(A) Stale cache scenario: page.ai used cached instructions that are semantically wrong → need cache deletion + retry
|
|
1216
|
+
(B) Legitimate failure: page.ai cache was correct, but assertions reveal real issues → need code/expectation fixes
|
|
1217
|
+
|
|
1218
|
+
THE HARDEST CASE TO DETECT:
|
|
1219
|
+
When page.ai uses stale cache, actions may SUCCEED (selectors exist, clicks work) BUT interact with WRONG elements
|
|
1220
|
+
due to page redesign. This manifests as: page.ai completes successfully + assertions fail with unexpected state.
|
|
1221
|
+
This is AMBIGUOUS - could be stale cache OR legitimate test failure!
|
|
1222
|
+
|
|
1223
|
+
Key indicators for stale cache (scenario A):
|
|
1224
|
+
- heuristics.staleCacheIndicators.usedDeterministicMode === true (cache was active)
|
|
1225
|
+
- heuristics.staleCacheIndicators.assertionsFailedAfterSuccessfulPageAi === true + usedDeterministicMode === true + isRetryAttempt === false
|
|
1226
|
+
(CRITICAL: page.ai succeeded but assertions failed - may have clicked wrong elements)
|
|
1227
|
+
- heuristics.staleCacheIndicators.selectorFailedDuringPageAi === true (cached selector no longer exists)
|
|
1228
|
+
- heuristics.staleCacheIndicators.quickFailurePattern === true (fast failure typical of stale cache)
|
|
1229
|
+
|
|
1230
|
+
Key indicators AGAINST stale cache (scenario B - legitimate failure):
|
|
1231
|
+
- heuristics.staleCacheIndicators.isRetryAttempt === true (STRONGEST: already retried with fresh cache, NOT stale cache)
|
|
1232
|
+
- heuristics.staleCacheIndicators.usedDeterministicMode === false (no cache was used at all)
|
|
1233
|
+
|
|
1234
|
+
RECOMMENDATION STRATEGY:
|
|
1235
|
+
When assertionsFailedAfterSuccessfulPageAi + usedDeterministicMode + !isRetryAttempt:
|
|
1236
|
+
→ Recommend cache deletion + retry as FIRST step (low cost, rules out staleness)
|
|
1237
|
+
→ Even though ambiguous, retry is safe and informative
|
|
1238
|
+
|
|
1239
|
+
When isRetryAttempt === true:
|
|
1240
|
+
→ Do NOT recommend cache deletion (already done)
|
|
1241
|
+
→ Focus on code review and legitimate failure investigation`;
|
|
1242
|
+
const instructions = JSON.stringify(failureContext);
|
|
1243
|
+
try {
|
|
1244
|
+
const response = await gptClient.getStructuredOutput([
|
|
1245
|
+
{
|
|
1246
|
+
type: 'system',
|
|
1247
|
+
text: systemPrompt,
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
type: 'user',
|
|
1251
|
+
items: [
|
|
1252
|
+
{
|
|
1253
|
+
type: 'text',
|
|
1254
|
+
text: instructions,
|
|
1255
|
+
},
|
|
1256
|
+
],
|
|
1257
|
+
},
|
|
1258
|
+
], exports.TreatmentPlan);
|
|
1259
|
+
const plan = exports.TreatmentPlan.parse(response.output);
|
|
1260
|
+
return reconcileTreatmentPlan(plan, heuristics);
|
|
1261
|
+
}
|
|
1262
|
+
catch (error) {
|
|
1263
|
+
Logger_1.appLogger.warn(`GPT-driven triage failed for evidence ${evidence.evidenceId}, returning heuristic fallback treatment plan.`, error);
|
|
1264
|
+
return buildFallbackTreatmentPlan(heuristics);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
//# sourceMappingURL=triageTestFailure.js.map
|