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