@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/cli.js CHANGED
@@ -2229,6 +2229,209 @@ var testStatusPublicDiagnoseContractSchema = testStatusDiagnoseContractSchema.om
2229
2229
  function parseTestStatusProviderSupplement(input) {
2230
2230
  return testStatusProviderSupplementSchema.parse(JSON.parse(input));
2231
2231
  }
2232
+ var extendedBucketSpecs = [
2233
+ {
2234
+ prefix: "snapshot mismatch:",
2235
+ type: "snapshot_mismatch",
2236
+ label: "snapshot mismatch",
2237
+ genericTitle: "Snapshot mismatches",
2238
+ defaultCoverage: "failed",
2239
+ rootCauseConfidence: 0.84,
2240
+ why: "it contains the failing snapshot expectation behind this bucket",
2241
+ fix: "Update the snapshots if these output changes are intentional, then rerun the suite."
2242
+ },
2243
+ {
2244
+ prefix: "timeout:",
2245
+ type: "timeout_failure",
2246
+ label: "timeout",
2247
+ genericTitle: "Timeout failures",
2248
+ defaultCoverage: "mixed",
2249
+ rootCauseConfidence: 0.9,
2250
+ why: "it contains the test or fixture that exceeded the timeout threshold",
2251
+ fix: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning."
2252
+ },
2253
+ {
2254
+ prefix: "permission:",
2255
+ type: "permission_denied_failure",
2256
+ label: "permission denied",
2257
+ genericTitle: "Permission failures",
2258
+ defaultCoverage: "error",
2259
+ rootCauseConfidence: 0.85,
2260
+ why: "it contains the file, socket, or port access that was denied",
2261
+ fix: "Check file or port permissions in the CI environment before rerunning."
2262
+ },
2263
+ {
2264
+ prefix: "async loop:",
2265
+ type: "async_event_loop_failure",
2266
+ label: "async event loop",
2267
+ genericTitle: "Async event loop failures",
2268
+ defaultCoverage: "mixed",
2269
+ rootCauseConfidence: 0.88,
2270
+ why: "it contains the async setup or coroutine that caused the event loop error",
2271
+ fix: "Check event loop scope and pytest-asyncio configuration before rerunning."
2272
+ },
2273
+ {
2274
+ prefix: "fixture teardown:",
2275
+ type: "fixture_teardown_failure",
2276
+ label: "fixture teardown",
2277
+ genericTitle: "Fixture teardown failures",
2278
+ defaultCoverage: "error",
2279
+ rootCauseConfidence: 0.85,
2280
+ why: "it contains the fixture teardown path that failed after the test body completed",
2281
+ fix: "Inspect the teardown cleanup path and restore idempotent fixture cleanup before rerunning."
2282
+ },
2283
+ {
2284
+ prefix: "db migration:",
2285
+ type: "db_migration_failure",
2286
+ label: "db migration",
2287
+ genericTitle: "DB migration failures",
2288
+ defaultCoverage: "error",
2289
+ rootCauseConfidence: 0.9,
2290
+ why: "it contains the migration or model definition behind the missing table or relation",
2291
+ fix: "Run pending migrations or fix the expected model schema before rerunning."
2292
+ },
2293
+ {
2294
+ prefix: "configuration:",
2295
+ type: "configuration_error",
2296
+ label: "configuration error",
2297
+ genericTitle: "Configuration errors",
2298
+ defaultCoverage: "error",
2299
+ rootCauseConfidence: 0.95,
2300
+ dominantPriority: 4,
2301
+ dominantBlocker: true,
2302
+ why: "it contains the pytest configuration or conftest setup error that blocks the run",
2303
+ fix: "Fix the pytest configuration, CLI usage, or conftest import error before rerunning."
2304
+ },
2305
+ {
2306
+ prefix: "xdist worker crash:",
2307
+ type: "xdist_worker_crash",
2308
+ label: "xdist worker crash",
2309
+ genericTitle: "xdist worker crashes",
2310
+ defaultCoverage: "error",
2311
+ rootCauseConfidence: 0.92,
2312
+ dominantPriority: 3,
2313
+ why: "it contains the worker startup or shared-state path that crashed an xdist worker",
2314
+ fix: "Check shared state, worker startup hooks, or resource contention between workers before rerunning."
2315
+ },
2316
+ {
2317
+ prefix: "type error:",
2318
+ type: "type_error_failure",
2319
+ label: "type error",
2320
+ genericTitle: "Type errors",
2321
+ defaultCoverage: "mixed",
2322
+ rootCauseConfidence: 0.8,
2323
+ why: "it contains the call site or fixture value that triggered the type error",
2324
+ fix: "Inspect the mismatched argument or object shape and rerun the full suite at standard."
2325
+ },
2326
+ {
2327
+ prefix: "resource leak:",
2328
+ type: "resource_leak_warning",
2329
+ label: "resource leak",
2330
+ genericTitle: "Resource leak warnings",
2331
+ defaultCoverage: "mixed",
2332
+ rootCauseConfidence: 0.74,
2333
+ why: "it contains the warning source behind the leaked file, socket, or coroutine",
2334
+ fix: "Close the leaked resource or suppress the warning only if the cleanup is intentional."
2335
+ },
2336
+ {
2337
+ prefix: "django db access:",
2338
+ type: "django_db_access_denied",
2339
+ label: "django db access",
2340
+ genericTitle: "Django DB access failures",
2341
+ defaultCoverage: "error",
2342
+ rootCauseConfidence: 0.95,
2343
+ why: "it needs the @pytest.mark.django_db decorator or fixture permission to access the database",
2344
+ fix: "Add @pytest.mark.django_db to the test or class before rerunning."
2345
+ },
2346
+ {
2347
+ prefix: "network:",
2348
+ type: "network_failure",
2349
+ label: "network failure",
2350
+ genericTitle: "Network failures",
2351
+ defaultCoverage: "error",
2352
+ rootCauseConfidence: 0.88,
2353
+ dominantPriority: 2,
2354
+ why: "it contains the host, URL, or TLS path behind the network failure",
2355
+ fix: "Check DNS, outbound network access, retries, or TLS trust before rerunning."
2356
+ },
2357
+ {
2358
+ prefix: "segfault:",
2359
+ type: "subprocess_crash_segfault",
2360
+ label: "segfault",
2361
+ genericTitle: "Segfault crashes",
2362
+ defaultCoverage: "mixed",
2363
+ rootCauseConfidence: 0.8,
2364
+ why: "it contains the subprocess or native extension path that crashed with SIGSEGV",
2365
+ fix: "Inspect the native extension, subprocess boundary, or incompatible binary before rerunning."
2366
+ },
2367
+ {
2368
+ prefix: "flaky:",
2369
+ type: "flaky_test_detected",
2370
+ label: "flaky test",
2371
+ genericTitle: "Flaky test detections",
2372
+ defaultCoverage: "mixed",
2373
+ rootCauseConfidence: 0.72,
2374
+ why: "it contains the rerun-prone test that behaved inconsistently across attempts",
2375
+ fix: "Stabilize the nondeterministic test or fixture before relying on reruns."
2376
+ },
2377
+ {
2378
+ prefix: "serialization:",
2379
+ type: "serialization_encoding_failure",
2380
+ label: "serialization or encoding",
2381
+ genericTitle: "Serialization or encoding failures",
2382
+ defaultCoverage: "mixed",
2383
+ rootCauseConfidence: 0.78,
2384
+ why: "it contains the serialization or decoding path behind the malformed payload",
2385
+ fix: "Inspect the encoded payload, serializer, or fixture data before rerunning."
2386
+ },
2387
+ {
2388
+ prefix: "file not found:",
2389
+ type: "file_not_found_failure",
2390
+ label: "file not found",
2391
+ genericTitle: "Missing file failures",
2392
+ defaultCoverage: "mixed",
2393
+ rootCauseConfidence: 0.82,
2394
+ why: "it contains the missing file path or fixture artifact required by the test",
2395
+ fix: "Restore the missing file, fixture artifact, or working-directory assumption before rerunning."
2396
+ },
2397
+ {
2398
+ prefix: "memory:",
2399
+ type: "memory_error",
2400
+ label: "memory error",
2401
+ genericTitle: "Memory failures",
2402
+ defaultCoverage: "mixed",
2403
+ rootCauseConfidence: 0.78,
2404
+ why: "it contains the allocation path that exhausted available memory",
2405
+ fix: "Reduce memory pressure or investigate the large allocation before rerunning."
2406
+ },
2407
+ {
2408
+ prefix: "deprecation as error:",
2409
+ type: "deprecation_warning_as_error",
2410
+ label: "deprecation as error",
2411
+ genericTitle: "Deprecation warnings as errors",
2412
+ defaultCoverage: "mixed",
2413
+ rootCauseConfidence: 0.74,
2414
+ why: "it contains the deprecated API or warning filter that is failing the test run",
2415
+ fix: "Update the deprecated call site or relax the warning policy only if that is intentional."
2416
+ },
2417
+ {
2418
+ prefix: "xfail strict:",
2419
+ type: "xfail_strict_unexpected_pass",
2420
+ label: "strict xfail unexpected pass",
2421
+ genericTitle: "Strict xfail unexpected passes",
2422
+ defaultCoverage: "failed",
2423
+ rootCauseConfidence: 0.78,
2424
+ why: "it contains the strict xfail case that unexpectedly passed",
2425
+ fix: "Remove or update the strict xfail expectation if the test is now passing intentionally."
2426
+ }
2427
+ ];
2428
+ function findExtendedBucketSpec(reason) {
2429
+ return extendedBucketSpecs.find((spec) => reason.startsWith(spec.prefix)) ?? null;
2430
+ }
2431
+ function extractReasonDetail(reason, prefix) {
2432
+ const detail = reason.slice(prefix.length).trim();
2433
+ return detail.length > 0 ? detail : null;
2434
+ }
2232
2435
  function formatCount(count, singular, plural = `${singular}s`) {
2233
2436
  return `${count} ${count === 1 ? singular : plural}`;
2234
2437
  }
@@ -2282,6 +2485,10 @@ function formatTargetSummary(summary) {
2282
2485
  return `count=${summary.count}; families=${families}`;
2283
2486
  }
2284
2487
  function classifyGenericBucketType(reason) {
2488
+ const extended = findExtendedBucketSpec(reason);
2489
+ if (extended) {
2490
+ return extended.type;
2491
+ }
2285
2492
  if (reason.startsWith("missing test env:")) {
2286
2493
  return "shared_environment_blocker";
2287
2494
  }
@@ -2326,6 +2533,10 @@ function classifyVisibleStatusForLabel(args) {
2326
2533
  return "unknown";
2327
2534
  }
2328
2535
  function inferCoverageFromReason(reason) {
2536
+ const extended = findExtendedBucketSpec(reason);
2537
+ if (extended) {
2538
+ return extended.defaultCoverage;
2539
+ }
2329
2540
  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:")) {
2330
2541
  return "error";
2331
2542
  }
@@ -2386,7 +2597,13 @@ function buildGenericBuckets(analysis) {
2386
2597
  summaryLines: [],
2387
2598
  reason,
2388
2599
  count: 1,
2389
- confidence: reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62,
2600
+ confidence: (() => {
2601
+ const extended = findExtendedBucketSpec(reason);
2602
+ if (extended) {
2603
+ return Math.max(0.72, Math.min(extended.rootCauseConfidence, 0.82));
2604
+ }
2605
+ return reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62;
2606
+ })(),
2390
2607
  representativeItems: [item],
2391
2608
  entities: [],
2392
2609
  hint: void 0,
@@ -2403,7 +2620,7 @@ function buildGenericBuckets(analysis) {
2403
2620
  push(item.reason, item);
2404
2621
  }
2405
2622
  for (const bucket of grouped.values()) {
2406
- 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";
2623
+ 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");
2407
2624
  bucket.headline = `${title}: ${formatCount(bucket.count, "visible failure")} share ${bucket.reason}.`;
2408
2625
  bucket.summaryLines = [bucket.headline];
2409
2626
  bucket.overflowCount = Math.max(bucket.count - bucket.representativeItems.length, 0);
@@ -2476,13 +2693,13 @@ function inferFailureBucketCoverage(bucket, analysis) {
2476
2693
  }
2477
2694
  }
2478
2695
  const claimed = bucket.countClaimed ?? bucket.countVisible;
2479
- if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure") {
2696
+ if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch") {
2480
2697
  return {
2481
2698
  error,
2482
2699
  failed: Math.max(failed, claimed)
2483
2700
  };
2484
2701
  }
2485
- 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") {
2702
+ 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") {
2486
2703
  return {
2487
2704
  error: Math.max(error, claimed),
2488
2705
  failed
@@ -2557,6 +2774,10 @@ function dominantBucketPriority(bucket) {
2557
2774
  if (bucket.reason.startsWith("missing test env:")) {
2558
2775
  return 5;
2559
2776
  }
2777
+ const extended = findExtendedBucketSpec(bucket.reason);
2778
+ if (extended?.dominantPriority !== void 0) {
2779
+ return extended.dominantPriority;
2780
+ }
2560
2781
  if (bucket.type === "shared_environment_blocker") {
2561
2782
  return 4;
2562
2783
  }
@@ -2590,12 +2811,16 @@ function prioritizeBuckets(buckets) {
2590
2811
  });
2591
2812
  }
2592
2813
  function isDominantBlockerType(type) {
2593
- return type === "shared_environment_blocker" || type === "import_dependency_failure" || type === "collection_failure";
2814
+ return type === "shared_environment_blocker" || type === "configuration_error" || type === "import_dependency_failure" || type === "collection_failure";
2594
2815
  }
2595
2816
  function labelForBucket(bucket) {
2596
2817
  if (bucket.labelOverride) {
2597
2818
  return bucket.labelOverride;
2598
2819
  }
2820
+ const extended = findExtendedBucketSpec(bucket.reason);
2821
+ if (extended) {
2822
+ return extended.label;
2823
+ }
2599
2824
  if (bucket.reason.startsWith("missing test env:")) {
2600
2825
  return "missing test env";
2601
2826
  }
@@ -2629,6 +2854,9 @@ function labelForBucket(bucket) {
2629
2854
  if (bucket.type === "assertion_failure") {
2630
2855
  return "assertion failure";
2631
2856
  }
2857
+ if (bucket.type === "snapshot_mismatch") {
2858
+ return "snapshot mismatch";
2859
+ }
2632
2860
  if (bucket.type === "collection_failure") {
2633
2861
  return "collection failure";
2634
2862
  }
@@ -2647,6 +2875,10 @@ function rootCauseConfidenceFor(bucket) {
2647
2875
  if (isUnknownBucket(bucket)) {
2648
2876
  return 0.52;
2649
2877
  }
2878
+ const extended = findExtendedBucketSpec(bucket.reason);
2879
+ if (extended) {
2880
+ return extended.rootCauseConfidence;
2881
+ }
2650
2882
  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:")) {
2651
2883
  return 0.95;
2652
2884
  }
@@ -2687,6 +2919,10 @@ function buildReadTargetWhy(args) {
2687
2919
  if (envVar) {
2688
2920
  return `it contains the ${envVar} setup guard`;
2689
2921
  }
2922
+ const extended = findExtendedBucketSpec(args.bucket.reason);
2923
+ if (extended) {
2924
+ return extended.why;
2925
+ }
2690
2926
  if (args.bucket.reason.startsWith("fixture guard:")) {
2691
2927
  return "it contains the fixture/setup guard behind this bucket";
2692
2928
  }
@@ -2717,6 +2953,9 @@ function buildReadTargetWhy(args) {
2717
2953
  }
2718
2954
  return "it maps to the visible stale snapshot expectation";
2719
2955
  }
2956
+ if (args.bucket.type === "snapshot_mismatch") {
2957
+ return "it maps to the visible snapshot mismatch bucket";
2958
+ }
2720
2959
  if (args.bucket.type === "import_dependency_failure") {
2721
2960
  return "it is the first visible failing module in this missing dependency bucket";
2722
2961
  }
@@ -2728,11 +2967,54 @@ function buildReadTargetWhy(args) {
2728
2967
  }
2729
2968
  return `it maps to the visible ${args.bucketLabel} bucket`;
2730
2969
  }
2970
+ function buildExtendedBucketSearchHint(bucket, anchor) {
2971
+ const extended = findExtendedBucketSpec(bucket.reason);
2972
+ if (!extended) {
2973
+ return null;
2974
+ }
2975
+ const detail = extractReasonDetail(bucket.reason, extended.prefix);
2976
+ if (!detail) {
2977
+ return anchor.label.split("::")[1]?.trim() ?? anchor.label ?? null;
2978
+ }
2979
+ if (extended.type === "timeout_failure") {
2980
+ const duration = detail.match(/>\s*([0-9]+(?:\.[0-9]+)?s?)/i)?.[1];
2981
+ return duration ?? anchor.label.split("::")[1]?.trim() ?? detail;
2982
+ }
2983
+ if (extended.type === "db_migration_failure") {
2984
+ const relation = detail.match(/\b(?:relation|table)\s+([A-Za-z0-9_.-]+)/i)?.[1];
2985
+ return relation ?? detail;
2986
+ }
2987
+ if (extended.type === "network_failure") {
2988
+ const url = detail.match(/\bhttps?:\/\/[^\s)'"`]+/i)?.[0];
2989
+ const host = detail.match(/\b(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}\b/)?.[0];
2990
+ return url ?? host ?? detail;
2991
+ }
2992
+ if (extended.type === "xdist_worker_crash") {
2993
+ return detail.match(/\bgw\d+\b/)?.[0] ?? detail;
2994
+ }
2995
+ if (extended.type === "fixture_teardown_failure") {
2996
+ return detail.replace(/^of\s+/i, "") || anchor.label;
2997
+ }
2998
+ if (extended.type === "file_not_found_failure") {
2999
+ const path8 = detail.match(/['"]([^'"]+)['"]/)?.[1];
3000
+ return path8 ?? detail;
3001
+ }
3002
+ if (extended.type === "permission_denied_failure") {
3003
+ const path8 = detail.match(/['"]([^'"]+)['"]/)?.[1];
3004
+ const port = detail.match(/\bport\s+(\d+)\b/i)?.[1];
3005
+ return path8 ?? (port ? `port ${port}` : detail);
3006
+ }
3007
+ return detail;
3008
+ }
2731
3009
  function buildReadTargetSearchHint(bucket, anchor) {
2732
3010
  const envVar = bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
2733
3011
  if (envVar) {
2734
3012
  return envVar;
2735
3013
  }
3014
+ const extendedHint = buildExtendedBucketSearchHint(bucket, anchor);
3015
+ if (extendedHint) {
3016
+ return extendedHint;
3017
+ }
2736
3018
  if (bucket.type === "contract_snapshot_drift") {
2737
3019
  return bucket.entities.find((value) => value.startsWith("/api/")) ?? bucket.entities[0] ?? null;
2738
3020
  }
@@ -2847,6 +3129,10 @@ function extractMiniDiff(input, bucket) {
2847
3129
  };
2848
3130
  }
2849
3131
  function inferSupplementCoverageKind(args) {
3132
+ const extended = findExtendedBucketSpec(args.rootCause);
3133
+ if (extended?.defaultCoverage === "error" || extended?.defaultCoverage === "failed") {
3134
+ return extended.defaultCoverage;
3135
+ }
2850
3136
  const normalized = `${args.label} ${args.rootCause}`.toLowerCase();
2851
3137
  if (/env|setup|fixture|import|dependency|service|db|database|auth bypass|collection|connection refused/.test(
2852
3138
  normalized
@@ -3090,6 +3376,10 @@ function buildStandardFixText(args) {
3090
3376
  if (args.bucket.hint) {
3091
3377
  return args.bucket.hint;
3092
3378
  }
3379
+ const extended = findExtendedBucketSpec(args.bucket.reason);
3380
+ if (extended) {
3381
+ return extended.fix;
3382
+ }
3093
3383
  const envVar = args.bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
3094
3384
  if (envVar) {
3095
3385
  return `Set ${envVar} before rerunning the affected tests.`;
@@ -3119,6 +3409,9 @@ function buildStandardFixText(args) {
3119
3409
  if (args.bucket.type === "contract_snapshot_drift") {
3120
3410
  return "Review the visible drift and regenerate the contract snapshots if the changes are intentional.";
3121
3411
  }
3412
+ if (args.bucket.type === "snapshot_mismatch") {
3413
+ return "Update the snapshots if these output changes are intentional, then rerun the full suite at standard.";
3414
+ }
3122
3415
  if (args.bucket.type === "assertion_failure") {
3123
3416
  return "Inspect the failing assertion and rerun the full suite at standard.";
3124
3417
  }
@@ -3777,6 +4070,63 @@ function getCount(input, label) {
3777
4070
  const lastMatch = matches.at(-1);
3778
4071
  return lastMatch ? Number(lastMatch[1]) : 0;
3779
4072
  }
4073
+ function detectTestRunner(input) {
4074
+ 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)) {
4075
+ return "vitest";
4076
+ }
4077
+ 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)) {
4078
+ return "jest";
4079
+ }
4080
+ 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)) {
4081
+ return "pytest";
4082
+ }
4083
+ return "unknown";
4084
+ }
4085
+ function extractVitestLineCount(input, label, metric) {
4086
+ const matcher = new RegExp(`^\\s*${label}\\s+(.+)$`, "gmi");
4087
+ const lines = [...input.matchAll(matcher)];
4088
+ const line = lines.at(-1)?.[1];
4089
+ if (!line) {
4090
+ return null;
4091
+ }
4092
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
4093
+ return metricMatch ? Number(metricMatch[1]) : null;
4094
+ }
4095
+ function extractJestLineCount(input, label, metric) {
4096
+ const matcher = new RegExp(`^\\s*${label}:\\s+(.+)$`, "gmi");
4097
+ const lines = [...input.matchAll(matcher)];
4098
+ const line = lines.at(-1)?.[1];
4099
+ if (!line) {
4100
+ return null;
4101
+ }
4102
+ const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
4103
+ return metricMatch ? Number(metricMatch[1]) : null;
4104
+ }
4105
+ function extractTestStatusCounts(input, runner) {
4106
+ if (runner === "vitest") {
4107
+ return {
4108
+ passed: extractVitestLineCount(input, "Tests?", "passed") ?? getCount(input, "passed"),
4109
+ failed: extractVitestLineCount(input, "Tests?", "failed") ?? getCount(input, "failed"),
4110
+ errors: extractVitestLineCount(input, "Errors?", "error") ?? extractVitestLineCount(input, "Errors?", "errors") ?? Math.max(getCount(input, "errors"), getCount(input, "error")),
4111
+ skipped: extractVitestLineCount(input, "Tests?", "skipped") ?? getCount(input, "skipped"),
4112
+ snapshotFailures: extractVitestLineCount(input, "Snapshots?", "failed") ?? void 0
4113
+ };
4114
+ }
4115
+ if (runner === "jest") {
4116
+ return {
4117
+ passed: extractJestLineCount(input, "Tests", "passed") ?? getCount(input, "passed"),
4118
+ failed: extractJestLineCount(input, "Tests", "failed") ?? getCount(input, "failed"),
4119
+ errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
4120
+ skipped: extractJestLineCount(input, "Tests", "skipped") ?? getCount(input, "skipped")
4121
+ };
4122
+ }
4123
+ return {
4124
+ passed: getCount(input, "passed"),
4125
+ failed: getCount(input, "failed"),
4126
+ errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
4127
+ skipped: getCount(input, "skipped")
4128
+ };
4129
+ }
3780
4130
  function formatCount2(count, singular, plural = `${singular}s`) {
3781
4131
  return `${count} ${count === 1 ? singular : plural}`;
3782
4132
  }
@@ -3809,7 +4159,8 @@ function normalizeAnchorFile(value) {
3809
4159
  return value.replace(/\\/g, "/").trim();
3810
4160
  }
3811
4161
  function inferFileFromLabel(label) {
3812
- const candidate = cleanFailureLabel(label).split("::")[0]?.trim();
4162
+ const cleaned = cleanFailureLabel(label);
4163
+ const candidate = cleaned.split("::")[0]?.split(" > ")[0]?.trim();
3813
4164
  if (!candidate) {
3814
4165
  return null;
3815
4166
  }
@@ -3864,6 +4215,15 @@ function parseObservedAnchor(line) {
3864
4215
  anchor_confidence: 0.92
3865
4216
  };
3866
4217
  }
4218
+ const vitestTraceback = normalized.match(/^\s*❯\s+([^:\s][^:]*\.[A-Za-z0-9]+):(\d+)(?::\d+)?/);
4219
+ if (vitestTraceback) {
4220
+ return {
4221
+ file: normalizeAnchorFile(vitestTraceback[1]),
4222
+ line: Number(vitestTraceback[2]),
4223
+ anchor_kind: "traceback",
4224
+ anchor_confidence: 1
4225
+ };
4226
+ }
3867
4227
  return null;
3868
4228
  }
3869
4229
  function resolveAnchorForLabel(args) {
@@ -3880,15 +4240,27 @@ function isLowValueInternalReason(normalized) {
3880
4240
  ) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
3881
4241
  }
3882
4242
  function scoreFailureReason(reason) {
4243
+ if (reason.startsWith("configuration:")) {
4244
+ return 6;
4245
+ }
3883
4246
  if (reason.startsWith("missing test env:")) {
3884
4247
  return 6;
3885
4248
  }
3886
4249
  if (reason.startsWith("missing module:")) {
3887
4250
  return 5;
3888
4251
  }
4252
+ if (reason.startsWith("snapshot mismatch:")) {
4253
+ return 4;
4254
+ }
3889
4255
  if (reason.startsWith("assertion failed:")) {
3890
4256
  return 4;
3891
4257
  }
4258
+ if (reason.startsWith("timeout:") || reason.startsWith("async loop:") || reason.startsWith("django db access:") || reason.startsWith("db migration:")) {
4259
+ return 3;
4260
+ }
4261
+ 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:")) {
4262
+ return 2;
4263
+ }
3892
4264
  if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
3893
4265
  return 3;
3894
4266
  }
@@ -3897,6 +4269,16 @@ function scoreFailureReason(reason) {
3897
4269
  }
3898
4270
  return 1;
3899
4271
  }
4272
+ function buildClassifiedReason(prefix, detail) {
4273
+ return `${prefix}: ${detail}`.slice(0, 120);
4274
+ }
4275
+ function buildExcerptDetail(value, fallback) {
4276
+ const trimmed = value.trim().replace(/\s+/g, " ");
4277
+ return trimmed.length > 0 ? trimmed : fallback;
4278
+ }
4279
+ function sharedBlockerThreshold(reason) {
4280
+ return reason.startsWith("configuration:") ? 1 : 3;
4281
+ }
3900
4282
  function extractEnvBlockerName(normalized) {
3901
4283
  const directMatch = normalized.match(
3902
4284
  /\bDB-isolated tests require\s+([A-Z][A-Z0-9_]{2,})\b/
@@ -3996,6 +4378,226 @@ function classifyFailureReason(line, options) {
3996
4378
  group: "authentication test setup failures"
3997
4379
  };
3998
4380
  }
4381
+ const snapshotMismatch = normalized.match(
4382
+ /((?:Error:\s*)?Snapshot\b.+\bmismatched\b[^$]*|Snapshot comparison failed[^$]*)/i
4383
+ );
4384
+ if (snapshotMismatch) {
4385
+ return {
4386
+ reason: buildClassifiedReason(
4387
+ "snapshot mismatch",
4388
+ buildExcerptDetail(snapshotMismatch[1] ?? normalized, "snapshot output changed")
4389
+ ),
4390
+ group: "snapshot mismatches"
4391
+ };
4392
+ }
4393
+ const timeoutFailure = normalized.match(
4394
+ /(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
4395
+ );
4396
+ if (timeoutFailure) {
4397
+ return {
4398
+ reason: buildClassifiedReason(
4399
+ "timeout",
4400
+ buildExcerptDetail(timeoutFailure[1] ?? normalized, "test exceeded timeout threshold")
4401
+ ),
4402
+ group: "timeout failures"
4403
+ };
4404
+ }
4405
+ const asyncLoopFailure = normalized.match(
4406
+ /(Event loop is closed|no current event loop|coroutine .* was never awaited|coroutine was never awaited)/i
4407
+ );
4408
+ if (asyncLoopFailure) {
4409
+ return {
4410
+ reason: buildClassifiedReason(
4411
+ "async loop",
4412
+ buildExcerptDetail(asyncLoopFailure[1] ?? normalized, "async event loop failure")
4413
+ ),
4414
+ group: "async event loop failures"
4415
+ };
4416
+ }
4417
+ const permissionFailure = normalized.match(
4418
+ /(PermissionError:\s*\[Errno 13\][^$]*|Address already in use)/i
4419
+ );
4420
+ if (permissionFailure) {
4421
+ return {
4422
+ reason: buildClassifiedReason(
4423
+ "permission",
4424
+ buildExcerptDetail(permissionFailure[1] ?? normalized, "permission denied or resource locked")
4425
+ ),
4426
+ group: "permission or locked resource failures"
4427
+ };
4428
+ }
4429
+ const xdistWorkerCrash = normalized.match(
4430
+ /(worker ['"][^'"]+['"] crashed|node down:\s*[^,;]+|WorkerLost[^,;]*|Worker exited unexpectedly[^,;]*|worker exited unexpectedly[^,;]*)/i
4431
+ );
4432
+ if (xdistWorkerCrash) {
4433
+ return {
4434
+ reason: buildClassifiedReason(
4435
+ "xdist worker crash",
4436
+ buildExcerptDetail(xdistWorkerCrash[1] ?? normalized, "pytest-xdist worker crashed")
4437
+ ),
4438
+ group: "xdist worker crashes"
4439
+ };
4440
+ }
4441
+ if (/Worker terminated due to reaching memory limit/i.test(normalized)) {
4442
+ return {
4443
+ reason: "memory: Worker terminated due to reaching memory limit",
4444
+ group: "memory exhaustion failures"
4445
+ };
4446
+ }
4447
+ if (/Database access not allowed, use the ["']django_db["'] mark/i.test(normalized)) {
4448
+ return {
4449
+ reason: 'django db access: Database access not allowed, use the "django_db" mark',
4450
+ group: "django database marker failures"
4451
+ };
4452
+ }
4453
+ const networkFailure = normalized.match(
4454
+ /(Max retries exceeded[^,;]*|gaierror[^,;]*|SSLCertVerificationError[^,;]*|Network is unreachable)/i
4455
+ );
4456
+ if (networkFailure) {
4457
+ return {
4458
+ reason: buildClassifiedReason(
4459
+ "network",
4460
+ buildExcerptDetail(networkFailure[1] ?? normalized, "network dependency failure")
4461
+ ),
4462
+ group: "network dependency failures"
4463
+ };
4464
+ }
4465
+ const relationMigration = normalized.match(/relation ["'`]([^"'`]+)["'`] does not exist/i);
4466
+ if (relationMigration) {
4467
+ return {
4468
+ reason: buildClassifiedReason("db migration", `relation ${relationMigration[1]} does not exist`),
4469
+ group: "database migration or schema failures"
4470
+ };
4471
+ }
4472
+ const noSuchTable = normalized.match(/no such table(?::)?\s*([A-Za-z0-9_.-]+)/i);
4473
+ if (noSuchTable) {
4474
+ return {
4475
+ reason: buildClassifiedReason("db migration", `no such table ${noSuchTable[1]}`),
4476
+ group: "database migration or schema failures"
4477
+ };
4478
+ }
4479
+ if (/InconsistentMigrationHistory/i.test(normalized)) {
4480
+ return {
4481
+ reason: "db migration: InconsistentMigrationHistory",
4482
+ group: "database migration or schema failures"
4483
+ };
4484
+ }
4485
+ if (/(Segmentation fault|SIGSEGV|\bexit 139\b)/i.test(normalized)) {
4486
+ return {
4487
+ reason: buildClassifiedReason(
4488
+ "segfault",
4489
+ buildExcerptDetail(normalized, "subprocess crashed with SIGSEGV")
4490
+ ),
4491
+ group: "subprocess crash failures"
4492
+ };
4493
+ }
4494
+ if (/(MemoryError\b|\bexit 137\b|OOMKilled|OutOfMemory)/i.test(normalized)) {
4495
+ return {
4496
+ reason: buildClassifiedReason(
4497
+ "memory",
4498
+ buildExcerptDetail(normalized, "process exhausted available memory")
4499
+ ),
4500
+ group: "memory exhaustion failures"
4501
+ };
4502
+ }
4503
+ const typeErrorFailure = normalized.match(/TypeError:\s*(.+)$/i);
4504
+ if (typeErrorFailure) {
4505
+ return {
4506
+ reason: buildClassifiedReason(
4507
+ "type error",
4508
+ buildExcerptDetail(typeErrorFailure[1] ?? normalized, "TypeError")
4509
+ ),
4510
+ group: "type errors"
4511
+ };
4512
+ }
4513
+ const serializationFailure = normalized.match(
4514
+ /\b(UnicodeDecodeError|JSONDecodeError|PicklingError):\s*(.+)$/i
4515
+ );
4516
+ if (serializationFailure) {
4517
+ return {
4518
+ reason: buildClassifiedReason(
4519
+ "serialization",
4520
+ `${serializationFailure[1]}: ${buildExcerptDetail(serializationFailure[2] ?? "", serializationFailure[1] ?? "serialization failure")}`
4521
+ ),
4522
+ group: "serialization and encoding failures"
4523
+ };
4524
+ }
4525
+ const fileNotFoundFailure = normalized.match(/FileNotFoundError:\s*(.+)$/i);
4526
+ if (fileNotFoundFailure) {
4527
+ return {
4528
+ reason: buildClassifiedReason(
4529
+ "file not found",
4530
+ buildExcerptDetail(fileNotFoundFailure[1] ?? normalized, "missing file during test execution")
4531
+ ),
4532
+ group: "missing file failures"
4533
+ };
4534
+ }
4535
+ const deprecationFailure = normalized.match(
4536
+ /\b(DeprecationWarning|FutureWarning|PytestRemovedIn9Warning):\s*(.+)$/i
4537
+ );
4538
+ if (deprecationFailure) {
4539
+ return {
4540
+ reason: buildClassifiedReason(
4541
+ "deprecation as error",
4542
+ `${deprecationFailure[1]}: ${buildExcerptDetail(deprecationFailure[2] ?? "", deprecationFailure[1] ?? "warning treated as error")}`
4543
+ ),
4544
+ group: "warnings treated as errors"
4545
+ };
4546
+ }
4547
+ const strictXfail = normalized.match(/XPASS\(strict\)\s*:?\s*(.+)?$/i);
4548
+ if (strictXfail) {
4549
+ return {
4550
+ reason: buildClassifiedReason(
4551
+ "xfail strict",
4552
+ buildExcerptDetail(strictXfail[1] ?? normalized, "strict xfail unexpectedly passed")
4553
+ ),
4554
+ group: "strict xfail expectation failures"
4555
+ };
4556
+ }
4557
+ const resourceLeak = normalized.match(
4558
+ /(PytestUnraisableExceptionWarning[^,;]*|ResourceWarning:\s*unclosed[^,;]*)/i
4559
+ );
4560
+ if (resourceLeak) {
4561
+ return {
4562
+ reason: buildClassifiedReason(
4563
+ "resource leak",
4564
+ buildExcerptDetail(resourceLeak[1] ?? normalized, "resource leak warning promoted to failure")
4565
+ ),
4566
+ group: "resource leak warnings"
4567
+ };
4568
+ }
4569
+ const flakyFailure = normalized.match(/\b(RERUN|[0-9]+\s+rerun|Flaky test passed)\b[^$]*/i);
4570
+ if (flakyFailure) {
4571
+ return {
4572
+ reason: buildClassifiedReason(
4573
+ "flaky",
4574
+ buildExcerptDetail(flakyFailure[0] ?? normalized, "test required reruns before passing")
4575
+ ),
4576
+ group: "flaky test detections"
4577
+ };
4578
+ }
4579
+ const teardownFailure = normalized.match(/ERROR at teardown of\s+(.+)$/i);
4580
+ if (teardownFailure) {
4581
+ return {
4582
+ reason: buildClassifiedReason(
4583
+ "fixture teardown",
4584
+ buildExcerptDetail(teardownFailure[1] ?? normalized, "fixture teardown failed")
4585
+ ),
4586
+ group: "fixture teardown failures"
4587
+ };
4588
+ }
4589
+ const configurationFailure = normalized.match(
4590
+ /(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
4591
+ );
4592
+ if (configurationFailure) {
4593
+ return {
4594
+ reason: buildClassifiedReason(
4595
+ "configuration",
4596
+ buildExcerptDetail(configurationFailure[1] ?? normalized, "test configuration error")
4597
+ ),
4598
+ group: "test configuration failures"
4599
+ };
4600
+ }
3999
4601
  const pythonMissingModule = normalized.match(
4000
4602
  /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
4001
4603
  );
@@ -4012,6 +4614,20 @@ function classifyFailureReason(line, options) {
4012
4614
  group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
4013
4615
  };
4014
4616
  }
4617
+ const importResolutionFailure = normalized.match(/Failed to resolve import ['"]([^'"]+)['"]/i);
4618
+ if (importResolutionFailure) {
4619
+ return {
4620
+ reason: `missing module: ${importResolutionFailure[1]}`,
4621
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
4622
+ };
4623
+ }
4624
+ const esmModuleFailure = normalized.match(/ERR_MODULE_NOT_FOUND[^'"`]*['"`]([^'"`]+)['"`]/i) ?? normalized.match(/Cannot find package ['"`]([^'"`]+)['"`]/i);
4625
+ if (esmModuleFailure) {
4626
+ return {
4627
+ reason: `missing module: ${esmModuleFailure[1]}`,
4628
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
4629
+ };
4630
+ }
4015
4631
  const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
4016
4632
  if (assertionFailure) {
4017
4633
  return {
@@ -4019,6 +4635,16 @@ function classifyFailureReason(line, options) {
4019
4635
  group: "assertion failures"
4020
4636
  };
4021
4637
  }
4638
+ const vitestUnhandled = normalized.match(/Vitest caught\s+\d+\s+unhandled errors?/i);
4639
+ if (vitestUnhandled) {
4640
+ return {
4641
+ reason: `RuntimeError: ${buildExcerptDetail(vitestUnhandled[0] ?? normalized, "Vitest caught unhandled errors")}`.slice(
4642
+ 0,
4643
+ 120
4644
+ ),
4645
+ group: "runtime failures"
4646
+ };
4647
+ }
4022
4648
  const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
4023
4649
  if (genericError) {
4024
4650
  const errorType = genericError[1];
@@ -4063,6 +4689,125 @@ function chooseStrongestFailureItems(items) {
4063
4689
  }
4064
4690
  return order.map((label) => strongest.get(label));
4065
4691
  }
4692
+ function extractJsTestFile(value) {
4693
+ const match = value.match(/([A-Za-z0-9_./-]+\.(?:test|spec)\.[cm]?[jt]sx?)/i);
4694
+ return match ? normalizeAnchorFile(match[1]) : null;
4695
+ }
4696
+ function normalizeJsFailureLabel(label) {
4697
+ return cleanFailureLabel(label).replace(/^[❯×]\s*/, "").replace(/\s+\[[^\]]+\]\s*$/, "").replace(/\s+/g, " ").trim();
4698
+ }
4699
+ function classifyFailureLines(args) {
4700
+ let observedAnchor = null;
4701
+ let strongest = null;
4702
+ for (const line of args.lines) {
4703
+ observedAnchor = parseObservedAnchor(line) ?? observedAnchor;
4704
+ const classification = classifyFailureReason(line, {
4705
+ duringCollection: args.duringCollection
4706
+ });
4707
+ if (!classification) {
4708
+ continue;
4709
+ }
4710
+ const score = scoreFailureReason(classification.reason);
4711
+ if (!strongest || score > strongest.score) {
4712
+ strongest = {
4713
+ classification,
4714
+ score,
4715
+ observedAnchor: parseObservedAnchor(line) ?? observedAnchor
4716
+ };
4717
+ }
4718
+ }
4719
+ if (!strongest) {
4720
+ return null;
4721
+ }
4722
+ return {
4723
+ classification: strongest.classification,
4724
+ observedAnchor: strongest.observedAnchor ?? observedAnchor
4725
+ };
4726
+ }
4727
+ function collectJsFailureBlocks(input) {
4728
+ const blocks = [];
4729
+ let current = null;
4730
+ let section = null;
4731
+ let currentFile = null;
4732
+ const flushCurrent = () => {
4733
+ if (!current) {
4734
+ return;
4735
+ }
4736
+ blocks.push(current);
4737
+ current = null;
4738
+ };
4739
+ for (const rawLine of input.split("\n")) {
4740
+ const line = rawLine.trimEnd();
4741
+ const trimmed = line.trim();
4742
+ if (/⎯{2,}\s+Failed Tests?\s+\d+\s+⎯{2,}/.test(line)) {
4743
+ flushCurrent();
4744
+ section = "failed_tests";
4745
+ continue;
4746
+ }
4747
+ if (/⎯{2,}\s+Failed Suites?\s+\d+\s+⎯{2,}/.test(line)) {
4748
+ flushCurrent();
4749
+ section = "failed_suites";
4750
+ continue;
4751
+ }
4752
+ if (section && /^⎯{2,}.+⎯{2,}\s*$/.test(line)) {
4753
+ flushCurrent();
4754
+ section = null;
4755
+ continue;
4756
+ }
4757
+ const progress = line.match(
4758
+ /^(.+?\.(?:test|spec)\.[cm]?[jt]sx?(?:\s+>.+?)?)\s+(FAILED|ERROR)\s+\[[^\]]+\]\s*$/
4759
+ );
4760
+ if (progress) {
4761
+ flushCurrent();
4762
+ const label = normalizeJsFailureLabel(progress[1]);
4763
+ current = {
4764
+ label,
4765
+ status: progress[2] === "ERROR" ? "error" : "failed",
4766
+ detailLines: []
4767
+ };
4768
+ currentFile = extractJsTestFile(label);
4769
+ continue;
4770
+ }
4771
+ const failHeader = line.match(/^\s*FAIL\s+(.+)$/);
4772
+ if (failHeader) {
4773
+ const label = normalizeJsFailureLabel(failHeader[1]);
4774
+ if (extractJsTestFile(label)) {
4775
+ flushCurrent();
4776
+ current = {
4777
+ label,
4778
+ status: section === "failed_suites" || !label.includes(" > ") ? "error" : "failed",
4779
+ detailLines: []
4780
+ };
4781
+ currentFile = extractJsTestFile(label);
4782
+ continue;
4783
+ }
4784
+ }
4785
+ const failedTest = line.match(/^\s*×\s+(.+)$/);
4786
+ if (failedTest && (section === "failed_tests" || extractJsTestFile(failedTest[1]))) {
4787
+ flushCurrent();
4788
+ const candidate = normalizeJsFailureLabel(failedTest[1]);
4789
+ const file = extractJsTestFile(candidate) ?? currentFile;
4790
+ const label = file && !extractJsTestFile(candidate) ? `${file} > ${candidate}` : candidate;
4791
+ current = {
4792
+ label,
4793
+ status: "failed",
4794
+ detailLines: []
4795
+ };
4796
+ currentFile = extractJsTestFile(label) ?? currentFile;
4797
+ continue;
4798
+ }
4799
+ if (/^\s*(?:Tests?|Snapshots?|Test Files?|Test Suites?)\b/.test(line)) {
4800
+ flushCurrent();
4801
+ section = null;
4802
+ continue;
4803
+ }
4804
+ if (current && trimmed.length > 0) {
4805
+ current.detailLines.push(line);
4806
+ }
4807
+ }
4808
+ flushCurrent();
4809
+ return blocks;
4810
+ }
4066
4811
  function collectCollectionFailureItems(input) {
4067
4812
  const items = [];
4068
4813
  const lines = input.split("\n");
@@ -4070,6 +4815,24 @@ function collectCollectionFailureItems(input) {
4070
4815
  let pendingGenericReason = null;
4071
4816
  let currentAnchor = null;
4072
4817
  for (const line of lines) {
4818
+ 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];
4819
+ if (standaloneCollectionLabel) {
4820
+ const classification2 = classifyFailureReason(line, {
4821
+ duringCollection: true
4822
+ });
4823
+ if (classification2) {
4824
+ pushFocusedFailureItem(items, {
4825
+ label: cleanFailureLabel(standaloneCollectionLabel),
4826
+ reason: classification2.reason,
4827
+ group: classification2.group,
4828
+ ...resolveAnchorForLabel({
4829
+ label: cleanFailureLabel(standaloneCollectionLabel),
4830
+ observedAnchor: parseObservedAnchor(line)
4831
+ })
4832
+ });
4833
+ }
4834
+ continue;
4835
+ }
4073
4836
  const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
4074
4837
  if (collecting) {
4075
4838
  if (currentLabel && pendingGenericReason) {
@@ -4158,6 +4921,24 @@ function collectInlineFailureItems(input) {
4158
4921
  })
4159
4922
  });
4160
4923
  }
4924
+ for (const block of collectJsFailureBlocks(input)) {
4925
+ const resolved = classifyFailureLines({
4926
+ lines: block.detailLines,
4927
+ duringCollection: block.status === "error"
4928
+ });
4929
+ if (!resolved) {
4930
+ continue;
4931
+ }
4932
+ pushFocusedFailureItem(items, {
4933
+ label: block.label,
4934
+ reason: resolved.classification.reason,
4935
+ group: resolved.classification.group,
4936
+ ...resolveAnchorForLabel({
4937
+ label: block.label,
4938
+ observedAnchor: resolved.observedAnchor
4939
+ })
4940
+ });
4941
+ }
4161
4942
  return items;
4162
4943
  }
4163
4944
  function collectInlineFailureItemsWithStatus(input) {
@@ -4192,16 +4973,42 @@ function collectInlineFailureItemsWithStatus(input) {
4192
4973
  })
4193
4974
  });
4194
4975
  }
4976
+ for (const block of collectJsFailureBlocks(input)) {
4977
+ const resolved = classifyFailureLines({
4978
+ lines: block.detailLines,
4979
+ duringCollection: block.status === "error"
4980
+ });
4981
+ if (!resolved) {
4982
+ continue;
4983
+ }
4984
+ items.push({
4985
+ label: block.label,
4986
+ reason: resolved.classification.reason,
4987
+ group: resolved.classification.group,
4988
+ status: block.status,
4989
+ ...resolveAnchorForLabel({
4990
+ label: block.label,
4991
+ observedAnchor: resolved.observedAnchor
4992
+ })
4993
+ });
4994
+ }
4195
4995
  return items;
4196
4996
  }
4197
4997
  function collectStandaloneErrorClassifications(input) {
4198
4998
  const classifications = [];
4199
4999
  for (const line of input.split("\n")) {
5000
+ const trimmed = line.trim();
5001
+ if (!trimmed) {
5002
+ continue;
5003
+ }
4200
5004
  const standalone = line.match(/^\s*E\s+(.+)$/);
4201
- if (!standalone) {
5005
+ const candidate = standalone?.[1] ?? (/^(INTERNALERROR>|ConftestImportFailure\b|UsageError:|ERROR:\s*usage:|pytest:\s*error:)/i.test(
5006
+ trimmed
5007
+ ) ? trimmed : null);
5008
+ if (!candidate) {
4202
5009
  continue;
4203
5010
  }
4204
- const classification = classifyFailureReason(standalone[1], {
5011
+ const classification = classifyFailureReason(candidate, {
4205
5012
  duringCollection: false
4206
5013
  });
4207
5014
  if (!classification || classification.reason === "import error during collection") {
@@ -4317,6 +5124,9 @@ function collectFailureLabels(input) {
4317
5124
  pushLabel(summary[2], summary[1] === "FAILED" ? "failed" : "error");
4318
5125
  }
4319
5126
  }
5127
+ for (const block of collectJsFailureBlocks(input)) {
5128
+ pushLabel(block.label, block.status);
5129
+ }
4320
5130
  return labels;
4321
5131
  }
4322
5132
  function classifyBucketTypeFromReason(reason) {
@@ -4326,6 +5136,60 @@ function classifyBucketTypeFromReason(reason) {
4326
5136
  if (reason.startsWith("fixture guard:")) {
4327
5137
  return "fixture_guard_failure";
4328
5138
  }
5139
+ if (reason.startsWith("timeout:")) {
5140
+ return "timeout_failure";
5141
+ }
5142
+ if (reason.startsWith("permission:")) {
5143
+ return "permission_denied_failure";
5144
+ }
5145
+ if (reason.startsWith("async loop:")) {
5146
+ return "async_event_loop_failure";
5147
+ }
5148
+ if (reason.startsWith("fixture teardown:")) {
5149
+ return "fixture_teardown_failure";
5150
+ }
5151
+ if (reason.startsWith("db migration:")) {
5152
+ return "db_migration_failure";
5153
+ }
5154
+ if (reason.startsWith("configuration:")) {
5155
+ return "configuration_error";
5156
+ }
5157
+ if (reason.startsWith("xdist worker crash:")) {
5158
+ return "xdist_worker_crash";
5159
+ }
5160
+ if (reason.startsWith("type error:")) {
5161
+ return "type_error_failure";
5162
+ }
5163
+ if (reason.startsWith("resource leak:")) {
5164
+ return "resource_leak_warning";
5165
+ }
5166
+ if (reason.startsWith("django db access:")) {
5167
+ return "django_db_access_denied";
5168
+ }
5169
+ if (reason.startsWith("network:")) {
5170
+ return "network_failure";
5171
+ }
5172
+ if (reason.startsWith("segfault:")) {
5173
+ return "subprocess_crash_segfault";
5174
+ }
5175
+ if (reason.startsWith("flaky:")) {
5176
+ return "flaky_test_detected";
5177
+ }
5178
+ if (reason.startsWith("serialization:")) {
5179
+ return "serialization_encoding_failure";
5180
+ }
5181
+ if (reason.startsWith("file not found:")) {
5182
+ return "file_not_found_failure";
5183
+ }
5184
+ if (reason.startsWith("memory:")) {
5185
+ return "memory_error";
5186
+ }
5187
+ if (reason.startsWith("deprecation as error:")) {
5188
+ return "deprecation_warning_as_error";
5189
+ }
5190
+ if (reason.startsWith("xfail strict:")) {
5191
+ return "xfail_strict_unexpected_pass";
5192
+ }
4329
5193
  if (reason.startsWith("service unavailable:")) {
4330
5194
  return "service_unavailable";
4331
5195
  }
@@ -4335,6 +5199,9 @@ function classifyBucketTypeFromReason(reason) {
4335
5199
  if (reason.startsWith("auth bypass absent:")) {
4336
5200
  return "auth_bypass_absent";
4337
5201
  }
5202
+ if (reason.startsWith("snapshot mismatch:")) {
5203
+ return "snapshot_mismatch";
5204
+ }
4338
5205
  if (reason.startsWith("missing module:")) {
4339
5206
  return "import_dependency_failure";
4340
5207
  }
@@ -4347,9 +5214,6 @@ function classifyBucketTypeFromReason(reason) {
4347
5214
  return "unknown_failure";
4348
5215
  }
4349
5216
  function synthesizeSharedBlockerBucket(args) {
4350
- if (args.errors === 0) {
4351
- return null;
4352
- }
4353
5217
  const visibleReasonGroups = /* @__PURE__ */ new Map();
4354
5218
  for (const item of args.visibleErrorItems) {
4355
5219
  const entry = visibleReasonGroups.get(item.reason);
@@ -4364,7 +5228,7 @@ function synthesizeSharedBlockerBucket(args) {
4364
5228
  items: [item]
4365
5229
  });
4366
5230
  }
4367
- const top = [...visibleReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
5231
+ const top = [...visibleReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
4368
5232
  const standaloneReasonGroups = /* @__PURE__ */ new Map();
4369
5233
  for (const classification of collectStandaloneErrorClassifications(args.input)) {
4370
5234
  const entry = standaloneReasonGroups.get(classification.reason);
@@ -4377,7 +5241,7 @@ function synthesizeSharedBlockerBucket(args) {
4377
5241
  group: classification.group
4378
5242
  });
4379
5243
  }
4380
- const standaloneTop = [...standaloneReasonGroups.entries()].filter(([, entry]) => entry.count >= 3).sort((left, right) => right[1].count - left[1].count)[0];
5244
+ const standaloneTop = [...standaloneReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
4381
5245
  const visibleTopReason = top?.[0];
4382
5246
  const visibleTopStats = top?.[1];
4383
5247
  const standaloneTopReason = standaloneTop?.[0];
@@ -4416,6 +5280,12 @@ function synthesizeSharedBlockerBucket(args) {
4416
5280
  let hint;
4417
5281
  if (envVar) {
4418
5282
  hint = `Set ${envVar} (or pass --pgtest-dsn) before rerunning DB-isolated tests.`;
5283
+ } else if (effectiveReason.startsWith("configuration:")) {
5284
+ hint = "Fix the pytest configuration or conftest import error before rerunning the suite.";
5285
+ } else if (effectiveReason.startsWith("xdist worker crash:")) {
5286
+ hint = "Check shared state, worker startup, or resource contention between xdist workers before rerunning.";
5287
+ } else if (effectiveReason.startsWith("network:")) {
5288
+ hint = "Restore DNS, TLS, or outbound network access for the affected dependency before rerunning.";
4419
5289
  } else if (effectiveReason.startsWith("fixture guard:")) {
4420
5290
  hint = "Unblock the required fixture or setup guard before rerunning the affected tests.";
4421
5291
  } else if (effectiveReason.startsWith("db refused:")) {
@@ -4430,6 +5300,12 @@ function synthesizeSharedBlockerBucket(args) {
4430
5300
  let headline;
4431
5301
  if (envVar) {
4432
5302
  headline = `Shared blocker: ${atLeastPrefix}${countText} errors require ${envVar} for DB-isolated tests.`;
5303
+ } else if (effectiveReason.startsWith("configuration:")) {
5304
+ headline = `Shared blocker: ${atLeastPrefix}${countText} visible failure${countText === 1 ? "" : "s"} are caused by a pytest configuration error.`;
5305
+ } else if (effectiveReason.startsWith("xdist worker crash:")) {
5306
+ headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by xdist worker crashes.`;
5307
+ } else if (effectiveReason.startsWith("network:")) {
5308
+ headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by a network dependency failure.`;
4433
5309
  } else if (effectiveReason.startsWith("fixture guard:")) {
4434
5310
  headline = `Shared blocker: ${atLeastPrefix}${countText} errors are gated by the same fixture/setup guard.`;
4435
5311
  } else if (effectiveReason.startsWith("db refused:")) {
@@ -4460,11 +5336,17 @@ function synthesizeSharedBlockerBucket(args) {
4460
5336
  };
4461
5337
  }
4462
5338
  function synthesizeImportDependencyBucket(args) {
4463
- if (args.errors === 0) {
4464
- return null;
4465
- }
4466
- const importItems = args.visibleErrorItems.filter((item) => item.reason.startsWith("missing module:"));
4467
- if (importItems.length < 2) {
5339
+ const visibleImportItems = args.visibleErrorItems.filter(
5340
+ (item) => item.reason.startsWith("missing module:")
5341
+ );
5342
+ const inlineImportItems = chooseStrongestFailureItems(
5343
+ args.inlineItems.filter((item) => item.reason.startsWith("missing module:"))
5344
+ );
5345
+ const importItems = visibleImportItems.length > 0 ? visibleImportItems : inlineImportItems.map((item) => ({
5346
+ ...item,
5347
+ status: "failed"
5348
+ }));
5349
+ if (importItems.length === 0) {
4468
5350
  return null;
4469
5351
  }
4470
5352
  const allVisibleErrorsAreImportRelated = args.visibleErrorItems.length > 0 && args.visibleErrorItems.every((item) => item.reason.startsWith("missing module:"));
@@ -4475,7 +5357,7 @@ function synthesizeImportDependencyBucket(args) {
4475
5357
  )
4476
5358
  ).slice(0, 6);
4477
5359
  const headlineCount = countClaimed ?? importItems.length;
4478
- 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.`;
5360
+ 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.`;
4479
5361
  const summaryLines = [headline];
4480
5362
  if (modules.length > 0) {
4481
5363
  summaryLines.push(`Missing modules include ${modules.join(", ")}.`);
@@ -4485,7 +5367,7 @@ function synthesizeImportDependencyBucket(args) {
4485
5367
  headline,
4486
5368
  countVisible: importItems.length,
4487
5369
  countClaimed,
4488
- reason: "missing dependencies during test collection",
5370
+ reason: modules.length === 1 ? `missing module: ${modules[0]}` : "missing dependencies during test collection",
4489
5371
  representativeItems: importItems.slice(0, 4).map((item) => ({
4490
5372
  label: item.label,
4491
5373
  reason: item.reason,
@@ -4504,7 +5386,7 @@ function synthesizeImportDependencyBucket(args) {
4504
5386
  };
4505
5387
  }
4506
5388
  function isContractDriftLabel(label) {
4507
- return /(freeze|snapshot|contract|manifest|openapi|golden)/i.test(label);
5389
+ return /(freeze|contract|manifest|openapi|golden)/i.test(label);
4508
5390
  }
4509
5391
  function looksLikeTaskKey(value) {
4510
5392
  return /^[a-z]+(?:_[a-z0-9]+)+$/i.test(value) && !value.startsWith("/api/");
@@ -4635,13 +5517,67 @@ function synthesizeContractDriftBucket(args) {
4635
5517
  overflowLabel: "changed entities"
4636
5518
  };
4637
5519
  }
5520
+ function synthesizeSnapshotMismatchBucket(args) {
5521
+ const snapshotItems = chooseStrongestFailureItems(
5522
+ args.inlineItems.filter((item) => item.reason.startsWith("snapshot mismatch:"))
5523
+ );
5524
+ if (snapshotItems.length === 0) {
5525
+ return null;
5526
+ }
5527
+ const countClaimed = args.snapshotFailures && args.snapshotFailures >= snapshotItems.length ? args.snapshotFailures : void 0;
5528
+ const countText = countClaimed ?? snapshotItems.length;
5529
+ const summaryLines = [
5530
+ `Snapshot mismatches: ${formatCount2(countText, "snapshot expectation")} ${countText === 1 ? "is" : "are"} out of date with current output.`
5531
+ ];
5532
+ return {
5533
+ type: "snapshot_mismatch",
5534
+ headline: summaryLines[0],
5535
+ countVisible: snapshotItems.length,
5536
+ countClaimed,
5537
+ reason: "snapshot mismatch: snapshot expectations differ from current output",
5538
+ representativeItems: snapshotItems.slice(0, 4),
5539
+ entities: snapshotItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
5540
+ hint: "Update the snapshots if these output changes are intentional.",
5541
+ confidence: countClaimed ? 0.92 : 0.8,
5542
+ summaryLines,
5543
+ overflowCount: Math.max((countClaimed ?? snapshotItems.length) - Math.min(snapshotItems.length, 4), 0),
5544
+ overflowLabel: "snapshot failures"
5545
+ };
5546
+ }
5547
+ function synthesizeTimeoutBucket(args) {
5548
+ const timeoutItems = chooseStrongestFailureItems(
5549
+ args.inlineItems.filter((item) => item.reason.startsWith("timeout:"))
5550
+ );
5551
+ if (timeoutItems.length === 0) {
5552
+ return null;
5553
+ }
5554
+ const summaryLines = [
5555
+ `Timeout failures: ${formatCount2(timeoutItems.length, "test")} exceeded the configured timeout threshold.`
5556
+ ];
5557
+ return {
5558
+ type: "timeout_failure",
5559
+ headline: summaryLines[0],
5560
+ countVisible: timeoutItems.length,
5561
+ countClaimed: timeoutItems.length,
5562
+ reason: timeoutItems.length === 1 ? timeoutItems[0].reason : "timeout: tests exceeded the configured timeout threshold",
5563
+ representativeItems: timeoutItems.slice(0, 4),
5564
+ entities: timeoutItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
5565
+ hint: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning.",
5566
+ confidence: 0.84,
5567
+ summaryLines,
5568
+ overflowCount: Math.max(timeoutItems.length - Math.min(timeoutItems.length, 4), 0),
5569
+ overflowLabel: "timeout failures"
5570
+ };
5571
+ }
4638
5572
  function analyzeTestStatus(input) {
4639
- const passed = getCount(input, "passed");
4640
- const failed = getCount(input, "failed");
4641
- const errors = Math.max(getCount(input, "errors"), getCount(input, "error"));
4642
- const skipped = getCount(input, "skipped");
5573
+ const runner = detectTestRunner(input);
5574
+ const counts = extractTestStatusCounts(input, runner);
5575
+ const passed = counts.passed;
5576
+ const failed = counts.failed;
5577
+ const errors = counts.errors;
5578
+ const skipped = counts.skipped;
4643
5579
  const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
4644
- const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
5580
+ 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);
4645
5581
  const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
4646
5582
  const collectionItems = chooseStrongestFailureItems(collectCollectionFailureItems(input));
4647
5583
  const inlineItems = chooseStrongestFailureItems(collectInlineFailureItems(input));
@@ -4668,7 +5604,8 @@ function analyzeTestStatus(input) {
4668
5604
  if (!sharedBlocker) {
4669
5605
  const importDependencyBucket = synthesizeImportDependencyBucket({
4670
5606
  errors,
4671
- visibleErrorItems
5607
+ visibleErrorItems,
5608
+ inlineItems
4672
5609
  });
4673
5610
  if (importDependencyBucket) {
4674
5611
  buckets.push(importDependencyBucket);
@@ -4681,11 +5618,26 @@ function analyzeTestStatus(input) {
4681
5618
  if (contractDrift) {
4682
5619
  buckets.push(contractDrift);
4683
5620
  }
5621
+ const snapshotMismatch = synthesizeSnapshotMismatchBucket({
5622
+ inlineItems,
5623
+ snapshotFailures: counts.snapshotFailures
5624
+ });
5625
+ if (snapshotMismatch) {
5626
+ buckets.push(snapshotMismatch);
5627
+ }
5628
+ const timeoutBucket = synthesizeTimeoutBucket({
5629
+ inlineItems
5630
+ });
5631
+ if (timeoutBucket) {
5632
+ buckets.push(timeoutBucket);
5633
+ }
4684
5634
  return {
5635
+ runner,
4685
5636
  passed,
4686
5637
  failed,
4687
5638
  errors,
4688
5639
  skipped,
5640
+ snapshotFailures: counts.snapshotFailures,
4689
5641
  noTestsCollected,
4690
5642
  interrupted,
4691
5643
  collectionErrorCount: collectionErrors ? Number(collectionErrors[1]) : void 0,
@@ -5694,10 +6646,29 @@ var detailSchema = z3.enum(["standard", "focused", "verbose"]);
5694
6646
  var failureBucketTypeSchema = z3.enum([
5695
6647
  "shared_environment_blocker",
5696
6648
  "fixture_guard_failure",
6649
+ "timeout_failure",
6650
+ "permission_denied_failure",
6651
+ "async_event_loop_failure",
6652
+ "fixture_teardown_failure",
6653
+ "db_migration_failure",
6654
+ "configuration_error",
6655
+ "xdist_worker_crash",
6656
+ "type_error_failure",
6657
+ "resource_leak_warning",
6658
+ "django_db_access_denied",
6659
+ "network_failure",
6660
+ "subprocess_crash_segfault",
6661
+ "flaky_test_detected",
6662
+ "serialization_encoding_failure",
6663
+ "file_not_found_failure",
6664
+ "memory_error",
6665
+ "deprecation_warning_as_error",
6666
+ "xfail_strict_unexpected_pass",
5697
6667
  "service_unavailable",
5698
6668
  "db_connection_failure",
5699
6669
  "auth_bypass_absent",
5700
6670
  "contract_snapshot_drift",
6671
+ "snapshot_mismatch",
5701
6672
  "import_dependency_failure",
5702
6673
  "collection_failure",
5703
6674
  "assertion_failure",