@sentinelqa/playwright-reporter 0.1.50 → 0.1.51

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.
@@ -1,5 +1,5 @@
1
1
  type DiagnosisSignal = "timeout" | "assertion_mismatch" | "locator_not_found" | "actionability" | "network" | "runtime" | "infra" | "unknown";
2
- type QuickDiagnosis = {
2
+ export type QuickDiagnosis = {
3
3
  lines: string[];
4
4
  footer?: string[];
5
5
  };
@@ -860,31 +860,25 @@ const buildQuickDiagnosis = (playwrightJsonPath) => {
860
860
  const failed = failures[0];
861
861
  const suspects = commitWindow.trusted ? rankCommitsForFailure(failed, commitWindow) : [];
862
862
  const top = suspects[0];
863
- const alt = suspects[1];
864
- const lines = [`${formatTitle(failed.title)} failed.`, `What broke: ${(0, exports.describeFailure)(failed)}`];
863
+ const lines = [`Test: ${shortenTitle(failed.title)}`, `Likely cause: ${(0, exports.describeFailure)(failed)}`];
865
864
  const primaryLocation = buildLocationLine(failed);
866
- if (primaryLocation)
867
- lines.push(`Where: ${withoutPrefix(withoutPrefix(primaryLocation, "Error location:"), "Likely file:")}`);
868
- if (failed.firstErrorLine)
869
- lines.push(`Error: ${failed.firstErrorLine}`);
870
- for (const evidence of buildSecondaryEvidenceLines(failed))
871
- lines.push(evidence);
872
- if (top) {
873
- lines.push(compactCommitLine(top));
874
- lines.push(`Why Sentinel picked it: ${compactWhyLine(top)}.`);
875
- if (top.touchedFiles.length) {
876
- lines.push(`Touched files: ${top.touchedFiles.map((file) => basename(file)).join(", ")}`);
865
+ const confidence = top ? confidenceLabel(top.score).toLowerCase() : "medium";
866
+ lines.push(`Confidence: ${confidence}`);
867
+ if (top && top.score >= 0.62) {
868
+ lines.push(`Likely introduced in: "${top.commit.message}"`);
869
+ if (top.reasons.length) {
870
+ lines.push(`Reason: ${compactWhyLine(top)}.`);
877
871
  }
878
- if (alt)
879
- lines.push(alternateCommitLine(alt));
880
872
  }
881
- lines.push(`Check first: ${checkFirst(failed)}.`);
873
+ lines.push("Check first:");
874
+ lines.push(`- ${checkFirst(failed)}`);
875
+ if (primaryLocation)
876
+ lines.push(`Where: ${withoutPrefix(withoutPrefix(primaryLocation, "Error location:"), "Likely file:")}`);
877
+ if (failed.codeContext?.action)
878
+ lines.push(`Failing step: ${failed.codeContext.action}`);
882
879
  return {
883
880
  lines,
884
- footer: [
885
- ...(top ? [`Confidence: ${confidenceLabel(top.score)}`] : []),
886
- ...(commitWindow.trusted ? [`Commits analyzed: ${commitWindow.commits.length}`] : [`Commit blame skipped: ${commitWindow.reason}`])
887
- ]
881
+ footer: []
888
882
  };
889
883
  }
890
884
  const clusterMap = new Map();
@@ -916,29 +910,24 @@ const buildQuickDiagnosis = (playwrightJsonPath) => {
916
910
  const locationValue = clusterLocation
917
911
  ? withoutPrefix(clusterLocation, clusterLocation.startsWith("Error locations:") ? "Error locations:" : "Error location:")
918
912
  : null;
919
- const rootCause = compactRootCauseSummary(cluster);
920
- const errorLine = compactErrorLine(cluster.sample);
913
+ const rootCause = cluster.count === 1 ? (0, exports.describeFailure)(cluster.sample) : compactRootCauseSummary(cluster);
921
914
  lines.push(`[${index + 1}] ${rootCauseLabel(cluster.sample)} (${cluster.count} tests)`);
922
- lines.push(` Root cause: ${rootCause}`);
923
- if (errorLine) {
924
- lines.push(` Error: ${errorLine}`);
915
+ lines.push(` Likely cause: ${rootCause}`);
916
+ if (top && top.score >= 0.62) {
917
+ lines.push(` Likely introduced in: "${top.commit.message}"`);
918
+ if (top.reasons.length) {
919
+ lines.push(` Reason: ${compactWhyLine(top)}.`);
920
+ }
925
921
  }
922
+ lines.push(` Check first: ${clusterCheckFirst(cluster)}.`);
926
923
  if (locationValue) {
927
924
  lines.push(` Where: ${locationValue}`);
928
925
  }
929
- if (top && top.touchedFiles.length) {
930
- lines.push(` Changed files: ${top.touchedFiles.map((file) => basename(file)).join(", ")}`);
931
- }
932
- lines.push(` Check first: ${clusterCheckFirst(cluster)}.`);
926
+ lines.push(` Shared impact: ${cluster.count} test${cluster.count === 1 ? "" : "s"} failing with ${cluster.count === 1 ? "this" : "same"} root cause`);
933
927
  }
934
928
  return {
935
929
  lines,
936
- footer: topCluster
937
- ? [
938
- ...(topCluster.suspects[0] ? [`Top confidence: ${confidenceLabel(topCluster.suspects[0].score)}`] : []),
939
- ...(commitWindow.trusted ? [`Commits analyzed: ${commitWindow.commits.length}`] : [`Commit blame skipped: ${commitWindow.reason}`])
940
- ]
941
- : []
930
+ footer: topCluster?.suspects[0] ? [`Confidence: ${confidenceLabel(topCluster.suspects[0].score).toLowerCase()}`] : []
942
931
  };
943
932
  };
944
933
  exports.buildQuickDiagnosis = buildQuickDiagnosis;
package/dist/reporter.js CHANGED
@@ -14,6 +14,7 @@ const colorize = (value, code) => {
14
14
  const green = (value) => colorize(value, "32");
15
15
  const yellow = (value) => colorize(value, "33");
16
16
  const dim = (value) => colorize(value, "2");
17
+ const divider = () => "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━";
17
18
  const readFinalFailedCount = (playwrightJsonPath) => {
18
19
  try {
19
20
  const parsed = JSON.parse(require("node:fs").readFileSync(playwrightJsonPath, "utf8"));
@@ -94,21 +95,14 @@ class SentinelReporter {
94
95
  console.log("");
95
96
  if (passingSummary) {
96
97
  console.log(green("Sentinel run summary"));
98
+ console.log(divider());
99
+ console.log("");
97
100
  for (const line of passingSummary.lines) {
98
- console.log(` ${line}`);
99
- }
100
- if (passingSummary.risks.length > 0) {
101
- console.log("");
102
- console.log(yellow("Potential risks"));
103
- for (const line of passingSummary.risks) {
104
- console.log(` ${line}`);
105
- }
106
- }
107
- if (!hasWorkspaceToken) {
108
- console.log("");
109
- console.log("Activate a free workspace at sentinelqa.com/register");
101
+ console.log(line);
110
102
  }
111
103
  console.log("");
104
+ console.log(divider());
105
+ console.log("");
112
106
  }
113
107
  if (effectiveFailedCount > 0) {
114
108
  (0, telemetry_1.emitFailedRunTelemetry)();
@@ -118,31 +112,38 @@ class SentinelReporter {
118
112
  return;
119
113
  }
120
114
  if (hasWorkspaceToken && !hasCiEnv && !localUploadEnabled) {
121
- console.log([
122
- "Sentinel: Upload skipped.",
123
- "Reason: Local workspace uploads require SENTINEL_UPLOAD_LOCAL=1.",
124
- "",
125
- "Next step:",
126
- "- Set SENTINEL_UPLOAD_LOCAL=1 to allow a local workspace upload.",
127
- "- Or remove SENTINEL_TOKEN to generate a free public hosted report instead."
128
- ].join("\n"));
115
+ console.log("Uploading debug report skipped");
116
+ console.log("Set SENTINEL_UPLOAD_LOCAL=1 for local workspace uploads.");
129
117
  return;
130
118
  }
131
- if (hasWorkspaceToken) {
119
+ if (quickDiagnosis?.lines.length) {
120
+ console.log(yellow("Sentinel diagnosis"));
121
+ console.log(divider());
132
122
  console.log("");
133
- console.log(green("✔ Artifacts collected"));
134
- }
135
- console.log("");
136
- if (hasWorkspaceToken) {
137
- console.log("Uploading hosted debugging report to Sentinel...");
138
- }
139
- else if (usingImplicitLocalPublicMode) {
140
- console.log("Creating free hosted debug report with Sentinel...");
141
- console.log(dim("No API key detected. Using the free public report flow for this local run."));
142
- }
143
- else {
144
- console.log("Creating free hosted debug report with Sentinel...");
123
+ if (failedRunHistory?.passStreakBeforeFailure && failedRunHistory.passStreakBeforeFailure > 0) {
124
+ console.log(`NEW FAILURE after ${failedRunHistory.passStreakBeforeFailure} passing runs`);
125
+ console.log("");
126
+ }
127
+ else if ((failedRunHistory?.recurringCount || 0) > 0) {
128
+ console.log(`RECURRING FAILURE (${failedRunHistory?.recurringCount} previous runs)`);
129
+ console.log("");
130
+ }
131
+ for (const line of quickDiagnosis.lines) {
132
+ console.log(line);
133
+ }
134
+ console.log("");
135
+ console.log(divider());
136
+ console.log("");
137
+ if (failedRunHistory?.newFailures && failedRunHistory.newFailures > 0) {
138
+ console.log(`Impact: ${failedRunHistory.newFailures} newly failing in this run`);
139
+ console.log("");
140
+ }
141
+ else if (failedRunHistory?.stillFailing && failedRunHistory.stillFailing > 0) {
142
+ console.log(`Impact: ${failedRunHistory.stillFailing} tests still failing`);
143
+ console.log("");
144
+ }
145
145
  }
146
+ console.log("Uploading debug report...");
146
147
  console.log("");
147
148
  const upload = (await (0, node_1.runSentinelUpload)({
148
149
  playwrightJsonPath: this.options.playwrightJsonPath,
@@ -159,43 +160,22 @@ class SentinelReporter {
159
160
  if (upload.exitCode !== 0) {
160
161
  throw new Error(`Sentinel upload failed with exit code ${upload.exitCode}`);
161
162
  }
162
- const backendReady = upload.diagnosis?.status === "ready" && Boolean(upload.diagnosis?.lines?.length);
163
- const diagnosis = backendReady ? upload.diagnosis : quickDiagnosis;
164
- if (diagnosis?.lines.length) {
165
- console.log("");
163
+ if (!quickDiagnosis?.lines.length && upload.diagnosis?.lines.length) {
166
164
  console.log(yellow("Sentinel diagnosis"));
167
- for (const line of diagnosis.lines) {
168
- console.log(` ${dim(line)}`);
169
- }
170
- if (diagnosis.footer?.length) {
171
- console.log("");
172
- for (const line of diagnosis.footer) {
173
- console.log(` ${dim(line)}`);
174
- }
175
- }
176
- }
177
- const historyLines = failedRunHistory?.lines || [];
178
- const normalizedHistory = backendReady
179
- ? historyLines.filter((line) => !line.includes("Seen before:"))
180
- : historyLines;
181
- if (normalizedHistory.length) {
165
+ console.log(divider());
182
166
  console.log("");
183
- console.log(yellow("Run context"));
184
- for (const line of normalizedHistory) {
185
- console.log(` ${dim(line)}`);
167
+ for (const line of upload.diagnosis.lines) {
168
+ console.log(line);
186
169
  }
170
+ console.log("");
171
+ console.log(divider());
187
172
  }
188
173
  console.log("");
189
- console.log("View full debug report");
174
+ console.log("Debug report ready");
190
175
  console.log(` ${upload.shareRunUrl || upload.internalRunUrl}`);
191
176
  if (upload.shareLabel) {
192
177
  console.log(` ${dim(upload.shareLabel)}`);
193
178
  }
194
- if (!hasWorkspaceToken) {
195
- console.log("");
196
- console.log("Create a free workspace to keep reports private, compare runs, and unlock deeper AI debugging");
197
- console.log(` ${dim("https://sentinelqa.com/register")}`);
198
- }
199
179
  }
200
180
  }
201
181
  module.exports = SentinelReporter;
@@ -5,6 +5,13 @@ type RunDiffSummary = {
5
5
  };
6
6
  export type FailedRunHistorySummary = {
7
7
  lines: string[];
8
+ passStreakBeforeFailure: number;
9
+ previousWasGreen: boolean;
10
+ newFailures: number;
11
+ fixedTests: number;
12
+ stillFailing: number;
13
+ recurringCount: number;
14
+ recurringTitle: string | null;
8
15
  };
9
16
  export declare const buildRunDiffSummary: (playwrightJsonPath: string) => RunDiffSummary | null;
10
17
  export declare const buildFailedRunHistorySummary: (playwrightJsonPath: string) => FailedRunHistorySummary | null;
@@ -221,6 +221,17 @@ const buildFailedRunHistorySummary = (playwrightJsonPath) => {
221
221
  if (topRecurring) {
222
222
  lines.push(`- Recurring across ${topRecurring.occurrences + 1} recorded failed runs in local history (${topRecurring.failure.title})`);
223
223
  }
224
- return lines.length ? { lines } : null;
224
+ return lines.length
225
+ ? {
226
+ lines,
227
+ passStreakBeforeFailure,
228
+ previousWasGreen: Boolean(previousRun && (previousRun.failedCount || 0) === 0),
229
+ newFailures,
230
+ fixedTests,
231
+ stillFailing,
232
+ recurringCount: topRecurring ? topRecurring.occurrences : 0,
233
+ recurringTitle: topRecurring?.failure.title || null
234
+ }
235
+ : null;
225
236
  };
226
237
  exports.buildFailedRunHistorySummary = buildFailedRunHistorySummary;
@@ -21,7 +21,6 @@ type HistorySnapshot = {
21
21
  };
22
22
  export type PassingRunSummary = {
23
23
  lines: string[];
24
- risks: string[];
25
24
  };
26
25
  type PassingRunSummaryOptions = {
27
26
  observedRunDurationMs?: number | null;
@@ -343,56 +343,25 @@ const buildPassingRunSummary = (playwrightJsonPath, options) => {
343
343
  const hasActiveRisk = flakyLookingCount > 0;
344
344
  const totalDurationMs = snapshot.tests.reduce((sum, test) => sum + test.durationMs, 0);
345
345
  const displayedRunDurationMs = options?.observedRunDurationMs || snapshot.wallDurationMs || totalDurationMs;
346
- const lines = [
347
- `- ${snapshot.passedCount} tests passed in ${formatDuration(displayedRunDurationMs)} of wall time`
348
- ];
349
- if (hasActiveRisk) {
350
- if (retryPassedCount > 0)
351
- lines.push(`- Retries detected: ${retryPassedCount}`);
352
- lines.push(`- Flaky-looking tests: ${flakyLookingCount}`);
353
- if (topNearFailures[0]) {
354
- lines.push(`- Most at risk next: ${topNearFailures[0].title} (${topNearFailures[0].primaryReason})`);
355
- }
356
- if (passStreak > 1)
357
- lines.push(`- Pass streak: ${passStreak} runs`);
346
+ const lines = ["All tests passed", `${snapshot.passedCount} tests in ${formatDuration(displayedRunDurationMs)}`];
347
+ if (hasActiveRisk && topNearFailures[0]) {
348
+ lines.push(`At risk: ${topNearFailures[0].title} ${topNearFailures[0].primaryReason}`);
349
+ lines.push(topNearFailures[0].retries > 0 || topNearFailures[0].historicalRetries >= 2
350
+ ? "Why this matters: Similar timing and retry patterns often turn into flaky failures"
351
+ : "Why this matters: Performance regressions often lead to flaky failures");
358
352
  if (lastFailureRunsAgo !== null)
359
- lines.push(`- Last failure: ${lastFailureRunsAgo} runs ago`);
353
+ lines.push(`Last failure: ${lastFailureRunsAgo} runs ago`);
354
+ lines.push(topNearFailures[0].retries > 0
355
+ ? "Recommendation: inspect retry behavior or timing around this test"
356
+ : "Recommendation: monitor the next runs or investigate the slowdown");
360
357
  }
361
358
  else {
362
- lines.push(`- Pipeline health: Stable`);
363
- if (passStreak > 1)
364
- lines.push(`- Pass streak: ${passStreak} runs`);
359
+ lines.push("No anomalies detected");
365
360
  if (lastFailureRunsAgo !== null)
366
- lines.push(`- Last failure: ${lastFailureRunsAgo} runs ago`);
367
- }
368
- lines.push(`- Sentinel active: traces, screenshots, video enabled`);
369
- const risks = [];
370
- for (const candidate of topNearFailures) {
371
- if (candidate.ratio && candidate.ratio >= 1.8) {
372
- risks.push(`- ${candidate.title} took ${formatShortDuration(candidate.currentDurationMs)} vs ${formatShortDuration(candidate.medianDurationMs)} recent median (${candidate.ratio.toFixed(1)}x)`);
373
- }
374
- if (candidate.retries > 0) {
375
- risks.push(`- ${candidate.title} passed after ${candidate.retries} retr${candidate.retries === 1 ? 'y' : 'ies'}`);
376
- }
377
- if (candidate.historicalRetries >= 2) {
378
- risks.push(`- ${candidate.title} has needed retries in ${candidate.historicalRetries} of the last 10 passing runs`);
379
- }
380
- if (candidate.repeatedSlowPasses >= 3) {
381
- risks.push(`- ${candidate.title} has been unusually slow in ${candidate.repeatedSlowPasses} of the last 10 passing runs`);
382
- }
383
- if (candidate.recentTrendRatio && candidate.recentTrendRatio >= 1.5) {
384
- risks.push(`- ${candidate.title} is trending slower over recent runs`);
385
- }
386
- if (candidate.timeoutUtilization && candidate.timeoutUtilization >= 0.65) {
387
- risks.push(`- ${candidate.title} used ${(candidate.timeoutUtilization * 100).toFixed(0)}% of its timeout budget`);
388
- }
389
- if (candidate.historicalFailures >= 5 &&
390
- lastFailureRunsAgo !== null &&
391
- lastFailureRunsAgo <= 3 &&
392
- (candidate.currentRunSignal || candidate.historicalRetries >= 2 || candidate.repeatedSlowPasses >= 3)) {
393
- risks.push(`- ${candidate.title} failed in ${candidate.historicalFailures} of the last 10 runs`);
394
- }
361
+ lines.push(`Last failure: ${lastFailureRunsAgo} runs ago`);
362
+ lines.push("Status: Stable");
395
363
  }
396
- return { lines, risks: Array.from(new Set(risks)).slice(0, 4) };
364
+ lines.push("Artifacts ready: traces, screenshots, video");
365
+ return { lines };
397
366
  };
398
367
  exports.buildPassingRunSummary = buildPassingRunSummary;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentinelqa/playwright-reporter",
3
- "version": "0.1.50",
3
+ "version": "0.1.51",
4
4
  "private": false,
5
5
  "description": "Playwright reporter for CI debugging with optional Sentinel cloud dashboards",
6
6
  "license": "MIT",