@bilalimamoglu/sift 0.3.0 → 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 +127 -312
- package/dist/cli.js +1939 -194
- package/dist/index.d.ts +11 -2
- package/dist/index.js +1528 -68
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -62,7 +62,7 @@ function evaluateGate(args) {
|
|
|
62
62
|
// src/core/testStatusDecision.ts
|
|
63
63
|
import { z } from "zod";
|
|
64
64
|
var TEST_STATUS_DIAGNOSE_JSON_CONTRACT = '{"status":"ok|insufficient","diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","dominant_blocker_bucket_index":number|null,"provider_used":boolean,"provider_confidence":number|null,"provider_failed":boolean,"raw_slice_used":boolean,"raw_slice_strategy":"none|bucket_evidence|traceback_window|head_tail","resolved_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_summary":{"count":number,"families":[{"prefix":string,"count":number}]},"remaining_subset_available":boolean,"main_buckets":[{"bucket_index":number,"label":string,"count":number,"root_cause":string,"evidence":string[],"bucket_confidence":number,"root_cause_confidence":number,"dominant":boolean,"secondary_visible_despite_blocker":boolean,"mini_diff":{"added_paths"?:number,"removed_models"?:number,"changed_task_mappings"?:number}|null}],"read_targets":[{"file":string,"line":number|null,"why":string,"bucket_index":number,"context_hint":{"start_line":number|null,"end_line":number|null,"search_hint":string|null}}],"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string},"resolved_tests"?:string[],"remaining_tests"?:string[]}';
|
|
65
|
-
var TEST_STATUS_PROVIDER_SUPPLEMENT_JSON_CONTRACT = '{"diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","provider_confidence":number|null,"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string}}';
|
|
65
|
+
var TEST_STATUS_PROVIDER_SUPPLEMENT_JSON_CONTRACT = '{"diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","provider_confidence":number|null,"bucket_supplements":[{"label":string,"count":number,"root_cause":string,"anchor":{"file":string|null,"line":number|null,"search_hint":string|null},"fix_hint":string|null,"confidence":number}],"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string}}';
|
|
66
66
|
var nextBestActionSchema = z.object({
|
|
67
67
|
code: z.enum([
|
|
68
68
|
"fix_dominant_blocker",
|
|
@@ -80,6 +80,20 @@ var testStatusProviderSupplementSchema = z.object({
|
|
|
80
80
|
read_raw_only_if: z.string().nullable(),
|
|
81
81
|
decision: z.enum(["stop", "zoom", "read_source", "read_raw"]),
|
|
82
82
|
provider_confidence: z.number().min(0).max(1).nullable(),
|
|
83
|
+
bucket_supplements: z.array(
|
|
84
|
+
z.object({
|
|
85
|
+
label: z.string().min(1),
|
|
86
|
+
count: z.number().int().positive(),
|
|
87
|
+
root_cause: z.string().min(1),
|
|
88
|
+
anchor: z.object({
|
|
89
|
+
file: z.string().nullable(),
|
|
90
|
+
line: z.number().int().nullable(),
|
|
91
|
+
search_hint: z.string().nullable()
|
|
92
|
+
}),
|
|
93
|
+
fix_hint: z.string().nullable(),
|
|
94
|
+
confidence: z.number().min(0).max(1)
|
|
95
|
+
})
|
|
96
|
+
).max(2),
|
|
83
97
|
next_best_action: nextBestActionSchema
|
|
84
98
|
});
|
|
85
99
|
var testStatusDiagnoseContractSchema = z.object({
|
|
@@ -152,6 +166,209 @@ var testStatusPublicDiagnoseContractSchema = testStatusDiagnoseContractSchema.om
|
|
|
152
166
|
function parseTestStatusProviderSupplement(input) {
|
|
153
167
|
return testStatusProviderSupplementSchema.parse(JSON.parse(input));
|
|
154
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
|
+
}
|
|
155
372
|
function formatCount(count, singular, plural = `${singular}s`) {
|
|
156
373
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
157
374
|
}
|
|
@@ -205,6 +422,10 @@ function formatTargetSummary(summary) {
|
|
|
205
422
|
return `count=${summary.count}; families=${families}`;
|
|
206
423
|
}
|
|
207
424
|
function classifyGenericBucketType(reason) {
|
|
425
|
+
const extended = findExtendedBucketSpec(reason);
|
|
426
|
+
if (extended) {
|
|
427
|
+
return extended.type;
|
|
428
|
+
}
|
|
208
429
|
if (reason.startsWith("missing test env:")) {
|
|
209
430
|
return "shared_environment_blocker";
|
|
210
431
|
}
|
|
@@ -231,14 +452,77 @@ function classifyGenericBucketType(reason) {
|
|
|
231
452
|
}
|
|
232
453
|
return "unknown_failure";
|
|
233
454
|
}
|
|
455
|
+
function isUnknownBucket(bucket) {
|
|
456
|
+
return bucket.source === "unknown" || bucket.reason.startsWith("unknown ");
|
|
457
|
+
}
|
|
458
|
+
function classifyVisibleStatusForLabel(args) {
|
|
459
|
+
const isError = args.errorLabels.has(args.label);
|
|
460
|
+
const isFailed = args.failedLabels.has(args.label);
|
|
461
|
+
if (isError && isFailed) {
|
|
462
|
+
return "mixed";
|
|
463
|
+
}
|
|
464
|
+
if (isError) {
|
|
465
|
+
return "error";
|
|
466
|
+
}
|
|
467
|
+
if (isFailed) {
|
|
468
|
+
return "failed";
|
|
469
|
+
}
|
|
470
|
+
return "unknown";
|
|
471
|
+
}
|
|
472
|
+
function inferCoverageFromReason(reason) {
|
|
473
|
+
const extended = findExtendedBucketSpec(reason);
|
|
474
|
+
if (extended) {
|
|
475
|
+
return extended.defaultCoverage;
|
|
476
|
+
}
|
|
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:")) {
|
|
478
|
+
return "error";
|
|
479
|
+
}
|
|
480
|
+
if (reason.startsWith("assertion failed:")) {
|
|
481
|
+
return "failed";
|
|
482
|
+
}
|
|
483
|
+
return "mixed";
|
|
484
|
+
}
|
|
485
|
+
function buildCoverageCounts(args) {
|
|
486
|
+
if (args.coverageKind === "error") {
|
|
487
|
+
return {
|
|
488
|
+
error: args.count,
|
|
489
|
+
failed: 0
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (args.coverageKind === "failed") {
|
|
493
|
+
return {
|
|
494
|
+
error: 0,
|
|
495
|
+
failed: args.count
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
error: 0,
|
|
500
|
+
failed: 0
|
|
501
|
+
};
|
|
502
|
+
}
|
|
234
503
|
function buildGenericBuckets(analysis) {
|
|
235
504
|
const buckets = [];
|
|
236
505
|
const grouped = /* @__PURE__ */ new Map();
|
|
506
|
+
const errorLabels = new Set(analysis.visibleErrorLabels);
|
|
507
|
+
const failedLabels = new Set(analysis.visibleFailedLabels);
|
|
237
508
|
const push = (reason, item) => {
|
|
238
|
-
const
|
|
509
|
+
const coverageKind = (() => {
|
|
510
|
+
const status = classifyVisibleStatusForLabel({
|
|
511
|
+
label: item.label,
|
|
512
|
+
errorLabels,
|
|
513
|
+
failedLabels
|
|
514
|
+
});
|
|
515
|
+
return status === "unknown" ? inferCoverageFromReason(reason) : status;
|
|
516
|
+
})();
|
|
517
|
+
const key = `${classifyGenericBucketType(reason)}:${coverageKind}:${reason}`;
|
|
239
518
|
const existing = grouped.get(key);
|
|
240
519
|
if (existing) {
|
|
241
520
|
existing.count += 1;
|
|
521
|
+
if (coverageKind === "error") {
|
|
522
|
+
existing.coverage.error += 1;
|
|
523
|
+
} else if (coverageKind === "failed") {
|
|
524
|
+
existing.coverage.failed += 1;
|
|
525
|
+
}
|
|
242
526
|
if (!existing.representativeItems.some((entry) => entry.label === item.label) && existing.representativeItems.length < 6) {
|
|
243
527
|
existing.representativeItems.push(item);
|
|
244
528
|
}
|
|
@@ -250,19 +534,30 @@ function buildGenericBuckets(analysis) {
|
|
|
250
534
|
summaryLines: [],
|
|
251
535
|
reason,
|
|
252
536
|
count: 1,
|
|
253
|
-
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
|
+
})(),
|
|
254
544
|
representativeItems: [item],
|
|
255
545
|
entities: [],
|
|
256
546
|
hint: void 0,
|
|
257
547
|
overflowCount: 0,
|
|
258
|
-
overflowLabel: "failing tests/modules"
|
|
548
|
+
overflowLabel: "failing tests/modules",
|
|
549
|
+
coverage: buildCoverageCounts({
|
|
550
|
+
count: 1,
|
|
551
|
+
coverageKind
|
|
552
|
+
}),
|
|
553
|
+
source: "heuristic"
|
|
259
554
|
});
|
|
260
555
|
};
|
|
261
556
|
for (const item of [...analysis.collectionItems, ...analysis.inlineItems]) {
|
|
262
557
|
push(item.reason, item);
|
|
263
558
|
}
|
|
264
559
|
for (const bucket of grouped.values()) {
|
|
265
|
-
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");
|
|
266
561
|
bucket.headline = `${title}: ${formatCount(bucket.count, "visible failure")} share ${bucket.reason}.`;
|
|
267
562
|
bucket.summaryLines = [bucket.headline];
|
|
268
563
|
bucket.overflowCount = Math.max(bucket.count - bucket.representativeItems.length, 0);
|
|
@@ -308,10 +603,51 @@ function mergeBucketDetails(existing, incoming) {
|
|
|
308
603
|
incoming.overflowCount,
|
|
309
604
|
count - representativeItems.length
|
|
310
605
|
),
|
|
311
|
-
overflowLabel: existing.overflowLabel || incoming.overflowLabel
|
|
606
|
+
overflowLabel: existing.overflowLabel || incoming.overflowLabel,
|
|
607
|
+
labelOverride: existing.labelOverride ?? incoming.labelOverride,
|
|
608
|
+
coverage: {
|
|
609
|
+
error: Math.max(existing.coverage.error, incoming.coverage.error),
|
|
610
|
+
failed: Math.max(existing.coverage.failed, incoming.coverage.failed)
|
|
611
|
+
},
|
|
612
|
+
source: existing.source
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function inferFailureBucketCoverage(bucket, analysis) {
|
|
616
|
+
const errorLabels = new Set(analysis.visibleErrorLabels);
|
|
617
|
+
const failedLabels = new Set(analysis.visibleFailedLabels);
|
|
618
|
+
let error = 0;
|
|
619
|
+
let failed = 0;
|
|
620
|
+
for (const item of bucket.representativeItems) {
|
|
621
|
+
const status = classifyVisibleStatusForLabel({
|
|
622
|
+
label: item.label,
|
|
623
|
+
errorLabels,
|
|
624
|
+
failedLabels
|
|
625
|
+
});
|
|
626
|
+
if (status === "error") {
|
|
627
|
+
error += 1;
|
|
628
|
+
} else if (status === "failed") {
|
|
629
|
+
failed += 1;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const claimed = bucket.countClaimed ?? bucket.countVisible;
|
|
633
|
+
if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch") {
|
|
634
|
+
return {
|
|
635
|
+
error,
|
|
636
|
+
failed: Math.max(failed, claimed)
|
|
637
|
+
};
|
|
638
|
+
}
|
|
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") {
|
|
640
|
+
return {
|
|
641
|
+
error: Math.max(error, claimed),
|
|
642
|
+
failed
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
error,
|
|
647
|
+
failed
|
|
312
648
|
};
|
|
313
649
|
}
|
|
314
|
-
function mergeBuckets(analysis) {
|
|
650
|
+
function mergeBuckets(analysis, extraBuckets = []) {
|
|
315
651
|
const mergedByIdentity = /* @__PURE__ */ new Map();
|
|
316
652
|
const merged = [];
|
|
317
653
|
const pushBucket = (bucket) => {
|
|
@@ -340,7 +676,9 @@ function mergeBuckets(analysis) {
|
|
|
340
676
|
entities: [...bucket2.entities],
|
|
341
677
|
hint: bucket2.hint,
|
|
342
678
|
overflowCount: bucket2.overflowCount,
|
|
343
|
-
overflowLabel: bucket2.overflowLabel
|
|
679
|
+
overflowLabel: bucket2.overflowLabel,
|
|
680
|
+
coverage: inferFailureBucketCoverage(bucket2, analysis),
|
|
681
|
+
source: "heuristic"
|
|
344
682
|
}))) {
|
|
345
683
|
pushBucket(bucket);
|
|
346
684
|
}
|
|
@@ -364,12 +702,19 @@ function mergeBuckets(analysis) {
|
|
|
364
702
|
coveredLabels.add(item.label);
|
|
365
703
|
}
|
|
366
704
|
}
|
|
705
|
+
for (const bucket of extraBuckets) {
|
|
706
|
+
pushBucket(bucket);
|
|
707
|
+
}
|
|
367
708
|
return merged;
|
|
368
709
|
}
|
|
369
710
|
function dominantBucketPriority(bucket) {
|
|
370
711
|
if (bucket.reason.startsWith("missing test env:")) {
|
|
371
712
|
return 5;
|
|
372
713
|
}
|
|
714
|
+
const extended = findExtendedBucketSpec(bucket.reason);
|
|
715
|
+
if (extended?.dominantPriority !== void 0) {
|
|
716
|
+
return extended.dominantPriority;
|
|
717
|
+
}
|
|
373
718
|
if (bucket.type === "shared_environment_blocker") {
|
|
374
719
|
return 4;
|
|
375
720
|
}
|
|
@@ -379,6 +724,9 @@ function dominantBucketPriority(bucket) {
|
|
|
379
724
|
if (bucket.type === "collection_failure") {
|
|
380
725
|
return 2;
|
|
381
726
|
}
|
|
727
|
+
if (isUnknownBucket(bucket)) {
|
|
728
|
+
return 2;
|
|
729
|
+
}
|
|
382
730
|
if (bucket.type === "contract_snapshot_drift") {
|
|
383
731
|
return 1;
|
|
384
732
|
}
|
|
@@ -400,9 +748,16 @@ function prioritizeBuckets(buckets) {
|
|
|
400
748
|
});
|
|
401
749
|
}
|
|
402
750
|
function isDominantBlockerType(type) {
|
|
403
|
-
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";
|
|
404
752
|
}
|
|
405
753
|
function labelForBucket(bucket) {
|
|
754
|
+
if (bucket.labelOverride) {
|
|
755
|
+
return bucket.labelOverride;
|
|
756
|
+
}
|
|
757
|
+
const extended = findExtendedBucketSpec(bucket.reason);
|
|
758
|
+
if (extended) {
|
|
759
|
+
return extended.label;
|
|
760
|
+
}
|
|
406
761
|
if (bucket.reason.startsWith("missing test env:")) {
|
|
407
762
|
return "missing test env";
|
|
408
763
|
}
|
|
@@ -436,21 +791,40 @@ function labelForBucket(bucket) {
|
|
|
436
791
|
if (bucket.type === "assertion_failure") {
|
|
437
792
|
return "assertion failure";
|
|
438
793
|
}
|
|
794
|
+
if (bucket.type === "snapshot_mismatch") {
|
|
795
|
+
return "snapshot mismatch";
|
|
796
|
+
}
|
|
439
797
|
if (bucket.type === "collection_failure") {
|
|
440
798
|
return "collection failure";
|
|
441
799
|
}
|
|
442
800
|
if (bucket.type === "runtime_failure") {
|
|
443
801
|
return "runtime failure";
|
|
444
802
|
}
|
|
803
|
+
if (bucket.reason.startsWith("unknown setup blocker:")) {
|
|
804
|
+
return "unknown setup blocker";
|
|
805
|
+
}
|
|
806
|
+
if (bucket.reason.startsWith("unknown failure family:")) {
|
|
807
|
+
return "unknown failure family";
|
|
808
|
+
}
|
|
445
809
|
return "unknown failure";
|
|
446
810
|
}
|
|
447
811
|
function rootCauseConfidenceFor(bucket) {
|
|
812
|
+
if (isUnknownBucket(bucket)) {
|
|
813
|
+
return 0.52;
|
|
814
|
+
}
|
|
815
|
+
const extended = findExtendedBucketSpec(bucket.reason);
|
|
816
|
+
if (extended) {
|
|
817
|
+
return extended.rootCauseConfidence;
|
|
818
|
+
}
|
|
448
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:")) {
|
|
449
820
|
return 0.95;
|
|
450
821
|
}
|
|
451
822
|
if (bucket.type === "contract_snapshot_drift") {
|
|
452
823
|
return bucket.entities.length > 0 ? 0.92 : 0.76;
|
|
453
824
|
}
|
|
825
|
+
if (bucket.source === "provider") {
|
|
826
|
+
return Math.max(0.6, Math.min(bucket.confidence, 0.82));
|
|
827
|
+
}
|
|
454
828
|
return Math.max(0.6, Math.min(bucket.confidence, 0.88));
|
|
455
829
|
}
|
|
456
830
|
function buildBucketEvidence(bucket) {
|
|
@@ -482,6 +856,10 @@ function buildReadTargetWhy(args) {
|
|
|
482
856
|
if (envVar) {
|
|
483
857
|
return `it contains the ${envVar} setup guard`;
|
|
484
858
|
}
|
|
859
|
+
const extended = findExtendedBucketSpec(args.bucket.reason);
|
|
860
|
+
if (extended) {
|
|
861
|
+
return extended.why;
|
|
862
|
+
}
|
|
485
863
|
if (args.bucket.reason.startsWith("fixture guard:")) {
|
|
486
864
|
return "it contains the fixture/setup guard behind this bucket";
|
|
487
865
|
}
|
|
@@ -494,6 +872,12 @@ function buildReadTargetWhy(args) {
|
|
|
494
872
|
if (args.bucket.reason.startsWith("auth bypass absent:")) {
|
|
495
873
|
return "it contains the auth bypass setup behind this bucket";
|
|
496
874
|
}
|
|
875
|
+
if (args.bucket.reason.startsWith("unknown setup blocker:")) {
|
|
876
|
+
return "it is the first anchored setup failure in this unknown bucket";
|
|
877
|
+
}
|
|
878
|
+
if (args.bucket.reason.startsWith("unknown failure family:")) {
|
|
879
|
+
return "it is the first anchored failing test in this unknown bucket";
|
|
880
|
+
}
|
|
497
881
|
if (args.bucket.type === "contract_snapshot_drift") {
|
|
498
882
|
if (args.bucketLabel === "route drift") {
|
|
499
883
|
return "it maps to the visible route drift bucket";
|
|
@@ -506,6 +890,9 @@ function buildReadTargetWhy(args) {
|
|
|
506
890
|
}
|
|
507
891
|
return "it maps to the visible stale snapshot expectation";
|
|
508
892
|
}
|
|
893
|
+
if (args.bucket.type === "snapshot_mismatch") {
|
|
894
|
+
return "it maps to the visible snapshot mismatch bucket";
|
|
895
|
+
}
|
|
509
896
|
if (args.bucket.type === "import_dependency_failure") {
|
|
510
897
|
return "it is the first visible failing module in this missing dependency bucket";
|
|
511
898
|
}
|
|
@@ -517,11 +904,54 @@ function buildReadTargetWhy(args) {
|
|
|
517
904
|
}
|
|
518
905
|
return `it maps to the visible ${args.bucketLabel} bucket`;
|
|
519
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
|
+
}
|
|
520
946
|
function buildReadTargetSearchHint(bucket, anchor) {
|
|
521
947
|
const envVar = bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
|
|
522
948
|
if (envVar) {
|
|
523
949
|
return envVar;
|
|
524
950
|
}
|
|
951
|
+
const extendedHint = buildExtendedBucketSearchHint(bucket, anchor);
|
|
952
|
+
if (extendedHint) {
|
|
953
|
+
return extendedHint;
|
|
954
|
+
}
|
|
525
955
|
if (bucket.type === "contract_snapshot_drift") {
|
|
526
956
|
return bucket.entities.find((value) => value.startsWith("/api/")) ?? bucket.entities[0] ?? null;
|
|
527
957
|
}
|
|
@@ -543,6 +973,9 @@ function buildReadTargetSearchHint(bucket, anchor) {
|
|
|
543
973
|
if (assertionText) {
|
|
544
974
|
return assertionText;
|
|
545
975
|
}
|
|
976
|
+
if (bucket.reason.startsWith("unknown ")) {
|
|
977
|
+
return anchor.reason;
|
|
978
|
+
}
|
|
546
979
|
const fallbackLabel = anchor.label.split("::")[1]?.trim();
|
|
547
980
|
return fallbackLabel || null;
|
|
548
981
|
}
|
|
@@ -602,6 +1035,12 @@ function buildConcreteNextNote(args) {
|
|
|
602
1035
|
if (args.nextBestAction.code === "read_source_for_bucket") {
|
|
603
1036
|
return lead;
|
|
604
1037
|
}
|
|
1038
|
+
if (args.nextBestAction.code === "insufficient_signal") {
|
|
1039
|
+
if (args.nextBestAction.note.startsWith("Provider follow-up failed")) {
|
|
1040
|
+
return args.nextBestAction.note;
|
|
1041
|
+
}
|
|
1042
|
+
return `${lead} Then take one deeper sift pass before raw traceback.`;
|
|
1043
|
+
}
|
|
605
1044
|
return args.nextBestAction.note;
|
|
606
1045
|
}
|
|
607
1046
|
function extractMiniDiff(input, bucket) {
|
|
@@ -626,6 +1065,156 @@ function extractMiniDiff(input, bucket) {
|
|
|
626
1065
|
...changedTaskMappings > 0 ? { changed_task_mappings: changedTaskMappings } : {}
|
|
627
1066
|
};
|
|
628
1067
|
}
|
|
1068
|
+
function inferSupplementCoverageKind(args) {
|
|
1069
|
+
const extended = findExtendedBucketSpec(args.rootCause);
|
|
1070
|
+
if (extended?.defaultCoverage === "error" || extended?.defaultCoverage === "failed") {
|
|
1071
|
+
return extended.defaultCoverage;
|
|
1072
|
+
}
|
|
1073
|
+
const normalized = `${args.label} ${args.rootCause}`.toLowerCase();
|
|
1074
|
+
if (/env|setup|fixture|import|dependency|service|db|database|auth bypass|collection|connection refused/.test(
|
|
1075
|
+
normalized
|
|
1076
|
+
)) {
|
|
1077
|
+
return "error";
|
|
1078
|
+
}
|
|
1079
|
+
if (/snapshot|contract|drift|assertion|expected|actual|golden/.test(normalized)) {
|
|
1080
|
+
return "failed";
|
|
1081
|
+
}
|
|
1082
|
+
if (args.remainingErrors > 0 && args.remainingFailed === 0) {
|
|
1083
|
+
return "error";
|
|
1084
|
+
}
|
|
1085
|
+
return "failed";
|
|
1086
|
+
}
|
|
1087
|
+
function buildProviderSupplementBuckets(args) {
|
|
1088
|
+
let remainingErrors = args.remainingErrors;
|
|
1089
|
+
let remainingFailed = args.remainingFailed;
|
|
1090
|
+
return args.supplements.flatMap((supplement) => {
|
|
1091
|
+
const coverageKind = inferSupplementCoverageKind({
|
|
1092
|
+
label: supplement.label,
|
|
1093
|
+
rootCause: supplement.root_cause,
|
|
1094
|
+
remainingErrors,
|
|
1095
|
+
remainingFailed
|
|
1096
|
+
});
|
|
1097
|
+
const budget = coverageKind === "error" ? remainingErrors : remainingFailed;
|
|
1098
|
+
const count = Math.max(0, Math.min(supplement.count, budget));
|
|
1099
|
+
if (count === 0) {
|
|
1100
|
+
return [];
|
|
1101
|
+
}
|
|
1102
|
+
if (coverageKind === "error") {
|
|
1103
|
+
remainingErrors -= count;
|
|
1104
|
+
} else {
|
|
1105
|
+
remainingFailed -= count;
|
|
1106
|
+
}
|
|
1107
|
+
const representativeLabel = supplement.anchor.file ?? `${supplement.label} supplement`;
|
|
1108
|
+
const representativeItem = {
|
|
1109
|
+
label: representativeLabel,
|
|
1110
|
+
reason: supplement.root_cause,
|
|
1111
|
+
group: supplement.label,
|
|
1112
|
+
file: supplement.anchor.file,
|
|
1113
|
+
line: supplement.anchor.line,
|
|
1114
|
+
anchor_kind: supplement.anchor.file && supplement.anchor.line !== null ? "traceback" : supplement.anchor.file ? "test_label" : supplement.anchor.search_hint ? "entity" : "none",
|
|
1115
|
+
anchor_confidence: Math.max(0.4, Math.min(supplement.confidence, 0.82))
|
|
1116
|
+
};
|
|
1117
|
+
return [
|
|
1118
|
+
{
|
|
1119
|
+
type: classifyGenericBucketType(supplement.root_cause),
|
|
1120
|
+
headline: `${supplement.label}: ${formatCount(count, "visible failure")} share ${supplement.root_cause}.`,
|
|
1121
|
+
summaryLines: [
|
|
1122
|
+
`${supplement.label}: ${formatCount(count, "visible failure")} share ${supplement.root_cause}.`
|
|
1123
|
+
],
|
|
1124
|
+
reason: supplement.root_cause,
|
|
1125
|
+
count,
|
|
1126
|
+
confidence: Math.max(0.4, Math.min(supplement.confidence, 0.82)),
|
|
1127
|
+
representativeItems: [representativeItem],
|
|
1128
|
+
entities: supplement.anchor.search_hint ? [supplement.anchor.search_hint] : [],
|
|
1129
|
+
hint: supplement.fix_hint ?? void 0,
|
|
1130
|
+
overflowCount: Math.max(count - 1, 0),
|
|
1131
|
+
overflowLabel: "failing tests/modules",
|
|
1132
|
+
labelOverride: supplement.label,
|
|
1133
|
+
coverage: buildCoverageCounts({
|
|
1134
|
+
count,
|
|
1135
|
+
coverageKind
|
|
1136
|
+
}),
|
|
1137
|
+
source: "provider"
|
|
1138
|
+
}
|
|
1139
|
+
];
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
function pickUnknownAnchor(args) {
|
|
1143
|
+
const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : null;
|
|
1144
|
+
if (fromStatusItems) {
|
|
1145
|
+
return {
|
|
1146
|
+
label: fromStatusItems.label,
|
|
1147
|
+
reason: fromStatusItems.reason,
|
|
1148
|
+
group: fromStatusItems.group,
|
|
1149
|
+
file: fromStatusItems.file,
|
|
1150
|
+
line: fromStatusItems.line,
|
|
1151
|
+
anchor_kind: fromStatusItems.anchor_kind,
|
|
1152
|
+
anchor_confidence: fromStatusItems.anchor_confidence
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
const label = args.kind === "error" ? args.analysis.visibleErrorLabels[0] : args.analysis.visibleFailedLabels[0];
|
|
1156
|
+
if (label) {
|
|
1157
|
+
const normalizedLabel = normalizeTestId(label);
|
|
1158
|
+
const fileMatch = normalizedLabel.match(/^([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)\b/);
|
|
1159
|
+
const file = fileMatch?.[1] ?? normalizedLabel.split("::")[0] ?? null;
|
|
1160
|
+
return {
|
|
1161
|
+
label,
|
|
1162
|
+
reason: args.kind === "error" ? "setup failures share a repeated but unclassified pattern" : "failing tests share a repeated but unclassified pattern",
|
|
1163
|
+
group: args.kind === "error" ? "unknown setup blocker" : "unknown failure family",
|
|
1164
|
+
file: file && file !== label ? file : null,
|
|
1165
|
+
line: null,
|
|
1166
|
+
anchor_kind: file && file !== label ? "test_label" : "none",
|
|
1167
|
+
anchor_confidence: file && file !== label ? 0.6 : 0
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
function buildUnknownBucket(args) {
|
|
1173
|
+
if (args.count <= 0) {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
const anchor = pickUnknownAnchor(args);
|
|
1177
|
+
const isError = args.kind === "error";
|
|
1178
|
+
const label = isError ? "unknown setup blocker" : "unknown failure family";
|
|
1179
|
+
const reason = isError ? "unknown setup blocker: setup failures share a repeated but unclassified pattern" : "unknown failure family: failing tests share a repeated but unclassified pattern";
|
|
1180
|
+
return {
|
|
1181
|
+
type: "unknown_failure",
|
|
1182
|
+
headline: `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
|
|
1183
|
+
summaryLines: [
|
|
1184
|
+
`${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`
|
|
1185
|
+
],
|
|
1186
|
+
reason,
|
|
1187
|
+
count: args.count,
|
|
1188
|
+
confidence: 0.45,
|
|
1189
|
+
representativeItems: anchor ? [anchor] : [],
|
|
1190
|
+
entities: [],
|
|
1191
|
+
hint: isError ? "Take one deeper sift pass or inspect the first anchored setup failure." : "Take one deeper sift pass or inspect the first anchored failing test.",
|
|
1192
|
+
overflowCount: Math.max(args.count - (anchor ? 1 : 0), 0),
|
|
1193
|
+
overflowLabel: "failing tests/modules",
|
|
1194
|
+
labelOverride: label,
|
|
1195
|
+
coverage: buildCoverageCounts({
|
|
1196
|
+
count: args.count,
|
|
1197
|
+
coverageKind: isError ? "error" : "failed"
|
|
1198
|
+
}),
|
|
1199
|
+
source: "unknown"
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
function buildCoverageResiduals(args) {
|
|
1203
|
+
const covered = args.buckets.reduce(
|
|
1204
|
+
(totals, bucket) => ({
|
|
1205
|
+
error: totals.error + bucket.coverage.error,
|
|
1206
|
+
failed: totals.failed + bucket.coverage.failed
|
|
1207
|
+
}),
|
|
1208
|
+
{
|
|
1209
|
+
error: 0,
|
|
1210
|
+
failed: 0
|
|
1211
|
+
}
|
|
1212
|
+
);
|
|
1213
|
+
return {
|
|
1214
|
+
remainingErrors: Math.max(args.analysis.errors - Math.min(args.analysis.errors, covered.error), 0),
|
|
1215
|
+
remainingFailed: Math.max(args.analysis.failed - Math.min(args.analysis.failed, covered.failed), 0)
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
629
1218
|
function buildOutcomeLines(analysis) {
|
|
630
1219
|
if (analysis.noTestsCollected) {
|
|
631
1220
|
return ["- Tests did not run.", "- Collected 0 items."];
|
|
@@ -724,6 +1313,10 @@ function buildStandardFixText(args) {
|
|
|
724
1313
|
if (args.bucket.hint) {
|
|
725
1314
|
return args.bucket.hint;
|
|
726
1315
|
}
|
|
1316
|
+
const extended = findExtendedBucketSpec(args.bucket.reason);
|
|
1317
|
+
if (extended) {
|
|
1318
|
+
return extended.fix;
|
|
1319
|
+
}
|
|
727
1320
|
const envVar = args.bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
|
|
728
1321
|
if (envVar) {
|
|
729
1322
|
return `Set ${envVar} before rerunning the affected tests.`;
|
|
@@ -744,9 +1337,18 @@ function buildStandardFixText(args) {
|
|
|
744
1337
|
if (args.bucket.reason.startsWith("auth bypass absent:")) {
|
|
745
1338
|
return "Restore the test auth bypass setup and rerun the full suite at standard.";
|
|
746
1339
|
}
|
|
1340
|
+
if (args.bucket.reason.startsWith("unknown setup blocker:")) {
|
|
1341
|
+
return "Take one deeper sift pass or inspect the first anchored setup failure before rerunning.";
|
|
1342
|
+
}
|
|
1343
|
+
if (args.bucket.reason.startsWith("unknown failure family:")) {
|
|
1344
|
+
return "Take one deeper sift pass or inspect the first anchored failing test before rerunning.";
|
|
1345
|
+
}
|
|
747
1346
|
if (args.bucket.type === "contract_snapshot_drift") {
|
|
748
1347
|
return "Review the visible drift and regenerate the contract snapshots if the changes are intentional.";
|
|
749
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
|
+
}
|
|
750
1352
|
if (args.bucket.type === "assertion_failure") {
|
|
751
1353
|
return "Inspect the failing assertion and rerun the full suite at standard.";
|
|
752
1354
|
}
|
|
@@ -840,7 +1442,35 @@ function renderVerbose(args) {
|
|
|
840
1442
|
return lines.join("\n");
|
|
841
1443
|
}
|
|
842
1444
|
function buildTestStatusDiagnoseContract(args) {
|
|
843
|
-
const
|
|
1445
|
+
const heuristicBuckets = mergeBuckets(args.analysis);
|
|
1446
|
+
const preUnknownSimpleCollectionFailure = args.analysis.collectionErrorCount !== void 0 && args.analysis.collectionItems.length === 0 && heuristicBuckets.length === 0 && (args.providerBucketSupplements?.length ?? 0) === 0;
|
|
1447
|
+
const heuristicResiduals = buildCoverageResiduals({
|
|
1448
|
+
analysis: args.analysis,
|
|
1449
|
+
buckets: heuristicBuckets
|
|
1450
|
+
});
|
|
1451
|
+
const providerSupplementBuckets = buildProviderSupplementBuckets({
|
|
1452
|
+
supplements: args.providerBucketSupplements ?? [],
|
|
1453
|
+
remainingErrors: heuristicResiduals.remainingErrors,
|
|
1454
|
+
remainingFailed: heuristicResiduals.remainingFailed
|
|
1455
|
+
});
|
|
1456
|
+
const combinedBuckets = mergeBuckets(args.analysis, providerSupplementBuckets);
|
|
1457
|
+
const residuals = buildCoverageResiduals({
|
|
1458
|
+
analysis: args.analysis,
|
|
1459
|
+
buckets: combinedBuckets
|
|
1460
|
+
});
|
|
1461
|
+
const unknownBuckets = preUnknownSimpleCollectionFailure ? [] : [
|
|
1462
|
+
buildUnknownBucket({
|
|
1463
|
+
analysis: args.analysis,
|
|
1464
|
+
kind: "error",
|
|
1465
|
+
count: residuals.remainingErrors
|
|
1466
|
+
}),
|
|
1467
|
+
buildUnknownBucket({
|
|
1468
|
+
analysis: args.analysis,
|
|
1469
|
+
kind: "failed",
|
|
1470
|
+
count: residuals.remainingFailed
|
|
1471
|
+
})
|
|
1472
|
+
].filter((bucket) => Boolean(bucket));
|
|
1473
|
+
const buckets = prioritizeBuckets([...combinedBuckets, ...unknownBuckets]).slice(0, 3);
|
|
844
1474
|
const simpleCollectionFailure = args.analysis.collectionErrorCount !== void 0 && args.analysis.collectionItems.length === 0 && buckets.length === 0;
|
|
845
1475
|
const dominantBucket = buckets.map((bucket, index) => ({
|
|
846
1476
|
bucket,
|
|
@@ -851,8 +1481,10 @@ function buildTestStatusDiagnoseContract(args) {
|
|
|
851
1481
|
}
|
|
852
1482
|
return right.bucket.confidence - left.bucket.confidence;
|
|
853
1483
|
})[0] ?? null;
|
|
854
|
-
const
|
|
855
|
-
const
|
|
1484
|
+
const hasUnknownBucket = buckets.some((bucket) => isUnknownBucket(bucket));
|
|
1485
|
+
const hasConcreteCoverage = args.analysis.failed === 0 && args.analysis.errors === 0 ? true : residuals.remainingErrors === 0 && residuals.remainingFailed === 0;
|
|
1486
|
+
const diagnosisComplete = args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure || buckets.length > 0 && hasConcreteCoverage && !hasUnknownBucket && (dominantBucket?.bucket.confidence ?? 0) >= 0.6;
|
|
1487
|
+
const rawNeeded = buckets.length === 0 ? !(args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure) : !diagnosisComplete && !hasUnknownBucket && buckets.every((bucket) => bucket.confidence < 0.7);
|
|
856
1488
|
const dominantBlockerBucketIndex = dominantBucket && isDominantBlockerType(dominantBucket.bucket.type) ? dominantBucket.index + 1 : null;
|
|
857
1489
|
const readTargets = buildReadTargets({
|
|
858
1490
|
buckets,
|
|
@@ -887,6 +1519,12 @@ function buildTestStatusDiagnoseContract(args) {
|
|
|
887
1519
|
bucket_index: null,
|
|
888
1520
|
note: "Inspect the collection traceback or setup code next; the run failed before tests executed."
|
|
889
1521
|
};
|
|
1522
|
+
} else if (hasUnknownBucket) {
|
|
1523
|
+
nextBestAction = {
|
|
1524
|
+
code: "insufficient_signal",
|
|
1525
|
+
bucket_index: dominantBucket ? dominantBucket.index + 1 : null,
|
|
1526
|
+
note: "Take one deeper sift pass or inspect the first anchored failure before falling back to raw traceback."
|
|
1527
|
+
};
|
|
890
1528
|
} else if (!diagnosisComplete) {
|
|
891
1529
|
nextBestAction = {
|
|
892
1530
|
code: rawNeeded ? "read_raw_for_exact_traceback" : "insufficient_signal",
|
|
@@ -924,11 +1562,15 @@ function buildTestStatusDiagnoseContract(args) {
|
|
|
924
1562
|
read_targets: readTargets,
|
|
925
1563
|
next_best_action: nextBestAction
|
|
926
1564
|
};
|
|
1565
|
+
const effectiveDiagnosisComplete = Boolean(args.contractOverrides?.diagnosis_complete ?? diagnosisComplete) && !hasUnknownBucket;
|
|
1566
|
+
const requestedDecision = args.contractOverrides?.decision;
|
|
1567
|
+
const effectiveDecision = hasUnknownBucket && requestedDecision && (requestedDecision === "stop" || requestedDecision === "read_source") ? "zoom" : requestedDecision;
|
|
927
1568
|
const effectiveNextBestAction = args.contractOverrides?.next_best_action ?? baseContract.next_best_action;
|
|
928
1569
|
const mergedContractWithoutDecision = {
|
|
929
1570
|
...baseContract,
|
|
930
1571
|
...args.contractOverrides,
|
|
931
|
-
|
|
1572
|
+
diagnosis_complete: effectiveDiagnosisComplete,
|
|
1573
|
+
status: effectiveDiagnosisComplete ? "ok" : "insufficient",
|
|
932
1574
|
next_best_action: {
|
|
933
1575
|
...effectiveNextBestAction,
|
|
934
1576
|
note: buildConcreteNextNote({
|
|
@@ -942,7 +1584,7 @@ function buildTestStatusDiagnoseContract(args) {
|
|
|
942
1584
|
};
|
|
943
1585
|
const contract = testStatusDiagnoseContractSchema.parse({
|
|
944
1586
|
...mergedContractWithoutDecision,
|
|
945
|
-
decision:
|
|
1587
|
+
decision: effectiveDecision ?? deriveDecision(mergedContractWithoutDecision)
|
|
946
1588
|
});
|
|
947
1589
|
return {
|
|
948
1590
|
contract,
|
|
@@ -1032,6 +1674,63 @@ function getCount(input, label) {
|
|
|
1032
1674
|
const lastMatch = matches.at(-1);
|
|
1033
1675
|
return lastMatch ? Number(lastMatch[1]) : 0;
|
|
1034
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
|
+
}
|
|
1035
1734
|
function formatCount2(count, singular, plural = `${singular}s`) {
|
|
1036
1735
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
1037
1736
|
}
|
|
@@ -1064,7 +1763,8 @@ function normalizeAnchorFile(value) {
|
|
|
1064
1763
|
return value.replace(/\\/g, "/").trim();
|
|
1065
1764
|
}
|
|
1066
1765
|
function inferFileFromLabel(label) {
|
|
1067
|
-
const
|
|
1766
|
+
const cleaned = cleanFailureLabel(label);
|
|
1767
|
+
const candidate = cleaned.split("::")[0]?.split(" > ")[0]?.trim();
|
|
1068
1768
|
if (!candidate) {
|
|
1069
1769
|
return null;
|
|
1070
1770
|
}
|
|
@@ -1119,6 +1819,15 @@ function parseObservedAnchor(line) {
|
|
|
1119
1819
|
anchor_confidence: 0.92
|
|
1120
1820
|
};
|
|
1121
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
|
+
}
|
|
1122
1831
|
return null;
|
|
1123
1832
|
}
|
|
1124
1833
|
function resolveAnchorForLabel(args) {
|
|
@@ -1135,15 +1844,27 @@ function isLowValueInternalReason(normalized) {
|
|
|
1135
1844
|
) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
|
|
1136
1845
|
}
|
|
1137
1846
|
function scoreFailureReason(reason) {
|
|
1847
|
+
if (reason.startsWith("configuration:")) {
|
|
1848
|
+
return 6;
|
|
1849
|
+
}
|
|
1138
1850
|
if (reason.startsWith("missing test env:")) {
|
|
1139
1851
|
return 6;
|
|
1140
1852
|
}
|
|
1141
1853
|
if (reason.startsWith("missing module:")) {
|
|
1142
1854
|
return 5;
|
|
1143
1855
|
}
|
|
1856
|
+
if (reason.startsWith("snapshot mismatch:")) {
|
|
1857
|
+
return 4;
|
|
1858
|
+
}
|
|
1144
1859
|
if (reason.startsWith("assertion failed:")) {
|
|
1145
1860
|
return 4;
|
|
1146
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
|
+
}
|
|
1147
1868
|
if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
|
|
1148
1869
|
return 3;
|
|
1149
1870
|
}
|
|
@@ -1152,6 +1873,16 @@ function scoreFailureReason(reason) {
|
|
|
1152
1873
|
}
|
|
1153
1874
|
return 1;
|
|
1154
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
|
+
}
|
|
1155
1886
|
function extractEnvBlockerName(normalized) {
|
|
1156
1887
|
const directMatch = normalized.match(
|
|
1157
1888
|
/\bDB-isolated tests require\s+([A-Z][A-Z0-9_]{2,})\b/
|
|
@@ -1162,7 +1893,25 @@ function extractEnvBlockerName(normalized) {
|
|
|
1162
1893
|
const fallbackMatch = normalized.match(
|
|
1163
1894
|
/\b([A-Z][A-Z0-9_]{2,})\b(?=[^.\n]*DB-isolated tests)/
|
|
1164
1895
|
);
|
|
1165
|
-
|
|
1896
|
+
if (fallbackMatch) {
|
|
1897
|
+
return fallbackMatch[1];
|
|
1898
|
+
}
|
|
1899
|
+
const leadingEnvMatch = normalized.match(
|
|
1900
|
+
/\b([A-Z][A-Z0-9_]{2,})\b(?=[^.\n]{0,80}\b(?:is\s+)?(?:missing|unset|not set|not configured|required)\b)/
|
|
1901
|
+
);
|
|
1902
|
+
if (leadingEnvMatch) {
|
|
1903
|
+
return leadingEnvMatch[1];
|
|
1904
|
+
}
|
|
1905
|
+
const trailingEnvMatch = normalized.match(
|
|
1906
|
+
/\b(?:missing|unset|not set|not configured|required)\b[^.\n]{0,80}\b([A-Z][A-Z0-9_]{2,})\b/
|
|
1907
|
+
);
|
|
1908
|
+
if (trailingEnvMatch) {
|
|
1909
|
+
return trailingEnvMatch[1];
|
|
1910
|
+
}
|
|
1911
|
+
const validationEnvMatch = normalized.match(
|
|
1912
|
+
/\bValidationError\b[^.\n]{0,120}\b([A-Z][A-Z0-9_]{2,})\b/
|
|
1913
|
+
);
|
|
1914
|
+
return validationEnvMatch?.[1] ?? null;
|
|
1166
1915
|
}
|
|
1167
1916
|
function classifyFailureReason(line, options) {
|
|
1168
1917
|
const normalized = line.trim().replace(/^[A-Z]\s+/, "");
|
|
@@ -1183,7 +1932,7 @@ function classifyFailureReason(line, options) {
|
|
|
1183
1932
|
};
|
|
1184
1933
|
}
|
|
1185
1934
|
const missingEnv = normalized.match(
|
|
1186
|
-
/\b(?:environment variable|env(?:ironment)? var(?:iable)?|
|
|
1935
|
+
/\b(?:environment variable|env(?:ironment)? var(?:iable)?|missing required env(?:ironment)? variable)\s+([A-Z][A-Z0-9_]{2,})\b/
|
|
1187
1936
|
);
|
|
1188
1937
|
if (missingEnv) {
|
|
1189
1938
|
return {
|
|
@@ -1215,6 +1964,12 @@ function classifyFailureReason(line, options) {
|
|
|
1215
1964
|
group: "database connectivity failures"
|
|
1216
1965
|
};
|
|
1217
1966
|
}
|
|
1967
|
+
if (/(ECONNREFUSED|ConnectionRefusedError|connection refused)/i.test(normalized)) {
|
|
1968
|
+
return {
|
|
1969
|
+
reason: "service unavailable: dependency connection was refused",
|
|
1970
|
+
group: "service availability failures"
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1218
1973
|
if (/(503\b|service unavailable|temporarily unavailable)/i.test(normalized)) {
|
|
1219
1974
|
return {
|
|
1220
1975
|
reason: "service unavailable: dependency service is unavailable",
|
|
@@ -1227,6 +1982,226 @@ function classifyFailureReason(line, options) {
|
|
|
1227
1982
|
group: "authentication test setup failures"
|
|
1228
1983
|
};
|
|
1229
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
|
+
}
|
|
1230
2205
|
const pythonMissingModule = normalized.match(
|
|
1231
2206
|
/ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
|
|
1232
2207
|
);
|
|
@@ -1243,6 +2218,20 @@ function classifyFailureReason(line, options) {
|
|
|
1243
2218
|
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
1244
2219
|
};
|
|
1245
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
|
+
}
|
|
1246
2235
|
const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
|
|
1247
2236
|
if (assertionFailure) {
|
|
1248
2237
|
return {
|
|
@@ -1250,6 +2239,16 @@ function classifyFailureReason(line, options) {
|
|
|
1250
2239
|
group: "assertion failures"
|
|
1251
2240
|
};
|
|
1252
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
|
+
}
|
|
1253
2252
|
const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
|
|
1254
2253
|
if (genericError) {
|
|
1255
2254
|
const errorType = genericError[1];
|
|
@@ -1294,6 +2293,125 @@ function chooseStrongestFailureItems(items) {
|
|
|
1294
2293
|
}
|
|
1295
2294
|
return order.map((label) => strongest.get(label));
|
|
1296
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
|
+
}
|
|
1297
2415
|
function collectCollectionFailureItems(input) {
|
|
1298
2416
|
const items = [];
|
|
1299
2417
|
const lines = input.split("\n");
|
|
@@ -1301,6 +2419,24 @@ function collectCollectionFailureItems(input) {
|
|
|
1301
2419
|
let pendingGenericReason = null;
|
|
1302
2420
|
let currentAnchor = null;
|
|
1303
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
|
+
}
|
|
1304
2440
|
const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
|
|
1305
2441
|
if (collecting) {
|
|
1306
2442
|
if (currentLabel && pendingGenericReason) {
|
|
@@ -1389,6 +2525,24 @@ function collectInlineFailureItems(input) {
|
|
|
1389
2525
|
})
|
|
1390
2526
|
});
|
|
1391
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
|
+
}
|
|
1392
2546
|
return items;
|
|
1393
2547
|
}
|
|
1394
2548
|
function collectInlineFailureItemsWithStatus(input) {
|
|
@@ -1423,16 +2577,42 @@ function collectInlineFailureItemsWithStatus(input) {
|
|
|
1423
2577
|
})
|
|
1424
2578
|
});
|
|
1425
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
|
+
}
|
|
1426
2599
|
return items;
|
|
1427
2600
|
}
|
|
1428
2601
|
function collectStandaloneErrorClassifications(input) {
|
|
1429
2602
|
const classifications = [];
|
|
1430
2603
|
for (const line of input.split("\n")) {
|
|
2604
|
+
const trimmed = line.trim();
|
|
2605
|
+
if (!trimmed) {
|
|
2606
|
+
continue;
|
|
2607
|
+
}
|
|
1431
2608
|
const standalone = line.match(/^\s*E\s+(.+)$/);
|
|
1432
|
-
|
|
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) {
|
|
1433
2613
|
continue;
|
|
1434
2614
|
}
|
|
1435
|
-
const classification = classifyFailureReason(
|
|
2615
|
+
const classification = classifyFailureReason(candidate, {
|
|
1436
2616
|
duringCollection: false
|
|
1437
2617
|
});
|
|
1438
2618
|
if (!classification || classification.reason === "import error during collection") {
|
|
@@ -1548,6 +2728,9 @@ function collectFailureLabels(input) {
|
|
|
1548
2728
|
pushLabel(summary[2], summary[1] === "FAILED" ? "failed" : "error");
|
|
1549
2729
|
}
|
|
1550
2730
|
}
|
|
2731
|
+
for (const block of collectJsFailureBlocks(input)) {
|
|
2732
|
+
pushLabel(block.label, block.status);
|
|
2733
|
+
}
|
|
1551
2734
|
return labels;
|
|
1552
2735
|
}
|
|
1553
2736
|
function classifyBucketTypeFromReason(reason) {
|
|
@@ -1557,6 +2740,60 @@ function classifyBucketTypeFromReason(reason) {
|
|
|
1557
2740
|
if (reason.startsWith("fixture guard:")) {
|
|
1558
2741
|
return "fixture_guard_failure";
|
|
1559
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
|
+
}
|
|
1560
2797
|
if (reason.startsWith("service unavailable:")) {
|
|
1561
2798
|
return "service_unavailable";
|
|
1562
2799
|
}
|
|
@@ -1566,6 +2803,9 @@ function classifyBucketTypeFromReason(reason) {
|
|
|
1566
2803
|
if (reason.startsWith("auth bypass absent:")) {
|
|
1567
2804
|
return "auth_bypass_absent";
|
|
1568
2805
|
}
|
|
2806
|
+
if (reason.startsWith("snapshot mismatch:")) {
|
|
2807
|
+
return "snapshot_mismatch";
|
|
2808
|
+
}
|
|
1569
2809
|
if (reason.startsWith("missing module:")) {
|
|
1570
2810
|
return "import_dependency_failure";
|
|
1571
2811
|
}
|
|
@@ -1578,9 +2818,6 @@ function classifyBucketTypeFromReason(reason) {
|
|
|
1578
2818
|
return "unknown_failure";
|
|
1579
2819
|
}
|
|
1580
2820
|
function synthesizeSharedBlockerBucket(args) {
|
|
1581
|
-
if (args.errors === 0) {
|
|
1582
|
-
return null;
|
|
1583
|
-
}
|
|
1584
2821
|
const visibleReasonGroups = /* @__PURE__ */ new Map();
|
|
1585
2822
|
for (const item of args.visibleErrorItems) {
|
|
1586
2823
|
const entry = visibleReasonGroups.get(item.reason);
|
|
@@ -1595,7 +2832,7 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
1595
2832
|
items: [item]
|
|
1596
2833
|
});
|
|
1597
2834
|
}
|
|
1598
|
-
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];
|
|
1599
2836
|
const standaloneReasonGroups = /* @__PURE__ */ new Map();
|
|
1600
2837
|
for (const classification of collectStandaloneErrorClassifications(args.input)) {
|
|
1601
2838
|
const entry = standaloneReasonGroups.get(classification.reason);
|
|
@@ -1608,7 +2845,7 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
1608
2845
|
group: classification.group
|
|
1609
2846
|
});
|
|
1610
2847
|
}
|
|
1611
|
-
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];
|
|
1612
2849
|
const visibleTopReason = top?.[0];
|
|
1613
2850
|
const visibleTopStats = top?.[1];
|
|
1614
2851
|
const standaloneTopReason = standaloneTop?.[0];
|
|
@@ -1647,6 +2884,12 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
1647
2884
|
let hint;
|
|
1648
2885
|
if (envVar) {
|
|
1649
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.";
|
|
1650
2893
|
} else if (effectiveReason.startsWith("fixture guard:")) {
|
|
1651
2894
|
hint = "Unblock the required fixture or setup guard before rerunning the affected tests.";
|
|
1652
2895
|
} else if (effectiveReason.startsWith("db refused:")) {
|
|
@@ -1661,6 +2904,12 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
1661
2904
|
let headline;
|
|
1662
2905
|
if (envVar) {
|
|
1663
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.`;
|
|
1664
2913
|
} else if (effectiveReason.startsWith("fixture guard:")) {
|
|
1665
2914
|
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are gated by the same fixture/setup guard.`;
|
|
1666
2915
|
} else if (effectiveReason.startsWith("db refused:")) {
|
|
@@ -1691,22 +2940,28 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
1691
2940
|
};
|
|
1692
2941
|
}
|
|
1693
2942
|
function synthesizeImportDependencyBucket(args) {
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
const
|
|
1698
|
-
|
|
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) {
|
|
1699
2954
|
return null;
|
|
1700
2955
|
}
|
|
1701
2956
|
const allVisibleErrorsAreImportRelated = args.visibleErrorItems.length > 0 && args.visibleErrorItems.every((item) => item.reason.startsWith("missing module:"));
|
|
1702
|
-
const countClaimed = allVisibleErrorsAreImportRelated && importItems.length >=
|
|
2957
|
+
const countClaimed = allVisibleErrorsAreImportRelated && importItems.length >= 2 && args.errors >= importItems.length ? args.errors : void 0;
|
|
1703
2958
|
const modules = Array.from(
|
|
1704
2959
|
new Set(
|
|
1705
2960
|
importItems.map((item) => item.reason.replace("missing module:", "").trim()).filter(Boolean)
|
|
1706
2961
|
)
|
|
1707
2962
|
).slice(0, 6);
|
|
1708
2963
|
const headlineCount = countClaimed ?? importItems.length;
|
|
1709
|
-
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.`;
|
|
1710
2965
|
const summaryLines = [headline];
|
|
1711
2966
|
if (modules.length > 0) {
|
|
1712
2967
|
summaryLines.push(`Missing modules include ${modules.join(", ")}.`);
|
|
@@ -1716,7 +2971,7 @@ function synthesizeImportDependencyBucket(args) {
|
|
|
1716
2971
|
headline,
|
|
1717
2972
|
countVisible: importItems.length,
|
|
1718
2973
|
countClaimed,
|
|
1719
|
-
reason: "missing dependencies during test collection",
|
|
2974
|
+
reason: modules.length === 1 ? `missing module: ${modules[0]}` : "missing dependencies during test collection",
|
|
1720
2975
|
representativeItems: importItems.slice(0, 4).map((item) => ({
|
|
1721
2976
|
label: item.label,
|
|
1722
2977
|
reason: item.reason,
|
|
@@ -1735,7 +2990,7 @@ function synthesizeImportDependencyBucket(args) {
|
|
|
1735
2990
|
};
|
|
1736
2991
|
}
|
|
1737
2992
|
function isContractDriftLabel(label) {
|
|
1738
|
-
return /(freeze|
|
|
2993
|
+
return /(freeze|contract|manifest|openapi|golden)/i.test(label);
|
|
1739
2994
|
}
|
|
1740
2995
|
function looksLikeTaskKey(value) {
|
|
1741
2996
|
return /^[a-z]+(?:_[a-z0-9]+)+$/i.test(value) && !value.startsWith("/api/");
|
|
@@ -1866,13 +3121,67 @@ function synthesizeContractDriftBucket(args) {
|
|
|
1866
3121
|
overflowLabel: "changed entities"
|
|
1867
3122
|
};
|
|
1868
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
|
+
}
|
|
1869
3176
|
function analyzeTestStatus(input) {
|
|
1870
|
-
const
|
|
1871
|
-
const
|
|
1872
|
-
const
|
|
1873
|
-
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;
|
|
1874
3183
|
const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
|
|
1875
|
-
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);
|
|
1876
3185
|
const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
|
|
1877
3186
|
const collectionItems = chooseStrongestFailureItems(collectCollectionFailureItems(input));
|
|
1878
3187
|
const inlineItems = chooseStrongestFailureItems(collectInlineFailureItems(input));
|
|
@@ -1899,7 +3208,8 @@ function analyzeTestStatus(input) {
|
|
|
1899
3208
|
if (!sharedBlocker) {
|
|
1900
3209
|
const importDependencyBucket = synthesizeImportDependencyBucket({
|
|
1901
3210
|
errors,
|
|
1902
|
-
visibleErrorItems
|
|
3211
|
+
visibleErrorItems,
|
|
3212
|
+
inlineItems
|
|
1903
3213
|
});
|
|
1904
3214
|
if (importDependencyBucket) {
|
|
1905
3215
|
buckets.push(importDependencyBucket);
|
|
@@ -1912,11 +3222,26 @@ function analyzeTestStatus(input) {
|
|
|
1912
3222
|
if (contractDrift) {
|
|
1913
3223
|
buckets.push(contractDrift);
|
|
1914
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
|
+
}
|
|
1915
3238
|
return {
|
|
3239
|
+
runner,
|
|
1916
3240
|
passed,
|
|
1917
3241
|
failed,
|
|
1918
3242
|
errors,
|
|
1919
3243
|
skipped,
|
|
3244
|
+
snapshotFailures: counts.snapshotFailures,
|
|
1920
3245
|
noTestsCollected,
|
|
1921
3246
|
interrupted,
|
|
1922
3247
|
collectionErrorCount: collectionErrors ? Number(collectionErrors[1]) : void 0,
|
|
@@ -2224,10 +3549,11 @@ async function buildOpenAICompatibleError(response) {
|
|
|
2224
3549
|
return new Error(detail);
|
|
2225
3550
|
}
|
|
2226
3551
|
var OpenAICompatibleProvider = class {
|
|
2227
|
-
name
|
|
3552
|
+
name;
|
|
2228
3553
|
baseUrl;
|
|
2229
3554
|
apiKey;
|
|
2230
3555
|
constructor(options) {
|
|
3556
|
+
this.name = options.name ?? "openai-compatible";
|
|
2231
3557
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
2232
3558
|
this.apiKey = options.apiKey;
|
|
2233
3559
|
}
|
|
@@ -2303,6 +3629,13 @@ function createProvider(config) {
|
|
|
2303
3629
|
apiKey: config.provider.apiKey
|
|
2304
3630
|
});
|
|
2305
3631
|
}
|
|
3632
|
+
if (config.provider.provider === "openrouter") {
|
|
3633
|
+
return new OpenAICompatibleProvider({
|
|
3634
|
+
baseUrl: config.provider.baseUrl,
|
|
3635
|
+
apiKey: config.provider.apiKey,
|
|
3636
|
+
name: "openrouter"
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
2306
3639
|
throw new Error(`Unsupported provider: ${config.provider.provider}`);
|
|
2307
3640
|
}
|
|
2308
3641
|
|
|
@@ -2475,9 +3808,12 @@ function resolvePromptPolicy(args) {
|
|
|
2475
3808
|
"Return only valid JSON.",
|
|
2476
3809
|
`Use this exact contract: ${args.outputContract ?? TEST_STATUS_DIAGNOSE_JSON_CONTRACT}.`,
|
|
2477
3810
|
"Treat the heuristic context as extraction guidance, but do not invent hidden failures.",
|
|
2478
|
-
"Use the heuristic extract as the bucket truth unless the visible command output clearly disproves it.",
|
|
3811
|
+
"Use the heuristic extract as the base bucket truth unless the visible command output clearly disproves it.",
|
|
3812
|
+
"If some visible failure or error families remain unexplained, add at most 2 bucket_supplements for the residual families only.",
|
|
3813
|
+
"Do not rewrite or delete heuristic buckets; only supplement missing residual coverage.",
|
|
3814
|
+
"Keep bucket_supplement counts within the unexplained residual failures or errors.",
|
|
2479
3815
|
"Identify the dominant blocker, remaining visible failure buckets, the decision, and the next best action.",
|
|
2480
|
-
"Set diagnosis_complete to true only when the visible output is already sufficient to stop and act.",
|
|
3816
|
+
"Set diagnosis_complete to true only when the visible output is already sufficient to stop and act and no unknown residual family remains.",
|
|
2481
3817
|
"Set raw_needed to true only when exact traceback lines are still required.",
|
|
2482
3818
|
"Set provider_confidence to a number between 0 and 1, or null only when confidence cannot be estimated."
|
|
2483
3819
|
] : [
|
|
@@ -2994,6 +4330,7 @@ function buildGenericRawSlice(args) {
|
|
|
2994
4330
|
|
|
2995
4331
|
// src/core/run.ts
|
|
2996
4332
|
var RETRY_DELAY_MS = 300;
|
|
4333
|
+
var PROVIDER_PENDING_NOTICE_DELAY_MS = 150;
|
|
2997
4334
|
function estimateTokenCount(text) {
|
|
2998
4335
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
2999
4336
|
}
|
|
@@ -3014,6 +4351,8 @@ function logVerboseTestStatusTelemetry(args) {
|
|
|
3014
4351
|
`${pc.dim("sift")} diagnosis_complete_at_layer=${getDiagnosisCompleteAtLayer(args.contract)}`,
|
|
3015
4352
|
`${pc.dim("sift")} heuristic_short_circuit=${!args.contract.provider_used && args.contract.diagnosis_complete && !args.contract.raw_needed && !args.contract.provider_failed}`,
|
|
3016
4353
|
`${pc.dim("sift")} raw_input_chars=${args.request.stdin.length}`,
|
|
4354
|
+
`${pc.dim("sift")} heuristic_input_chars=${args.heuristicInputChars}`,
|
|
4355
|
+
`${pc.dim("sift")} heuristic_input_truncated=${args.heuristicInputTruncated}`,
|
|
3017
4356
|
`${pc.dim("sift")} prepared_input_chars=${args.prepared.meta.finalLength}`,
|
|
3018
4357
|
`${pc.dim("sift")} raw_slice_chars=${args.rawSliceChars ?? 0}`,
|
|
3019
4358
|
`${pc.dim("sift")} provider_input_chars=${args.providerInputChars ?? 0}`,
|
|
@@ -3055,6 +4394,7 @@ function buildDryRunOutput(args) {
|
|
|
3055
4394
|
responseMode: args.responseMode,
|
|
3056
4395
|
policy: args.request.policyName ?? null,
|
|
3057
4396
|
heuristicOutput: args.heuristicOutput ?? null,
|
|
4397
|
+
heuristicInput: args.heuristicInput,
|
|
3058
4398
|
input: {
|
|
3059
4399
|
originalLength: args.prepared.meta.originalLength,
|
|
3060
4400
|
finalLength: args.prepared.meta.finalLength,
|
|
@@ -3071,6 +4411,25 @@ function buildDryRunOutput(args) {
|
|
|
3071
4411
|
async function delay(ms) {
|
|
3072
4412
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
3073
4413
|
}
|
|
4414
|
+
function startProviderPendingNotice() {
|
|
4415
|
+
if (!process.stderr.isTTY) {
|
|
4416
|
+
return () => {
|
|
4417
|
+
};
|
|
4418
|
+
}
|
|
4419
|
+
const message = "sift waiting for provider...";
|
|
4420
|
+
let shown = false;
|
|
4421
|
+
const timer = setTimeout(() => {
|
|
4422
|
+
shown = true;
|
|
4423
|
+
process.stderr.write(`${message}\r`);
|
|
4424
|
+
}, PROVIDER_PENDING_NOTICE_DELAY_MS);
|
|
4425
|
+
return () => {
|
|
4426
|
+
clearTimeout(timer);
|
|
4427
|
+
if (!shown) {
|
|
4428
|
+
return;
|
|
4429
|
+
}
|
|
4430
|
+
process.stderr.write(`\r${" ".repeat(message.length)}\r`);
|
|
4431
|
+
};
|
|
4432
|
+
}
|
|
3074
4433
|
function withInsufficientHint(args) {
|
|
3075
4434
|
if (!isInsufficientSignalOutput(args.output)) {
|
|
3076
4435
|
return args.output;
|
|
@@ -3091,22 +4450,27 @@ async function generateWithRetry(args) {
|
|
|
3091
4450
|
responseMode: args.responseMode,
|
|
3092
4451
|
jsonResponseFormat: args.request.config.provider.jsonResponseFormat
|
|
3093
4452
|
});
|
|
4453
|
+
const stopPendingNotice = startProviderPendingNotice();
|
|
3094
4454
|
try {
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
4455
|
+
try {
|
|
4456
|
+
return await generate();
|
|
4457
|
+
} catch (error) {
|
|
4458
|
+
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
4459
|
+
if (!isRetriableReason(reason)) {
|
|
4460
|
+
throw error;
|
|
4461
|
+
}
|
|
4462
|
+
if (args.request.config.runtime.verbose) {
|
|
4463
|
+
process.stderr.write(
|
|
4464
|
+
`${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
|
|
3104
4465
|
`
|
|
3105
|
-
|
|
4466
|
+
);
|
|
4467
|
+
}
|
|
4468
|
+
await delay(RETRY_DELAY_MS);
|
|
3106
4469
|
}
|
|
3107
|
-
await
|
|
4470
|
+
return await generate();
|
|
4471
|
+
} finally {
|
|
4472
|
+
stopPendingNotice();
|
|
3108
4473
|
}
|
|
3109
|
-
return generate();
|
|
3110
4474
|
}
|
|
3111
4475
|
function hasRecognizableTestStatusSignal(input) {
|
|
3112
4476
|
const analysis = analyzeTestStatus(input);
|
|
@@ -3161,11 +4525,22 @@ function buildTestStatusProviderFailureDecision(args) {
|
|
|
3161
4525
|
}
|
|
3162
4526
|
async function runSift(request) {
|
|
3163
4527
|
const prepared = prepareInput(request.stdin, request.config.input);
|
|
4528
|
+
const heuristicInput = prepared.redacted;
|
|
4529
|
+
const heuristicInputTruncated = false;
|
|
4530
|
+
const heuristicPrepared = {
|
|
4531
|
+
...prepared,
|
|
4532
|
+
truncated: heuristicInput,
|
|
4533
|
+
meta: {
|
|
4534
|
+
...prepared.meta,
|
|
4535
|
+
finalLength: heuristicInput.length,
|
|
4536
|
+
truncatedApplied: heuristicInputTruncated
|
|
4537
|
+
}
|
|
4538
|
+
};
|
|
3164
4539
|
const provider = createProvider(request.config);
|
|
3165
|
-
const hasTestStatusSignal = request.policyName === "test-status" && hasRecognizableTestStatusSignal(
|
|
3166
|
-
const testStatusAnalysis = hasTestStatusSignal ? analyzeTestStatus(
|
|
4540
|
+
const hasTestStatusSignal = request.policyName === "test-status" && hasRecognizableTestStatusSignal(heuristicInput);
|
|
4541
|
+
const testStatusAnalysis = hasTestStatusSignal ? analyzeTestStatus(heuristicInput) : null;
|
|
3167
4542
|
const testStatusDecision = hasTestStatusSignal && testStatusAnalysis ? buildTestStatusDiagnoseContract({
|
|
3168
|
-
input:
|
|
4543
|
+
input: heuristicInput,
|
|
3169
4544
|
analysis: testStatusAnalysis,
|
|
3170
4545
|
resolvedTests: request.testStatusContext?.resolvedTests,
|
|
3171
4546
|
remainingTests: request.testStatusContext?.remainingTests
|
|
@@ -3180,7 +4555,7 @@ async function runSift(request) {
|
|
|
3180
4555
|
`
|
|
3181
4556
|
);
|
|
3182
4557
|
}
|
|
3183
|
-
const heuristicOutput = request.policyName === "test-status" ? testStatusDecision?.contract.diagnosis_complete ? testStatusHeuristicOutput : null : applyHeuristicPolicy(request.policyName,
|
|
4558
|
+
const heuristicOutput = request.policyName === "test-status" ? testStatusDecision?.contract.diagnosis_complete ? testStatusHeuristicOutput : null : applyHeuristicPolicy(request.policyName, heuristicInput, request.detail);
|
|
3184
4559
|
if (heuristicOutput) {
|
|
3185
4560
|
if (request.config.runtime.verbose) {
|
|
3186
4561
|
process.stderr.write(`${pc.dim("sift")} heuristic=${request.policyName}
|
|
@@ -3190,7 +4565,7 @@ async function runSift(request) {
|
|
|
3190
4565
|
question: request.question,
|
|
3191
4566
|
format: request.format,
|
|
3192
4567
|
goal: request.goal,
|
|
3193
|
-
input:
|
|
4568
|
+
input: heuristicInput,
|
|
3194
4569
|
detail: request.detail,
|
|
3195
4570
|
policyName: request.policyName,
|
|
3196
4571
|
outputContract: request.policyName === "test-status" && request.goal === "diagnose" && request.format === "json" ? request.outputContract ?? TEST_STATUS_DIAGNOSE_JSON_CONTRACT : request.outputContract,
|
|
@@ -3210,6 +4585,11 @@ async function runSift(request) {
|
|
|
3210
4585
|
prompt: heuristicPrompt.prompt,
|
|
3211
4586
|
responseMode: heuristicPrompt.responseMode,
|
|
3212
4587
|
prepared,
|
|
4588
|
+
heuristicInput: {
|
|
4589
|
+
length: heuristicInput.length,
|
|
4590
|
+
truncatedApplied: heuristicInputTruncated,
|
|
4591
|
+
strategy: "full-redacted"
|
|
4592
|
+
},
|
|
3213
4593
|
heuristicOutput,
|
|
3214
4594
|
strategy: "heuristic"
|
|
3215
4595
|
});
|
|
@@ -3223,6 +4603,8 @@ async function runSift(request) {
|
|
|
3223
4603
|
logVerboseTestStatusTelemetry({
|
|
3224
4604
|
request,
|
|
3225
4605
|
prepared,
|
|
4606
|
+
heuristicInputChars: heuristicInput.length,
|
|
4607
|
+
heuristicInputTruncated,
|
|
3226
4608
|
contract: testStatusDecision.contract,
|
|
3227
4609
|
finalOutput
|
|
3228
4610
|
});
|
|
@@ -3274,6 +4656,11 @@ async function runSift(request) {
|
|
|
3274
4656
|
prompt: prompt.prompt,
|
|
3275
4657
|
responseMode: prompt.responseMode,
|
|
3276
4658
|
prepared: providerPrepared2,
|
|
4659
|
+
heuristicInput: {
|
|
4660
|
+
length: heuristicInput.length,
|
|
4661
|
+
truncatedApplied: heuristicInputTruncated,
|
|
4662
|
+
strategy: "full-redacted"
|
|
4663
|
+
},
|
|
3277
4664
|
heuristicOutput: testStatusHeuristicOutput,
|
|
3278
4665
|
strategy: "hybrid"
|
|
3279
4666
|
});
|
|
@@ -3287,10 +4674,11 @@ async function runSift(request) {
|
|
|
3287
4674
|
});
|
|
3288
4675
|
const supplement = parseTestStatusProviderSupplement(result.text);
|
|
3289
4676
|
const mergedDecision = buildTestStatusDiagnoseContract({
|
|
3290
|
-
input:
|
|
4677
|
+
input: heuristicInput,
|
|
3291
4678
|
analysis: testStatusAnalysis,
|
|
3292
4679
|
resolvedTests: request.testStatusContext?.resolvedTests,
|
|
3293
4680
|
remainingTests: request.testStatusContext?.remainingTests,
|
|
4681
|
+
providerBucketSupplements: supplement.bucket_supplements,
|
|
3294
4682
|
contractOverrides: {
|
|
3295
4683
|
diagnosis_complete: supplement.diagnosis_complete,
|
|
3296
4684
|
raw_needed: supplement.raw_needed,
|
|
@@ -3312,6 +4700,8 @@ async function runSift(request) {
|
|
|
3312
4700
|
logVerboseTestStatusTelemetry({
|
|
3313
4701
|
request,
|
|
3314
4702
|
prepared,
|
|
4703
|
+
heuristicInputChars: heuristicInput.length,
|
|
4704
|
+
heuristicInputTruncated,
|
|
3315
4705
|
contract: mergedDecision.contract,
|
|
3316
4706
|
finalOutput,
|
|
3317
4707
|
rawSliceChars: rawSlice.text.length,
|
|
@@ -3324,7 +4714,7 @@ async function runSift(request) {
|
|
|
3324
4714
|
const failureDecision = buildTestStatusProviderFailureDecision({
|
|
3325
4715
|
request,
|
|
3326
4716
|
baseDecision: testStatusDecision,
|
|
3327
|
-
input:
|
|
4717
|
+
input: heuristicInput,
|
|
3328
4718
|
analysis: testStatusAnalysis,
|
|
3329
4719
|
reason,
|
|
3330
4720
|
rawSliceUsed: rawSlice.used,
|
|
@@ -3345,6 +4735,8 @@ async function runSift(request) {
|
|
|
3345
4735
|
logVerboseTestStatusTelemetry({
|
|
3346
4736
|
request,
|
|
3347
4737
|
prepared,
|
|
4738
|
+
heuristicInputChars: heuristicInput.length,
|
|
4739
|
+
heuristicInputTruncated,
|
|
3348
4740
|
contract: failureDecision.contract,
|
|
3349
4741
|
finalOutput,
|
|
3350
4742
|
rawSliceChars: rawSlice.text.length,
|
|
@@ -3383,6 +4775,11 @@ async function runSift(request) {
|
|
|
3383
4775
|
prompt: providerPrompt.prompt,
|
|
3384
4776
|
responseMode: providerPrompt.responseMode,
|
|
3385
4777
|
prepared: providerPrepared,
|
|
4778
|
+
heuristicInput: {
|
|
4779
|
+
length: heuristicInput.length,
|
|
4780
|
+
truncatedApplied: heuristicInputTruncated,
|
|
4781
|
+
strategy: "full-redacted"
|
|
4782
|
+
},
|
|
3386
4783
|
heuristicOutput: testStatusDecision ? testStatusHeuristicOutput : null,
|
|
3387
4784
|
strategy: testStatusDecision ? "hybrid" : "provider"
|
|
3388
4785
|
});
|
|
@@ -3430,10 +4827,29 @@ var detailSchema = z2.enum(["standard", "focused", "verbose"]);
|
|
|
3430
4827
|
var failureBucketTypeSchema = z2.enum([
|
|
3431
4828
|
"shared_environment_blocker",
|
|
3432
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",
|
|
3433
4848
|
"service_unavailable",
|
|
3434
4849
|
"db_connection_failure",
|
|
3435
4850
|
"auth_bypass_absent",
|
|
3436
4851
|
"contract_snapshot_drift",
|
|
4852
|
+
"snapshot_mismatch",
|
|
3437
4853
|
"import_dependency_failure",
|
|
3438
4854
|
"collection_failure",
|
|
3439
4855
|
"assertion_failure",
|
|
@@ -4419,13 +5835,16 @@ var OPENAI_COMPATIBLE_BASE_URL_ENV = [
|
|
|
4419
5835
|
{ prefix: "https://api.together.xyz/", envName: "TOGETHER_API_KEY" },
|
|
4420
5836
|
{ prefix: "https://api.groq.com/openai/", envName: "GROQ_API_KEY" }
|
|
4421
5837
|
];
|
|
5838
|
+
var NATIVE_PROVIDER_API_KEY_ENV = {
|
|
5839
|
+
openai: "OPENAI_API_KEY",
|
|
5840
|
+
openrouter: "OPENROUTER_API_KEY"
|
|
5841
|
+
};
|
|
4422
5842
|
var PROVIDER_API_KEY_ENV = {
|
|
4423
5843
|
anthropic: "ANTHROPIC_API_KEY",
|
|
4424
5844
|
claude: "ANTHROPIC_API_KEY",
|
|
4425
5845
|
groq: "GROQ_API_KEY",
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
together: "TOGETHER_API_KEY"
|
|
5846
|
+
together: "TOGETHER_API_KEY",
|
|
5847
|
+
...NATIVE_PROVIDER_API_KEY_ENV
|
|
4429
5848
|
};
|
|
4430
5849
|
function normalizeBaseUrl(baseUrl) {
|
|
4431
5850
|
if (!baseUrl) {
|
|
@@ -4463,7 +5882,11 @@ function resolveProviderApiKey(provider, baseUrl, env) {
|
|
|
4463
5882
|
|
|
4464
5883
|
// src/config/schema.ts
|
|
4465
5884
|
import { z as z3 } from "zod";
|
|
4466
|
-
var providerNameSchema = z3.enum([
|
|
5885
|
+
var providerNameSchema = z3.enum([
|
|
5886
|
+
"openai",
|
|
5887
|
+
"openai-compatible",
|
|
5888
|
+
"openrouter"
|
|
5889
|
+
]);
|
|
4467
5890
|
var outputFormatSchema = z3.enum([
|
|
4468
5891
|
"brief",
|
|
4469
5892
|
"bullets",
|
|
@@ -4492,6 +5915,15 @@ var providerConfigSchema = z3.object({
|
|
|
4492
5915
|
temperature: z3.number().min(0).max(2),
|
|
4493
5916
|
maxOutputTokens: z3.number().int().positive()
|
|
4494
5917
|
});
|
|
5918
|
+
var providerProfileSchema = z3.object({
|
|
5919
|
+
model: z3.string().min(1).optional(),
|
|
5920
|
+
baseUrl: z3.string().url().optional(),
|
|
5921
|
+
apiKey: z3.string().optional()
|
|
5922
|
+
});
|
|
5923
|
+
var providerProfilesSchema = z3.object({
|
|
5924
|
+
openai: providerProfileSchema.optional(),
|
|
5925
|
+
openrouter: providerProfileSchema.optional()
|
|
5926
|
+
}).optional();
|
|
4495
5927
|
var inputConfigSchema = z3.object({
|
|
4496
5928
|
stripAnsi: z3.boolean(),
|
|
4497
5929
|
redact: z3.boolean(),
|
|
@@ -4516,10 +5948,19 @@ var siftConfigSchema = z3.object({
|
|
|
4516
5948
|
provider: providerConfigSchema,
|
|
4517
5949
|
input: inputConfigSchema,
|
|
4518
5950
|
runtime: runtimeConfigSchema,
|
|
4519
|
-
presets: z3.record(presetDefinitionSchema)
|
|
5951
|
+
presets: z3.record(presetDefinitionSchema),
|
|
5952
|
+
providerProfiles: providerProfilesSchema
|
|
4520
5953
|
});
|
|
4521
5954
|
|
|
4522
5955
|
// src/config/resolve.ts
|
|
5956
|
+
var PROVIDER_DEFAULT_OVERRIDES = {
|
|
5957
|
+
openrouter: {
|
|
5958
|
+
provider: {
|
|
5959
|
+
model: "openrouter/free",
|
|
5960
|
+
baseUrl: "https://openrouter.ai/api/v1"
|
|
5961
|
+
}
|
|
5962
|
+
}
|
|
5963
|
+
};
|
|
4523
5964
|
function isRecord(value) {
|
|
4524
5965
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
4525
5966
|
}
|
|
@@ -4582,13 +6023,32 @@ function buildCredentialEnvOverrides(env, context) {
|
|
|
4582
6023
|
}
|
|
4583
6024
|
};
|
|
4584
6025
|
}
|
|
6026
|
+
function getBaseConfigForProvider(provider) {
|
|
6027
|
+
return mergeDefined(defaultConfig, provider ? PROVIDER_DEFAULT_OVERRIDES[provider] : {});
|
|
6028
|
+
}
|
|
6029
|
+
function resolveProvisionalProvider(args) {
|
|
6030
|
+
const provisional = mergeDefined(
|
|
6031
|
+
mergeDefined(
|
|
6032
|
+
mergeDefined(defaultConfig, args.fileConfig),
|
|
6033
|
+
args.nonCredentialEnvConfig
|
|
6034
|
+
),
|
|
6035
|
+
stripApiKey(args.cliOverrides) ?? {}
|
|
6036
|
+
);
|
|
6037
|
+
return provisional.provider.provider;
|
|
6038
|
+
}
|
|
4585
6039
|
function resolveConfig(options = {}) {
|
|
4586
6040
|
const env = options.env ?? process.env;
|
|
4587
6041
|
const fileConfig = loadRawConfig(options.configPath);
|
|
4588
6042
|
const nonCredentialEnvConfig = buildNonCredentialEnvOverrides(env);
|
|
6043
|
+
const provisionalProvider = resolveProvisionalProvider({
|
|
6044
|
+
fileConfig,
|
|
6045
|
+
nonCredentialEnvConfig,
|
|
6046
|
+
cliOverrides: options.cliOverrides
|
|
6047
|
+
});
|
|
6048
|
+
const baseConfig = getBaseConfigForProvider(provisionalProvider);
|
|
4589
6049
|
const contextConfig = mergeDefined(
|
|
4590
6050
|
mergeDefined(
|
|
4591
|
-
mergeDefined(
|
|
6051
|
+
mergeDefined(baseConfig, fileConfig),
|
|
4592
6052
|
nonCredentialEnvConfig
|
|
4593
6053
|
),
|
|
4594
6054
|
stripApiKey(options.cliOverrides) ?? {}
|
|
@@ -4600,7 +6060,7 @@ function resolveConfig(options = {}) {
|
|
|
4600
6060
|
const merged = mergeDefined(
|
|
4601
6061
|
mergeDefined(
|
|
4602
6062
|
mergeDefined(
|
|
4603
|
-
mergeDefined(
|
|
6063
|
+
mergeDefined(baseConfig, fileConfig),
|
|
4604
6064
|
nonCredentialEnvConfig
|
|
4605
6065
|
),
|
|
4606
6066
|
credentialEnvConfig
|