donobu 5.4.0 → 5.6.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/apis/FlowsApi.d.ts +2 -2
- package/dist/apis/FlowsApi.js +2 -2
- package/dist/cli/donobu-cli.js +22 -5
- package/dist/cli/donobu-cli.js.map +1 -1
- package/dist/cli/playwright-json-to-html.js +163 -51
- package/dist/cli/playwright-json-to-html.js.map +1 -1
- package/dist/codegen/CodeGenerator.d.ts.map +1 -1
- package/dist/codegen/CodeGenerator.js +7 -2
- package/dist/codegen/CodeGenerator.js.map +1 -1
- package/dist/esm/apis/FlowsApi.d.ts +2 -2
- package/dist/esm/apis/FlowsApi.js +2 -2
- package/dist/esm/cli/donobu-cli.js +22 -5
- package/dist/esm/cli/donobu-cli.js.map +1 -1
- package/dist/esm/cli/playwright-json-to-html.js +163 -51
- package/dist/esm/cli/playwright-json-to-html.js.map +1 -1
- package/dist/esm/codegen/CodeGenerator.d.ts.map +1 -1
- package/dist/esm/codegen/CodeGenerator.js +7 -2
- package/dist/esm/codegen/CodeGenerator.js.map +1 -1
- package/dist/esm/lib/ai/cache/assertCache.js +2 -2
- package/dist/esm/lib/page/extendPage.d.ts.map +1 -1
- package/dist/esm/lib/page/extendPage.js +12 -21
- package/dist/esm/lib/page/extendPage.js.map +1 -1
- package/dist/esm/lib/test/testExtension.d.ts.map +1 -1
- package/dist/esm/lib/test/testExtension.js +10 -1
- package/dist/esm/lib/test/testExtension.js.map +1 -1
- package/dist/esm/lib/test/utils/triageTestFailure.d.ts +213 -9
- package/dist/esm/lib/test/utils/triageTestFailure.d.ts.map +1 -1
- package/dist/esm/lib/test/utils/triageTestFailure.js +553 -38
- package/dist/esm/lib/test/utils/triageTestFailure.js.map +1 -1
- package/dist/esm/managers/DonobuFlowsManager.d.ts +1 -0
- package/dist/esm/managers/DonobuFlowsManager.d.ts.map +1 -1
- package/dist/esm/managers/DonobuFlowsManager.js +3 -2
- package/dist/esm/managers/DonobuFlowsManager.js.map +1 -1
- package/dist/esm/tools/AssertTool.d.ts.map +1 -1
- package/dist/esm/tools/AssertTool.js +1 -0
- package/dist/esm/tools/AssertTool.js.map +1 -1
- package/dist/lib/ai/cache/assertCache.js +2 -2
- package/dist/lib/page/extendPage.d.ts.map +1 -1
- package/dist/lib/page/extendPage.js +12 -21
- package/dist/lib/page/extendPage.js.map +1 -1
- package/dist/lib/test/testExtension.d.ts.map +1 -1
- package/dist/lib/test/testExtension.js +10 -1
- package/dist/lib/test/testExtension.js.map +1 -1
- package/dist/lib/test/utils/triageTestFailure.d.ts +213 -9
- package/dist/lib/test/utils/triageTestFailure.d.ts.map +1 -1
- package/dist/lib/test/utils/triageTestFailure.js +553 -38
- package/dist/lib/test/utils/triageTestFailure.js.map +1 -1
- package/dist/managers/DonobuFlowsManager.d.ts +1 -0
- package/dist/managers/DonobuFlowsManager.d.ts.map +1 -1
- package/dist/managers/DonobuFlowsManager.js +3 -2
- package/dist/managers/DonobuFlowsManager.js.map +1 -1
- package/dist/tools/AssertTool.d.ts.map +1 -1
- package/dist/tools/AssertTool.js +1 -0
- package/dist/tools/AssertTool.js.map +1 -1
- package/package.json +1 -1
|
@@ -36,10 +36,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.TreatmentPlan = exports.RemediationStepSchema = exports.FailureReasonSchema = exports.AdditionalDataRequestSchema = void 0;
|
|
40
|
-
exports.
|
|
39
|
+
exports.TRIAGE_PERSISTENCE_FILE_IDS = exports.TreatmentPlan = exports.RemediationStepSchema = exports.FailureReasonSchema = exports.AdditionalDataRequestSchema = void 0;
|
|
40
|
+
exports.deriveHeuristicAssessment = deriveHeuristicAssessment;
|
|
41
|
+
exports.deriveHistoricalSignals = deriveHistoricalSignals;
|
|
41
42
|
exports.gatherTestFailureEvidence = gatherTestFailureEvidence;
|
|
42
43
|
exports.generateTreatmentPlanFromEvidence = generateTreatmentPlanFromEvidence;
|
|
44
|
+
exports.reconcileTreatmentPlan = reconcileTreatmentPlan;
|
|
45
|
+
exports.summarizeFlowHistory = summarizeFlowHistory;
|
|
43
46
|
const crypto_1 = require("crypto");
|
|
44
47
|
const fs = __importStar(require("fs/promises"));
|
|
45
48
|
const path = __importStar(require("path"));
|
|
@@ -52,12 +55,155 @@ const SummarizeLearningsTool_1 = require("../../../tools/SummarizeLearningsTool"
|
|
|
52
55
|
const Logger_1 = require("../../../utils/Logger");
|
|
53
56
|
const cacheLocator_1 = require("../../ai/cache/cacheLocator");
|
|
54
57
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* # Test Failure Triage System
|
|
59
|
+
*
|
|
60
|
+
* Transforms Playwright test failures in Donobu-powered test suites into structured,
|
|
61
|
+
* actionable **treatment plans**. A treatment plan tells both humans and automation *why*
|
|
62
|
+
* the test failed, *how confident* the system is in that diagnosis, and *what to do next*
|
|
63
|
+
* — whether that is retrying the automation, deleting a stale page.ai cache, filing a
|
|
64
|
+
* product bug, or updating selectors in the test code.
|
|
65
|
+
*
|
|
66
|
+
* ---
|
|
67
|
+
*
|
|
68
|
+
* ## Architecture Overview
|
|
69
|
+
*
|
|
70
|
+
* The system operates in two phases that run in sequence:
|
|
71
|
+
*
|
|
72
|
+
* ### Phase 1 — Evidence Collection (`gatherTestFailureEvidence`)
|
|
73
|
+
*
|
|
74
|
+
* Called automatically by the Donobu test extension (`testExtension.ts`) in the
|
|
75
|
+
* Playwright `afterEach` hook whenever a test fails. This phase:
|
|
76
|
+
*
|
|
77
|
+
* 1. Extracts error messages, stack traces, and assertion details from `TestInfo`.
|
|
78
|
+
* 2. Loads the Donobu flow metadata (objective, run mode, state) and recent tool call
|
|
79
|
+
* history from the persistence layer.
|
|
80
|
+
* 3. Fetches **historical runs** of the same flow (by name) from the flows manager to
|
|
81
|
+
* detect flakiness, regression patterns, and prior self-heal success.
|
|
82
|
+
* 4. Captures the **failure screenshot** (last tool call screenshot from the current
|
|
83
|
+
* run) and the **baseline screenshot** (last tool call screenshot from the most
|
|
84
|
+
* recent successful historical run) for visual comparison.
|
|
85
|
+
* 5. Reads the source of the failing test case for contextual grounding.
|
|
86
|
+
* 6. Runs the **heuristic classifier** (`deriveHeuristicAssessment`) which uses
|
|
87
|
+
* rule-based pattern matching over errors, tool calls, stale-cache indicators,
|
|
88
|
+
* and historical signals to produce a preliminary diagnosis — including a failure
|
|
89
|
+
* reason, confidence score, and retry recommendation.
|
|
90
|
+
* 7. Persists the complete evidence bundle (JSON + screenshots) to disk as a
|
|
91
|
+
* `FailureEvidenceRecord`.
|
|
92
|
+
*
|
|
93
|
+
* ### Phase 2 — Treatment Plan Generation (`generateTreatmentPlanFromEvidence`)
|
|
94
|
+
*
|
|
95
|
+
* Called by the Donobu CLI (`donobu-cli.ts`) after evidence files are collected. This
|
|
96
|
+
* phase reads the persisted evidence and **requires a GPT client** — there is no
|
|
97
|
+
* heuristic-only fallback path. It:
|
|
98
|
+
*
|
|
99
|
+
* 1. Sends the full evidence bundle — including screenshots as vision input — to a
|
|
100
|
+
* GPT model with a detailed system prompt, requesting a structured `TreatmentPlan`
|
|
101
|
+
* response.
|
|
102
|
+
* 2. **Reconciles** the GPT plan with heuristic signals (`reconcileTreatmentPlan`) to
|
|
103
|
+
* enforce invariants the LLM might miss (e.g., forcing `shouldRetryAutomation` when
|
|
104
|
+
* historical data shows prior self-heal success, or overriding retry-step priority
|
|
105
|
+
* for stale-cache scenarios).
|
|
106
|
+
* 3. Returns the final `TreatmentPlan` for the CLI to act on — potentially triggering
|
|
107
|
+
* automatic retries, cache deletion, or surfacing remediation steps to the engineer.
|
|
108
|
+
*
|
|
109
|
+
* ---
|
|
110
|
+
*
|
|
111
|
+
* ## Data Signals
|
|
112
|
+
*
|
|
113
|
+
* The triage system draws from several complementary data sources, each targeting
|
|
114
|
+
* different failure modes:
|
|
115
|
+
*
|
|
116
|
+
* | Signal | Source | What it reveals |
|
|
117
|
+
* |-------------------------|-------------------------------|----------------------------------------------------|
|
|
118
|
+
* | Error messages & stacks | `TestInfo.errors` | Direct cause (assertion, timeout, selector) |
|
|
119
|
+
* | Tool call history | `FlowsPersistence` | What actions the AI took and their outcomes |
|
|
120
|
+
* | Tool call parameters | `ToolCall.parameters` | Exact selectors, URLs, and inputs attempted |
|
|
121
|
+
* | Flow metadata | `DonobuExtendedPage._dnb` | Run mode, objective, allowed tools, timing |
|
|
122
|
+
* | Stale cache indicators | Derived from above | Whether page.ai cache staleness is the root cause |
|
|
123
|
+
* | Historical flow runs | `DonobuFlowsManager.getFlows` | Flakiness, regression patterns, prior self-heal |
|
|
124
|
+
* | Failure screenshot | Last tool call screenshot | Visual state of the page when the failure occurred |
|
|
125
|
+
* | Baseline screenshot | Last successful run's screenshot | Visual reference for what the page *should* look like |
|
|
126
|
+
* | Test source snippet | TypeScript AST parsing | The test's expectations and structure |
|
|
127
|
+
*
|
|
128
|
+
* ---
|
|
129
|
+
*
|
|
130
|
+
* ## Failure Classification
|
|
131
|
+
*
|
|
132
|
+
* Every treatment plan assigns one of the following failure reasons:
|
|
133
|
+
*
|
|
134
|
+
* - `SELECTOR_REGRESSION` — UI locators have gone stale.
|
|
135
|
+
* - `STALE_CACHE_OR_INSTRUCTIONS` — The page.ai deterministic cache is outdated.
|
|
136
|
+
* - `TIMING_OR_SYNCHRONISATION` — Race conditions, slow loads, or flaky waits.
|
|
137
|
+
* - `NETWORK_OR_DEPENDENCY` — External service failures or connectivity issues.
|
|
138
|
+
* - `APPLICATION_DEFECT` — A real bug in the product under test.
|
|
139
|
+
* - `ASSERTION_DRIFT` — Test expectations no longer match valid application behavior.
|
|
140
|
+
* - `AUTOMATION_SCRIPT_ISSUE` — The test script itself is incorrect.
|
|
141
|
+
* - `AUTHENTICATION_FAILURE` — Session/auth problems prevented the test from running.
|
|
142
|
+
* - `ENVIRONMENT_CONFIGURATION` — Infrastructure or environment misconfiguration.
|
|
143
|
+
* - `TEST_DATA_UNAVAILABLE` — Required test data is missing or invalid.
|
|
144
|
+
* - `UNKNOWN` — Insufficient signal to determine the cause.
|
|
145
|
+
*
|
|
146
|
+
* ---
|
|
147
|
+
*
|
|
148
|
+
* ## Getting the Most Out of This System
|
|
149
|
+
*
|
|
150
|
+
* ### 1. Name your flows consistently
|
|
151
|
+
*
|
|
152
|
+
* Historical analysis works by matching flows by name. If every test uses a unique,
|
|
153
|
+
* stable flow name, the system can compare the current failure against all prior runs
|
|
154
|
+
* of the same flow and detect flakiness, regressions, and self-heal patterns:
|
|
155
|
+
*
|
|
156
|
+
* ```ts
|
|
157
|
+
* test('checkout flow adds item and completes purchase', async ({ page }) => {
|
|
158
|
+
* const ai = await page.ai('Checkout — add item and purchase', { ... });
|
|
159
|
+
* // ...
|
|
160
|
+
* });
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* ### 2. Let evidence persist to disk
|
|
164
|
+
*
|
|
165
|
+
* The default behavior writes evidence JSON and screenshots to the run directory. This
|
|
166
|
+
* enables the CLI's Phase 2 to enrich the diagnosis with GPT and visual comparison.
|
|
167
|
+
* Ensure `DONOBU_TRIAGE_DISABLED` is not set, and that `runDirectory` is writable:
|
|
168
|
+
*
|
|
169
|
+
* ```ts
|
|
170
|
+
* // Evidence is gathered automatically on failure — no extra code needed.
|
|
171
|
+
* // To customize the output directory:
|
|
172
|
+
* await gatherTestFailureEvidence(testInfo, page, {
|
|
173
|
+
* runDirectory: '/path/to/custom/output',
|
|
174
|
+
* });
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* ### 3. Ensure a GPT client is available
|
|
178
|
+
*
|
|
179
|
+
* A GPT client is **required** for treatment plan generation. The LLM performs semantic
|
|
180
|
+
* reasoning: it reads the test source, interprets tool call parameters, compares
|
|
181
|
+
* screenshots visually, and produces human-readable remediation steps. The CLI
|
|
182
|
+
* instantiates a GPT client automatically from configured credentials.
|
|
183
|
+
*
|
|
184
|
+
* ### 4. Use deterministic (cached) mode for stable flows
|
|
185
|
+
*
|
|
186
|
+
* When flows run in `DETERMINISTIC` mode with a page.ai cache, the triage system
|
|
187
|
+
* activates its stale-cache detection pipeline — a composite scoring system that
|
|
188
|
+
* weighs whether the cached instructions have gone stale versus whether the failure
|
|
189
|
+
* is a legitimate test issue. This is the system's strongest diagnostic capability.
|
|
190
|
+
*
|
|
191
|
+
* ### 5. Inspect the evidence files
|
|
192
|
+
*
|
|
193
|
+
* Each failure produces a `failure-evidence-<id>.json` file (plus optional PNG
|
|
194
|
+
* screenshots) in the run directory. These files are self-contained and can be
|
|
195
|
+
* re-processed, shared for debugging, or fed back into `generateTreatmentPlanFromEvidence`
|
|
196
|
+
* independently.
|
|
197
|
+
*
|
|
198
|
+
* ---
|
|
199
|
+
*
|
|
200
|
+
* ## Key Exports
|
|
201
|
+
*
|
|
202
|
+
* - `gatherTestFailureEvidence` — Phase 1 entry point. Call from a Playwright afterEach hook.
|
|
203
|
+
* - `generateTreatmentPlanFromEvidence` — Phase 2 entry point. Requires a `GptClient` and a
|
|
204
|
+
* `FailureEvidenceRecord`.
|
|
205
|
+
* - `TreatmentPlan` — The Zod schema defining the treatment plan structure.
|
|
206
|
+
* - `FailureReasonSchema` — The Zod enum of all possible failure classifications.
|
|
61
207
|
*/
|
|
62
208
|
const FailureReasonSchema = v4_1.z
|
|
63
209
|
.enum([
|
|
@@ -178,8 +324,15 @@ const TreatmentPlan = v4_1.z.object({
|
|
|
178
324
|
});
|
|
179
325
|
exports.TreatmentPlan = TreatmentPlan;
|
|
180
326
|
const MAX_SERIALIZED_STRING_LENGTH = 10000;
|
|
327
|
+
const MAX_TOOL_CALL_PARAMETERS_LENGTH = 500;
|
|
181
328
|
const MAX_TOOL_CALLS_TO_INCLUDE = 12;
|
|
182
|
-
const TRIAGE_EVIDENCE_SCHEMA_VERSION =
|
|
329
|
+
const TRIAGE_EVIDENCE_SCHEMA_VERSION = 2;
|
|
330
|
+
const TRIAGE_PERSISTENCE_FILE_IDS = {
|
|
331
|
+
evidence: 'triage-evidence.json',
|
|
332
|
+
failureScreenshot: 'triage-failure-screenshot.png',
|
|
333
|
+
baselineScreenshot: 'triage-baseline-screenshot.png',
|
|
334
|
+
};
|
|
335
|
+
exports.TRIAGE_PERSISTENCE_FILE_IDS = TRIAGE_PERSISTENCE_FILE_IDS;
|
|
183
336
|
/**
|
|
184
337
|
* Ensures text blobs captured from tool calls or error messages fit within storage
|
|
185
338
|
* limits without losing useful context by padding or truncation artifacts.
|
|
@@ -251,7 +404,211 @@ function summarizeToolCalls(toolCalls) {
|
|
|
251
404
|
page: toolCall.page,
|
|
252
405
|
startedAtIso: new Date(toolCall.startedAt).toISOString(),
|
|
253
406
|
completedAtIso: new Date(toolCall.completedAt).toISOString(),
|
|
407
|
+
parameters: safeStringify(toolCall.parameters, MAX_TOOL_CALL_PARAMETERS_LENGTH, 200),
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
const HISTORY_QUERY_WINDOW_DAYS = 14;
|
|
411
|
+
const HISTORY_QUERY_LIMIT = 20;
|
|
412
|
+
const HISTORY_RECENT_RUNS_CAP = 10;
|
|
413
|
+
/**
|
|
414
|
+
* Compresses a set of historical flow runs into an aggregate summary compact
|
|
415
|
+
* enough for both heuristic reasoning and inclusion in GPT prompts.
|
|
416
|
+
*/
|
|
417
|
+
function summarizeFlowHistory(flowName, flows) {
|
|
418
|
+
const sorted = [...flows].sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
|
|
419
|
+
let successCount = 0;
|
|
420
|
+
let failureCount = 0;
|
|
421
|
+
let otherCount = 0;
|
|
422
|
+
for (const flow of sorted) {
|
|
423
|
+
if (flow.state === 'SUCCESS') {
|
|
424
|
+
successCount++;
|
|
425
|
+
}
|
|
426
|
+
else if (flow.state === 'FAILED') {
|
|
427
|
+
failureCount++;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
otherCount++;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const totalRuns = sorted.length;
|
|
434
|
+
const passRate = totalRuns > 0 ? successCount / totalRuns : 0;
|
|
435
|
+
const recentRuns = sorted
|
|
436
|
+
.slice(0, HISTORY_RECENT_RUNS_CAP)
|
|
437
|
+
.map((f) => ({
|
|
438
|
+
id: f.id,
|
|
439
|
+
state: f.state,
|
|
440
|
+
runMode: f.runMode,
|
|
441
|
+
startedAt: f.startedAt,
|
|
442
|
+
completedAt: f.completedAt,
|
|
443
|
+
durationMs: f.startedAt !== null && f.completedAt !== null
|
|
444
|
+
? f.completedAt - f.startedAt
|
|
445
|
+
: null,
|
|
254
446
|
}));
|
|
447
|
+
let streakState = 'MIXED';
|
|
448
|
+
let streakLength = 0;
|
|
449
|
+
if (recentRuns.length > 0) {
|
|
450
|
+
const firstState = recentRuns[0].state;
|
|
451
|
+
if (firstState === 'SUCCESS' || firstState === 'FAILED') {
|
|
452
|
+
streakState = firstState;
|
|
453
|
+
streakLength = 1;
|
|
454
|
+
for (let i = 1; i < recentRuns.length; i++) {
|
|
455
|
+
if (recentRuns[i].state === firstState) {
|
|
456
|
+
streakLength++;
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const lastSuccessfulRun = sorted.find((f) => f.state === 'SUCCESS');
|
|
465
|
+
return {
|
|
466
|
+
flowName,
|
|
467
|
+
totalRuns,
|
|
468
|
+
successCount,
|
|
469
|
+
failureCount,
|
|
470
|
+
otherCount,
|
|
471
|
+
passRate,
|
|
472
|
+
recentRuns,
|
|
473
|
+
currentStreak: { state: streakState, length: streakLength },
|
|
474
|
+
lastSuccessfulRunId: lastSuccessfulRun?.id ?? null,
|
|
475
|
+
queryWindowDays: HISTORY_QUERY_WINDOW_DAYS,
|
|
476
|
+
queriedAt: new Date().toISOString(),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Derives actionable signals from historical flow run data to feed into the
|
|
481
|
+
* heuristic classifier: flakiness, regression likelihood, prior self-heal
|
|
482
|
+
* success, and whether the page.ai cache was recently validated.
|
|
483
|
+
*/
|
|
484
|
+
function deriveHistoricalSignals(history) {
|
|
485
|
+
const { recentRuns, passRate } = history;
|
|
486
|
+
// Flakiness: proportion of state transitions in recent runs
|
|
487
|
+
let transitions = 0;
|
|
488
|
+
for (let i = 1; i < recentRuns.length; i++) {
|
|
489
|
+
if (recentRuns[i].state !== recentRuns[i - 1].state) {
|
|
490
|
+
transitions++;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const maxTransitions = Math.max(1, recentRuns.length - 1);
|
|
494
|
+
const flakinessScore = recentRuns.length > 1 ? transitions / maxTransitions : 0;
|
|
495
|
+
// Regression: consecutive failures at the head after passes existed
|
|
496
|
+
let regressionLikelihood = 0;
|
|
497
|
+
if (history.currentStreak.state === 'FAILED' && history.successCount > 0) {
|
|
498
|
+
const streakWeight = Math.min(history.currentStreak.length / 5, 1);
|
|
499
|
+
regressionLikelihood = Math.min(1, passRate * 0.6 + streakWeight * 0.4);
|
|
500
|
+
}
|
|
501
|
+
// Prior self-heal: a FAILED run immediately followed by SUCCESS (newest-first)
|
|
502
|
+
let priorSelfHealSuccess = false;
|
|
503
|
+
for (let i = 0; i < recentRuns.length - 1; i++) {
|
|
504
|
+
if (recentRuns[i].state === 'SUCCESS' &&
|
|
505
|
+
recentRuns[i + 1].state === 'FAILED') {
|
|
506
|
+
priorSelfHealSuccess = true;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// Cache recently valid: any recent DETERMINISTIC SUCCESS
|
|
511
|
+
const cacheWasRecentlyValid = recentRuns.some((r) => r.runMode === 'DETERMINISTIC' && r.state === 'SUCCESS');
|
|
512
|
+
return {
|
|
513
|
+
flakinessScore,
|
|
514
|
+
regressionLikelihood,
|
|
515
|
+
recentPassRate: passRate,
|
|
516
|
+
priorSelfHealSuccess,
|
|
517
|
+
cacheWasRecentlyValid,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Fetches historical runs of the same flow by name and returns a compact
|
|
522
|
+
* summary. Runs concurrently with other evidence collection and fails open
|
|
523
|
+
* so triage proceeds even if the history lookup encounters errors.
|
|
524
|
+
*/
|
|
525
|
+
async function fetchFlowHistory(page) {
|
|
526
|
+
const flowName = page._dnb?.donobuFlowMetadata?.name;
|
|
527
|
+
if (!flowName) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
const flowsManager = page._dnb?.donobuStack?.flowsManager;
|
|
531
|
+
if (!flowsManager) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const cutoffMs = Date.now() - HISTORY_QUERY_WINDOW_DAYS * 24 * 60 * 60 * 1000;
|
|
535
|
+
const currentFlowId = page._dnb.donobuFlowMetadata.id;
|
|
536
|
+
try {
|
|
537
|
+
const result = await flowsManager.getFlows({
|
|
538
|
+
name: flowName,
|
|
539
|
+
startedAfter: cutoffMs,
|
|
540
|
+
limit: HISTORY_QUERY_LIMIT,
|
|
541
|
+
});
|
|
542
|
+
const historicalFlows = result.items.filter((f) => f.id !== currentFlowId);
|
|
543
|
+
if (historicalFlows.length === 0) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
return summarizeFlowHistory(flowName, historicalFlows);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
Logger_1.appLogger.warn(`Failed to fetch historical flow data for "${flowName}", proceeding without history.`, error);
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Retrieves the screenshot from the last completed tool call in the current flow.
|
|
555
|
+
* Returns the raw PNG/JPEG buffer if available, or null. Fails open so triage
|
|
556
|
+
* proceeds even if the screenshot cannot be loaded.
|
|
557
|
+
*/
|
|
558
|
+
async function fetchLastToolCallScreenshot(page) {
|
|
559
|
+
const flowId = page._dnb?.donobuFlowMetadata?.id;
|
|
560
|
+
const persistence = page._dnb?.persistence;
|
|
561
|
+
if (!flowId || !persistence) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
const toolCalls = await persistence.getToolCalls(flowId);
|
|
566
|
+
if (toolCalls.length === 0) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
// Walk backwards to find the last tool call with a screenshot
|
|
570
|
+
for (let i = toolCalls.length - 1; i >= 0; i--) {
|
|
571
|
+
const screenshotId = toolCalls[i].postCallImageId;
|
|
572
|
+
if (screenshotId) {
|
|
573
|
+
return await persistence.getScreenShot(flowId, screenshotId);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
Logger_1.appLogger.debug(`Failed to fetch last tool call screenshot for flow ${flowId}.`, error);
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Loads the final screenshot from a historical successful run to serve as a
|
|
585
|
+
* visual baseline for comparison with the current failure state. This enables
|
|
586
|
+
* the GPT triage agent to detect page redesigns and stale cache scenarios by
|
|
587
|
+
* comparing "what the page looked like when it last worked" vs "what it looks
|
|
588
|
+
* like now." Fails open — returns null if the screenshot cannot be retrieved.
|
|
589
|
+
*/
|
|
590
|
+
async function fetchBaselineScreenshot(page, historicalFlowId) {
|
|
591
|
+
const persistence = page._dnb?.persistence;
|
|
592
|
+
if (!persistence) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
const toolCalls = await persistence.getToolCalls(historicalFlowId);
|
|
597
|
+
if (toolCalls.length === 0) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
for (let i = toolCalls.length - 1; i >= 0; i--) {
|
|
601
|
+
const screenshotId = toolCalls[i].postCallImageId;
|
|
602
|
+
if (screenshotId) {
|
|
603
|
+
return await persistence.getScreenShot(historicalFlowId, screenshotId);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
Logger_1.appLogger.debug(`Failed to fetch baseline screenshot from historical flow ${historicalFlowId}.`, error);
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
255
612
|
}
|
|
256
613
|
/**
|
|
257
614
|
* Loads the failing Playwright test file and extracts the statement block that
|
|
@@ -832,7 +1189,7 @@ function reasonAttributesFor(reason, context) {
|
|
|
832
1189
|
* Builds the heuristic triage assessment by combining rule-based inference,
|
|
833
1190
|
* contextual flags, and derived remediation guidance ahead of GPT enrichment.
|
|
834
1191
|
*/
|
|
835
|
-
function deriveHeuristicAssessment(testInfo, errorSummaries, toolCalls, flowMetadata) {
|
|
1192
|
+
function deriveHeuristicAssessment(testInfo, errorSummaries, toolCalls, flowMetadata, flowHistory = null) {
|
|
836
1193
|
const inference = inferFailureReason(errorSummaries, toolCalls);
|
|
837
1194
|
const occurredDuringPageAi = didFailureOccurDuringPageAi(errorSummaries, toolCalls);
|
|
838
1195
|
const staleCacheIndicators = analyzeStaleCacheIndicators(testInfo, errorSummaries, toolCalls, flowMetadata);
|
|
@@ -872,6 +1229,50 @@ function deriveHeuristicAssessment(testInfo, errorSummaries, toolCalls, flowMeta
|
|
|
872
1229
|
notes.push('Selector regression detected, but stale cache is unlikely. This may be a legitimate test failure requiring manual review.');
|
|
873
1230
|
}
|
|
874
1231
|
}
|
|
1232
|
+
// ---- Historical signal adjustments ----
|
|
1233
|
+
let historicalSignals = null;
|
|
1234
|
+
if (flowHistory) {
|
|
1235
|
+
historicalSignals = deriveHistoricalSignals(flowHistory);
|
|
1236
|
+
// Flakiness + TIMING/NETWORK => boost confidence
|
|
1237
|
+
if (historicalSignals.flakinessScore > 0.5 &&
|
|
1238
|
+
(finalReason === 'TIMING_OR_SYNCHRONISATION' ||
|
|
1239
|
+
finalReason === 'NETWORK_OR_DEPENDENCY')) {
|
|
1240
|
+
finalConfidence = Math.min(0.9, finalConfidence + 0.15);
|
|
1241
|
+
evidence.push(`Historical flakiness detected (score: ${historicalSignals.flakinessScore.toFixed(2)}). ` +
|
|
1242
|
+
`Pass rate: ${(historicalSignals.recentPassRate * 100).toFixed(0)}%.`);
|
|
1243
|
+
}
|
|
1244
|
+
// Flakiness + APPLICATION_DEFECT => reclassify to timing
|
|
1245
|
+
if (historicalSignals.flakinessScore > 0.6 &&
|
|
1246
|
+
finalReason === 'APPLICATION_DEFECT') {
|
|
1247
|
+
finalReason = 'TIMING_OR_SYNCHRONISATION';
|
|
1248
|
+
finalConfidence = 0.55;
|
|
1249
|
+
evidence.push('Reclassified from APPLICATION_DEFECT to TIMING_OR_SYNCHRONISATION: ' +
|
|
1250
|
+
'historical alternating pass/fail pattern indicates flakiness, not a persistent defect.');
|
|
1251
|
+
}
|
|
1252
|
+
// Regression: consecutive failures after a history of passes
|
|
1253
|
+
if (historicalSignals.regressionLikelihood > 0.6) {
|
|
1254
|
+
if (finalReason === 'TIMING_OR_SYNCHRONISATION' ||
|
|
1255
|
+
finalReason === 'UNKNOWN') {
|
|
1256
|
+
finalConfidence = Math.max(finalConfidence, 0.6);
|
|
1257
|
+
evidence.push(`Regression pattern detected: ${flowHistory.currentStreak.length} consecutive failures ` +
|
|
1258
|
+
`after prior pass rate of ${(historicalSignals.recentPassRate * 100).toFixed(0)}%.`);
|
|
1259
|
+
}
|
|
1260
|
+
else if (finalReason === 'SELECTOR_REGRESSION') {
|
|
1261
|
+
finalConfidence = Math.min(0.9, finalConfidence + 0.1);
|
|
1262
|
+
evidence.push('Historical pass-then-fail pattern corroborates selector regression.');
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
// Stale cache calibration: cache was recently valid
|
|
1266
|
+
if (historicalSignals.cacheWasRecentlyValid &&
|
|
1267
|
+
staleCacheScore.isLikelyStaleCache) {
|
|
1268
|
+
finalConfidence = Math.min(0.9, finalConfidence + 0.1);
|
|
1269
|
+
evidence.push('Recent DETERMINISTIC SUCCESS confirms cache was valid recently, strengthening stale-cache hypothesis.');
|
|
1270
|
+
}
|
|
1271
|
+
// Prior self-heal success
|
|
1272
|
+
if (historicalSignals.priorSelfHealSuccess) {
|
|
1273
|
+
notes.push('Historical data shows a prior run recovered on retry. Automated retry has high likelihood of success.');
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
875
1276
|
if (testInfo.status === 'timedOut') {
|
|
876
1277
|
notes.push('Playwright marked the test as timed out.');
|
|
877
1278
|
}
|
|
@@ -885,6 +1286,14 @@ function deriveHeuristicAssessment(testInfo, errorSummaries, toolCalls, flowMeta
|
|
|
885
1286
|
const attributes = reasonAttributesFor(finalReason, {
|
|
886
1287
|
occurredDuringPageAi,
|
|
887
1288
|
});
|
|
1289
|
+
// Override retry recommendation based on historical signals
|
|
1290
|
+
if (historicalSignals?.priorSelfHealSuccess && !attributes.shouldRetry) {
|
|
1291
|
+
attributes.shouldRetry = true;
|
|
1292
|
+
evidence.push('Overriding shouldRetryAutomation to true based on prior self-heal success.');
|
|
1293
|
+
}
|
|
1294
|
+
if (historicalSignals && historicalSignals.flakinessScore > 0.5) {
|
|
1295
|
+
attributes.shouldRetry = true;
|
|
1296
|
+
}
|
|
888
1297
|
return {
|
|
889
1298
|
failureReason: finalReason,
|
|
890
1299
|
evidence,
|
|
@@ -904,6 +1313,7 @@ function deriveHeuristicAssessment(testInfo, errorSummaries, toolCalls, flowMeta
|
|
|
904
1313
|
notes: notes.length > 0 ? notes.join(' ') : undefined,
|
|
905
1314
|
occurredDuringPageAi,
|
|
906
1315
|
staleCacheIndicators,
|
|
1316
|
+
historicalSignals,
|
|
907
1317
|
};
|
|
908
1318
|
}
|
|
909
1319
|
/**
|
|
@@ -1028,6 +1438,9 @@ function reconcileTreatmentPlan(plan, heuristics) {
|
|
|
1028
1438
|
: step);
|
|
1029
1439
|
}
|
|
1030
1440
|
}
|
|
1441
|
+
if (heuristics.historicalSignals?.priorSelfHealSuccess) {
|
|
1442
|
+
adjusted.shouldRetryAutomation = true;
|
|
1443
|
+
}
|
|
1031
1444
|
adjusted.automationDirectives = deriveAutomationDirectives(plan.automationDirectives, heuristics, adjusted.failureReason);
|
|
1032
1445
|
return adjusted;
|
|
1033
1446
|
}
|
|
@@ -1035,21 +1448,6 @@ function reconcileTreatmentPlan(plan, heuristics) {
|
|
|
1035
1448
|
* Ensures callers always receive a complete treatment plan even when the GPT
|
|
1036
1449
|
* orchestration fails, leaning entirely on the heuristic assessment data.
|
|
1037
1450
|
*/
|
|
1038
|
-
function buildFallbackTreatmentPlan(heuristics) {
|
|
1039
|
-
return {
|
|
1040
|
-
failureSummary: heuristics.failureSummary,
|
|
1041
|
-
failureReason: heuristics.failureReason,
|
|
1042
|
-
confidence: heuristics.confidence,
|
|
1043
|
-
observedIndicators: heuristics.evidence,
|
|
1044
|
-
remediationSteps: heuristics.remediationSteps,
|
|
1045
|
-
additionalDataRequests: heuristics.additionalDataRequests,
|
|
1046
|
-
shouldRetryAutomation: heuristics.shouldRetryAutomation,
|
|
1047
|
-
requiresCodeChange: heuristics.requiresCodeChange,
|
|
1048
|
-
requiresProductFix: heuristics.requiresProductFix,
|
|
1049
|
-
notes: heuristics.notes,
|
|
1050
|
-
automationDirectives: deriveAutomationDirectives(undefined, heuristics, heuristics.failureReason),
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
1451
|
function sanitizeFilenameSegment(value, maxLength = 48) {
|
|
1054
1452
|
const sanitized = value
|
|
1055
1453
|
.toLowerCase()
|
|
@@ -1099,6 +1497,8 @@ async function collectFailureContext(testInfo, page) {
|
|
|
1099
1497
|
path: 'path' in attachment ? (attachment.path ?? null) : null,
|
|
1100
1498
|
})) ?? [];
|
|
1101
1499
|
const errorSummaries = buildErrorSummaries(testInfo);
|
|
1500
|
+
// Launch history fetch concurrently with metadata/tool-call loading
|
|
1501
|
+
const flowHistoryPromise = fetchFlowHistory(page);
|
|
1102
1502
|
let flowMetadata = null;
|
|
1103
1503
|
let recentToolCalls = [];
|
|
1104
1504
|
const flowId = page._dnb?.donobuFlowMetadata?.id;
|
|
@@ -1123,8 +1523,9 @@ async function collectFailureContext(testInfo, page) {
|
|
|
1123
1523
|
else {
|
|
1124
1524
|
flowMetadata = page._dnb?.donobuFlowMetadata ?? null;
|
|
1125
1525
|
}
|
|
1526
|
+
const flowHistory = await flowHistoryPromise;
|
|
1126
1527
|
const sanitizedMetadata = sanitizeFlowMetadata(flowMetadata);
|
|
1127
|
-
const heuristics = deriveHeuristicAssessment(testInfo, errorSummaries, recentToolCalls, sanitizedMetadata);
|
|
1528
|
+
const heuristics = deriveHeuristicAssessment(testInfo, errorSummaries, recentToolCalls, sanitizedMetadata, flowHistory);
|
|
1128
1529
|
const testSnippet = await extractTestCaseSnippet(testInfo.file, testInfo.title);
|
|
1129
1530
|
return {
|
|
1130
1531
|
testCase: {
|
|
@@ -1151,6 +1552,7 @@ async function collectFailureContext(testInfo, page) {
|
|
|
1151
1552
|
},
|
|
1152
1553
|
testSnippet,
|
|
1153
1554
|
heuristics,
|
|
1555
|
+
flowHistory,
|
|
1154
1556
|
};
|
|
1155
1557
|
}
|
|
1156
1558
|
function createEvidenceFilePath(testInfo, runDirectory, evidenceId) {
|
|
@@ -1160,9 +1562,6 @@ function createEvidenceFilePath(testInfo, runDirectory, evidenceId) {
|
|
|
1160
1562
|
const filename = `failure-evidence-${projectSegment}-${titleSegment}-${workerSegment}-${evidenceId}.json`;
|
|
1161
1563
|
return path.join(runDirectory, filename);
|
|
1162
1564
|
}
|
|
1163
|
-
function buildTreatmentPlanFromHeuristics(evidence) {
|
|
1164
|
-
return buildFallbackTreatmentPlan(evidence.failureContext.heuristics);
|
|
1165
|
-
}
|
|
1166
1565
|
async function gatherTestFailureEvidence(testInfo, page, options = {}) {
|
|
1167
1566
|
if (!options.force && process.env.DONOBU_TRIAGE_DISABLED === '1') {
|
|
1168
1567
|
Logger_1.appLogger.debug('Skipping Donobu triage evidence gathering because DONOBU_TRIAGE_DISABLED=1.');
|
|
@@ -1171,6 +1570,14 @@ async function gatherTestFailureEvidence(testInfo, page, options = {}) {
|
|
|
1171
1570
|
const failureContext = await collectFailureContext(testInfo, page);
|
|
1172
1571
|
const runDirectory = resolveRunDirectory(testInfo, options);
|
|
1173
1572
|
const evidenceId = (0, crypto_1.randomUUID)();
|
|
1573
|
+
// Capture screenshots for visual triage: current failure + baseline from last success
|
|
1574
|
+
const lastSuccessfulRunId = failureContext.flowHistory?.lastSuccessfulRunId ?? null;
|
|
1575
|
+
const [screenshotBuffer, baselineBuffer] = await Promise.all([
|
|
1576
|
+
fetchLastToolCallScreenshot(page),
|
|
1577
|
+
lastSuccessfulRunId
|
|
1578
|
+
? fetchBaselineScreenshot(page, lastSuccessfulRunId)
|
|
1579
|
+
: Promise.resolve(null),
|
|
1580
|
+
]);
|
|
1174
1581
|
const evidence = {
|
|
1175
1582
|
schemaVersion: TRIAGE_EVIDENCE_SCHEMA_VERSION,
|
|
1176
1583
|
evidenceId,
|
|
@@ -1178,12 +1585,38 @@ async function gatherTestFailureEvidence(testInfo, page, options = {}) {
|
|
|
1178
1585
|
runDirectory,
|
|
1179
1586
|
collectedAtIso: new Date().toISOString(),
|
|
1180
1587
|
failureContext,
|
|
1588
|
+
failureScreenshotPath: null,
|
|
1589
|
+
baselineScreenshotPath: null,
|
|
1181
1590
|
};
|
|
1182
1591
|
const persistToDisk = options.persistToDisk ?? true;
|
|
1183
1592
|
let filePath = null;
|
|
1184
1593
|
if (persistToDisk) {
|
|
1185
1594
|
try {
|
|
1186
1595
|
await fs.mkdir(runDirectory, { recursive: true });
|
|
1596
|
+
// Save failure screenshot alongside evidence if available
|
|
1597
|
+
if (screenshotBuffer) {
|
|
1598
|
+
try {
|
|
1599
|
+
const screenshotFilename = `failure-screenshot-${evidenceId}.png`;
|
|
1600
|
+
const screenshotPath = path.join(runDirectory, screenshotFilename);
|
|
1601
|
+
await fs.writeFile(screenshotPath, screenshotBuffer);
|
|
1602
|
+
evidence.failureScreenshotPath = screenshotPath;
|
|
1603
|
+
}
|
|
1604
|
+
catch (screenshotError) {
|
|
1605
|
+
Logger_1.appLogger.debug('Failed to persist failure screenshot for triage evidence.', screenshotError);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
// Save baseline screenshot from last successful run if available
|
|
1609
|
+
if (baselineBuffer) {
|
|
1610
|
+
try {
|
|
1611
|
+
const baselineFilename = `baseline-screenshot-${evidenceId}.png`;
|
|
1612
|
+
const baselinePath = path.join(runDirectory, baselineFilename);
|
|
1613
|
+
await fs.writeFile(baselinePath, baselineBuffer);
|
|
1614
|
+
evidence.baselineScreenshotPath = baselinePath;
|
|
1615
|
+
}
|
|
1616
|
+
catch (baselineError) {
|
|
1617
|
+
Logger_1.appLogger.debug('Failed to persist baseline screenshot for triage evidence.', baselineError);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1187
1620
|
filePath = createEvidenceFilePath(testInfo, runDirectory, evidenceId);
|
|
1188
1621
|
await fs.writeFile(filePath, JSON.stringify(evidence, null, 2), 'utf8');
|
|
1189
1622
|
try {
|
|
@@ -1202,6 +1635,26 @@ async function gatherTestFailureEvidence(testInfo, page, options = {}) {
|
|
|
1202
1635
|
filePath = null;
|
|
1203
1636
|
}
|
|
1204
1637
|
}
|
|
1638
|
+
// Persist triage artifacts through the Donobu persistence layer (in addition
|
|
1639
|
+
// to disk) so they are accessible through whatever backend is configured.
|
|
1640
|
+
const flowId = page._dnb.donobuFlowMetadata.id;
|
|
1641
|
+
const persistence = page._dnb.persistence;
|
|
1642
|
+
if (flowId && persistence) {
|
|
1643
|
+
const writes = [];
|
|
1644
|
+
if (screenshotBuffer) {
|
|
1645
|
+
writes.push(persistence.setFlowFile(flowId, TRIAGE_PERSISTENCE_FILE_IDS.failureScreenshot, screenshotBuffer));
|
|
1646
|
+
}
|
|
1647
|
+
if (baselineBuffer) {
|
|
1648
|
+
writes.push(persistence.setFlowFile(flowId, TRIAGE_PERSISTENCE_FILE_IDS.baselineScreenshot, baselineBuffer));
|
|
1649
|
+
}
|
|
1650
|
+
writes.push(persistence.setFlowFile(flowId, TRIAGE_PERSISTENCE_FILE_IDS.evidence, Buffer.from(JSON.stringify(evidence, null, 2), 'utf8')));
|
|
1651
|
+
try {
|
|
1652
|
+
await Promise.allSettled(writes);
|
|
1653
|
+
}
|
|
1654
|
+
catch (error) {
|
|
1655
|
+
Logger_1.appLogger.debug('Failed to persist triage artifacts through Donobu persistence layer.', error);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1205
1658
|
return {
|
|
1206
1659
|
evidence,
|
|
1207
1660
|
filePath,
|
|
@@ -1293,8 +1746,75 @@ When assertionsFailedAfterSuccessfulPageAi + usedDeterministicMode + !isRetryAtt
|
|
|
1293
1746
|
|
|
1294
1747
|
When isRetryAttempt === true:
|
|
1295
1748
|
→ Do NOT recommend cache deletion (already done).
|
|
1296
|
-
→ This is NOT a stale cache issue - focus on code review and legitimate failure investigation
|
|
1749
|
+
→ This is NOT a stale cache issue - focus on code review and legitimate failure investigation.
|
|
1750
|
+
|
|
1751
|
+
HISTORICAL RUN DATA:
|
|
1752
|
+
When failureContext.flowHistory is present, it contains aggregated data about prior runs of the
|
|
1753
|
+
same flow within a recent time window. Use this to improve your diagnosis:
|
|
1754
|
+
|
|
1755
|
+
- passRate: The ratio of successful runs in the query window. A high pass rate (>0.8) combined
|
|
1756
|
+
with the current failure suggests either a regression or transient issue, NOT a chronic problem.
|
|
1757
|
+
- currentStreak: If state is 'FAILED' with length > 1, consecutive failures suggest a real
|
|
1758
|
+
regression. If state is 'MIXED' or 'SUCCESS' with length 1, this may be a one-off flake.
|
|
1759
|
+
- recentRuns: The most recent runs in newest-first order. Look for patterns:
|
|
1760
|
+
* Alternating SUCCESS/FAILED = flaky test (likely TIMING_OR_SYNCHRONISATION or NETWORK_OR_DEPENDENCY)
|
|
1761
|
+
* Sudden switch from SUCCESS to FAILED = regression (SELECTOR_REGRESSION or APPLICATION_DEFECT)
|
|
1762
|
+
* All FAILED = persistent issue (likely APPLICATION_DEFECT or ENVIRONMENT_CONFIGURATION)
|
|
1763
|
+
|
|
1764
|
+
When using historical context:
|
|
1765
|
+
- If history shows flakiness, prefer TIMING_OR_SYNCHRONISATION over APPLICATION_DEFECT.
|
|
1766
|
+
- If history shows a regression pattern, increase confidence in the classification.
|
|
1767
|
+
- If prior DETERMINISTIC runs succeeded, the cache was recently valid — a change occurred.
|
|
1768
|
+
- Set shouldRetryAutomation=true when history shows the flow has recovered on retry before.
|
|
1769
|
+
- Note any historical patterns in the observedIndicators array.
|
|
1770
|
+
|
|
1771
|
+
TOOL CALL PARAMETERS:
|
|
1772
|
+
The failureContext.donobuFlow.recentToolCalls may include a parameters field with the arguments
|
|
1773
|
+
passed to each tool invocation. Use these to improve diagnosis:
|
|
1774
|
+
- For selector failures: the actual selector or element description attempted reveals what changed.
|
|
1775
|
+
- For navigation: the target URL helps distinguish navigation errors from selector issues.
|
|
1776
|
+
- For input actions: the values entered help assess whether the automation intent was correct.
|
|
1777
|
+
|
|
1778
|
+
SCREENSHOT EVIDENCE:
|
|
1779
|
+
You may receive one or two screenshots:
|
|
1780
|
+
1. "FAILURE SCREENSHOT" — the state of the page at or near the point of failure in the current run.
|
|
1781
|
+
2. "BASELINE SCREENSHOT" — the state of the page at the end of the most recent successful run of
|
|
1782
|
+
this same flow. This serves as a visual reference for what the page *should* look like.
|
|
1783
|
+
|
|
1784
|
+
When both screenshots are provided, compare them to:
|
|
1785
|
+
- Detect UI changes (redesigns, layout shifts, new modals) that would explain selector or cache failures.
|
|
1786
|
+
- Identify whether the failure screenshot shows a fundamentally different page state (error page, login wall)
|
|
1787
|
+
versus the expected baseline state.
|
|
1788
|
+
- Assess whether the page content matches but timing/loading is off (partial render vs fully loaded baseline).
|
|
1789
|
+
- Corroborate regression signals: if the baseline looks normal but the failure screenshot shows a different
|
|
1790
|
+
UI structure, this strongly supports SELECTOR_REGRESSION or STALE_CACHE_OR_INSTRUCTIONS.
|
|
1791
|
+
|
|
1792
|
+
When only the failure screenshot is provided (no baseline available), use it to:
|
|
1793
|
+
- Visually verify whether the UI matches the expected state described in the test assertions.
|
|
1794
|
+
- Detect page redesigns that would explain stale cache scenarios (elements exist but are wrong).
|
|
1795
|
+
- Identify error pages, login walls, or unexpected modals that explain the failure.
|
|
1796
|
+
- Corroborate or override text-based heuristic signals with visual evidence.`;
|
|
1297
1797
|
const instructions = JSON.stringify(failureContext);
|
|
1798
|
+
// Load screenshots if persisted, for visual grounding
|
|
1799
|
+
const userItems = [{ type: 'text', text: instructions }];
|
|
1800
|
+
if (evidence.baselineScreenshotPath) {
|
|
1801
|
+
try {
|
|
1802
|
+
const baselineBytes = await fs.readFile(evidence.baselineScreenshotPath);
|
|
1803
|
+
userItems.push({ type: 'text', text: 'BASELINE SCREENSHOT (last successful run):' }, { type: 'png', bytes: new Uint8Array(baselineBytes) });
|
|
1804
|
+
}
|
|
1805
|
+
catch (screenshotError) {
|
|
1806
|
+
Logger_1.appLogger.debug('Failed to load baseline screenshot for GPT triage, proceeding without it.', screenshotError);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
if (evidence.failureScreenshotPath) {
|
|
1810
|
+
try {
|
|
1811
|
+
const failureBytes = await fs.readFile(evidence.failureScreenshotPath);
|
|
1812
|
+
userItems.push({ type: 'text', text: 'FAILURE SCREENSHOT (current run):' }, { type: 'png', bytes: new Uint8Array(failureBytes) });
|
|
1813
|
+
}
|
|
1814
|
+
catch (screenshotError) {
|
|
1815
|
+
Logger_1.appLogger.debug('Failed to load failure screenshot for GPT triage, proceeding with text only.', screenshotError);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1298
1818
|
try {
|
|
1299
1819
|
const response = await gptClient.getStructuredOutput([
|
|
1300
1820
|
{
|
|
@@ -1303,20 +1823,15 @@ When isRetryAttempt === true:
|
|
|
1303
1823
|
},
|
|
1304
1824
|
{
|
|
1305
1825
|
type: 'user',
|
|
1306
|
-
items:
|
|
1307
|
-
{
|
|
1308
|
-
type: 'text',
|
|
1309
|
-
text: instructions,
|
|
1310
|
-
},
|
|
1311
|
-
],
|
|
1826
|
+
items: userItems,
|
|
1312
1827
|
},
|
|
1313
1828
|
], TreatmentPlan);
|
|
1314
1829
|
const plan = TreatmentPlan.parse(response.output);
|
|
1315
1830
|
return reconcileTreatmentPlan(plan, heuristics);
|
|
1316
1831
|
}
|
|
1317
1832
|
catch (error) {
|
|
1318
|
-
Logger_1.appLogger.warn(`GPT-driven triage failed for evidence ${evidence.evidenceId}
|
|
1319
|
-
|
|
1833
|
+
Logger_1.appLogger.warn(`GPT-driven triage failed for evidence ${evidence.evidenceId}.`, error);
|
|
1834
|
+
throw error;
|
|
1320
1835
|
}
|
|
1321
1836
|
}
|
|
1322
1837
|
//# sourceMappingURL=triageTestFailure.js.map
|