canary-test-cli 6.7.0 → 6.7.1

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.
@@ -73,7 +73,24 @@ export declare function runStatus(totals: {
73
73
  failed: number;
74
74
  flaky: number;
75
75
  }, fullResultStatus?: string): "passed" | "failed" | "flaky" | "cancelled";
76
- export declare function buildPayload(results: ResultEntry[], cfg: ResolvedConfig, timing: {
76
+ /**
77
+ * Collapse results that share a `full_title` into one row.
78
+ *
79
+ * TestTracker's ingest holds a unique index on `(run_id, full_title)` and
80
+ * rejects the WHOLE run on a collision, so a duplicate title is not a cosmetic
81
+ * problem — it takes the suite dark. The reporter keys its in-flight map by
82
+ * `test.id`, which is genuinely unique, but a title is not: a Playwright
83
+ * `dependencies:` setup project runs in full in EVERY shard (dependencies are
84
+ * not sharded), so a `merge-reports` payload over a sharded matrix legitimately
85
+ * carries the same setup title once per shard. Capwell's `capwell-web` suite
86
+ * had every nightly rejected this way and never ingested a single run.
87
+ *
88
+ * Collapse rule keeps the merged row honest: worst status wins, the first real
89
+ * error is preserved (so the SDET still sees why it failed), and duration and
90
+ * retries take the max rather than the last-seen value.
91
+ */
92
+ export declare function dedupeByFullTitle(results: ResultEntry[]): ResultEntry[];
93
+ export declare function buildPayload(rawResults: ResultEntry[], cfg: ResolvedConfig, timing: {
77
94
  startedAt: string;
78
95
  finishedAt: string;
79
96
  }, env?: NodeJS.ProcessEnv, fullResultStatus?: string): IngestPayload;
@@ -8,6 +8,7 @@ exports.resolveTestStatus = resolveTestStatus;
8
8
  exports.resolveConfig = resolveConfig;
9
9
  exports.shouldPush = shouldPush;
10
10
  exports.runStatus = runStatus;
11
+ exports.dedupeByFullTitle = dedupeByFullTitle;
11
12
  exports.buildPayload = buildPayload;
12
13
  const node_crypto_1 = __importDefault(require("node:crypto"));
13
14
  // Optional .env load — MUST NOT crash the suite if dotenv is absent.
@@ -92,7 +93,64 @@ function runStatus(totals, fullResultStatus) {
92
93
  return "flaky";
93
94
  return "passed";
94
95
  }
95
- function buildPayload(results, cfg, timing, env = process.env, fullResultStatus) {
96
+ /** Worst-first, so a collapsed row can never look healthier than its parts. */
97
+ const STATUS_SEVERITY = {
98
+ failed: 3,
99
+ flaky: 2,
100
+ passed: 1,
101
+ skipped: 0,
102
+ };
103
+ /**
104
+ * Collapse results that share a `full_title` into one row.
105
+ *
106
+ * TestTracker's ingest holds a unique index on `(run_id, full_title)` and
107
+ * rejects the WHOLE run on a collision, so a duplicate title is not a cosmetic
108
+ * problem — it takes the suite dark. The reporter keys its in-flight map by
109
+ * `test.id`, which is genuinely unique, but a title is not: a Playwright
110
+ * `dependencies:` setup project runs in full in EVERY shard (dependencies are
111
+ * not sharded), so a `merge-reports` payload over a sharded matrix legitimately
112
+ * carries the same setup title once per shard. Capwell's `capwell-web` suite
113
+ * had every nightly rejected this way and never ingested a single run.
114
+ *
115
+ * Collapse rule keeps the merged row honest: worst status wins, the first real
116
+ * error is preserved (so the SDET still sees why it failed), and duration and
117
+ * retries take the max rather than the last-seen value.
118
+ */
119
+ function dedupeByFullTitle(results) {
120
+ const merged = new Map();
121
+ for (const r of results) {
122
+ const prior = merged.get(r.full_title);
123
+ merged.set(r.full_title, prior ? mergeEntries(prior, r) : { ...r, tags: [...r.tags] });
124
+ }
125
+ return [...merged.values()];
126
+ }
127
+ function worstOf(a, b) {
128
+ return (STATUS_SEVERITY[b] ?? 0) > (STATUS_SEVERITY[a] ?? 0) ? b : a;
129
+ }
130
+ /** `Math.max` treats a missing duration as 0; absent-on-both must stay absent. */
131
+ function longerOf(a, b) {
132
+ if (a === undefined)
133
+ return b;
134
+ if (b === undefined)
135
+ return a;
136
+ return Math.max(a, b);
137
+ }
138
+ function mergeEntries(prior, next) {
139
+ return {
140
+ ...prior,
141
+ status: worstOf(prior.status, next.status),
142
+ error_message: prior.error_message ?? next.error_message,
143
+ error_stack: prior.error_stack ?? next.error_stack,
144
+ duration_ms: longerOf(prior.duration_ms, next.duration_ms),
145
+ retries: Math.max(prior.retries, next.retries),
146
+ tags: [...new Set([...prior.tags, ...next.tags])],
147
+ };
148
+ }
149
+ function buildPayload(rawResults, cfg, timing, env = process.env, fullResultStatus) {
150
+ // Deduped before the totals are counted, so `totals` always describes the
151
+ // rows actually sent — a payload whose totals disagree with `results.length`
152
+ // would misreport the run on the dashboard.
153
+ const results = dedupeByFullTitle(rawResults);
96
154
  const count = (s) => results.filter((r) => r.status === s).length;
97
155
  const passed = count("passed");
98
156
  const failed = count("failed");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canary-test-cli",
3
- "version": "6.7.0",
3
+ "version": "6.7.1",
4
4
  "description": "Canary — AI-powered test automation agent",
5
5
  "license": "MIT",
6
6
  "repository": {