@fixsense/cypress-plugin 0.2.0 → 0.2.4

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/README.md CHANGED
@@ -55,9 +55,28 @@ actually wrong, so a green suite stays quiet.
55
55
  On GitHub Actions the same table is written to the job summary, so it appears
56
56
  on the run's own page instead of scrolling past in the log.
57
57
 
58
+ On GitHub Actions it also writes an annotation for each flaky or broken test,
59
+ so the finding shows up inline on the pull request, carrying how many of the
60
+ last runs the test misbehaved in.
61
+
58
62
  `.fixsense/` is git-ignored for you: committing it would mean a diff on every
59
63
  run and merge conflicts in a file nobody reads.
60
64
 
65
+ ### Keeping the history in CI
66
+
67
+ Every CI job starts from a clean checkout, so the history is gone before the
68
+ next run reads it. The plugin says so in one line instead of staying silent.
69
+ On GitHub Actions, cache the directory between runs:
70
+
71
+ ```yaml
72
+ - name: Keep the test history between runs
73
+ uses: actions/cache@v4
74
+ with:
75
+ path: .fixsense
76
+ key: fixsense-history-${{ github.run_id }}
77
+ restore-keys: fixsense-history-
78
+ ```
79
+
61
80
  ### Why an account, then
62
81
 
63
82
  The local history lives on one machine and dies with the CI container. The
package/dist/index.d.ts CHANGED
@@ -40,4 +40,4 @@ export declare function registerFixSense(on: On, options?: FixSenseCypressOption
40
40
  export declare function sendRun(tests: TestRecord[], startedAt: Date, options: FixSenseCypressOptions): Promise<void>;
41
41
  export default registerFixSense;
42
42
  export * from "./payload";
43
- export { computeFlakiness, readHistory, recordAndReport, renderMarkdown, renderSummary, scoreHistory, HISTORY_DIR, HISTORY_FILE, MAX_RUNS, } from "./local";
43
+ export { computeFlakiness, readHistory, recordAndReport, renderMarkdown, renderSummary, renderAnnotations, scoreHistory, HISTORY_DIR, HISTORY_FILE, MAX_RUNS, } from "./local";
package/dist/index.js CHANGED
@@ -23,12 +23,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
23
23
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.MAX_RUNS = exports.HISTORY_FILE = exports.HISTORY_DIR = exports.scoreHistory = exports.renderSummary = exports.renderMarkdown = exports.recordAndReport = exports.readHistory = exports.computeFlakiness = void 0;
26
+ exports.MAX_RUNS = exports.HISTORY_FILE = exports.HISTORY_DIR = exports.scoreHistory = exports.renderAnnotations = exports.renderSummary = exports.renderMarkdown = exports.recordAndReport = exports.readHistory = exports.computeFlakiness = void 0;
27
27
  exports.registerFixSense = registerFixSense;
28
28
  exports.sendRun = sendRun;
29
29
  const payload_1 = require("./payload");
30
30
  const local_1 = require("./local");
31
31
  const DEFAULT_API_URL = "https://fix-sense.com";
32
+ const DOCS_URL = "https://fix-sense.com/docs/integrations/cypress-plugin#keeping-the-history-in-ci";
32
33
  const DEFAULT_TIMEOUT_MS = 10_000;
33
34
  /**
34
35
  * Register the plugin from `setupNodeEvents`.
@@ -60,16 +61,19 @@ async function sendRun(tests, startedAt, options) {
60
61
  return;
61
62
  // The local history first, because it needs no account and is the only
62
63
  // part most first-time users will ever see.
64
+ const apiKey = options.apiKey ?? process.env.FIXSENSE_API_KEY;
63
65
  if (options.local !== false) {
64
66
  try {
65
- (0, local_1.recordAndReport)(options.cwd ?? process.cwd(), tests);
67
+ (0, local_1.recordAndReport)(options.cwd ?? process.cwd(), tests, new Date(), {
68
+ connected: Boolean(apiKey),
69
+ docsUrl: DOCS_URL,
70
+ });
66
71
  }
67
72
  catch (error) {
68
73
  const reason = error instanceof Error ? error.message : String(error);
69
74
  console.error(`[fixsense] Local history not updated: ${reason}`);
70
75
  }
71
76
  }
72
- const apiKey = options.apiKey ?? process.env.FIXSENSE_API_KEY;
73
77
  // Silent without a key: a developer running Cypress locally has none and
74
78
  // does not need telling on every run.
75
79
  if (!apiKey)
@@ -119,6 +123,7 @@ Object.defineProperty(exports, "readHistory", { enumerable: true, get: function
119
123
  Object.defineProperty(exports, "recordAndReport", { enumerable: true, get: function () { return local_2.recordAndReport; } });
120
124
  Object.defineProperty(exports, "renderMarkdown", { enumerable: true, get: function () { return local_2.renderMarkdown; } });
121
125
  Object.defineProperty(exports, "renderSummary", { enumerable: true, get: function () { return local_2.renderSummary; } });
126
+ Object.defineProperty(exports, "renderAnnotations", { enumerable: true, get: function () { return local_2.renderAnnotations; } });
122
127
  Object.defineProperty(exports, "scoreHistory", { enumerable: true, get: function () { return local_2.scoreHistory; } });
123
128
  Object.defineProperty(exports, "HISTORY_DIR", { enumerable: true, get: function () { return local_2.HISTORY_DIR; } });
124
129
  Object.defineProperty(exports, "HISTORY_FILE", { enumerable: true, get: function () { return local_2.HISTORY_FILE; } });
package/dist/local.d.ts CHANGED
@@ -20,6 +20,8 @@ export type Outcome = "expected" | "unexpected" | "flaky" | "skipped";
20
20
  export interface StoredTest {
21
21
  title: string;
22
22
  file: string | null;
23
+ /** Where the test is declared, so a GitHub annotation can point at it. */
24
+ line?: number | null;
23
25
  outcome: Outcome;
24
26
  }
25
27
  export interface StoredRun {
@@ -32,6 +34,7 @@ export interface LocalHistory {
32
34
  }
33
35
  export declare const HISTORY_DIR = ".fixsense";
34
36
  export declare const HISTORY_FILE = "history.json";
37
+ export declare const SITE_URL = "https://fix-sense.com";
35
38
  /**
36
39
  * How many runs are kept.
37
40
  *
@@ -43,6 +46,10 @@ export declare const MAX_RUNS = 50;
43
46
  export declare const MIN_RUNS_FOR_VERDICT = 5;
44
47
  /** At or above this score a test is called flaky. */
45
48
  export declare const FLAKY_SCORE_THRESHOLD = 70;
49
+ /** Runs the runner itself called flaky before that alone is a verdict. */
50
+ export declare const MIN_FLAKY_RUNS = 2;
51
+ /** And how often, so a long-settled test is not judged on old history. */
52
+ export declare const MIN_FLAKY_RATE = 0.2;
46
53
  export type Verdict = "stable" | "broken" | "flaky" | "suspect";
47
54
  export interface FlakinessReport {
48
55
  score: number;
@@ -81,6 +88,8 @@ export declare function writeHistory(cwd: string, history: LocalHistory): void;
81
88
  export interface TestVerdict extends FlakinessReport {
82
89
  title: string;
83
90
  file: string | null;
91
+ /** The line this test is declared on, when the runner reports it. */
92
+ line: number | null;
84
93
  /** How many of the stored runs this test failed or needed a retry in. */
85
94
  badRuns: number;
86
95
  }
@@ -93,9 +102,9 @@ export declare function scoreHistory(history: LocalHistory): TestVerdict[];
93
102
  * so this stays empty unless the history actually found something. "suspect"
94
103
  * is deliberately not reported: it means the tool is not sure yet.
95
104
  */
96
- export declare function renderSummary(verdicts: TestVerdict[], totalRuns: number): string;
105
+ export declare function renderSummary(verdicts: TestVerdict[], totalRuns: number, connected?: boolean): string;
97
106
  /** The same summary as GitHub-flavoured Markdown, for the run's own page. */
98
- export declare function renderMarkdown(verdicts: TestVerdict[], totalRuns: number): string;
107
+ export declare function renderMarkdown(verdicts: TestVerdict[], totalRuns: number, connected?: boolean): string;
99
108
  /**
100
109
  * Write the Markdown where GitHub Actions renders it on the run's page.
101
110
  *
@@ -103,9 +112,59 @@ export declare function renderMarkdown(verdicts: TestVerdict[], totalRuns: numbe
103
112
  * page it is the first thing a reviewer sees.
104
113
  */
105
114
  export declare function writeStepSummary(markdown: string): void;
115
+ /**
116
+ * GitHub workflow annotations for the tests that earned a verdict.
117
+ *
118
+ * This is the one thing a single-run flaky reporter does that we did not:
119
+ * `::warning file=…,line=…::` makes GitHub draw the finding inline on the exact
120
+ * line in the pull request's "Files changed" tab, not only in the job log.
121
+ * Ours carries what a single run cannot — how many of the last N runs the test
122
+ * misbehaved in — so the annotation is a measurement, not a one-off retry.
123
+ *
124
+ * A `flaky` verdict is a warning: the test is noise to quarantine. A `broken`
125
+ * verdict — fails every run — is an error: a real failure hiding behind a
126
+ * retry, and the louder level is the point. "suspect" is never annotated, for
127
+ * the same reason it is never summarised: the tool is not sure yet.
128
+ */
129
+ export declare function renderAnnotations(verdicts: TestVerdict[], totalRuns: number): string[];
130
+ /** True when we are inside a GitHub Actions job, where annotations render. */
131
+ export declare function isGitHubActions(env: Record<string, string | undefined>): boolean;
132
+ /**
133
+ * Emit the annotations as workflow commands on stdout, where the runner reads
134
+ * them. Each must be alone on its line, so this writes them directly rather
135
+ * than folding them into the human summary.
136
+ */
137
+ export declare function writeAnnotations(lines: string[]): void;
138
+ /**
139
+ * One entry per test per run, whatever the runner reported.
140
+ *
141
+ * A run that listed the same test twice would count as two runs for it, and
142
+ * a retried test would read as several times as flaky as it is. The last
143
+ * entry wins: for a runner that reports each attempt, it is the one that
144
+ * knows how the test finally ended.
145
+ */
146
+ export declare function dedupe(tests: StoredTest[]): StoredTest[];
147
+ /** Every supported CI sets CI=true, except Azure Pipelines, which sets TF_BUILD. */
148
+ export declare function isCi(env: Record<string, string | undefined>): boolean;
149
+ /**
150
+ * Said when a CI job finds no earlier runs to compare with.
151
+ *
152
+ * A CI job starts from a clean checkout, so without a cache the history holds
153
+ * one run every time, never reaches a verdict, and the package stays silent
154
+ * forever — which reads as "it does nothing". One line saying why is the
155
+ * difference between a team fixing that and a team uninstalling.
156
+ */
157
+ export declare function renderNoHistoryHint(docsUrl: string): string;
158
+ export interface ReportContext {
159
+ /** An API key is set, so the history is also kept by the service. */
160
+ connected?: boolean;
161
+ /** Where the package's own docs explain keeping the history in CI. */
162
+ docsUrl?: string;
163
+ env?: Record<string, string | undefined>;
164
+ }
106
165
  /**
107
166
  * Record this run and report what the history now says.
108
167
  *
109
168
  * Returns the text that was printed, which is what the tests assert on.
110
169
  */
111
- export declare function recordAndReport(cwd: string, tests: StoredTest[], now?: Date): string;
170
+ export declare function recordAndReport(cwd: string, tests: StoredTest[], now?: Date, context?: ReportContext): string;
package/dist/local.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * asserts the two copies are the same file, so they cannot drift.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.FLAKY_SCORE_THRESHOLD = exports.MIN_RUNS_FOR_VERDICT = exports.MAX_RUNS = exports.HISTORY_FILE = exports.HISTORY_DIR = void 0;
21
+ exports.MIN_FLAKY_RATE = exports.MIN_FLAKY_RUNS = exports.FLAKY_SCORE_THRESHOLD = exports.MIN_RUNS_FOR_VERDICT = exports.MAX_RUNS = exports.SITE_URL = exports.HISTORY_FILE = exports.HISTORY_DIR = void 0;
22
22
  exports.countTransitions = countTransitions;
23
23
  exports.computeFlakiness = computeFlakiness;
24
24
  exports.historyPath = historyPath;
@@ -30,11 +30,18 @@ exports.scoreHistory = scoreHistory;
30
30
  exports.renderSummary = renderSummary;
31
31
  exports.renderMarkdown = renderMarkdown;
32
32
  exports.writeStepSummary = writeStepSummary;
33
+ exports.renderAnnotations = renderAnnotations;
34
+ exports.isGitHubActions = isGitHubActions;
35
+ exports.writeAnnotations = writeAnnotations;
36
+ exports.dedupe = dedupe;
37
+ exports.isCi = isCi;
38
+ exports.renderNoHistoryHint = renderNoHistoryHint;
33
39
  exports.recordAndReport = recordAndReport;
34
40
  const fs_1 = require("fs");
35
41
  const path_1 = require("path");
36
42
  exports.HISTORY_DIR = ".fixsense";
37
43
  exports.HISTORY_FILE = "history.json";
44
+ exports.SITE_URL = "https://fix-sense.com";
38
45
  /**
39
46
  * How many runs are kept.
40
47
  *
@@ -46,6 +53,10 @@ exports.MAX_RUNS = 50;
46
53
  exports.MIN_RUNS_FOR_VERDICT = 5;
47
54
  /** At or above this score a test is called flaky. */
48
55
  exports.FLAKY_SCORE_THRESHOLD = 70;
56
+ /** Runs the runner itself called flaky before that alone is a verdict. */
57
+ exports.MIN_FLAKY_RUNS = 2;
58
+ /** And how often, so a long-settled test is not judged on old history. */
59
+ exports.MIN_FLAKY_RATE = 0.2;
49
60
  function toPassFail(outcome) {
50
61
  if (outcome === "skipped")
51
62
  return null;
@@ -88,12 +99,19 @@ function computeFlakiness(history) {
88
99
  const selfReportedFlakyRate = selfReportedFlaky / history.length;
89
100
  const transitions = countTransitions(sequence);
90
101
  const transitionRate = runs > 1 ? transitions / (runs - 1) : 0;
102
+ // The score says how bad; this says whether we are sure. A run the runner
103
+ // itself called flaky is direct evidence — the test failed and then passed
104
+ // inside one run, which nothing but flakiness explains. A test that did
105
+ // that in a fifth of its runs is flaky even though the score, weighted to
106
+ // rank the worst offenders first, leaves it below the threshold. Two such
107
+ // runs are required so a single retry on a bad afternoon is not a verdict.
108
+ const isDirectlyEvidencedFlaky = selfReportedFlaky >= exports.MIN_FLAKY_RUNS && selfReportedFlakyRate >= exports.MIN_FLAKY_RATE;
91
109
  const score = Math.round(Math.min(100, selfReportedFlakyRate * 100 * 0.75 + transitionRate * 100 * 0.25));
92
110
  let verdict;
93
111
  if (runs < exports.MIN_RUNS_FOR_VERDICT) {
94
112
  verdict = score > 0 ? "suspect" : "stable";
95
113
  }
96
- else if (score >= exports.FLAKY_SCORE_THRESHOLD) {
114
+ else if (score >= exports.FLAKY_SCORE_THRESHOLD || isDirectlyEvidencedFlaky) {
97
115
  verdict = "flaky";
98
116
  }
99
117
  else if (failureRate >= 0.9 && transitionRate <= 0.1 && selfReportedFlakyRate <= 0.1) {
@@ -179,7 +197,11 @@ function scoreHistory(history) {
179
197
  for (const run of history.runs) {
180
198
  for (const test of run.tests ?? []) {
181
199
  const key = `${test.file ?? ""}\u0000${test.title}`;
182
- const entry = byTest.get(key) ?? { file: test.file ?? null, outcomes: [] };
200
+ const entry = byTest.get(key) ?? { file: test.file ?? null, line: test.line ?? null, outcomes: [] };
201
+ // The newest run wins on location: a test that moved is reported where
202
+ // it now lives, not where it first failed.
203
+ if (test.line != null)
204
+ entry.line = test.line;
183
205
  entry.outcomes.push(test.outcome);
184
206
  byTest.set(key, entry);
185
207
  }
@@ -191,6 +213,7 @@ function scoreHistory(history) {
191
213
  ...report,
192
214
  title: key.split("\u0000")[1] ?? "",
193
215
  file: entry.file,
216
+ line: entry.line,
194
217
  badRuns: entry.outcomes.filter((o) => o === "flaky" || o === "unexpected").length,
195
218
  });
196
219
  }
@@ -203,7 +226,7 @@ function scoreHistory(history) {
203
226
  * so this stays empty unless the history actually found something. "suspect"
204
227
  * is deliberately not reported: it means the tool is not sure yet.
205
228
  */
206
- function renderSummary(verdicts, totalRuns) {
229
+ function renderSummary(verdicts, totalRuns, connected = false) {
207
230
  const flaky = verdicts.filter((v) => v.verdict === "flaky");
208
231
  const broken = verdicts.filter((v) => v.verdict === "broken");
209
232
  if (flaky.length === 0 && broken.length === 0)
@@ -221,10 +244,13 @@ function renderSummary(verdicts, totalRuns) {
221
244
  lines.push(` ${v.title} — failed in ${v.badRuns} of ${v.runs} runs`);
222
245
  }
223
246
  }
247
+ if (!connected) {
248
+ lines.push(`Why they fail, and the fix as a pull request: ${exports.SITE_URL}`);
249
+ }
224
250
  return lines.join("\n");
225
251
  }
226
252
  /** The same summary as GitHub-flavoured Markdown, for the run's own page. */
227
- function renderMarkdown(verdicts, totalRuns) {
253
+ function renderMarkdown(verdicts, totalRuns, connected = false) {
228
254
  const interesting = verdicts.filter((v) => v.verdict === "flaky" || v.verdict === "broken");
229
255
  if (interesting.length === 0)
230
256
  return "";
@@ -238,6 +264,14 @@ function renderMarkdown(verdicts, totalRuns) {
238
264
  "| --- | --- | --- | --- |",
239
265
  rows,
240
266
  "",
267
+ // The run page is read by the whole team, not only whoever installed the
268
+ // package, so this is where the rest of them learn where the table came from.
269
+ ...(connected
270
+ ? []
271
+ : [
272
+ `<sub>Recorded by [FixSense](${exports.SITE_URL}?utm_source=step-summary) — it also says whether the test or the application is at fault, and opens the fix.</sub>`,
273
+ "",
274
+ ]),
241
275
  ].join("\n");
242
276
  }
243
277
  /**
@@ -257,22 +291,127 @@ function writeStepSummary(markdown) {
257
291
  // The step summary is a nicety; losing it is not worth a word.
258
292
  }
259
293
  }
294
+ /** A `::warning`/`::error` workflow command cannot carry a raw newline or `::`. */
295
+ function escapeAnnotation(message) {
296
+ return message.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
297
+ }
298
+ /**
299
+ * GitHub workflow annotations for the tests that earned a verdict.
300
+ *
301
+ * This is the one thing a single-run flaky reporter does that we did not:
302
+ * `::warning file=…,line=…::` makes GitHub draw the finding inline on the exact
303
+ * line in the pull request's "Files changed" tab, not only in the job log.
304
+ * Ours carries what a single run cannot — how many of the last N runs the test
305
+ * misbehaved in — so the annotation is a measurement, not a one-off retry.
306
+ *
307
+ * A `flaky` verdict is a warning: the test is noise to quarantine. A `broken`
308
+ * verdict — fails every run — is an error: a real failure hiding behind a
309
+ * retry, and the louder level is the point. "suspect" is never annotated, for
310
+ * the same reason it is never summarised: the tool is not sure yet.
311
+ */
312
+ function renderAnnotations(verdicts, totalRuns) {
313
+ const lines = [];
314
+ for (const v of verdicts) {
315
+ if (v.verdict !== "flaky" && v.verdict !== "broken")
316
+ continue;
317
+ const level = v.verdict === "broken" ? "error" : "warning";
318
+ const what = v.verdict === "broken"
319
+ ? `fails every run — a real failure, not flakiness`
320
+ : `flaky in ${v.badRuns} of ${v.runs} runs (score ${v.score})`;
321
+ const props = [
322
+ v.file ? `file=${v.file}` : null,
323
+ v.line != null ? `line=${v.line}` : null,
324
+ `title=FixSense: ${escapeAnnotation(v.verdict)} test`,
325
+ ]
326
+ .filter(Boolean)
327
+ .join(",");
328
+ lines.push(`::${level} ${props}::${escapeAnnotation(`${v.title} — ${what}, across the last ${totalRuns} runs`)}`);
329
+ }
330
+ return lines;
331
+ }
332
+ /** True when we are inside a GitHub Actions job, where annotations render. */
333
+ function isGitHubActions(env) {
334
+ return env.GITHUB_ACTIONS === "true";
335
+ }
336
+ /**
337
+ * Emit the annotations as workflow commands on stdout, where the runner reads
338
+ * them. Each must be alone on its line, so this writes them directly rather
339
+ * than folding them into the human summary.
340
+ */
341
+ function writeAnnotations(lines) {
342
+ for (const line of lines)
343
+ console.log(line);
344
+ }
345
+ /**
346
+ * One entry per test per run, whatever the runner reported.
347
+ *
348
+ * A run that listed the same test twice would count as two runs for it, and
349
+ * a retried test would read as several times as flaky as it is. The last
350
+ * entry wins: for a runner that reports each attempt, it is the one that
351
+ * knows how the test finally ended.
352
+ */
353
+ function dedupe(tests) {
354
+ const byKey = new Map();
355
+ for (const t of tests) {
356
+ byKey.set(`${t.file ?? ""}\u0000${t.title}`, {
357
+ title: t.title,
358
+ file: t.file,
359
+ line: t.line ?? null,
360
+ outcome: t.outcome,
361
+ });
362
+ }
363
+ return [...byKey.values()];
364
+ }
365
+ /** Every supported CI sets CI=true, except Azure Pipelines, which sets TF_BUILD. */
366
+ function isCi(env) {
367
+ return Boolean(env.CI && env.CI !== "false") || env.TF_BUILD === "True";
368
+ }
369
+ /**
370
+ * Said when a CI job finds no earlier runs to compare with.
371
+ *
372
+ * A CI job starts from a clean checkout, so without a cache the history holds
373
+ * one run every time, never reaches a verdict, and the package stays silent
374
+ * forever — which reads as "it does nothing". One line saying why is the
375
+ * difference between a team fixing that and a team uninstalling.
376
+ */
377
+ function renderNoHistoryHint(docsUrl) {
378
+ return [
379
+ "FixSense: recorded this run, but found no earlier runs to compare it with.",
380
+ " CI starts every job clean, so the history is lost between runs.",
381
+ ` Cache .fixsense/ between runs, or set FIXSENSE_API_KEY: ${docsUrl}`,
382
+ ].join("\n");
383
+ }
260
384
  /**
261
385
  * Record this run and report what the history now says.
262
386
  *
263
387
  * Returns the text that was printed, which is what the tests assert on.
264
388
  */
265
- function recordAndReport(cwd, tests, now = new Date()) {
389
+ function recordAndReport(cwd, tests, now = new Date(), context = {}) {
266
390
  const history = appendRun(readHistory(cwd), {
267
391
  at: now.toISOString(),
268
- tests: tests.map((t) => ({ title: t.title, file: t.file, outcome: t.outcome })),
392
+ tests: dedupe(tests),
269
393
  });
270
394
  writeHistory(cwd, history);
395
+ const connected = context.connected === true;
396
+ if (history.runs.length === 1 &&
397
+ !connected &&
398
+ context.docsUrl &&
399
+ isCi(context.env ?? process.env)) {
400
+ const hint = renderNoHistoryHint(context.docsUrl);
401
+ console.log(`\n${hint}\n`);
402
+ return hint;
403
+ }
271
404
  const verdicts = scoreHistory(history);
272
- const summary = renderSummary(verdicts, history.runs.length);
405
+ const summary = renderSummary(verdicts, history.runs.length, connected);
273
406
  if (summary) {
274
407
  console.log(`\n${summary}\n`);
275
- writeStepSummary(renderMarkdown(verdicts, history.runs.length));
408
+ writeStepSummary(renderMarkdown(verdicts, history.runs.length, connected));
409
+ // On GitHub, also pin each verdict to its line in the PR. The summary and
410
+ // the annotations carry the same findings to two different places a
411
+ // reviewer looks: the run page, and the diff.
412
+ if (isGitHubActions(context.env ?? process.env)) {
413
+ writeAnnotations(renderAnnotations(verdicts, history.runs.length));
414
+ }
276
415
  }
277
416
  return summary;
278
417
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fixsense/cypress-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.2.4",
4
4
  "description": "Cypress plugin that records test outcomes across runs so flaky tests can be identified over time.",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,14 @@
13
13
  "keywords": [
14
14
  "cypress",
15
15
  "cypress-plugin",
16
+ "flaky",
17
+ "flaky-test",
16
18
  "flaky-tests",
19
+ "flaky-test-detection",
20
+ "test-history",
21
+ "retry",
22
+ "quarantine",
23
+ "e2e",
17
24
  "ci",
18
25
  "test-reporting"
19
26
  ],
@@ -22,7 +29,7 @@
22
29
  "url": "https://github.com/CopilotMe/fix-sense.git",
23
30
  "directory": "packages/cypress-plugin"
24
31
  },
25
- "homepage": "https://fix-sense.com",
32
+ "homepage": "https://fix-sense.com/docs/integrations/cypress-plugin",
26
33
  "scripts": {
27
34
  "build": "tsc -p tsconfig.json",
28
35
  "prepublishOnly": "npm run build"