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.
Files changed (55) hide show
  1. package/dist/apis/FlowsApi.d.ts +2 -2
  2. package/dist/apis/FlowsApi.js +2 -2
  3. package/dist/cli/donobu-cli.js +22 -5
  4. package/dist/cli/donobu-cli.js.map +1 -1
  5. package/dist/cli/playwright-json-to-html.js +163 -51
  6. package/dist/cli/playwright-json-to-html.js.map +1 -1
  7. package/dist/codegen/CodeGenerator.d.ts.map +1 -1
  8. package/dist/codegen/CodeGenerator.js +7 -2
  9. package/dist/codegen/CodeGenerator.js.map +1 -1
  10. package/dist/esm/apis/FlowsApi.d.ts +2 -2
  11. package/dist/esm/apis/FlowsApi.js +2 -2
  12. package/dist/esm/cli/donobu-cli.js +22 -5
  13. package/dist/esm/cli/donobu-cli.js.map +1 -1
  14. package/dist/esm/cli/playwright-json-to-html.js +163 -51
  15. package/dist/esm/cli/playwright-json-to-html.js.map +1 -1
  16. package/dist/esm/codegen/CodeGenerator.d.ts.map +1 -1
  17. package/dist/esm/codegen/CodeGenerator.js +7 -2
  18. package/dist/esm/codegen/CodeGenerator.js.map +1 -1
  19. package/dist/esm/lib/ai/cache/assertCache.js +2 -2
  20. package/dist/esm/lib/page/extendPage.d.ts.map +1 -1
  21. package/dist/esm/lib/page/extendPage.js +12 -21
  22. package/dist/esm/lib/page/extendPage.js.map +1 -1
  23. package/dist/esm/lib/test/testExtension.d.ts.map +1 -1
  24. package/dist/esm/lib/test/testExtension.js +10 -1
  25. package/dist/esm/lib/test/testExtension.js.map +1 -1
  26. package/dist/esm/lib/test/utils/triageTestFailure.d.ts +213 -9
  27. package/dist/esm/lib/test/utils/triageTestFailure.d.ts.map +1 -1
  28. package/dist/esm/lib/test/utils/triageTestFailure.js +553 -38
  29. package/dist/esm/lib/test/utils/triageTestFailure.js.map +1 -1
  30. package/dist/esm/managers/DonobuFlowsManager.d.ts +1 -0
  31. package/dist/esm/managers/DonobuFlowsManager.d.ts.map +1 -1
  32. package/dist/esm/managers/DonobuFlowsManager.js +3 -2
  33. package/dist/esm/managers/DonobuFlowsManager.js.map +1 -1
  34. package/dist/esm/tools/AssertTool.d.ts.map +1 -1
  35. package/dist/esm/tools/AssertTool.js +1 -0
  36. package/dist/esm/tools/AssertTool.js.map +1 -1
  37. package/dist/lib/ai/cache/assertCache.js +2 -2
  38. package/dist/lib/page/extendPage.d.ts.map +1 -1
  39. package/dist/lib/page/extendPage.js +12 -21
  40. package/dist/lib/page/extendPage.js.map +1 -1
  41. package/dist/lib/test/testExtension.d.ts.map +1 -1
  42. package/dist/lib/test/testExtension.js +10 -1
  43. package/dist/lib/test/testExtension.js.map +1 -1
  44. package/dist/lib/test/utils/triageTestFailure.d.ts +213 -9
  45. package/dist/lib/test/utils/triageTestFailure.d.ts.map +1 -1
  46. package/dist/lib/test/utils/triageTestFailure.js +553 -38
  47. package/dist/lib/test/utils/triageTestFailure.js.map +1 -1
  48. package/dist/managers/DonobuFlowsManager.d.ts +1 -0
  49. package/dist/managers/DonobuFlowsManager.d.ts.map +1 -1
  50. package/dist/managers/DonobuFlowsManager.js +3 -2
  51. package/dist/managers/DonobuFlowsManager.js.map +1 -1
  52. package/dist/tools/AssertTool.d.ts.map +1 -1
  53. package/dist/tools/AssertTool.js +1 -0
  54. package/dist/tools/AssertTool.js.map +1 -1
  55. 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.buildTreatmentPlanFromHeuristics = buildTreatmentPlanFromHeuristics;
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
- * Utilities for transforming a Playwright test failure into a structured treatment plan
56
- * that Donobu Studio can surface to engineers or automation. The flow pivots between:
57
- * 1. Gathering heuristics from the failing run (errors, attachments, Donobu AI history).
58
- * 2. Asking the GPT-based triage agent for a canonical plan compliant with the schema.
59
- * 3. Falling back to heuristics when the LLM cannot respond, so automation still receives
60
- * next steps. The helper functions below exist to keep these stages composable.
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 = 1;
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}, returning heuristic fallback treatment plan.`, error);
1319
- return buildFallbackTreatmentPlan(heuristics);
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