@bilalimamoglu/sift 0.3.1 → 0.3.2

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 (4) hide show
  1. package/README.md +124 -337
  2. package/dist/cli.js +998 -27
  3. package/dist/index.js +998 -27
  4. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -166,6 +166,209 @@ var testStatusPublicDiagnoseContractSchema = testStatusDiagnoseContractSchema.om
166
166
  function parseTestStatusProviderSupplement(input) {
167
167
  return testStatusProviderSupplementSchema.parse(JSON.parse(input));
168
168
  }
169
+ var extendedBucketSpecs = [
170
+ {
171
+ prefix: "snapshot mismatch:",
172
+ type: "snapshot_mismatch",
173
+ label: "snapshot mismatch",
174
+ genericTitle: "Snapshot mismatches",
175
+ defaultCoverage: "failed",
176
+ rootCauseConfidence: 0.84,
177
+ why: "it contains the failing snapshot expectation behind this bucket",
178
+ fix: "Update the snapshots if these output changes are intentional, then rerun the suite."
179
+ },
180
+ {
181
+ prefix: "timeout:",
182
+ type: "timeout_failure",
183
+ label: "timeout",
184
+ genericTitle: "Timeout failures",
185
+ defaultCoverage: "mixed",
186
+ rootCauseConfidence: 0.9,
187
+ why: "it contains the test or fixture that exceeded the timeout threshold",
188
+ fix: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning."
189
+ },
190
+ {
191
+ prefix: "permission:",
192
+ type: "permission_denied_failure",
193
+ label: "permission denied",
194
+ genericTitle: "Permission failures",
195
+ defaultCoverage: "error",
196
+ rootCauseConfidence: 0.85,
197
+ why: "it contains the file, socket, or port access that was denied",
198
+ fix: "Check file or port permissions in the CI environment before rerunning."
199
+ },
200
+ {
201
+ prefix: "async loop:",
202
+ type: "async_event_loop_failure",
203
+ label: "async event loop",
204
+ genericTitle: "Async event loop failures",
205
+ defaultCoverage: "mixed",
206
+ rootCauseConfidence: 0.88,
207
+ why: "it contains the async setup or coroutine that caused the event loop error",
208
+ fix: "Check event loop scope and pytest-asyncio configuration before rerunning."
209
+ },
210
+ {
211
+ prefix: "fixture teardown:",
212
+ type: "fixture_teardown_failure",
213
+ label: "fixture teardown",
214
+ genericTitle: "Fixture teardown failures",
215
+ defaultCoverage: "error",
216
+ rootCauseConfidence: 0.85,
217
+ why: "it contains the fixture teardown path that failed after the test body completed",
218
+ fix: "Inspect the teardown cleanup path and restore idempotent fixture cleanup before rerunning."
219
+ },
220
+ {
221
+ prefix: "db migration:",
222
+ type: "db_migration_failure",
223
+ label: "db migration",
224
+ genericTitle: "DB migration failures",
225
+ defaultCoverage: "error",
226
+ rootCauseConfidence: 0.9,
227
+ why: "it contains the migration or model definition behind the missing table or relation",
228
+ fix: "Run pending migrations or fix the expected model schema before rerunning."
229
+ },
230
+ {
231
+ prefix: "configuration:",
232
+ type: "configuration_error",
233
+ label: "configuration error",
234
+ genericTitle: "Configuration errors",
235
+ defaultCoverage: "error",
236
+ rootCauseConfidence: 0.95,
237
+ dominantPriority: 4,
238
+ dominantBlocker: true,
239
+ why: "it contains the pytest configuration or conftest setup error that blocks the run",
240
+ fix: "Fix the pytest configuration, CLI usage, or conftest import error before rerunning."
241
+ },
242
+ {
243
+ prefix: "xdist worker crash:",
244
+ type: "xdist_worker_crash",
245
+ label: "xdist worker crash",
246
+ genericTitle: "xdist worker crashes",
247
+ defaultCoverage: "error",
248
+ rootCauseConfidence: 0.92,
249
+ dominantPriority: 3,
250
+ why: "it contains the worker startup or shared-state path that crashed an xdist worker",
251
+ fix: "Check shared state, worker startup hooks, or resource contention between workers before rerunning."
252
+ },
253
+ {
254
+ prefix: "type error:",
255
+ type: "type_error_failure",
256
+ label: "type error",
257
+ genericTitle: "Type errors",
258
+ defaultCoverage: "mixed",
259
+ rootCauseConfidence: 0.8,
260
+ why: "it contains the call site or fixture value that triggered the type error",
261
+ fix: "Inspect the mismatched argument or object shape and rerun the full suite at standard."
262
+ },
263
+ {
264
+ prefix: "resource leak:",
265
+ type: "resource_leak_warning",
266
+ label: "resource leak",
267
+ genericTitle: "Resource leak warnings",
268
+ defaultCoverage: "mixed",
269
+ rootCauseConfidence: 0.74,
270
+ why: "it contains the warning source behind the leaked file, socket, or coroutine",
271
+ fix: "Close the leaked resource or suppress the warning only if the cleanup is intentional."
272
+ },
273
+ {
274
+ prefix: "django db access:",
275
+ type: "django_db_access_denied",
276
+ label: "django db access",
277
+ genericTitle: "Django DB access failures",
278
+ defaultCoverage: "error",
279
+ rootCauseConfidence: 0.95,
280
+ why: "it needs the @pytest.mark.django_db decorator or fixture permission to access the database",
281
+ fix: "Add @pytest.mark.django_db to the test or class before rerunning."
282
+ },
283
+ {
284
+ prefix: "network:",
285
+ type: "network_failure",
286
+ label: "network failure",
287
+ genericTitle: "Network failures",
288
+ defaultCoverage: "error",
289
+ rootCauseConfidence: 0.88,
290
+ dominantPriority: 2,
291
+ why: "it contains the host, URL, or TLS path behind the network failure",
292
+ fix: "Check DNS, outbound network access, retries, or TLS trust before rerunning."
293
+ },
294
+ {
295
+ prefix: "segfault:",
296
+ type: "subprocess_crash_segfault",
297
+ label: "segfault",
298
+ genericTitle: "Segfault crashes",
299
+ defaultCoverage: "mixed",
300
+ rootCauseConfidence: 0.8,
301
+ why: "it contains the subprocess or native extension path that crashed with SIGSEGV",
302
+ fix: "Inspect the native extension, subprocess boundary, or incompatible binary before rerunning."
303
+ },
304
+ {
305
+ prefix: "flaky:",
306
+ type: "flaky_test_detected",
307
+ label: "flaky test",
308
+ genericTitle: "Flaky test detections",
309
+ defaultCoverage: "mixed",
310
+ rootCauseConfidence: 0.72,
311
+ why: "it contains the rerun-prone test that behaved inconsistently across attempts",
312
+ fix: "Stabilize the nondeterministic test or fixture before relying on reruns."
313
+ },
314
+ {
315
+ prefix: "serialization:",
316
+ type: "serialization_encoding_failure",
317
+ label: "serialization or encoding",
318
+ genericTitle: "Serialization or encoding failures",
319
+ defaultCoverage: "mixed",
320
+ rootCauseConfidence: 0.78,
321
+ why: "it contains the serialization or decoding path behind the malformed payload",
322
+ fix: "Inspect the encoded payload, serializer, or fixture data before rerunning."
323
+ },
324
+ {
325
+ prefix: "file not found:",
326
+ type: "file_not_found_failure",
327
+ label: "file not found",
328
+ genericTitle: "Missing file failures",
329
+ defaultCoverage: "mixed",
330
+ rootCauseConfidence: 0.82,
331
+ why: "it contains the missing file path or fixture artifact required by the test",
332
+ fix: "Restore the missing file, fixture artifact, or working-directory assumption before rerunning."
333
+ },
334
+ {
335
+ prefix: "memory:",
336
+ type: "memory_error",
337
+ label: "memory error",
338
+ genericTitle: "Memory failures",
339
+ defaultCoverage: "mixed",
340
+ rootCauseConfidence: 0.78,
341
+ why: "it contains the allocation path that exhausted available memory",
342
+ fix: "Reduce memory pressure or investigate the large allocation before rerunning."
343
+ },
344
+ {
345
+ prefix: "deprecation as error:",
346
+ type: "deprecation_warning_as_error",
347
+ label: "deprecation as error",
348
+ genericTitle: "Deprecation warnings as errors",
349
+ defaultCoverage: "mixed",
350
+ rootCauseConfidence: 0.74,
351
+ why: "it contains the deprecated API or warning filter that is failing the test run",
352
+ fix: "Update the deprecated call site or relax the warning policy only if that is intentional."
353
+ },
354
+ {
355
+ prefix: "xfail strict:",
356
+ type: "xfail_strict_unexpected_pass",
357
+ label: "strict xfail unexpected pass",
358
+ genericTitle: "Strict xfail unexpected passes",
359
+ defaultCoverage: "failed",
360
+ rootCauseConfidence: 0.78,
361
+ why: "it contains the strict xfail case that unexpectedly passed",
362
+ fix: "Remove or update the strict xfail expectation if the test is now passing intentionally."
363
+ }
364
+ ];
365
+ function findExtendedBucketSpec(reason) {
366
+ return extendedBucketSpecs.find((spec) => reason.startsWith(spec.prefix)) ?? null;
367
+ }
368
+ function extractReasonDetail(reason, prefix) {
369
+ const detail = reason.slice(prefix.length).trim();
370
+ return detail.length > 0 ? detail : null;
371
+ }
169
372
  function formatCount(count, singular, plural = `${singular}s`) {
170
373
  return `${count} ${count === 1 ? singular : plural}`;
171
374
  }
@@ -219,6 +422,10 @@ function formatTargetSummary(summary) {
219
422
  return `count=${summary.count}; families=${families}`;
220
423
  }
221
424
  function classifyGenericBucketType(reason) {
425
+ const extended = findExtendedBucketSpec(reason);
426
+ if (extended) {
427
+ return extended.type;
428
+ }
222
429
  if (reason.startsWith("missing test env:")) {
223
430
  return "shared_environment_blocker";
224
431
  }
@@ -263,6 +470,10 @@ function classifyVisibleStatusForLabel(args) {
263
470
  return "unknown";
264
471
  }
265
472
  function inferCoverageFromReason(reason) {
473
+ const extended = findExtendedBucketSpec(reason);
474
+ if (extended) {
475
+ return extended.defaultCoverage;
476
+ }
266
477
  if (reason.startsWith("missing test env:") || reason.startsWith("fixture guard:") || reason.startsWith("service unavailable:") || reason.startsWith("db refused:") || reason.startsWith("auth bypass absent:") || reason.startsWith("missing module:")) {
267
478
  return "error";
268
479
  }
@@ -323,7 +534,13 @@ function buildGenericBuckets(analysis) {
323
534
  summaryLines: [],
324
535
  reason,
325
536
  count: 1,
326
- confidence: reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62,
537
+ confidence: (() => {
538
+ const extended = findExtendedBucketSpec(reason);
539
+ if (extended) {
540
+ return Math.max(0.72, Math.min(extended.rootCauseConfidence, 0.82));
541
+ }
542
+ return reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62;
543
+ })(),
327
544
  representativeItems: [item],
328
545
  entities: [],
329
546
  hint: void 0,
@@ -340,7 +557,7 @@ function buildGenericBuckets(analysis) {
340
557
  push(item.reason, item);
341
558
  }
342
559
  for (const bucket of grouped.values()) {
343
- const title = bucket.type === "assertion_failure" ? "Assertion failures" : bucket.type === "import_dependency_failure" ? "Import/dependency failures" : bucket.type === "collection_failure" ? "Collection or fixture failures" : "Runtime failures";
560
+ const title = findExtendedBucketSpec(bucket.reason)?.genericTitle ?? (bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch" ? "Assertion failures" : bucket.type === "import_dependency_failure" ? "Import/dependency failures" : bucket.type === "collection_failure" ? "Collection or fixture failures" : "Runtime failures");
344
561
  bucket.headline = `${title}: ${formatCount(bucket.count, "visible failure")} share ${bucket.reason}.`;
345
562
  bucket.summaryLines = [bucket.headline];
346
563
  bucket.overflowCount = Math.max(bucket.count - bucket.representativeItems.length, 0);
@@ -413,13 +630,13 @@ function inferFailureBucketCoverage(bucket, analysis) {
413
630
  }
414
631
  }
415
632
  const claimed = bucket.countClaimed ?? bucket.countVisible;
416
- if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure") {
633
+ if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch") {
417
634
  return {
418
635
  error,
419
636
  failed: Math.max(failed, claimed)
420
637
  };
421
638
  }
422
- if (bucket.type === "shared_environment_blocker" || bucket.type === "import_dependency_failure" || bucket.type === "collection_failure" || bucket.type === "fixture_guard_failure" || bucket.type === "service_unavailable" || bucket.type === "db_connection_failure" || bucket.type === "auth_bypass_absent") {
639
+ if (bucket.type === "shared_environment_blocker" || bucket.type === "import_dependency_failure" || bucket.type === "collection_failure" || bucket.type === "fixture_guard_failure" || bucket.type === "permission_denied_failure" || bucket.type === "fixture_teardown_failure" || bucket.type === "db_migration_failure" || bucket.type === "configuration_error" || bucket.type === "xdist_worker_crash" || bucket.type === "django_db_access_denied" || bucket.type === "network_failure" || bucket.type === "service_unavailable" || bucket.type === "db_connection_failure" || bucket.type === "auth_bypass_absent") {
423
640
  return {
424
641
  error: Math.max(error, claimed),
425
642
  failed
@@ -494,6 +711,10 @@ function dominantBucketPriority(bucket) {
494
711
  if (bucket.reason.startsWith("missing test env:")) {
495
712
  return 5;
496
713
  }
714
+ const extended = findExtendedBucketSpec(bucket.reason);
715
+ if (extended?.dominantPriority !== void 0) {
716
+ return extended.dominantPriority;
717
+ }
497
718
  if (bucket.type === "shared_environment_blocker") {
498
719
  return 4;
499
720
  }
@@ -527,12 +748,16 @@ function prioritizeBuckets(buckets) {
527
748
  });
528
749
  }
529
750
  function isDominantBlockerType(type) {
530
- return type === "shared_environment_blocker" || type === "import_dependency_failure" || type === "collection_failure";
751
+ return type === "shared_environment_blocker" || type === "configuration_error" || type === "import_dependency_failure" || type === "collection_failure";
531
752
  }
532
753
  function labelForBucket(bucket) {
533
754
  if (bucket.labelOverride) {
534
755
  return bucket.labelOverride;
535
756
  }
757
+ const extended = findExtendedBucketSpec(bucket.reason);
758
+ if (extended) {
759
+ return extended.label;
760
+ }
536
761
  if (bucket.reason.startsWith("missing test env:")) {
537
762
  return "missing test env";
538
763
  }
@@ -566,6 +791,9 @@ function labelForBucket(bucket) {
566
791
  if (bucket.type === "assertion_failure") {
567
792
  return "assertion failure";
568
793
  }
794
+ if (bucket.type === "snapshot_mismatch") {
795
+ return "snapshot mismatch";
796
+ }
569
797
  if (bucket.type === "collection_failure") {
570
798
  return "collection failure";
571
799
  }
@@ -584,6 +812,10 @@ function rootCauseConfidenceFor(bucket) {
584
812
  if (isUnknownBucket(bucket)) {
585
813
  return 0.52;
586
814
  }
815
+ const extended = findExtendedBucketSpec(bucket.reason);
816
+ if (extended) {
817
+ return extended.rootCauseConfidence;
818
+ }
587
819
  if (bucket.reason.startsWith("missing test env:") || bucket.reason.startsWith("missing module:") || bucket.reason.startsWith("db refused:") || bucket.reason.startsWith("service unavailable:") || bucket.reason.startsWith("auth bypass absent:")) {
588
820
  return 0.95;
589
821
  }
@@ -624,6 +856,10 @@ function buildReadTargetWhy(args) {
624
856
  if (envVar) {
625
857
  return `it contains the ${envVar} setup guard`;
626
858
  }
859
+ const extended = findExtendedBucketSpec(args.bucket.reason);
860
+ if (extended) {
861
+ return extended.why;
862
+ }
627
863
  if (args.bucket.reason.startsWith("fixture guard:")) {
628
864
  return "it contains the fixture/setup guard behind this bucket";
629
865
  }
@@ -654,6 +890,9 @@ function buildReadTargetWhy(args) {
654
890
  }
655
891
  return "it maps to the visible stale snapshot expectation";
656
892
  }
893
+ if (args.bucket.type === "snapshot_mismatch") {
894
+ return "it maps to the visible snapshot mismatch bucket";
895
+ }
657
896
  if (args.bucket.type === "import_dependency_failure") {
658
897
  return "it is the first visible failing module in this missing dependency bucket";
659
898
  }
@@ -665,11 +904,54 @@ function buildReadTargetWhy(args) {
665
904
  }
666
905
  return `it maps to the visible ${args.bucketLabel} bucket`;
667
906
  }
907
+ function buildExtendedBucketSearchHint(bucket, anchor) {
908
+ const extended = findExtendedBucketSpec(bucket.reason);
909
+ if (!extended) {
910
+ return null;
911
+ }
912
+ const detail = extractReasonDetail(bucket.reason, extended.prefix);
913
+ if (!detail) {
914
+ return anchor.label.split("::")[1]?.trim() ?? anchor.label ?? null;
915
+ }
916
+ if (extended.type === "timeout_failure") {
917
+ const duration = detail.match(/>\s*([0-9]+(?:\.[0-9]+)?s?)/i)?.[1];
918
+ return duration ?? anchor.label.split("::")[1]?.trim() ?? detail;
919
+ }
920
+ if (extended.type === "db_migration_failure") {
921
+ const relation = detail.match(/\b(?:relation|table)\s+([A-Za-z0-9_.-]+)/i)?.[1];
922
+ return relation ?? detail;
923
+ }
924
+ if (extended.type === "network_failure") {
925
+ const url = detail.match(/\bhttps?:\/\/[^\s)'"`]+/i)?.[0];
926
+ const host = detail.match(/\b(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}\b/)?.[0];
927
+ return url ?? host ?? detail;
928
+ }
929
+ if (extended.type === "xdist_worker_crash") {
930
+ return detail.match(/\bgw\d+\b/)?.[0] ?? detail;
931
+ }
932
+ if (extended.type === "fixture_teardown_failure") {
933
+ return detail.replace(/^of\s+/i, "") || anchor.label;
934
+ }
935
+ if (extended.type === "file_not_found_failure") {
936
+ const path4 = detail.match(/['"]([^'"]+)['"]/)?.[1];
937
+ return path4 ?? detail;
938
+ }
939
+ if (extended.type === "permission_denied_failure") {
940
+ const path4 = detail.match(/['"]([^'"]+)['"]/)?.[1];
941
+ const port = detail.match(/\bport\s+(\d+)\b/i)?.[1];
942
+ return path4 ?? (port ? `port ${port}` : detail);
943
+ }
944
+ return detail;
945
+ }
668
946
  function buildReadTargetSearchHint(bucket, anchor) {
669
947
  const envVar = bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
670
948
  if (envVar) {
671
949
  return envVar;
672
950
  }
951
+ const extendedHint = buildExtendedBucketSearchHint(bucket, anchor);
952
+ if (extendedHint) {
953
+ return extendedHint;
954
+ }
673
955
  if (bucket.type === "contract_snapshot_drift") {
674
956
  return bucket.entities.find((value) => value.startsWith("/api/")) ?? bucket.entities[0] ?? null;
675
957
  }
@@ -784,6 +1066,10 @@ function extractMiniDiff(input, bucket) {
784
1066
  };
785
1067
  }
786
1068
  function inferSupplementCoverageKind(args) {
1069
+ const extended = findExtendedBucketSpec(args.rootCause);
1070
+ if (extended?.defaultCoverage === "error" || extended?.defaultCoverage === "failed") {
1071
+ return extended.defaultCoverage;
1072
+ }
787
1073
  const normalized = `${args.label} ${args.rootCause}`.toLowerCase();
788
1074
  if (/env|setup|fixture|import|dependency|service|db|database|auth bypass|collection|connection refused/.test(
789
1075
  normalized
@@ -1027,6 +1313,10 @@ function buildStandardFixText(args) {
1027
1313
  if (args.bucket.hint) {
1028
1314
  return args.bucket.hint;
1029
1315
  }
1316
+ const extended = findExtendedBucketSpec(args.bucket.reason);
1317
+ if (extended) {
1318
+ return extended.fix;
1319
+ }
1030
1320
  const envVar = args.bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
1031
1321
  if (envVar) {
1032
1322
  return `Set ${envVar} before rerunning the affected tests.`;
@@ -1056,6 +1346,9 @@ function buildStandardFixText(args) {
1056
1346
  if (args.bucket.type === "contract_snapshot_drift") {
1057
1347
  return "Review the visible drift and regenerate the contract snapshots if the changes are intentional.";
1058
1348
  }
1349
+ if (args.bucket.type === "snapshot_mismatch") {
1350
+ return "Update the snapshots if these output changes are intentional, then rerun the full suite at standard.";
1351
+ }
1059
1352
  if (args.bucket.type === "assertion_failure") {
1060
1353
  return "Inspect the failing assertion and rerun the full suite at standard.";
1061
1354
  }
@@ -1381,6 +1674,63 @@ function getCount(input, label) {
1381
1674
  const lastMatch = matches.at(-1);
1382
1675
  return lastMatch ? Number(lastMatch[1]) : 0;
1383
1676
  }
1677
+ function detectTestRunner(input) {
1678
+ if (/^\s*Test Files?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /^\s*Tests?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /^\s*Snapshots?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /⎯{2,}\s+Failed Tests?\s+\d+\s+⎯{2,}/.test(input)) {
1679
+ return "vitest";
1680
+ }
1681
+ if (/^\s*Test Suites:\s+\d+\s+failed,\s+\d+\s+passed(?:,\s+\d+\s+total)?/m.test(input) || /^\s*Tests:\s+\d+\s+failed,\s+\d+\s+passed(?:,\s+\d+\s+total)?/m.test(input)) {
1682
+ return "jest";
1683
+ }
1684
+ if (/\bpytest\b/i.test(input) || /^\s*=+.*\b\d+\s+failed\b.*=+\s*$/m.test(input) || /\bcollected\s+\d+\s+items\b/i.test(input)) {
1685
+ return "pytest";
1686
+ }
1687
+ return "unknown";
1688
+ }
1689
+ function extractVitestLineCount(input, label, metric) {
1690
+ const matcher = new RegExp(`^\\s*${label}\\s+(.+)$`, "gmi");
1691
+ const lines = [...input.matchAll(matcher)];
1692
+ const line = lines.at(-1)?.[1];
1693
+ if (!line) {
1694
+ return null;
1695
+ }
1696
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
1697
+ return metricMatch ? Number(metricMatch[1]) : null;
1698
+ }
1699
+ function extractJestLineCount(input, label, metric) {
1700
+ const matcher = new RegExp(`^\\s*${label}:\\s+(.+)$`, "gmi");
1701
+ const lines = [...input.matchAll(matcher)];
1702
+ const line = lines.at(-1)?.[1];
1703
+ if (!line) {
1704
+ return null;
1705
+ }
1706
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
1707
+ return metricMatch ? Number(metricMatch[1]) : null;
1708
+ }
1709
+ function extractTestStatusCounts(input, runner) {
1710
+ if (runner === "vitest") {
1711
+ return {
1712
+ passed: extractVitestLineCount(input, "Tests?", "passed") ?? getCount(input, "passed"),
1713
+ failed: extractVitestLineCount(input, "Tests?", "failed") ?? getCount(input, "failed"),
1714
+ errors: extractVitestLineCount(input, "Errors?", "error") ?? extractVitestLineCount(input, "Errors?", "errors") ?? Math.max(getCount(input, "errors"), getCount(input, "error")),
1715
+ skipped: extractVitestLineCount(input, "Tests?", "skipped") ?? getCount(input, "skipped"),
1716
+ snapshotFailures: extractVitestLineCount(input, "Snapshots?", "failed") ?? void 0
1717
+ };
1718
+ }
1719
+ if (runner === "jest") {
1720
+ return {
1721
+ passed: extractJestLineCount(input, "Tests", "passed") ?? getCount(input, "passed"),
1722
+ failed: extractJestLineCount(input, "Tests", "failed") ?? getCount(input, "failed"),
1723
+ errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
1724
+ skipped: extractJestLineCount(input, "Tests", "skipped") ?? getCount(input, "skipped")
1725
+ };
1726
+ }
1727
+ return {
1728
+ passed: getCount(input, "passed"),
1729
+ failed: getCount(input, "failed"),
1730
+ errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
1731
+ skipped: getCount(input, "skipped")
1732
+ };
1733
+ }
1384
1734
  function formatCount2(count, singular, plural = `${singular}s`) {
1385
1735
  return `${count} ${count === 1 ? singular : plural}`;
1386
1736
  }
@@ -1413,7 +1763,8 @@ function normalizeAnchorFile(value) {
1413
1763
  return value.replace(/\\/g, "/").trim();
1414
1764
  }
1415
1765
  function inferFileFromLabel(label) {
1416
- const candidate = cleanFailureLabel(label).split("::")[0]?.trim();
1766
+ const cleaned = cleanFailureLabel(label);
1767
+ const candidate = cleaned.split("::")[0]?.split(" > ")[0]?.trim();
1417
1768
  if (!candidate) {
1418
1769
  return null;
1419
1770
  }
@@ -1468,6 +1819,15 @@ function parseObservedAnchor(line) {
1468
1819
  anchor_confidence: 0.92
1469
1820
  };
1470
1821
  }
1822
+ const vitestTraceback = normalized.match(/^\s*❯\s+([^:\s][^:]*\.[A-Za-z0-9]+):(\d+)(?::\d+)?/);
1823
+ if (vitestTraceback) {
1824
+ return {
1825
+ file: normalizeAnchorFile(vitestTraceback[1]),
1826
+ line: Number(vitestTraceback[2]),
1827
+ anchor_kind: "traceback",
1828
+ anchor_confidence: 1
1829
+ };
1830
+ }
1471
1831
  return null;
1472
1832
  }
1473
1833
  function resolveAnchorForLabel(args) {
@@ -1484,15 +1844,27 @@ function isLowValueInternalReason(normalized) {
1484
1844
  ) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
1485
1845
  }
1486
1846
  function scoreFailureReason(reason) {
1847
+ if (reason.startsWith("configuration:")) {
1848
+ return 6;
1849
+ }
1487
1850
  if (reason.startsWith("missing test env:")) {
1488
1851
  return 6;
1489
1852
  }
1490
1853
  if (reason.startsWith("missing module:")) {
1491
1854
  return 5;
1492
1855
  }
1856
+ if (reason.startsWith("snapshot mismatch:")) {
1857
+ return 4;
1858
+ }
1493
1859
  if (reason.startsWith("assertion failed:")) {
1494
1860
  return 4;
1495
1861
  }
1862
+ if (reason.startsWith("timeout:") || reason.startsWith("async loop:") || reason.startsWith("django db access:") || reason.startsWith("db migration:")) {
1863
+ return 3;
1864
+ }
1865
+ if (reason.startsWith("permission:") || reason.startsWith("xdist worker crash:") || reason.startsWith("network:") || reason.startsWith("segfault:") || reason.startsWith("memory:") || reason.startsWith("type error:") || reason.startsWith("serialization:") || reason.startsWith("file not found:") || reason.startsWith("deprecation as error:") || reason.startsWith("xfail strict:") || reason.startsWith("resource leak:") || reason.startsWith("flaky:") || reason.startsWith("fixture teardown:")) {
1866
+ return 2;
1867
+ }
1496
1868
  if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
1497
1869
  return 3;
1498
1870
  }
@@ -1501,6 +1873,16 @@ function scoreFailureReason(reason) {
1501
1873
  }
1502
1874
  return 1;
1503
1875
  }
1876
+ function buildClassifiedReason(prefix, detail) {
1877
+ return `${prefix}: ${detail}`.slice(0, 120);
1878
+ }
1879
+ function buildExcerptDetail(value, fallback) {
1880
+ const trimmed = value.trim().replace(/\s+/g, " ");
1881
+ return trimmed.length > 0 ? trimmed : fallback;
1882
+ }
1883
+ function sharedBlockerThreshold(reason) {
1884
+ return reason.startsWith("configuration:") ? 1 : 3;
1885
+ }
1504
1886
  function extractEnvBlockerName(normalized) {
1505
1887
  const directMatch = normalized.match(
1506
1888
  /\bDB-isolated tests require\s+([A-Z][A-Z0-9_]{2,})\b/
@@ -1600,6 +1982,226 @@ function classifyFailureReason(line, options) {
1600
1982
  group: "authentication test setup failures"
1601
1983
  };
1602
1984
  }
1985
+ const snapshotMismatch = normalized.match(
1986
+ /((?:Error:\s*)?Snapshot\b.+\bmismatched\b[^$]*|Snapshot comparison failed[^$]*)/i
1987
+ );
1988
+ if (snapshotMismatch) {
1989
+ return {
1990
+ reason: buildClassifiedReason(
1991
+ "snapshot mismatch",
1992
+ buildExcerptDetail(snapshotMismatch[1] ?? normalized, "snapshot output changed")
1993
+ ),
1994
+ group: "snapshot mismatches"
1995
+ };
1996
+ }
1997
+ const timeoutFailure = normalized.match(
1998
+ /(Failed:\s*Timeout\s*>[^,;]+|asyncio\.exceptions\.TimeoutError:\s*.+|TimeoutError:\s*.+|(?:Test|Hook)\s+timed out in\s+\d+(?:\.\d+)?m?s[^$]*|(?:\[vitest-(?:worker|pool)\]:\s*)?Timeout[^$]*)$/i
1999
+ );
2000
+ if (timeoutFailure) {
2001
+ return {
2002
+ reason: buildClassifiedReason(
2003
+ "timeout",
2004
+ buildExcerptDetail(timeoutFailure[1] ?? normalized, "test exceeded timeout threshold")
2005
+ ),
2006
+ group: "timeout failures"
2007
+ };
2008
+ }
2009
+ const asyncLoopFailure = normalized.match(
2010
+ /(Event loop is closed|no current event loop|coroutine .* was never awaited|coroutine was never awaited)/i
2011
+ );
2012
+ if (asyncLoopFailure) {
2013
+ return {
2014
+ reason: buildClassifiedReason(
2015
+ "async loop",
2016
+ buildExcerptDetail(asyncLoopFailure[1] ?? normalized, "async event loop failure")
2017
+ ),
2018
+ group: "async event loop failures"
2019
+ };
2020
+ }
2021
+ const permissionFailure = normalized.match(
2022
+ /(PermissionError:\s*\[Errno 13\][^$]*|Address already in use)/i
2023
+ );
2024
+ if (permissionFailure) {
2025
+ return {
2026
+ reason: buildClassifiedReason(
2027
+ "permission",
2028
+ buildExcerptDetail(permissionFailure[1] ?? normalized, "permission denied or resource locked")
2029
+ ),
2030
+ group: "permission or locked resource failures"
2031
+ };
2032
+ }
2033
+ const xdistWorkerCrash = normalized.match(
2034
+ /(worker ['"][^'"]+['"] crashed|node down:\s*[^,;]+|WorkerLost[^,;]*|Worker exited unexpectedly[^,;]*|worker exited unexpectedly[^,;]*)/i
2035
+ );
2036
+ if (xdistWorkerCrash) {
2037
+ return {
2038
+ reason: buildClassifiedReason(
2039
+ "xdist worker crash",
2040
+ buildExcerptDetail(xdistWorkerCrash[1] ?? normalized, "pytest-xdist worker crashed")
2041
+ ),
2042
+ group: "xdist worker crashes"
2043
+ };
2044
+ }
2045
+ if (/Worker terminated due to reaching memory limit/i.test(normalized)) {
2046
+ return {
2047
+ reason: "memory: Worker terminated due to reaching memory limit",
2048
+ group: "memory exhaustion failures"
2049
+ };
2050
+ }
2051
+ if (/Database access not allowed, use the ["']django_db["'] mark/i.test(normalized)) {
2052
+ return {
2053
+ reason: 'django db access: Database access not allowed, use the "django_db" mark',
2054
+ group: "django database marker failures"
2055
+ };
2056
+ }
2057
+ const networkFailure = normalized.match(
2058
+ /(Max retries exceeded[^,;]*|gaierror[^,;]*|SSLCertVerificationError[^,;]*|Network is unreachable)/i
2059
+ );
2060
+ if (networkFailure) {
2061
+ return {
2062
+ reason: buildClassifiedReason(
2063
+ "network",
2064
+ buildExcerptDetail(networkFailure[1] ?? normalized, "network dependency failure")
2065
+ ),
2066
+ group: "network dependency failures"
2067
+ };
2068
+ }
2069
+ const relationMigration = normalized.match(/relation ["'`]([^"'`]+)["'`] does not exist/i);
2070
+ if (relationMigration) {
2071
+ return {
2072
+ reason: buildClassifiedReason("db migration", `relation ${relationMigration[1]} does not exist`),
2073
+ group: "database migration or schema failures"
2074
+ };
2075
+ }
2076
+ const noSuchTable = normalized.match(/no such table(?::)?\s*([A-Za-z0-9_.-]+)/i);
2077
+ if (noSuchTable) {
2078
+ return {
2079
+ reason: buildClassifiedReason("db migration", `no such table ${noSuchTable[1]}`),
2080
+ group: "database migration or schema failures"
2081
+ };
2082
+ }
2083
+ if (/InconsistentMigrationHistory/i.test(normalized)) {
2084
+ return {
2085
+ reason: "db migration: InconsistentMigrationHistory",
2086
+ group: "database migration or schema failures"
2087
+ };
2088
+ }
2089
+ if (/(Segmentation fault|SIGSEGV|\bexit 139\b)/i.test(normalized)) {
2090
+ return {
2091
+ reason: buildClassifiedReason(
2092
+ "segfault",
2093
+ buildExcerptDetail(normalized, "subprocess crashed with SIGSEGV")
2094
+ ),
2095
+ group: "subprocess crash failures"
2096
+ };
2097
+ }
2098
+ if (/(MemoryError\b|\bexit 137\b|OOMKilled|OutOfMemory)/i.test(normalized)) {
2099
+ return {
2100
+ reason: buildClassifiedReason(
2101
+ "memory",
2102
+ buildExcerptDetail(normalized, "process exhausted available memory")
2103
+ ),
2104
+ group: "memory exhaustion failures"
2105
+ };
2106
+ }
2107
+ const typeErrorFailure = normalized.match(/TypeError:\s*(.+)$/i);
2108
+ if (typeErrorFailure) {
2109
+ return {
2110
+ reason: buildClassifiedReason(
2111
+ "type error",
2112
+ buildExcerptDetail(typeErrorFailure[1] ?? normalized, "TypeError")
2113
+ ),
2114
+ group: "type errors"
2115
+ };
2116
+ }
2117
+ const serializationFailure = normalized.match(
2118
+ /\b(UnicodeDecodeError|JSONDecodeError|PicklingError):\s*(.+)$/i
2119
+ );
2120
+ if (serializationFailure) {
2121
+ return {
2122
+ reason: buildClassifiedReason(
2123
+ "serialization",
2124
+ `${serializationFailure[1]}: ${buildExcerptDetail(serializationFailure[2] ?? "", serializationFailure[1] ?? "serialization failure")}`
2125
+ ),
2126
+ group: "serialization and encoding failures"
2127
+ };
2128
+ }
2129
+ const fileNotFoundFailure = normalized.match(/FileNotFoundError:\s*(.+)$/i);
2130
+ if (fileNotFoundFailure) {
2131
+ return {
2132
+ reason: buildClassifiedReason(
2133
+ "file not found",
2134
+ buildExcerptDetail(fileNotFoundFailure[1] ?? normalized, "missing file during test execution")
2135
+ ),
2136
+ group: "missing file failures"
2137
+ };
2138
+ }
2139
+ const deprecationFailure = normalized.match(
2140
+ /\b(DeprecationWarning|FutureWarning|PytestRemovedIn9Warning):\s*(.+)$/i
2141
+ );
2142
+ if (deprecationFailure) {
2143
+ return {
2144
+ reason: buildClassifiedReason(
2145
+ "deprecation as error",
2146
+ `${deprecationFailure[1]}: ${buildExcerptDetail(deprecationFailure[2] ?? "", deprecationFailure[1] ?? "warning treated as error")}`
2147
+ ),
2148
+ group: "warnings treated as errors"
2149
+ };
2150
+ }
2151
+ const strictXfail = normalized.match(/XPASS\(strict\)\s*:?\s*(.+)?$/i);
2152
+ if (strictXfail) {
2153
+ return {
2154
+ reason: buildClassifiedReason(
2155
+ "xfail strict",
2156
+ buildExcerptDetail(strictXfail[1] ?? normalized, "strict xfail unexpectedly passed")
2157
+ ),
2158
+ group: "strict xfail expectation failures"
2159
+ };
2160
+ }
2161
+ const resourceLeak = normalized.match(
2162
+ /(PytestUnraisableExceptionWarning[^,;]*|ResourceWarning:\s*unclosed[^,;]*)/i
2163
+ );
2164
+ if (resourceLeak) {
2165
+ return {
2166
+ reason: buildClassifiedReason(
2167
+ "resource leak",
2168
+ buildExcerptDetail(resourceLeak[1] ?? normalized, "resource leak warning promoted to failure")
2169
+ ),
2170
+ group: "resource leak warnings"
2171
+ };
2172
+ }
2173
+ const flakyFailure = normalized.match(/\b(RERUN|[0-9]+\s+rerun|Flaky test passed)\b[^$]*/i);
2174
+ if (flakyFailure) {
2175
+ return {
2176
+ reason: buildClassifiedReason(
2177
+ "flaky",
2178
+ buildExcerptDetail(flakyFailure[0] ?? normalized, "test required reruns before passing")
2179
+ ),
2180
+ group: "flaky test detections"
2181
+ };
2182
+ }
2183
+ const teardownFailure = normalized.match(/ERROR at teardown of\s+(.+)$/i);
2184
+ if (teardownFailure) {
2185
+ return {
2186
+ reason: buildClassifiedReason(
2187
+ "fixture teardown",
2188
+ buildExcerptDetail(teardownFailure[1] ?? normalized, "fixture teardown failed")
2189
+ ),
2190
+ group: "fixture teardown failures"
2191
+ };
2192
+ }
2193
+ const configurationFailure = normalized.match(
2194
+ /(INTERNALERROR>.+|ConftestImportFailure[^,;]*|UsageError:\s*.+|ERROR:\s*usage:\s*.+|pytest:\s*error:\s*.+|Cannot use import statement outside a module[^$]*|Named export.+not found.+CommonJS[^$]*|failed to load config from.+|localStorage is not available[^$]*|No test suite found in file.+|No test found in suite.+)$/i
2195
+ );
2196
+ if (configurationFailure) {
2197
+ return {
2198
+ reason: buildClassifiedReason(
2199
+ "configuration",
2200
+ buildExcerptDetail(configurationFailure[1] ?? normalized, "test configuration error")
2201
+ ),
2202
+ group: "test configuration failures"
2203
+ };
2204
+ }
1603
2205
  const pythonMissingModule = normalized.match(
1604
2206
  /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
1605
2207
  );
@@ -1616,6 +2218,20 @@ function classifyFailureReason(line, options) {
1616
2218
  group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
1617
2219
  };
1618
2220
  }
2221
+ const importResolutionFailure = normalized.match(/Failed to resolve import ['"]([^'"]+)['"]/i);
2222
+ if (importResolutionFailure) {
2223
+ return {
2224
+ reason: `missing module: ${importResolutionFailure[1]}`,
2225
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
2226
+ };
2227
+ }
2228
+ const esmModuleFailure = normalized.match(/ERR_MODULE_NOT_FOUND[^'"`]*['"`]([^'"`]+)['"`]/i) ?? normalized.match(/Cannot find package ['"`]([^'"`]+)['"`]/i);
2229
+ if (esmModuleFailure) {
2230
+ return {
2231
+ reason: `missing module: ${esmModuleFailure[1]}`,
2232
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
2233
+ };
2234
+ }
1619
2235
  const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
1620
2236
  if (assertionFailure) {
1621
2237
  return {
@@ -1623,6 +2239,16 @@ function classifyFailureReason(line, options) {
1623
2239
  group: "assertion failures"
1624
2240
  };
1625
2241
  }
2242
+ const vitestUnhandled = normalized.match(/Vitest caught\s+\d+\s+unhandled errors?/i);
2243
+ if (vitestUnhandled) {
2244
+ return {
2245
+ reason: `RuntimeError: ${buildExcerptDetail(vitestUnhandled[0] ?? normalized, "Vitest caught unhandled errors")}`.slice(
2246
+ 0,
2247
+ 120
2248
+ ),
2249
+ group: "runtime failures"
2250
+ };
2251
+ }
1626
2252
  const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
1627
2253
  if (genericError) {
1628
2254
  const errorType = genericError[1];
@@ -1667,6 +2293,125 @@ function chooseStrongestFailureItems(items) {
1667
2293
  }
1668
2294
  return order.map((label) => strongest.get(label));
1669
2295
  }
2296
+ function extractJsTestFile(value) {
2297
+ const match = value.match(/([A-Za-z0-9_./-]+\.(?:test|spec)\.[cm]?[jt]sx?)/i);
2298
+ return match ? normalizeAnchorFile(match[1]) : null;
2299
+ }
2300
+ function normalizeJsFailureLabel(label) {
2301
+ return cleanFailureLabel(label).replace(/^[❯×]\s*/, "").replace(/\s+\[[^\]]+\]\s*$/, "").replace(/\s+/g, " ").trim();
2302
+ }
2303
+ function classifyFailureLines(args) {
2304
+ let observedAnchor = null;
2305
+ let strongest = null;
2306
+ for (const line of args.lines) {
2307
+ observedAnchor = parseObservedAnchor(line) ?? observedAnchor;
2308
+ const classification = classifyFailureReason(line, {
2309
+ duringCollection: args.duringCollection
2310
+ });
2311
+ if (!classification) {
2312
+ continue;
2313
+ }
2314
+ const score = scoreFailureReason(classification.reason);
2315
+ if (!strongest || score > strongest.score) {
2316
+ strongest = {
2317
+ classification,
2318
+ score,
2319
+ observedAnchor: parseObservedAnchor(line) ?? observedAnchor
2320
+ };
2321
+ }
2322
+ }
2323
+ if (!strongest) {
2324
+ return null;
2325
+ }
2326
+ return {
2327
+ classification: strongest.classification,
2328
+ observedAnchor: strongest.observedAnchor ?? observedAnchor
2329
+ };
2330
+ }
2331
+ function collectJsFailureBlocks(input) {
2332
+ const blocks = [];
2333
+ let current = null;
2334
+ let section = null;
2335
+ let currentFile = null;
2336
+ const flushCurrent = () => {
2337
+ if (!current) {
2338
+ return;
2339
+ }
2340
+ blocks.push(current);
2341
+ current = null;
2342
+ };
2343
+ for (const rawLine of input.split("\n")) {
2344
+ const line = rawLine.trimEnd();
2345
+ const trimmed = line.trim();
2346
+ if (/⎯{2,}\s+Failed Tests?\s+\d+\s+⎯{2,}/.test(line)) {
2347
+ flushCurrent();
2348
+ section = "failed_tests";
2349
+ continue;
2350
+ }
2351
+ if (/⎯{2,}\s+Failed Suites?\s+\d+\s+⎯{2,}/.test(line)) {
2352
+ flushCurrent();
2353
+ section = "failed_suites";
2354
+ continue;
2355
+ }
2356
+ if (section && /^⎯{2,}.+⎯{2,}\s*$/.test(line)) {
2357
+ flushCurrent();
2358
+ section = null;
2359
+ continue;
2360
+ }
2361
+ const progress = line.match(
2362
+ /^(.+?\.(?:test|spec)\.[cm]?[jt]sx?(?:\s+>.+?)?)\s+(FAILED|ERROR)\s+\[[^\]]+\]\s*$/
2363
+ );
2364
+ if (progress) {
2365
+ flushCurrent();
2366
+ const label = normalizeJsFailureLabel(progress[1]);
2367
+ current = {
2368
+ label,
2369
+ status: progress[2] === "ERROR" ? "error" : "failed",
2370
+ detailLines: []
2371
+ };
2372
+ currentFile = extractJsTestFile(label);
2373
+ continue;
2374
+ }
2375
+ const failHeader = line.match(/^\s*FAIL\s+(.+)$/);
2376
+ if (failHeader) {
2377
+ const label = normalizeJsFailureLabel(failHeader[1]);
2378
+ if (extractJsTestFile(label)) {
2379
+ flushCurrent();
2380
+ current = {
2381
+ label,
2382
+ status: section === "failed_suites" || !label.includes(" > ") ? "error" : "failed",
2383
+ detailLines: []
2384
+ };
2385
+ currentFile = extractJsTestFile(label);
2386
+ continue;
2387
+ }
2388
+ }
2389
+ const failedTest = line.match(/^\s*×\s+(.+)$/);
2390
+ if (failedTest && (section === "failed_tests" || extractJsTestFile(failedTest[1]))) {
2391
+ flushCurrent();
2392
+ const candidate = normalizeJsFailureLabel(failedTest[1]);
2393
+ const file = extractJsTestFile(candidate) ?? currentFile;
2394
+ const label = file && !extractJsTestFile(candidate) ? `${file} > ${candidate}` : candidate;
2395
+ current = {
2396
+ label,
2397
+ status: "failed",
2398
+ detailLines: []
2399
+ };
2400
+ currentFile = extractJsTestFile(label) ?? currentFile;
2401
+ continue;
2402
+ }
2403
+ if (/^\s*(?:Tests?|Snapshots?|Test Files?|Test Suites?)\b/.test(line)) {
2404
+ flushCurrent();
2405
+ section = null;
2406
+ continue;
2407
+ }
2408
+ if (current && trimmed.length > 0) {
2409
+ current.detailLines.push(line);
2410
+ }
2411
+ }
2412
+ flushCurrent();
2413
+ return blocks;
2414
+ }
1670
2415
  function collectCollectionFailureItems(input) {
1671
2416
  const items = [];
1672
2417
  const lines = input.split("\n");
@@ -1674,6 +2419,24 @@ function collectCollectionFailureItems(input) {
1674
2419
  let pendingGenericReason = null;
1675
2420
  let currentAnchor = null;
1676
2421
  for (const line of lines) {
2422
+ const standaloneCollectionLabel = line.match(/No test suite found in file\s+(.+)$/i)?.[1] ?? line.match(/No test found in suite\s+(.+)$/i)?.[1] ?? line.match(/failed to load config from\s+(.+)$/i)?.[1];
2423
+ if (standaloneCollectionLabel) {
2424
+ const classification2 = classifyFailureReason(line, {
2425
+ duringCollection: true
2426
+ });
2427
+ if (classification2) {
2428
+ pushFocusedFailureItem(items, {
2429
+ label: cleanFailureLabel(standaloneCollectionLabel),
2430
+ reason: classification2.reason,
2431
+ group: classification2.group,
2432
+ ...resolveAnchorForLabel({
2433
+ label: cleanFailureLabel(standaloneCollectionLabel),
2434
+ observedAnchor: parseObservedAnchor(line)
2435
+ })
2436
+ });
2437
+ }
2438
+ continue;
2439
+ }
1677
2440
  const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
1678
2441
  if (collecting) {
1679
2442
  if (currentLabel && pendingGenericReason) {
@@ -1762,6 +2525,24 @@ function collectInlineFailureItems(input) {
1762
2525
  })
1763
2526
  });
1764
2527
  }
2528
+ for (const block of collectJsFailureBlocks(input)) {
2529
+ const resolved = classifyFailureLines({
2530
+ lines: block.detailLines,
2531
+ duringCollection: block.status === "error"
2532
+ });
2533
+ if (!resolved) {
2534
+ continue;
2535
+ }
2536
+ pushFocusedFailureItem(items, {
2537
+ label: block.label,
2538
+ reason: resolved.classification.reason,
2539
+ group: resolved.classification.group,
2540
+ ...resolveAnchorForLabel({
2541
+ label: block.label,
2542
+ observedAnchor: resolved.observedAnchor
2543
+ })
2544
+ });
2545
+ }
1765
2546
  return items;
1766
2547
  }
1767
2548
  function collectInlineFailureItemsWithStatus(input) {
@@ -1796,16 +2577,42 @@ function collectInlineFailureItemsWithStatus(input) {
1796
2577
  })
1797
2578
  });
1798
2579
  }
2580
+ for (const block of collectJsFailureBlocks(input)) {
2581
+ const resolved = classifyFailureLines({
2582
+ lines: block.detailLines,
2583
+ duringCollection: block.status === "error"
2584
+ });
2585
+ if (!resolved) {
2586
+ continue;
2587
+ }
2588
+ items.push({
2589
+ label: block.label,
2590
+ reason: resolved.classification.reason,
2591
+ group: resolved.classification.group,
2592
+ status: block.status,
2593
+ ...resolveAnchorForLabel({
2594
+ label: block.label,
2595
+ observedAnchor: resolved.observedAnchor
2596
+ })
2597
+ });
2598
+ }
1799
2599
  return items;
1800
2600
  }
1801
2601
  function collectStandaloneErrorClassifications(input) {
1802
2602
  const classifications = [];
1803
2603
  for (const line of input.split("\n")) {
2604
+ const trimmed = line.trim();
2605
+ if (!trimmed) {
2606
+ continue;
2607
+ }
1804
2608
  const standalone = line.match(/^\s*E\s+(.+)$/);
1805
- if (!standalone) {
2609
+ const candidate = standalone?.[1] ?? (/^(INTERNALERROR>|ConftestImportFailure\b|UsageError:|ERROR:\s*usage:|pytest:\s*error:)/i.test(
2610
+ trimmed
2611
+ ) ? trimmed : null);
2612
+ if (!candidate) {
1806
2613
  continue;
1807
2614
  }
1808
- const classification = classifyFailureReason(standalone[1], {
2615
+ const classification = classifyFailureReason(candidate, {
1809
2616
  duringCollection: false
1810
2617
  });
1811
2618
  if (!classification || classification.reason === "import error during collection") {
@@ -1921,6 +2728,9 @@ function collectFailureLabels(input) {
1921
2728
  pushLabel(summary[2], summary[1] === "FAILED" ? "failed" : "error");
1922
2729
  }
1923
2730
  }
2731
+ for (const block of collectJsFailureBlocks(input)) {
2732
+ pushLabel(block.label, block.status);
2733
+ }
1924
2734
  return labels;
1925
2735
  }
1926
2736
  function classifyBucketTypeFromReason(reason) {
@@ -1930,6 +2740,60 @@ function classifyBucketTypeFromReason(reason) {
1930
2740
  if (reason.startsWith("fixture guard:")) {
1931
2741
  return "fixture_guard_failure";
1932
2742
  }
2743
+ if (reason.startsWith("timeout:")) {
2744
+ return "timeout_failure";
2745
+ }
2746
+ if (reason.startsWith("permission:")) {
2747
+ return "permission_denied_failure";
2748
+ }
2749
+ if (reason.startsWith("async loop:")) {
2750
+ return "async_event_loop_failure";
2751
+ }
2752
+ if (reason.startsWith("fixture teardown:")) {
2753
+ return "fixture_teardown_failure";
2754
+ }
2755
+ if (reason.startsWith("db migration:")) {
2756
+ return "db_migration_failure";
2757
+ }
2758
+ if (reason.startsWith("configuration:")) {
2759
+ return "configuration_error";
2760
+ }
2761
+ if (reason.startsWith("xdist worker crash:")) {
2762
+ return "xdist_worker_crash";
2763
+ }
2764
+ if (reason.startsWith("type error:")) {
2765
+ return "type_error_failure";
2766
+ }
2767
+ if (reason.startsWith("resource leak:")) {
2768
+ return "resource_leak_warning";
2769
+ }
2770
+ if (reason.startsWith("django db access:")) {
2771
+ return "django_db_access_denied";
2772
+ }
2773
+ if (reason.startsWith("network:")) {
2774
+ return "network_failure";
2775
+ }
2776
+ if (reason.startsWith("segfault:")) {
2777
+ return "subprocess_crash_segfault";
2778
+ }
2779
+ if (reason.startsWith("flaky:")) {
2780
+ return "flaky_test_detected";
2781
+ }
2782
+ if (reason.startsWith("serialization:")) {
2783
+ return "serialization_encoding_failure";
2784
+ }
2785
+ if (reason.startsWith("file not found:")) {
2786
+ return "file_not_found_failure";
2787
+ }
2788
+ if (reason.startsWith("memory:")) {
2789
+ return "memory_error";
2790
+ }
2791
+ if (reason.startsWith("deprecation as error:")) {
2792
+ return "deprecation_warning_as_error";
2793
+ }
2794
+ if (reason.startsWith("xfail strict:")) {
2795
+ return "xfail_strict_unexpected_pass";
2796
+ }
1933
2797
  if (reason.startsWith("service unavailable:")) {
1934
2798
  return "service_unavailable";
1935
2799
  }
@@ -1939,6 +2803,9 @@ function classifyBucketTypeFromReason(reason) {
1939
2803
  if (reason.startsWith("auth bypass absent:")) {
1940
2804
  return "auth_bypass_absent";
1941
2805
  }
2806
+ if (reason.startsWith("snapshot mismatch:")) {
2807
+ return "snapshot_mismatch";
2808
+ }
1942
2809
  if (reason.startsWith("missing module:")) {
1943
2810
  return "import_dependency_failure";
1944
2811
  }
@@ -1951,9 +2818,6 @@ function classifyBucketTypeFromReason(reason) {
1951
2818
  return "unknown_failure";
1952
2819
  }
1953
2820
  function synthesizeSharedBlockerBucket(args) {
1954
- if (args.errors === 0) {
1955
- return null;
1956
- }
1957
2821
  const visibleReasonGroups = /* @__PURE__ */ new Map();
1958
2822
  for (const item of args.visibleErrorItems) {
1959
2823
  const entry = visibleReasonGroups.get(item.reason);
@@ -1968,7 +2832,7 @@ function synthesizeSharedBlockerBucket(args) {
1968
2832
  items: [item]
1969
2833
  });
1970
2834
  }
1971
- const top = [...visibleReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
2835
+ const top = [...visibleReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
1972
2836
  const standaloneReasonGroups = /* @__PURE__ */ new Map();
1973
2837
  for (const classification of collectStandaloneErrorClassifications(args.input)) {
1974
2838
  const entry = standaloneReasonGroups.get(classification.reason);
@@ -1981,7 +2845,7 @@ function synthesizeSharedBlockerBucket(args) {
1981
2845
  group: classification.group
1982
2846
  });
1983
2847
  }
1984
- const standaloneTop = [...standaloneReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
2848
+ const standaloneTop = [...standaloneReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
1985
2849
  const visibleTopReason = top?.[0];
1986
2850
  const visibleTopStats = top?.[1];
1987
2851
  const standaloneTopReason = standaloneTop?.[0];
@@ -2020,6 +2884,12 @@ function synthesizeSharedBlockerBucket(args) {
2020
2884
  let hint;
2021
2885
  if (envVar) {
2022
2886
  hint = `Set ${envVar} (or pass --pgtest-dsn) before rerunning DB-isolated tests.`;
2887
+ } else if (effectiveReason.startsWith("configuration:")) {
2888
+ hint = "Fix the pytest configuration or conftest import error before rerunning the suite.";
2889
+ } else if (effectiveReason.startsWith("xdist worker crash:")) {
2890
+ hint = "Check shared state, worker startup, or resource contention between xdist workers before rerunning.";
2891
+ } else if (effectiveReason.startsWith("network:")) {
2892
+ hint = "Restore DNS, TLS, or outbound network access for the affected dependency before rerunning.";
2023
2893
  } else if (effectiveReason.startsWith("fixture guard:")) {
2024
2894
  hint = "Unblock the required fixture or setup guard before rerunning the affected tests.";
2025
2895
  } else if (effectiveReason.startsWith("db refused:")) {
@@ -2034,6 +2904,12 @@ function synthesizeSharedBlockerBucket(args) {
2034
2904
  let headline;
2035
2905
  if (envVar) {
2036
2906
  headline = `Shared blocker: ${atLeastPrefix}${countText} errors require ${envVar} for DB-isolated tests.`;
2907
+ } else if (effectiveReason.startsWith("configuration:")) {
2908
+ headline = `Shared blocker: ${atLeastPrefix}${countText} visible failure${countText === 1 ? "" : "s"} are caused by a pytest configuration error.`;
2909
+ } else if (effectiveReason.startsWith("xdist worker crash:")) {
2910
+ headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by xdist worker crashes.`;
2911
+ } else if (effectiveReason.startsWith("network:")) {
2912
+ headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by a network dependency failure.`;
2037
2913
  } else if (effectiveReason.startsWith("fixture guard:")) {
2038
2914
  headline = `Shared blocker: ${atLeastPrefix}${countText} errors are gated by the same fixture/setup guard.`;
2039
2915
  } else if (effectiveReason.startsWith("db refused:")) {
@@ -2064,11 +2940,17 @@ function synthesizeSharedBlockerBucket(args) {
2064
2940
  };
2065
2941
  }
2066
2942
  function synthesizeImportDependencyBucket(args) {
2067
- if (args.errors === 0) {
2068
- return null;
2069
- }
2070
- const importItems = args.visibleErrorItems.filter((item) => item.reason.startsWith("missing module:"));
2071
- if (importItems.length < 2) {
2943
+ const visibleImportItems = args.visibleErrorItems.filter(
2944
+ (item) => item.reason.startsWith("missing module:")
2945
+ );
2946
+ const inlineImportItems = chooseStrongestFailureItems(
2947
+ args.inlineItems.filter((item) => item.reason.startsWith("missing module:"))
2948
+ );
2949
+ const importItems = visibleImportItems.length > 0 ? visibleImportItems : inlineImportItems.map((item) => ({
2950
+ ...item,
2951
+ status: "failed"
2952
+ }));
2953
+ if (importItems.length === 0) {
2072
2954
  return null;
2073
2955
  }
2074
2956
  const allVisibleErrorsAreImportRelated = args.visibleErrorItems.length > 0 && args.visibleErrorItems.every((item) => item.reason.startsWith("missing module:"));
@@ -2079,7 +2961,7 @@ function synthesizeImportDependencyBucket(args) {
2079
2961
  )
2080
2962
  ).slice(0, 6);
2081
2963
  const headlineCount = countClaimed ?? importItems.length;
2082
- const headline = countClaimed ? `Import/dependency blocker: ${headlineCount} errors are caused by missing dependencies during test collection.` : `Import/dependency blocker: at least ${headlineCount} visible errors are caused by missing dependencies during test collection.`;
2964
+ const headline = countClaimed ? `Import/dependency blocker: ${headlineCount} errors are caused by missing dependencies during test collection.` : `Import/dependency blocker: at least ${headlineCount} visible failure${headlineCount === 1 ? "" : "s"} are caused by missing dependencies during test collection.`;
2083
2965
  const summaryLines = [headline];
2084
2966
  if (modules.length > 0) {
2085
2967
  summaryLines.push(`Missing modules include ${modules.join(", ")}.`);
@@ -2089,7 +2971,7 @@ function synthesizeImportDependencyBucket(args) {
2089
2971
  headline,
2090
2972
  countVisible: importItems.length,
2091
2973
  countClaimed,
2092
- reason: "missing dependencies during test collection",
2974
+ reason: modules.length === 1 ? `missing module: ${modules[0]}` : "missing dependencies during test collection",
2093
2975
  representativeItems: importItems.slice(0, 4).map((item) => ({
2094
2976
  label: item.label,
2095
2977
  reason: item.reason,
@@ -2108,7 +2990,7 @@ function synthesizeImportDependencyBucket(args) {
2108
2990
  };
2109
2991
  }
2110
2992
  function isContractDriftLabel(label) {
2111
- return /(freeze|snapshot|contract|manifest|openapi|golden)/i.test(label);
2993
+ return /(freeze|contract|manifest|openapi|golden)/i.test(label);
2112
2994
  }
2113
2995
  function looksLikeTaskKey(value) {
2114
2996
  return /^[a-z]+(?:_[a-z0-9]+)+$/i.test(value) && !value.startsWith("/api/");
@@ -2239,13 +3121,67 @@ function synthesizeContractDriftBucket(args) {
2239
3121
  overflowLabel: "changed entities"
2240
3122
  };
2241
3123
  }
3124
+ function synthesizeSnapshotMismatchBucket(args) {
3125
+ const snapshotItems = chooseStrongestFailureItems(
3126
+ args.inlineItems.filter((item) => item.reason.startsWith("snapshot mismatch:"))
3127
+ );
3128
+ if (snapshotItems.length === 0) {
3129
+ return null;
3130
+ }
3131
+ const countClaimed = args.snapshotFailures && args.snapshotFailures >= snapshotItems.length ? args.snapshotFailures : void 0;
3132
+ const countText = countClaimed ?? snapshotItems.length;
3133
+ const summaryLines = [
3134
+ `Snapshot mismatches: ${formatCount2(countText, "snapshot expectation")} ${countText === 1 ? "is" : "are"} out of date with current output.`
3135
+ ];
3136
+ return {
3137
+ type: "snapshot_mismatch",
3138
+ headline: summaryLines[0],
3139
+ countVisible: snapshotItems.length,
3140
+ countClaimed,
3141
+ reason: "snapshot mismatch: snapshot expectations differ from current output",
3142
+ representativeItems: snapshotItems.slice(0, 4),
3143
+ entities: snapshotItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
3144
+ hint: "Update the snapshots if these output changes are intentional.",
3145
+ confidence: countClaimed ? 0.92 : 0.8,
3146
+ summaryLines,
3147
+ overflowCount: Math.max((countClaimed ?? snapshotItems.length) - Math.min(snapshotItems.length, 4), 0),
3148
+ overflowLabel: "snapshot failures"
3149
+ };
3150
+ }
3151
+ function synthesizeTimeoutBucket(args) {
3152
+ const timeoutItems = chooseStrongestFailureItems(
3153
+ args.inlineItems.filter((item) => item.reason.startsWith("timeout:"))
3154
+ );
3155
+ if (timeoutItems.length === 0) {
3156
+ return null;
3157
+ }
3158
+ const summaryLines = [
3159
+ `Timeout failures: ${formatCount2(timeoutItems.length, "test")} exceeded the configured timeout threshold.`
3160
+ ];
3161
+ return {
3162
+ type: "timeout_failure",
3163
+ headline: summaryLines[0],
3164
+ countVisible: timeoutItems.length,
3165
+ countClaimed: timeoutItems.length,
3166
+ reason: timeoutItems.length === 1 ? timeoutItems[0].reason : "timeout: tests exceeded the configured timeout threshold",
3167
+ representativeItems: timeoutItems.slice(0, 4),
3168
+ entities: timeoutItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
3169
+ hint: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning.",
3170
+ confidence: 0.84,
3171
+ summaryLines,
3172
+ overflowCount: Math.max(timeoutItems.length - Math.min(timeoutItems.length, 4), 0),
3173
+ overflowLabel: "timeout failures"
3174
+ };
3175
+ }
2242
3176
  function analyzeTestStatus(input) {
2243
- const passed = getCount(input, "passed");
2244
- const failed = getCount(input, "failed");
2245
- const errors = Math.max(getCount(input, "errors"), getCount(input, "error"));
2246
- const skipped = getCount(input, "skipped");
3177
+ const runner = detectTestRunner(input);
3178
+ const counts = extractTestStatusCounts(input, runner);
3179
+ const passed = counts.passed;
3180
+ const failed = counts.failed;
3181
+ const errors = counts.errors;
3182
+ const skipped = counts.skipped;
2247
3183
  const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
2248
- const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
3184
+ const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input) || /No test suite found in file/i.test(input) || /No test found in suite/i.test(input);
2249
3185
  const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
2250
3186
  const collectionItems = chooseStrongestFailureItems(collectCollectionFailureItems(input));
2251
3187
  const inlineItems = chooseStrongestFailureItems(collectInlineFailureItems(input));
@@ -2272,7 +3208,8 @@ function analyzeTestStatus(input) {
2272
3208
  if (!sharedBlocker) {
2273
3209
  const importDependencyBucket = synthesizeImportDependencyBucket({
2274
3210
  errors,
2275
- visibleErrorItems
3211
+ visibleErrorItems,
3212
+ inlineItems
2276
3213
  });
2277
3214
  if (importDependencyBucket) {
2278
3215
  buckets.push(importDependencyBucket);
@@ -2285,11 +3222,26 @@ function analyzeTestStatus(input) {
2285
3222
  if (contractDrift) {
2286
3223
  buckets.push(contractDrift);
2287
3224
  }
3225
+ const snapshotMismatch = synthesizeSnapshotMismatchBucket({
3226
+ inlineItems,
3227
+ snapshotFailures: counts.snapshotFailures
3228
+ });
3229
+ if (snapshotMismatch) {
3230
+ buckets.push(snapshotMismatch);
3231
+ }
3232
+ const timeoutBucket = synthesizeTimeoutBucket({
3233
+ inlineItems
3234
+ });
3235
+ if (timeoutBucket) {
3236
+ buckets.push(timeoutBucket);
3237
+ }
2288
3238
  return {
3239
+ runner,
2289
3240
  passed,
2290
3241
  failed,
2291
3242
  errors,
2292
3243
  skipped,
3244
+ snapshotFailures: counts.snapshotFailures,
2293
3245
  noTestsCollected,
2294
3246
  interrupted,
2295
3247
  collectionErrorCount: collectionErrors ? Number(collectionErrors[1]) : void 0,
@@ -3875,10 +4827,29 @@ var detailSchema = z2.enum(["standard", "focused", "verbose"]);
3875
4827
  var failureBucketTypeSchema = z2.enum([
3876
4828
  "shared_environment_blocker",
3877
4829
  "fixture_guard_failure",
4830
+ "timeout_failure",
4831
+ "permission_denied_failure",
4832
+ "async_event_loop_failure",
4833
+ "fixture_teardown_failure",
4834
+ "db_migration_failure",
4835
+ "configuration_error",
4836
+ "xdist_worker_crash",
4837
+ "type_error_failure",
4838
+ "resource_leak_warning",
4839
+ "django_db_access_denied",
4840
+ "network_failure",
4841
+ "subprocess_crash_segfault",
4842
+ "flaky_test_detected",
4843
+ "serialization_encoding_failure",
4844
+ "file_not_found_failure",
4845
+ "memory_error",
4846
+ "deprecation_warning_as_error",
4847
+ "xfail_strict_unexpected_pass",
3878
4848
  "service_unavailable",
3879
4849
  "db_connection_failure",
3880
4850
  "auth_bypass_absent",
3881
4851
  "contract_snapshot_drift",
4852
+ "snapshot_mismatch",
3882
4853
  "import_dependency_failure",
3883
4854
  "collection_failure",
3884
4855
  "assertion_failure",