@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.
- package/README.md +124 -337
- package/dist/cli.js +998 -27
- package/dist/index.js +998 -27
- 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:
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 >=
|
|
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 >=
|
|
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
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
const
|
|
4467
|
-
|
|
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
|
|
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|
|
|
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
|
|
4640
|
-
const
|
|
4641
|
-
const
|
|
4642
|
-
const
|
|
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",
|