@bilalimamoglu/sift 0.3.1 → 0.3.3
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 +142 -386
- package/dist/cli.js +2500 -226
- package/dist/index.d.ts +15 -1
- package/dist/index.js +2474 -216
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/core/exec.ts
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { constants as osConstants } from "os";
|
|
4
|
-
import
|
|
4
|
+
import pc3 from "picocolors";
|
|
5
5
|
|
|
6
6
|
// src/constants.ts
|
|
7
7
|
import os from "os";
|
|
@@ -61,7 +61,7 @@ function evaluateGate(args) {
|
|
|
61
61
|
|
|
62
62
|
// src/core/testStatusDecision.ts
|
|
63
63
|
import { z } from "zod";
|
|
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[]}';
|
|
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","primary_suspect_kind":"test|app_code|config|environment|tooling|unknown","confidence_reason":string,"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,"suspect_kind":"test|app_code|config|environment|tooling|unknown","fix_hint":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
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([
|
|
@@ -103,6 +103,15 @@ var testStatusDiagnoseContractSchema = z.object({
|
|
|
103
103
|
additional_source_read_likely_low_value: z.boolean(),
|
|
104
104
|
read_raw_only_if: z.string().nullable(),
|
|
105
105
|
decision: z.enum(["stop", "zoom", "read_source", "read_raw"]),
|
|
106
|
+
primary_suspect_kind: z.enum([
|
|
107
|
+
"test",
|
|
108
|
+
"app_code",
|
|
109
|
+
"config",
|
|
110
|
+
"environment",
|
|
111
|
+
"tooling",
|
|
112
|
+
"unknown"
|
|
113
|
+
]),
|
|
114
|
+
confidence_reason: z.string().min(1),
|
|
106
115
|
dominant_blocker_bucket_index: z.number().int().nullable(),
|
|
107
116
|
provider_used: z.boolean(),
|
|
108
117
|
provider_confidence: z.number().min(0).max(1).nullable(),
|
|
@@ -117,6 +126,15 @@ var testStatusDiagnoseContractSchema = z.object({
|
|
|
117
126
|
label: z.string(),
|
|
118
127
|
count: z.number().int(),
|
|
119
128
|
root_cause: z.string(),
|
|
129
|
+
suspect_kind: z.enum([
|
|
130
|
+
"test",
|
|
131
|
+
"app_code",
|
|
132
|
+
"config",
|
|
133
|
+
"environment",
|
|
134
|
+
"tooling",
|
|
135
|
+
"unknown"
|
|
136
|
+
]),
|
|
137
|
+
fix_hint: z.string().min(1),
|
|
120
138
|
evidence: z.array(z.string()).max(2),
|
|
121
139
|
bucket_confidence: z.number(),
|
|
122
140
|
root_cause_confidence: z.number(),
|
|
@@ -166,6 +184,255 @@ var testStatusPublicDiagnoseContractSchema = testStatusDiagnoseContractSchema.om
|
|
|
166
184
|
function parseTestStatusProviderSupplement(input) {
|
|
167
185
|
return testStatusProviderSupplementSchema.parse(JSON.parse(input));
|
|
168
186
|
}
|
|
187
|
+
var extendedBucketSpecs = [
|
|
188
|
+
{
|
|
189
|
+
prefix: "service unavailable:",
|
|
190
|
+
type: "service_unavailable",
|
|
191
|
+
label: "service unavailable",
|
|
192
|
+
genericTitle: "Service unavailable failures",
|
|
193
|
+
defaultCoverage: "error",
|
|
194
|
+
rootCauseConfidence: 0.9,
|
|
195
|
+
dominantPriority: 2,
|
|
196
|
+
dominantBlocker: true,
|
|
197
|
+
why: "it contains the dependency service or API path that is unavailable in the test environment",
|
|
198
|
+
fix: "Restore the dependency service or test double before rerunning the full suite."
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
prefix: "db refused:",
|
|
202
|
+
type: "db_connection_failure",
|
|
203
|
+
label: "database connection",
|
|
204
|
+
genericTitle: "Database connection failures",
|
|
205
|
+
defaultCoverage: "error",
|
|
206
|
+
rootCauseConfidence: 0.9,
|
|
207
|
+
dominantPriority: 2,
|
|
208
|
+
dominantBlocker: true,
|
|
209
|
+
why: "it contains the database host, DSN, or startup path that is refusing connections",
|
|
210
|
+
fix: "Restore the test database connectivity before rerunning the full suite."
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
prefix: "auth bypass absent:",
|
|
214
|
+
type: "auth_bypass_absent",
|
|
215
|
+
label: "auth bypass missing",
|
|
216
|
+
genericTitle: "Auth bypass setup failures",
|
|
217
|
+
defaultCoverage: "error",
|
|
218
|
+
rootCauseConfidence: 0.86,
|
|
219
|
+
dominantPriority: 2,
|
|
220
|
+
dominantBlocker: true,
|
|
221
|
+
why: "it contains the auth bypass fixture or setup path that tests expected to be active",
|
|
222
|
+
fix: "Restore the test auth bypass fixture or mock before rerunning the full suite."
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
prefix: "snapshot mismatch:",
|
|
226
|
+
type: "snapshot_mismatch",
|
|
227
|
+
label: "snapshot mismatch",
|
|
228
|
+
genericTitle: "Snapshot mismatches",
|
|
229
|
+
defaultCoverage: "failed",
|
|
230
|
+
rootCauseConfidence: 0.84,
|
|
231
|
+
why: "it contains the failing snapshot expectation behind this bucket",
|
|
232
|
+
fix: "Update the snapshots if these output changes are intentional, then rerun the suite."
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
prefix: "timeout:",
|
|
236
|
+
type: "timeout_failure",
|
|
237
|
+
label: "timeout",
|
|
238
|
+
genericTitle: "Timeout failures",
|
|
239
|
+
defaultCoverage: "mixed",
|
|
240
|
+
rootCauseConfidence: 0.9,
|
|
241
|
+
why: "it contains the test or fixture that exceeded the timeout threshold",
|
|
242
|
+
fix: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning."
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
prefix: "permission:",
|
|
246
|
+
type: "permission_denied_failure",
|
|
247
|
+
label: "permission denied",
|
|
248
|
+
genericTitle: "Permission failures",
|
|
249
|
+
defaultCoverage: "error",
|
|
250
|
+
rootCauseConfidence: 0.85,
|
|
251
|
+
why: "it contains the file, socket, or port access that was denied",
|
|
252
|
+
fix: "Check file or port permissions in the CI environment before rerunning."
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
prefix: "async loop:",
|
|
256
|
+
type: "async_event_loop_failure",
|
|
257
|
+
label: "async event loop",
|
|
258
|
+
genericTitle: "Async event loop failures",
|
|
259
|
+
defaultCoverage: "mixed",
|
|
260
|
+
rootCauseConfidence: 0.88,
|
|
261
|
+
why: "it contains the async setup or coroutine that caused the event loop error",
|
|
262
|
+
fix: "Check event loop scope and pytest-asyncio configuration before rerunning."
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
prefix: "fixture teardown:",
|
|
266
|
+
type: "fixture_teardown_failure",
|
|
267
|
+
label: "fixture teardown",
|
|
268
|
+
genericTitle: "Fixture teardown failures",
|
|
269
|
+
defaultCoverage: "error",
|
|
270
|
+
rootCauseConfidence: 0.85,
|
|
271
|
+
why: "it contains the fixture teardown path that failed after the test body completed",
|
|
272
|
+
fix: "Inspect the teardown cleanup path and restore idempotent fixture cleanup before rerunning."
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
prefix: "db migration:",
|
|
276
|
+
type: "db_migration_failure",
|
|
277
|
+
label: "db migration",
|
|
278
|
+
genericTitle: "DB migration failures",
|
|
279
|
+
defaultCoverage: "error",
|
|
280
|
+
rootCauseConfidence: 0.9,
|
|
281
|
+
why: "it contains the migration or model definition behind the missing table or relation",
|
|
282
|
+
fix: "Run pending migrations or fix the expected model schema before rerunning."
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
prefix: "configuration:",
|
|
286
|
+
type: "configuration_error",
|
|
287
|
+
label: "configuration error",
|
|
288
|
+
genericTitle: "Configuration errors",
|
|
289
|
+
defaultCoverage: "error",
|
|
290
|
+
rootCauseConfidence: 0.95,
|
|
291
|
+
dominantPriority: 4,
|
|
292
|
+
dominantBlocker: true,
|
|
293
|
+
why: "it contains the pytest configuration or conftest setup error that blocks the run",
|
|
294
|
+
fix: "Fix the pytest configuration, CLI usage, or conftest import error before rerunning."
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
prefix: "xdist worker crash:",
|
|
298
|
+
type: "xdist_worker_crash",
|
|
299
|
+
label: "xdist worker crash",
|
|
300
|
+
genericTitle: "xdist worker crashes",
|
|
301
|
+
defaultCoverage: "error",
|
|
302
|
+
rootCauseConfidence: 0.92,
|
|
303
|
+
dominantPriority: 3,
|
|
304
|
+
why: "it contains the worker startup or shared-state path that crashed an xdist worker",
|
|
305
|
+
fix: "Check shared state, worker startup hooks, or resource contention between workers before rerunning."
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
prefix: "type error:",
|
|
309
|
+
type: "type_error_failure",
|
|
310
|
+
label: "type error",
|
|
311
|
+
genericTitle: "Type errors",
|
|
312
|
+
defaultCoverage: "mixed",
|
|
313
|
+
rootCauseConfidence: 0.8,
|
|
314
|
+
why: "it contains the call site or fixture value that triggered the type error",
|
|
315
|
+
fix: "Inspect the mismatched argument or object shape and rerun the full suite at standard."
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
prefix: "resource leak:",
|
|
319
|
+
type: "resource_leak_warning",
|
|
320
|
+
label: "resource leak",
|
|
321
|
+
genericTitle: "Resource leak warnings",
|
|
322
|
+
defaultCoverage: "mixed",
|
|
323
|
+
rootCauseConfidence: 0.74,
|
|
324
|
+
why: "it contains the warning source behind the leaked file, socket, or coroutine",
|
|
325
|
+
fix: "Close the leaked resource or suppress the warning only if the cleanup is intentional."
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
prefix: "django db access:",
|
|
329
|
+
type: "django_db_access_denied",
|
|
330
|
+
label: "django db access",
|
|
331
|
+
genericTitle: "Django DB access failures",
|
|
332
|
+
defaultCoverage: "error",
|
|
333
|
+
rootCauseConfidence: 0.95,
|
|
334
|
+
why: "it needs the @pytest.mark.django_db decorator or fixture permission to access the database",
|
|
335
|
+
fix: "Add @pytest.mark.django_db to the test or class before rerunning."
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
prefix: "network:",
|
|
339
|
+
type: "network_failure",
|
|
340
|
+
label: "network failure",
|
|
341
|
+
genericTitle: "Network failures",
|
|
342
|
+
defaultCoverage: "error",
|
|
343
|
+
rootCauseConfidence: 0.88,
|
|
344
|
+
dominantPriority: 2,
|
|
345
|
+
why: "it contains the host, URL, or TLS path behind the network failure",
|
|
346
|
+
fix: "Check DNS, outbound network access, retries, or TLS trust before rerunning."
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
prefix: "segfault:",
|
|
350
|
+
type: "subprocess_crash_segfault",
|
|
351
|
+
label: "segfault",
|
|
352
|
+
genericTitle: "Segfault crashes",
|
|
353
|
+
defaultCoverage: "mixed",
|
|
354
|
+
rootCauseConfidence: 0.8,
|
|
355
|
+
why: "it contains the subprocess or native extension path that crashed with SIGSEGV",
|
|
356
|
+
fix: "Inspect the native extension, subprocess boundary, or incompatible binary before rerunning."
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
prefix: "flaky:",
|
|
360
|
+
type: "flaky_test_detected",
|
|
361
|
+
label: "flaky test",
|
|
362
|
+
genericTitle: "Flaky test detections",
|
|
363
|
+
defaultCoverage: "mixed",
|
|
364
|
+
rootCauseConfidence: 0.72,
|
|
365
|
+
why: "it contains the rerun-prone test that behaved inconsistently across attempts",
|
|
366
|
+
fix: "Stabilize the nondeterministic test or fixture before relying on reruns."
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
prefix: "serialization:",
|
|
370
|
+
type: "serialization_encoding_failure",
|
|
371
|
+
label: "serialization or encoding",
|
|
372
|
+
genericTitle: "Serialization or encoding failures",
|
|
373
|
+
defaultCoverage: "mixed",
|
|
374
|
+
rootCauseConfidence: 0.78,
|
|
375
|
+
why: "it contains the serialization or decoding path behind the malformed payload",
|
|
376
|
+
fix: "Inspect the encoded payload, serializer, or fixture data before rerunning."
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
prefix: "file not found:",
|
|
380
|
+
type: "file_not_found_failure",
|
|
381
|
+
label: "file not found",
|
|
382
|
+
genericTitle: "Missing file failures",
|
|
383
|
+
defaultCoverage: "mixed",
|
|
384
|
+
rootCauseConfidence: 0.82,
|
|
385
|
+
why: "it contains the missing file path or fixture artifact required by the test",
|
|
386
|
+
fix: "Restore the missing file, fixture artifact, or working-directory assumption before rerunning."
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
prefix: "memory:",
|
|
390
|
+
type: "memory_error",
|
|
391
|
+
label: "memory error",
|
|
392
|
+
genericTitle: "Memory failures",
|
|
393
|
+
defaultCoverage: "mixed",
|
|
394
|
+
rootCauseConfidence: 0.78,
|
|
395
|
+
why: "it contains the allocation path that exhausted available memory",
|
|
396
|
+
fix: "Reduce memory pressure or investigate the large allocation before rerunning."
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
prefix: "deprecation as error:",
|
|
400
|
+
type: "deprecation_warning_as_error",
|
|
401
|
+
label: "deprecation as error",
|
|
402
|
+
genericTitle: "Deprecation warnings as errors",
|
|
403
|
+
defaultCoverage: "mixed",
|
|
404
|
+
rootCauseConfidence: 0.74,
|
|
405
|
+
why: "it contains the deprecated API or warning filter that is failing the test run",
|
|
406
|
+
fix: "Update the deprecated call site or relax the warning policy only if that is intentional."
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
prefix: "assertion failed:",
|
|
410
|
+
type: "assertion_failure",
|
|
411
|
+
label: "assertion failure",
|
|
412
|
+
genericTitle: "Assertion failures",
|
|
413
|
+
defaultCoverage: "failed",
|
|
414
|
+
rootCauseConfidence: 0.76,
|
|
415
|
+
why: "it contains the expected-versus-actual assertion that failed inside the visible test",
|
|
416
|
+
fix: "Read the assertion diff or expectation and fix the code or expected value before rerunning."
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
prefix: "xfail strict:",
|
|
420
|
+
type: "xfail_strict_unexpected_pass",
|
|
421
|
+
label: "strict xfail unexpected pass",
|
|
422
|
+
genericTitle: "Strict xfail unexpected passes",
|
|
423
|
+
defaultCoverage: "failed",
|
|
424
|
+
rootCauseConfidence: 0.78,
|
|
425
|
+
why: "it contains the strict xfail case that unexpectedly passed",
|
|
426
|
+
fix: "Remove or update the strict xfail expectation if the test is now passing intentionally."
|
|
427
|
+
}
|
|
428
|
+
];
|
|
429
|
+
function findExtendedBucketSpec(reason) {
|
|
430
|
+
return extendedBucketSpecs.find((spec) => reason.startsWith(spec.prefix)) ?? null;
|
|
431
|
+
}
|
|
432
|
+
function extractReasonDetail(reason, prefix) {
|
|
433
|
+
const detail = reason.slice(prefix.length).trim();
|
|
434
|
+
return detail.length > 0 ? detail : null;
|
|
435
|
+
}
|
|
169
436
|
function formatCount(count, singular, plural = `${singular}s`) {
|
|
170
437
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
171
438
|
}
|
|
@@ -219,6 +486,10 @@ function formatTargetSummary(summary) {
|
|
|
219
486
|
return `count=${summary.count}; families=${families}`;
|
|
220
487
|
}
|
|
221
488
|
function classifyGenericBucketType(reason) {
|
|
489
|
+
const extended = findExtendedBucketSpec(reason);
|
|
490
|
+
if (extended) {
|
|
491
|
+
return extended.type;
|
|
492
|
+
}
|
|
222
493
|
if (reason.startsWith("missing test env:")) {
|
|
223
494
|
return "shared_environment_blocker";
|
|
224
495
|
}
|
|
@@ -263,6 +534,10 @@ function classifyVisibleStatusForLabel(args) {
|
|
|
263
534
|
return "unknown";
|
|
264
535
|
}
|
|
265
536
|
function inferCoverageFromReason(reason) {
|
|
537
|
+
const extended = findExtendedBucketSpec(reason);
|
|
538
|
+
if (extended) {
|
|
539
|
+
return extended.defaultCoverage;
|
|
540
|
+
}
|
|
266
541
|
if (reason.startsWith("missing test env:") || reason.startsWith("fixture guard:") || reason.startsWith("service unavailable:") || reason.startsWith("db refused:") || reason.startsWith("auth bypass absent:") || reason.startsWith("missing module:")) {
|
|
267
542
|
return "error";
|
|
268
543
|
}
|
|
@@ -323,7 +598,13 @@ function buildGenericBuckets(analysis) {
|
|
|
323
598
|
summaryLines: [],
|
|
324
599
|
reason,
|
|
325
600
|
count: 1,
|
|
326
|
-
confidence:
|
|
601
|
+
confidence: (() => {
|
|
602
|
+
const extended = findExtendedBucketSpec(reason);
|
|
603
|
+
if (extended) {
|
|
604
|
+
return Math.max(0.72, Math.min(extended.rootCauseConfidence, 0.82));
|
|
605
|
+
}
|
|
606
|
+
return reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62;
|
|
607
|
+
})(),
|
|
327
608
|
representativeItems: [item],
|
|
328
609
|
entities: [],
|
|
329
610
|
hint: void 0,
|
|
@@ -340,7 +621,7 @@ function buildGenericBuckets(analysis) {
|
|
|
340
621
|
push(item.reason, item);
|
|
341
622
|
}
|
|
342
623
|
for (const bucket of grouped.values()) {
|
|
343
|
-
const title = bucket.type === "assertion_failure" ? "Assertion failures" : bucket.type === "import_dependency_failure" ? "Import/dependency failures" : bucket.type === "collection_failure" ? "Collection or fixture failures" : "Runtime failures";
|
|
624
|
+
const title = findExtendedBucketSpec(bucket.reason)?.genericTitle ?? (bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch" ? "Assertion failures" : bucket.type === "import_dependency_failure" ? "Import/dependency failures" : bucket.type === "collection_failure" ? "Collection or fixture failures" : "Runtime failures");
|
|
344
625
|
bucket.headline = `${title}: ${formatCount(bucket.count, "visible failure")} share ${bucket.reason}.`;
|
|
345
626
|
bucket.summaryLines = [bucket.headline];
|
|
346
627
|
bucket.overflowCount = Math.max(bucket.count - bucket.representativeItems.length, 0);
|
|
@@ -413,13 +694,13 @@ function inferFailureBucketCoverage(bucket, analysis) {
|
|
|
413
694
|
}
|
|
414
695
|
}
|
|
415
696
|
const claimed = bucket.countClaimed ?? bucket.countVisible;
|
|
416
|
-
if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure") {
|
|
697
|
+
if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch") {
|
|
417
698
|
return {
|
|
418
699
|
error,
|
|
419
700
|
failed: Math.max(failed, claimed)
|
|
420
701
|
};
|
|
421
702
|
}
|
|
422
|
-
if (bucket.type === "shared_environment_blocker" || bucket.type === "import_dependency_failure" || bucket.type === "collection_failure" || bucket.type === "fixture_guard_failure" || bucket.type === "service_unavailable" || bucket.type === "db_connection_failure" || bucket.type === "auth_bypass_absent") {
|
|
703
|
+
if (bucket.type === "shared_environment_blocker" || bucket.type === "import_dependency_failure" || bucket.type === "collection_failure" || bucket.type === "fixture_guard_failure" || bucket.type === "permission_denied_failure" || bucket.type === "fixture_teardown_failure" || bucket.type === "db_migration_failure" || bucket.type === "configuration_error" || bucket.type === "xdist_worker_crash" || bucket.type === "django_db_access_denied" || bucket.type === "network_failure" || bucket.type === "service_unavailable" || bucket.type === "db_connection_failure" || bucket.type === "auth_bypass_absent") {
|
|
423
704
|
return {
|
|
424
705
|
error: Math.max(error, claimed),
|
|
425
706
|
failed
|
|
@@ -494,6 +775,10 @@ function dominantBucketPriority(bucket) {
|
|
|
494
775
|
if (bucket.reason.startsWith("missing test env:")) {
|
|
495
776
|
return 5;
|
|
496
777
|
}
|
|
778
|
+
const extended = findExtendedBucketSpec(bucket.reason);
|
|
779
|
+
if (extended?.dominantPriority !== void 0) {
|
|
780
|
+
return extended.dominantPriority;
|
|
781
|
+
}
|
|
497
782
|
if (bucket.type === "shared_environment_blocker") {
|
|
498
783
|
return 4;
|
|
499
784
|
}
|
|
@@ -527,12 +812,16 @@ function prioritizeBuckets(buckets) {
|
|
|
527
812
|
});
|
|
528
813
|
}
|
|
529
814
|
function isDominantBlockerType(type) {
|
|
530
|
-
return type === "shared_environment_blocker" || type === "import_dependency_failure" || type === "collection_failure";
|
|
815
|
+
return type === "shared_environment_blocker" || type === "configuration_error" || type === "import_dependency_failure" || type === "collection_failure";
|
|
531
816
|
}
|
|
532
817
|
function labelForBucket(bucket) {
|
|
533
818
|
if (bucket.labelOverride) {
|
|
534
819
|
return bucket.labelOverride;
|
|
535
820
|
}
|
|
821
|
+
const extended = findExtendedBucketSpec(bucket.reason);
|
|
822
|
+
if (extended) {
|
|
823
|
+
return extended.label;
|
|
824
|
+
}
|
|
536
825
|
if (bucket.reason.startsWith("missing test env:")) {
|
|
537
826
|
return "missing test env";
|
|
538
827
|
}
|
|
@@ -566,6 +855,9 @@ function labelForBucket(bucket) {
|
|
|
566
855
|
if (bucket.type === "assertion_failure") {
|
|
567
856
|
return "assertion failure";
|
|
568
857
|
}
|
|
858
|
+
if (bucket.type === "snapshot_mismatch") {
|
|
859
|
+
return "snapshot mismatch";
|
|
860
|
+
}
|
|
569
861
|
if (bucket.type === "collection_failure") {
|
|
570
862
|
return "collection failure";
|
|
571
863
|
}
|
|
@@ -584,6 +876,10 @@ function rootCauseConfidenceFor(bucket) {
|
|
|
584
876
|
if (isUnknownBucket(bucket)) {
|
|
585
877
|
return 0.52;
|
|
586
878
|
}
|
|
879
|
+
const extended = findExtendedBucketSpec(bucket.reason);
|
|
880
|
+
if (extended) {
|
|
881
|
+
return extended.rootCauseConfidence;
|
|
882
|
+
}
|
|
587
883
|
if (bucket.reason.startsWith("missing test env:") || bucket.reason.startsWith("missing module:") || bucket.reason.startsWith("db refused:") || bucket.reason.startsWith("service unavailable:") || bucket.reason.startsWith("auth bypass absent:")) {
|
|
588
884
|
return 0.95;
|
|
589
885
|
}
|
|
@@ -624,6 +920,10 @@ function buildReadTargetWhy(args) {
|
|
|
624
920
|
if (envVar) {
|
|
625
921
|
return `it contains the ${envVar} setup guard`;
|
|
626
922
|
}
|
|
923
|
+
const extended = findExtendedBucketSpec(args.bucket.reason);
|
|
924
|
+
if (extended) {
|
|
925
|
+
return extended.why;
|
|
926
|
+
}
|
|
627
927
|
if (args.bucket.reason.startsWith("fixture guard:")) {
|
|
628
928
|
return "it contains the fixture/setup guard behind this bucket";
|
|
629
929
|
}
|
|
@@ -654,6 +954,9 @@ function buildReadTargetWhy(args) {
|
|
|
654
954
|
}
|
|
655
955
|
return "it maps to the visible stale snapshot expectation";
|
|
656
956
|
}
|
|
957
|
+
if (args.bucket.type === "snapshot_mismatch") {
|
|
958
|
+
return "it maps to the visible snapshot mismatch bucket";
|
|
959
|
+
}
|
|
657
960
|
if (args.bucket.type === "import_dependency_failure") {
|
|
658
961
|
return "it is the first visible failing module in this missing dependency bucket";
|
|
659
962
|
}
|
|
@@ -665,11 +968,54 @@ function buildReadTargetWhy(args) {
|
|
|
665
968
|
}
|
|
666
969
|
return `it maps to the visible ${args.bucketLabel} bucket`;
|
|
667
970
|
}
|
|
971
|
+
function buildExtendedBucketSearchHint(bucket, anchor) {
|
|
972
|
+
const extended = findExtendedBucketSpec(bucket.reason);
|
|
973
|
+
if (!extended) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
const detail = extractReasonDetail(bucket.reason, extended.prefix);
|
|
977
|
+
if (!detail) {
|
|
978
|
+
return anchor.label.split("::")[1]?.trim() ?? anchor.label ?? null;
|
|
979
|
+
}
|
|
980
|
+
if (extended.type === "timeout_failure") {
|
|
981
|
+
const duration = detail.match(/>\s*([0-9]+(?:\.[0-9]+)?s?)/i)?.[1];
|
|
982
|
+
return duration ?? anchor.label.split("::")[1]?.trim() ?? detail;
|
|
983
|
+
}
|
|
984
|
+
if (extended.type === "db_migration_failure") {
|
|
985
|
+
const relation = detail.match(/\b(?:relation|table)\s+([A-Za-z0-9_.-]+)/i)?.[1];
|
|
986
|
+
return relation ?? detail;
|
|
987
|
+
}
|
|
988
|
+
if (extended.type === "network_failure") {
|
|
989
|
+
const url = detail.match(/\bhttps?:\/\/[^\s)'"`]+/i)?.[0];
|
|
990
|
+
const host = detail.match(/\b(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}\b/)?.[0];
|
|
991
|
+
return url ?? host ?? detail;
|
|
992
|
+
}
|
|
993
|
+
if (extended.type === "xdist_worker_crash") {
|
|
994
|
+
return detail.match(/\bgw\d+\b/)?.[0] ?? detail;
|
|
995
|
+
}
|
|
996
|
+
if (extended.type === "fixture_teardown_failure") {
|
|
997
|
+
return detail.replace(/^of\s+/i, "") || anchor.label;
|
|
998
|
+
}
|
|
999
|
+
if (extended.type === "file_not_found_failure") {
|
|
1000
|
+
const path4 = detail.match(/['"]([^'"]+)['"]/)?.[1];
|
|
1001
|
+
return path4 ?? detail;
|
|
1002
|
+
}
|
|
1003
|
+
if (extended.type === "permission_denied_failure") {
|
|
1004
|
+
const path4 = detail.match(/['"]([^'"]+)['"]/)?.[1];
|
|
1005
|
+
const port = detail.match(/\bport\s+(\d+)\b/i)?.[1];
|
|
1006
|
+
return path4 ?? (port ? `port ${port}` : detail);
|
|
1007
|
+
}
|
|
1008
|
+
return detail;
|
|
1009
|
+
}
|
|
668
1010
|
function buildReadTargetSearchHint(bucket, anchor) {
|
|
669
1011
|
const envVar = bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
|
|
670
1012
|
if (envVar) {
|
|
671
1013
|
return envVar;
|
|
672
1014
|
}
|
|
1015
|
+
const extendedHint = buildExtendedBucketSearchHint(bucket, anchor);
|
|
1016
|
+
if (extendedHint) {
|
|
1017
|
+
return extendedHint;
|
|
1018
|
+
}
|
|
673
1019
|
if (bucket.type === "contract_snapshot_drift") {
|
|
674
1020
|
return bucket.entities.find((value) => value.startsWith("/api/")) ?? bucket.entities[0] ?? null;
|
|
675
1021
|
}
|
|
@@ -784,6 +1130,10 @@ function extractMiniDiff(input, bucket) {
|
|
|
784
1130
|
};
|
|
785
1131
|
}
|
|
786
1132
|
function inferSupplementCoverageKind(args) {
|
|
1133
|
+
const extended = findExtendedBucketSpec(args.rootCause);
|
|
1134
|
+
if (extended?.defaultCoverage === "error" || extended?.defaultCoverage === "failed") {
|
|
1135
|
+
return extended.defaultCoverage;
|
|
1136
|
+
}
|
|
787
1137
|
const normalized = `${args.label} ${args.rootCause}`.toLowerCase();
|
|
788
1138
|
if (/env|setup|fixture|import|dependency|service|db|database|auth bypass|collection|connection refused/.test(
|
|
789
1139
|
normalized
|
|
@@ -854,7 +1204,7 @@ function buildProviderSupplementBuckets(args) {
|
|
|
854
1204
|
});
|
|
855
1205
|
}
|
|
856
1206
|
function pickUnknownAnchor(args) {
|
|
857
|
-
const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] :
|
|
1207
|
+
const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : args.analysis.visibleFailedItems[0];
|
|
858
1208
|
if (fromStatusItems) {
|
|
859
1209
|
return {
|
|
860
1210
|
label: fromStatusItems.label,
|
|
@@ -891,12 +1241,14 @@ function buildUnknownBucket(args) {
|
|
|
891
1241
|
const isError = args.kind === "error";
|
|
892
1242
|
const label = isError ? "unknown setup blocker" : "unknown failure family";
|
|
893
1243
|
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";
|
|
1244
|
+
const firstConcreteSignal = anchor && anchor.reason !== reason && anchor.reason !== "setup failures share a repeated but unclassified pattern" && anchor.reason !== "failing tests share a repeated but unclassified pattern" ? `First concrete signal: ${anchor.reason}` : null;
|
|
894
1245
|
return {
|
|
895
1246
|
type: "unknown_failure",
|
|
896
1247
|
headline: `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
|
|
897
1248
|
summaryLines: [
|
|
898
|
-
`${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern
|
|
899
|
-
|
|
1249
|
+
`${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
|
|
1250
|
+
firstConcreteSignal
|
|
1251
|
+
].filter((value) => Boolean(value)),
|
|
900
1252
|
reason,
|
|
901
1253
|
count: args.count,
|
|
902
1254
|
confidence: 0.45,
|
|
@@ -1023,10 +1375,14 @@ function buildStandardAnchorText(target) {
|
|
|
1023
1375
|
}
|
|
1024
1376
|
return formatReadTargetLocation(target);
|
|
1025
1377
|
}
|
|
1026
|
-
function
|
|
1378
|
+
function resolveBucketFixHint(args) {
|
|
1027
1379
|
if (args.bucket.hint) {
|
|
1028
1380
|
return args.bucket.hint;
|
|
1029
1381
|
}
|
|
1382
|
+
const extended = findExtendedBucketSpec(args.bucket.reason);
|
|
1383
|
+
if (extended) {
|
|
1384
|
+
return extended.fix;
|
|
1385
|
+
}
|
|
1030
1386
|
const envVar = args.bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
|
|
1031
1387
|
if (envVar) {
|
|
1032
1388
|
return `Set ${envVar} before rerunning the affected tests.`;
|
|
@@ -1056,6 +1412,9 @@ function buildStandardFixText(args) {
|
|
|
1056
1412
|
if (args.bucket.type === "contract_snapshot_drift") {
|
|
1057
1413
|
return "Review the visible drift and regenerate the contract snapshots if the changes are intentional.";
|
|
1058
1414
|
}
|
|
1415
|
+
if (args.bucket.type === "snapshot_mismatch") {
|
|
1416
|
+
return "Update the snapshots if these output changes are intentional, then rerun the full suite at standard.";
|
|
1417
|
+
}
|
|
1059
1418
|
if (args.bucket.type === "assertion_failure") {
|
|
1060
1419
|
return "Inspect the failing assertion and rerun the full suite at standard.";
|
|
1061
1420
|
}
|
|
@@ -1065,13 +1424,75 @@ function buildStandardFixText(args) {
|
|
|
1065
1424
|
if (args.bucket.type === "runtime_failure") {
|
|
1066
1425
|
return `Fix the visible ${args.bucketLabel} and rerun the full suite at standard.`;
|
|
1067
1426
|
}
|
|
1068
|
-
return
|
|
1427
|
+
return "Inspect the first visible anchor for this bucket, apply the smallest fix that explains it, then rerun the full suite at standard.";
|
|
1428
|
+
}
|
|
1429
|
+
function deriveBucketSuspectKind(args) {
|
|
1430
|
+
if (args.bucket.type === "shared_environment_blocker" || args.bucket.type === "fixture_guard_failure" || args.bucket.type === "permission_denied_failure" || args.bucket.type === "django_db_access_denied" || args.bucket.type === "network_failure" || args.bucket.type === "service_unavailable" || args.bucket.type === "db_connection_failure" || args.bucket.type === "auth_bypass_absent" || args.bucket.type === "fixture_teardown_failure") {
|
|
1431
|
+
return "environment";
|
|
1432
|
+
}
|
|
1433
|
+
if (args.bucket.type === "configuration_error" || args.bucket.type === "db_migration_failure" || args.bucket.type === "import_dependency_failure" || args.bucket.type === "collection_failure" || args.bucket.type === "no_tests_collected" || args.bucket.type === "deprecation_warning_as_error" || args.bucket.type === "file_not_found_failure") {
|
|
1434
|
+
return "config";
|
|
1435
|
+
}
|
|
1436
|
+
if (args.bucket.type === "contract_snapshot_drift" || args.bucket.type === "snapshot_mismatch" || args.bucket.type === "flaky_test_detected" || args.bucket.type === "xfail_strict_unexpected_pass") {
|
|
1437
|
+
return "test";
|
|
1438
|
+
}
|
|
1439
|
+
if (args.bucket.type === "xdist_worker_crash" || args.bucket.type === "timeout_failure" || args.bucket.type === "async_event_loop_failure" || args.bucket.type === "subprocess_crash_segfault" || args.bucket.type === "memory_error" || args.bucket.type === "resource_leak_warning" || args.bucket.type === "interrupted_run") {
|
|
1440
|
+
return "tooling";
|
|
1441
|
+
}
|
|
1442
|
+
if (args.bucket.type === "unknown_failure") {
|
|
1443
|
+
return "unknown";
|
|
1444
|
+
}
|
|
1445
|
+
if (args.bucket.type === "assertion_failure" || args.bucket.type === "runtime_failure" || args.bucket.type === "type_error_failure" || args.bucket.type === "serialization_encoding_failure") {
|
|
1446
|
+
const file = args.readTarget?.file ?? "";
|
|
1447
|
+
if (file.startsWith("src/")) {
|
|
1448
|
+
return "app_code";
|
|
1449
|
+
}
|
|
1450
|
+
if (file.startsWith("test/") || file.startsWith("tests/")) {
|
|
1451
|
+
return "test";
|
|
1452
|
+
}
|
|
1453
|
+
return "unknown";
|
|
1454
|
+
}
|
|
1455
|
+
return "unknown";
|
|
1456
|
+
}
|
|
1457
|
+
function derivePrimarySuspectKind(args) {
|
|
1458
|
+
const primaryBucket = (args.dominantBlockerBucketIndex !== null ? args.mainBuckets.find((bucket) => bucket.bucket_index === args.dominantBlockerBucketIndex) : null) ?? args.mainBuckets[0];
|
|
1459
|
+
return primaryBucket?.suspect_kind ?? "unknown";
|
|
1460
|
+
}
|
|
1461
|
+
function buildConfidenceReason(args) {
|
|
1462
|
+
const primaryBucket = args.mainBuckets.find((bucket) => bucket.dominant) ?? args.mainBuckets[0];
|
|
1463
|
+
if (args.decision === "stop" && primaryBucket && args.primarySuspectKind !== "unknown") {
|
|
1464
|
+
return `Dominant blocker (${primaryBucket.label}) is anchored and actionable.`;
|
|
1465
|
+
}
|
|
1466
|
+
if (args.decision === "zoom") {
|
|
1467
|
+
return "Unknown or low-confidence buckets remain; one deeper sift pass is justified.";
|
|
1468
|
+
}
|
|
1469
|
+
if (args.decision === "read_source") {
|
|
1470
|
+
return "The bucket is identified, but source context is still needed to make the next fix clear.";
|
|
1471
|
+
}
|
|
1472
|
+
return "Heuristic signal is still insufficient; exact traceback lines are needed.";
|
|
1473
|
+
}
|
|
1474
|
+
function formatSuspectKindLabel(kind) {
|
|
1475
|
+
switch (kind) {
|
|
1476
|
+
case "test":
|
|
1477
|
+
return "test code";
|
|
1478
|
+
case "app_code":
|
|
1479
|
+
return "application code";
|
|
1480
|
+
case "config":
|
|
1481
|
+
return "test or project configuration";
|
|
1482
|
+
case "environment":
|
|
1483
|
+
return "environment setup";
|
|
1484
|
+
case "tooling":
|
|
1485
|
+
return "test runner or tooling";
|
|
1486
|
+
default:
|
|
1487
|
+
return "unknown";
|
|
1488
|
+
}
|
|
1069
1489
|
}
|
|
1070
1490
|
function buildStandardBucketSupport(args) {
|
|
1071
1491
|
return {
|
|
1072
1492
|
headline: args.bucket.summaryLines[0] ? `- ${args.bucket.summaryLines[0]}` : renderBucketHeadline(args.contractBucket),
|
|
1493
|
+
firstConcreteSignalText: args.bucket.source === "unknown" ? args.bucket.summaryLines[1] ?? null : null,
|
|
1073
1494
|
anchorText: buildStandardAnchorText(args.readTarget),
|
|
1074
|
-
fixText:
|
|
1495
|
+
fixText: resolveBucketFixHint({
|
|
1075
1496
|
bucket: args.bucket,
|
|
1076
1497
|
bucketLabel: args.contractBucket.label
|
|
1077
1498
|
})
|
|
@@ -1094,6 +1515,9 @@ function renderStandard(args) {
|
|
|
1094
1515
|
)
|
|
1095
1516
|
});
|
|
1096
1517
|
lines.push(support.headline);
|
|
1518
|
+
if (support.firstConcreteSignalText) {
|
|
1519
|
+
lines.push(`- ${support.firstConcreteSignalText}`);
|
|
1520
|
+
}
|
|
1097
1521
|
if (support.anchorText) {
|
|
1098
1522
|
lines.push(`- Anchor: ${support.anchorText}`);
|
|
1099
1523
|
}
|
|
@@ -1103,6 +1527,7 @@ function renderStandard(args) {
|
|
|
1103
1527
|
}
|
|
1104
1528
|
}
|
|
1105
1529
|
lines.push(buildDecisionLine(args.contract));
|
|
1530
|
+
lines.push(`- Likely owner: ${formatSuspectKindLabel(args.contract.primary_suspect_kind)}`);
|
|
1106
1531
|
lines.push(`- Next: ${args.contract.next_best_action.note}`);
|
|
1107
1532
|
lines.push(buildStopSignal(args.contract));
|
|
1108
1533
|
return lines.join("\n");
|
|
@@ -1190,29 +1615,49 @@ function buildTestStatusDiagnoseContract(args) {
|
|
|
1190
1615
|
})[0] ?? null;
|
|
1191
1616
|
const hasUnknownBucket = buckets.some((bucket) => isUnknownBucket(bucket));
|
|
1192
1617
|
const hasConcreteCoverage = args.analysis.failed === 0 && args.analysis.errors === 0 ? true : residuals.remainingErrors === 0 && residuals.remainingFailed === 0;
|
|
1193
|
-
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;
|
|
1194
|
-
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);
|
|
1195
1618
|
const dominantBlockerBucketIndex = dominantBucket && isDominantBlockerType(dominantBucket.bucket.type) ? dominantBucket.index + 1 : null;
|
|
1196
1619
|
const readTargets = buildReadTargets({
|
|
1197
1620
|
buckets,
|
|
1198
1621
|
dominantBucketIndex: dominantBlockerBucketIndex
|
|
1199
1622
|
});
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1623
|
+
const dominantBucketHasConcreteAnchor = dominantBucket !== null && (readTargets.some((target) => target.bucket_index === dominantBucket.index + 1 && target.file.length > 0) || dominantBucket.bucket.representativeItems.some((item) => item.anchor_kind !== "none"));
|
|
1624
|
+
const smallConcreteSuite = args.analysis.failed + args.analysis.errors <= 2 && residuals.remainingErrors === 0 && residuals.remainingFailed === 0 && buckets.length === 1 && !hasUnknownBucket && dominantBucket !== null && dominantBucketHasConcreteAnchor;
|
|
1625
|
+
const dominantConfidenceThreshold = smallConcreteSuite ? 0.55 : 0.6;
|
|
1626
|
+
const diagnosisComplete = args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0 || simpleCollectionFailure || buckets.length > 0 && hasConcreteCoverage && !hasUnknownBucket && (dominantBucket?.bucket.confidence ?? 0) >= dominantConfidenceThreshold;
|
|
1627
|
+
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);
|
|
1628
|
+
const mainBuckets = buckets.map((bucket, index) => {
|
|
1629
|
+
const bucketIndex = index + 1;
|
|
1630
|
+
const label = labelForBucket(bucket);
|
|
1631
|
+
const readTarget = readTargets.find((target) => target.bucket_index === bucketIndex);
|
|
1632
|
+
return {
|
|
1633
|
+
bucket_index: bucketIndex,
|
|
1634
|
+
label,
|
|
1635
|
+
count: bucket.count,
|
|
1636
|
+
root_cause: bucket.reason,
|
|
1637
|
+
suspect_kind: deriveBucketSuspectKind({
|
|
1638
|
+
bucket,
|
|
1639
|
+
readTarget
|
|
1640
|
+
}),
|
|
1641
|
+
fix_hint: resolveBucketFixHint({
|
|
1642
|
+
bucket,
|
|
1643
|
+
bucketLabel: label
|
|
1644
|
+
}),
|
|
1645
|
+
evidence: buildBucketEvidence(bucket),
|
|
1646
|
+
bucket_confidence: Number(bucket.confidence.toFixed(2)),
|
|
1647
|
+
root_cause_confidence: Number(rootCauseConfidenceFor(bucket).toFixed(2)),
|
|
1648
|
+
dominant: dominantBucket?.index === index,
|
|
1649
|
+
secondary_visible_despite_blocker: dominantBlockerBucketIndex !== null && dominantBlockerBucketIndex !== bucketIndex,
|
|
1650
|
+
mini_diff: extractMiniDiff(args.input, bucket)
|
|
1651
|
+
};
|
|
1652
|
+
});
|
|
1212
1653
|
const resolvedTests = unique(args.resolvedTests ?? []);
|
|
1213
1654
|
const remainingTests = unique(
|
|
1214
1655
|
args.remainingTests ?? unique([...args.analysis.visibleErrorLabels, ...args.analysis.visibleFailedLabels])
|
|
1215
1656
|
);
|
|
1657
|
+
const primarySuspectKind = derivePrimarySuspectKind({
|
|
1658
|
+
mainBuckets,
|
|
1659
|
+
dominantBlockerBucketIndex
|
|
1660
|
+
});
|
|
1216
1661
|
let nextBestAction;
|
|
1217
1662
|
if (args.analysis.failed === 0 && args.analysis.errors === 0 && args.analysis.passed > 0) {
|
|
1218
1663
|
nextBestAction = {
|
|
@@ -1258,6 +1703,8 @@ function buildTestStatusDiagnoseContract(args) {
|
|
|
1258
1703
|
additional_source_read_likely_low_value: diagnosisComplete && !rawNeeded,
|
|
1259
1704
|
read_raw_only_if: rawNeeded ? "you still need exact traceback lines after focused or verbose detail" : null,
|
|
1260
1705
|
dominant_blocker_bucket_index: dominantBlockerBucketIndex,
|
|
1706
|
+
primary_suspect_kind: primarySuspectKind,
|
|
1707
|
+
confidence_reason: "Unknown or low-confidence buckets remain; one deeper sift pass is justified.",
|
|
1261
1708
|
provider_used: false,
|
|
1262
1709
|
provider_confidence: null,
|
|
1263
1710
|
provider_failed: false,
|
|
@@ -1289,9 +1736,16 @@ function buildTestStatusDiagnoseContract(args) {
|
|
|
1289
1736
|
})
|
|
1290
1737
|
}
|
|
1291
1738
|
};
|
|
1739
|
+
const resolvedDecision = effectiveDecision ?? deriveDecision(mergedContractWithoutDecision);
|
|
1740
|
+
const resolvedConfidenceReason = buildConfidenceReason({
|
|
1741
|
+
decision: resolvedDecision,
|
|
1742
|
+
mainBuckets,
|
|
1743
|
+
primarySuspectKind: mergedContractWithoutDecision.primary_suspect_kind
|
|
1744
|
+
});
|
|
1292
1745
|
const contract = testStatusDiagnoseContractSchema.parse({
|
|
1293
1746
|
...mergedContractWithoutDecision,
|
|
1294
|
-
|
|
1747
|
+
confidence_reason: resolvedConfidenceReason,
|
|
1748
|
+
decision: resolvedDecision
|
|
1295
1749
|
});
|
|
1296
1750
|
return {
|
|
1297
1751
|
contract,
|
|
@@ -1363,6 +1817,27 @@ function buildTestStatusAnalysisContext(args) {
|
|
|
1363
1817
|
var RISK_LINE_PATTERN = /(destroy|delete|drop|recreate|replace|revoke|deny|downtime|data loss|iam|network exposure)/i;
|
|
1364
1818
|
var ZERO_DESTRUCTIVE_SUMMARY_PATTERN = /\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i;
|
|
1365
1819
|
var SAFE_LINE_PATTERN = /(no changes|up-to-date|up to date|no risky changes|safe to apply)/i;
|
|
1820
|
+
var RESOURCE_DESTROY_HEADER_PATTERN = /^#\s+.+\bwill be (destroyed|deleted|replaced)\b/i;
|
|
1821
|
+
var DESTROY_ERROR_PATTERN = /(instance cannot be destroyed|prevent_destroy|downtime|data loss)/i;
|
|
1822
|
+
var ACTION_DESTROY_PATTERN = /^-\s+destroy$/i;
|
|
1823
|
+
var TSC_CODE_LABELS = {
|
|
1824
|
+
TS1002: "syntax error",
|
|
1825
|
+
TS1005: "syntax error",
|
|
1826
|
+
TS2304: "cannot find name",
|
|
1827
|
+
TS2307: "cannot find module",
|
|
1828
|
+
TS2322: "type mismatch",
|
|
1829
|
+
TS2339: "missing property on type",
|
|
1830
|
+
TS2345: "argument type mismatch",
|
|
1831
|
+
TS2554: "wrong argument count",
|
|
1832
|
+
TS2741: "missing required property",
|
|
1833
|
+
TS2769: "no matching overload",
|
|
1834
|
+
TS5083: "config file error",
|
|
1835
|
+
TS6133: "declared but unused",
|
|
1836
|
+
TS7006: "implicit any",
|
|
1837
|
+
TS18003: "no inputs were found",
|
|
1838
|
+
TS18046: "unknown type",
|
|
1839
|
+
TS18048: "possibly undefined"
|
|
1840
|
+
};
|
|
1366
1841
|
function collectEvidence(input, matcher, limit = 3) {
|
|
1367
1842
|
return input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && matcher.test(line)).slice(0, limit);
|
|
1368
1843
|
}
|
|
@@ -1376,11 +1851,236 @@ function inferPackage(line) {
|
|
|
1376
1851
|
function inferRemediation(pkg) {
|
|
1377
1852
|
return `Upgrade ${pkg} to a patched version.`;
|
|
1378
1853
|
}
|
|
1854
|
+
function parseCompactAuditVulnerability(line) {
|
|
1855
|
+
if (/^Severity:\s*/i.test(line)) {
|
|
1856
|
+
return null;
|
|
1857
|
+
}
|
|
1858
|
+
if (!/\b(critical|high)\b/i.test(line)) {
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
const pkg = inferPackage(line);
|
|
1862
|
+
if (!pkg) {
|
|
1863
|
+
return null;
|
|
1864
|
+
}
|
|
1865
|
+
return {
|
|
1866
|
+
package: pkg,
|
|
1867
|
+
severity: inferSeverity(line),
|
|
1868
|
+
remediation: inferRemediation(pkg)
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function inferAuditPackageHeader(line) {
|
|
1872
|
+
const trimmed = line.trim();
|
|
1873
|
+
if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.includes(":") || /^node_modules\//i.test(trimmed)) {
|
|
1874
|
+
return null;
|
|
1875
|
+
}
|
|
1876
|
+
const match = trimmed.match(/^([@a-z0-9._/-]+)(?:\s{2,}|\s+(?:[<>=~^*]|\d))/i);
|
|
1877
|
+
return match?.[1] ?? null;
|
|
1878
|
+
}
|
|
1879
|
+
function collectAuditCriticalVulnerabilities(input) {
|
|
1880
|
+
const lines = input.split("\n");
|
|
1881
|
+
const vulnerabilities = [];
|
|
1882
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1883
|
+
const pushVulnerability = (pkg, severity) => {
|
|
1884
|
+
const key = `${pkg}:${severity}`;
|
|
1885
|
+
if (seen.has(key)) {
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
seen.add(key);
|
|
1889
|
+
vulnerabilities.push({
|
|
1890
|
+
package: pkg,
|
|
1891
|
+
severity,
|
|
1892
|
+
remediation: inferRemediation(pkg)
|
|
1893
|
+
});
|
|
1894
|
+
};
|
|
1895
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1896
|
+
const line = lines[index].trim();
|
|
1897
|
+
if (!line) {
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
const compact = parseCompactAuditVulnerability(line);
|
|
1901
|
+
if (compact) {
|
|
1902
|
+
pushVulnerability(compact.package, compact.severity);
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
const pkg = inferAuditPackageHeader(line);
|
|
1906
|
+
if (!pkg) {
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
for (let cursor = index + 1; cursor < Math.min(lines.length, index + 5); cursor += 1) {
|
|
1910
|
+
const candidate = lines[cursor].trim();
|
|
1911
|
+
if (!candidate) {
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
const severityMatch = candidate.match(/^Severity:\s*(critical|high)\b/i);
|
|
1915
|
+
if (severityMatch) {
|
|
1916
|
+
pushVulnerability(pkg, severityMatch[1].toLowerCase());
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
if (inferAuditPackageHeader(candidate) || parseCompactAuditVulnerability(candidate)) {
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
return vulnerabilities;
|
|
1925
|
+
}
|
|
1379
1926
|
function getCount(input, label) {
|
|
1380
1927
|
const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
|
|
1381
1928
|
const lastMatch = matches.at(-1);
|
|
1382
1929
|
return lastMatch ? Number(lastMatch[1]) : 0;
|
|
1383
1930
|
}
|
|
1931
|
+
function collectInfraRiskEvidence(input) {
|
|
1932
|
+
const lines = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1933
|
+
const evidence = [];
|
|
1934
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1935
|
+
const pushMatches = (matcher, options) => {
|
|
1936
|
+
let added = 0;
|
|
1937
|
+
for (const line of lines) {
|
|
1938
|
+
if (!matcher.test(line)) {
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
if (options?.exclude?.test(line)) {
|
|
1942
|
+
continue;
|
|
1943
|
+
}
|
|
1944
|
+
if (seen.has(line)) {
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
evidence.push(line);
|
|
1948
|
+
seen.add(line);
|
|
1949
|
+
added += 1;
|
|
1950
|
+
if (options?.limit && added >= options.limit) {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
if (evidence.length >= (options?.maxEvidence ?? 4)) {
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
pushMatches(/Plan:/i, {
|
|
1959
|
+
exclude: ZERO_DESTRUCTIVE_SUMMARY_PATTERN,
|
|
1960
|
+
limit: 1
|
|
1961
|
+
});
|
|
1962
|
+
if (evidence.length < 4) {
|
|
1963
|
+
pushMatches(RESOURCE_DESTROY_HEADER_PATTERN, { limit: 2 });
|
|
1964
|
+
}
|
|
1965
|
+
if (evidence.length < 4) {
|
|
1966
|
+
pushMatches(DESTROY_ERROR_PATTERN, { limit: 1 });
|
|
1967
|
+
}
|
|
1968
|
+
if (evidence.length < 4) {
|
|
1969
|
+
pushMatches(ACTION_DESTROY_PATTERN, { limit: 1 });
|
|
1970
|
+
}
|
|
1971
|
+
if (evidence.length < 4) {
|
|
1972
|
+
pushMatches(RISK_LINE_PATTERN, {
|
|
1973
|
+
exclude: /->\s+null$|\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i,
|
|
1974
|
+
maxEvidence: 4
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
return evidence.slice(0, 4);
|
|
1978
|
+
}
|
|
1979
|
+
function collectInfraDestroyTargets(input) {
|
|
1980
|
+
const targets = [];
|
|
1981
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1982
|
+
for (const line of input.split("\n").map((entry) => entry.trim())) {
|
|
1983
|
+
const match = line.match(/^#\s+(.+?)\s+will be (destroyed|deleted|replaced)\b/i);
|
|
1984
|
+
const target = match?.[1]?.trim();
|
|
1985
|
+
if (!target || seen.has(target)) {
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
seen.add(target);
|
|
1989
|
+
targets.push(target);
|
|
1990
|
+
}
|
|
1991
|
+
return targets;
|
|
1992
|
+
}
|
|
1993
|
+
function inferInfraDestroyCount(input, destroyTargets) {
|
|
1994
|
+
const matches = [
|
|
1995
|
+
...input.matchAll(/\b(\d+)\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/gi)
|
|
1996
|
+
];
|
|
1997
|
+
const lastMatch = matches.at(-1);
|
|
1998
|
+
return lastMatch ? Number(lastMatch[1]) : destroyTargets.length;
|
|
1999
|
+
}
|
|
2000
|
+
function collectInfraBlockers(input) {
|
|
2001
|
+
const lines = input.split("\n");
|
|
2002
|
+
const blockers = [];
|
|
2003
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2004
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
2005
|
+
const trimmed = lines[index]?.trim();
|
|
2006
|
+
const errorMatch = trimmed?.match(/^(?:[│|]\s*)?Error:\s+(.+)$/);
|
|
2007
|
+
if (!errorMatch) {
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
const message = errorMatch[1].trim();
|
|
2011
|
+
const nearby = lines.slice(index, index + 8).join("\n");
|
|
2012
|
+
const preventDestroyTarget = nearby.match(/Resource\s+([^\s]+)\s+has lifecycle\.prevent_destroy set/i)?.[1] ?? null;
|
|
2013
|
+
const type = preventDestroyTarget ? "prevent_destroy" : "destroy_blocked";
|
|
2014
|
+
const key = `${type}:${preventDestroyTarget ?? ""}:${message}`;
|
|
2015
|
+
if (seen.has(key)) {
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
seen.add(key);
|
|
2019
|
+
blockers.push({
|
|
2020
|
+
type,
|
|
2021
|
+
target: preventDestroyTarget,
|
|
2022
|
+
message
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
return blockers;
|
|
2026
|
+
}
|
|
2027
|
+
function detectTestRunner(input) {
|
|
2028
|
+
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)) {
|
|
2029
|
+
return "vitest";
|
|
2030
|
+
}
|
|
2031
|
+
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)) {
|
|
2032
|
+
return "jest";
|
|
2033
|
+
}
|
|
2034
|
+
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)) {
|
|
2035
|
+
return "pytest";
|
|
2036
|
+
}
|
|
2037
|
+
return "unknown";
|
|
2038
|
+
}
|
|
2039
|
+
function extractVitestLineCount(input, label, metric) {
|
|
2040
|
+
const matcher = new RegExp(`^\\s*${label}\\s+(.+)$`, "gmi");
|
|
2041
|
+
const lines = [...input.matchAll(matcher)];
|
|
2042
|
+
const line = lines.at(-1)?.[1];
|
|
2043
|
+
if (!line) {
|
|
2044
|
+
return null;
|
|
2045
|
+
}
|
|
2046
|
+
const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
|
|
2047
|
+
return metricMatch ? Number(metricMatch[1]) : null;
|
|
2048
|
+
}
|
|
2049
|
+
function extractJestLineCount(input, label, metric) {
|
|
2050
|
+
const matcher = new RegExp(`^\\s*${label}:\\s+(.+)$`, "gmi");
|
|
2051
|
+
const lines = [...input.matchAll(matcher)];
|
|
2052
|
+
const line = lines.at(-1)?.[1];
|
|
2053
|
+
if (!line) {
|
|
2054
|
+
return null;
|
|
2055
|
+
}
|
|
2056
|
+
const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
|
|
2057
|
+
return metricMatch ? Number(metricMatch[1]) : null;
|
|
2058
|
+
}
|
|
2059
|
+
function extractTestStatusCounts(input, runner) {
|
|
2060
|
+
if (runner === "vitest") {
|
|
2061
|
+
return {
|
|
2062
|
+
passed: extractVitestLineCount(input, "Tests?", "passed") ?? getCount(input, "passed"),
|
|
2063
|
+
failed: extractVitestLineCount(input, "Tests?", "failed") ?? getCount(input, "failed"),
|
|
2064
|
+
errors: extractVitestLineCount(input, "Errors?", "error") ?? extractVitestLineCount(input, "Errors?", "errors") ?? Math.max(getCount(input, "errors"), getCount(input, "error")),
|
|
2065
|
+
skipped: extractVitestLineCount(input, "Tests?", "skipped") ?? getCount(input, "skipped"),
|
|
2066
|
+
snapshotFailures: extractVitestLineCount(input, "Snapshots?", "failed") ?? void 0
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
if (runner === "jest") {
|
|
2070
|
+
return {
|
|
2071
|
+
passed: extractJestLineCount(input, "Tests", "passed") ?? getCount(input, "passed"),
|
|
2072
|
+
failed: extractJestLineCount(input, "Tests", "failed") ?? getCount(input, "failed"),
|
|
2073
|
+
errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
|
|
2074
|
+
skipped: extractJestLineCount(input, "Tests", "skipped") ?? getCount(input, "skipped")
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
return {
|
|
2078
|
+
passed: getCount(input, "passed"),
|
|
2079
|
+
failed: getCount(input, "failed"),
|
|
2080
|
+
errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
|
|
2081
|
+
skipped: getCount(input, "skipped")
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
1384
2084
|
function formatCount2(count, singular, plural = `${singular}s`) {
|
|
1385
2085
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
1386
2086
|
}
|
|
@@ -1401,6 +2101,21 @@ function collectUniqueMatches(input, matcher, limit = 6) {
|
|
|
1401
2101
|
}
|
|
1402
2102
|
return values;
|
|
1403
2103
|
}
|
|
2104
|
+
function compactDisplayFile(file) {
|
|
2105
|
+
const normalized = file.replace(/\\/g, "/").trim();
|
|
2106
|
+
if (!normalized) {
|
|
2107
|
+
return file;
|
|
2108
|
+
}
|
|
2109
|
+
const looksAbsolute = normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized);
|
|
2110
|
+
if (!looksAbsolute && normalized.length <= 60) {
|
|
2111
|
+
return normalized;
|
|
2112
|
+
}
|
|
2113
|
+
const basename = normalized.split("/").at(-1);
|
|
2114
|
+
return basename && basename.length > 0 ? basename : normalized;
|
|
2115
|
+
}
|
|
2116
|
+
function formatDisplayedFiles(files, limit = 3) {
|
|
2117
|
+
return [...new Set([...files].map((file) => file.trim()).filter(Boolean))].sort((left, right) => left.localeCompare(right)).slice(0, limit).map((file) => compactDisplayFile(file));
|
|
2118
|
+
}
|
|
1404
2119
|
function emptyAnchor() {
|
|
1405
2120
|
return {
|
|
1406
2121
|
file: null,
|
|
@@ -1413,7 +2128,8 @@ function normalizeAnchorFile(value) {
|
|
|
1413
2128
|
return value.replace(/\\/g, "/").trim();
|
|
1414
2129
|
}
|
|
1415
2130
|
function inferFileFromLabel(label) {
|
|
1416
|
-
const
|
|
2131
|
+
const cleaned = cleanFailureLabel(label);
|
|
2132
|
+
const candidate = cleaned.split("::")[0]?.split(" > ")[0]?.trim();
|
|
1417
2133
|
if (!candidate) {
|
|
1418
2134
|
return null;
|
|
1419
2135
|
}
|
|
@@ -1468,6 +2184,15 @@ function parseObservedAnchor(line) {
|
|
|
1468
2184
|
anchor_confidence: 0.92
|
|
1469
2185
|
};
|
|
1470
2186
|
}
|
|
2187
|
+
const vitestTraceback = normalized.match(/^\s*❯\s+([^:\s][^:]*\.[A-Za-z0-9]+):(\d+)(?::\d+)?/);
|
|
2188
|
+
if (vitestTraceback) {
|
|
2189
|
+
return {
|
|
2190
|
+
file: normalizeAnchorFile(vitestTraceback[1]),
|
|
2191
|
+
line: Number(vitestTraceback[2]),
|
|
2192
|
+
anchor_kind: "traceback",
|
|
2193
|
+
anchor_confidence: 1
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
1471
2196
|
return null;
|
|
1472
2197
|
}
|
|
1473
2198
|
function resolveAnchorForLabel(args) {
|
|
@@ -1484,15 +2209,27 @@ function isLowValueInternalReason(normalized) {
|
|
|
1484
2209
|
) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
|
|
1485
2210
|
}
|
|
1486
2211
|
function scoreFailureReason(reason) {
|
|
2212
|
+
if (reason.startsWith("configuration:")) {
|
|
2213
|
+
return 6;
|
|
2214
|
+
}
|
|
1487
2215
|
if (reason.startsWith("missing test env:")) {
|
|
1488
2216
|
return 6;
|
|
1489
2217
|
}
|
|
1490
2218
|
if (reason.startsWith("missing module:")) {
|
|
1491
2219
|
return 5;
|
|
1492
2220
|
}
|
|
2221
|
+
if (reason.startsWith("snapshot mismatch:")) {
|
|
2222
|
+
return 4;
|
|
2223
|
+
}
|
|
1493
2224
|
if (reason.startsWith("assertion failed:")) {
|
|
1494
2225
|
return 4;
|
|
1495
2226
|
}
|
|
2227
|
+
if (reason.startsWith("timeout:") || reason.startsWith("async loop:") || reason.startsWith("django db access:") || reason.startsWith("db migration:")) {
|
|
2228
|
+
return 3;
|
|
2229
|
+
}
|
|
2230
|
+
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:")) {
|
|
2231
|
+
return 2;
|
|
2232
|
+
}
|
|
1496
2233
|
if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
|
|
1497
2234
|
return 3;
|
|
1498
2235
|
}
|
|
@@ -1501,6 +2238,16 @@ function scoreFailureReason(reason) {
|
|
|
1501
2238
|
}
|
|
1502
2239
|
return 1;
|
|
1503
2240
|
}
|
|
2241
|
+
function buildClassifiedReason(prefix, detail) {
|
|
2242
|
+
return `${prefix}: ${detail}`.slice(0, 120);
|
|
2243
|
+
}
|
|
2244
|
+
function buildExcerptDetail(value, fallback) {
|
|
2245
|
+
const trimmed = value.trim().replace(/\s+/g, " ");
|
|
2246
|
+
return trimmed.length > 0 ? trimmed : fallback;
|
|
2247
|
+
}
|
|
2248
|
+
function sharedBlockerThreshold(reason) {
|
|
2249
|
+
return reason.startsWith("configuration:") ? 1 : 3;
|
|
2250
|
+
}
|
|
1504
2251
|
function extractEnvBlockerName(normalized) {
|
|
1505
2252
|
const directMatch = normalized.match(
|
|
1506
2253
|
/\bDB-isolated tests require\s+([A-Z][A-Z0-9_]{2,})\b/
|
|
@@ -1600,60 +2347,366 @@ function classifyFailureReason(line, options) {
|
|
|
1600
2347
|
group: "authentication test setup failures"
|
|
1601
2348
|
};
|
|
1602
2349
|
}
|
|
1603
|
-
const
|
|
1604
|
-
/
|
|
2350
|
+
const snapshotMismatch = normalized.match(
|
|
2351
|
+
/((?:Error:\s*)?Snapshot\b.+\bmismatched\b[^$]*|Snapshot comparison failed[^$]*)/i
|
|
1605
2352
|
);
|
|
1606
|
-
if (
|
|
2353
|
+
if (snapshotMismatch) {
|
|
1607
2354
|
return {
|
|
1608
|
-
reason:
|
|
1609
|
-
|
|
2355
|
+
reason: buildClassifiedReason(
|
|
2356
|
+
"snapshot mismatch",
|
|
2357
|
+
buildExcerptDetail(snapshotMismatch[1] ?? normalized, "snapshot output changed")
|
|
2358
|
+
),
|
|
2359
|
+
group: "snapshot mismatches"
|
|
1610
2360
|
};
|
|
1611
2361
|
}
|
|
1612
|
-
const
|
|
1613
|
-
|
|
2362
|
+
const timeoutFailure = normalized.match(
|
|
2363
|
+
/(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
|
|
2364
|
+
);
|
|
2365
|
+
if (timeoutFailure) {
|
|
1614
2366
|
return {
|
|
1615
|
-
reason:
|
|
1616
|
-
|
|
2367
|
+
reason: buildClassifiedReason(
|
|
2368
|
+
"timeout",
|
|
2369
|
+
buildExcerptDetail(timeoutFailure[1] ?? normalized, "test exceeded timeout threshold")
|
|
2370
|
+
),
|
|
2371
|
+
group: "timeout failures"
|
|
1617
2372
|
};
|
|
1618
2373
|
}
|
|
1619
|
-
const
|
|
1620
|
-
|
|
2374
|
+
const asyncLoopFailure = normalized.match(
|
|
2375
|
+
/(Event loop is closed|no current event loop|coroutine .* was never awaited|coroutine was never awaited)/i
|
|
2376
|
+
);
|
|
2377
|
+
if (asyncLoopFailure) {
|
|
1621
2378
|
return {
|
|
1622
|
-
reason:
|
|
1623
|
-
|
|
2379
|
+
reason: buildClassifiedReason(
|
|
2380
|
+
"async loop",
|
|
2381
|
+
buildExcerptDetail(asyncLoopFailure[1] ?? normalized, "async event loop failure")
|
|
2382
|
+
),
|
|
2383
|
+
group: "async event loop failures"
|
|
1624
2384
|
};
|
|
1625
2385
|
}
|
|
1626
|
-
const
|
|
1627
|
-
|
|
1628
|
-
|
|
2386
|
+
const permissionFailure = normalized.match(
|
|
2387
|
+
/(PermissionError:\s*\[Errno 13\][^$]*|Address already in use)/i
|
|
2388
|
+
);
|
|
2389
|
+
if (permissionFailure) {
|
|
1629
2390
|
return {
|
|
1630
|
-
reason:
|
|
1631
|
-
|
|
2391
|
+
reason: buildClassifiedReason(
|
|
2392
|
+
"permission",
|
|
2393
|
+
buildExcerptDetail(permissionFailure[1] ?? normalized, "permission denied or resource locked")
|
|
2394
|
+
),
|
|
2395
|
+
group: "permission or locked resource failures"
|
|
1632
2396
|
};
|
|
1633
2397
|
}
|
|
1634
|
-
|
|
2398
|
+
const osDiskFullFailure = normalized.match(
|
|
2399
|
+
/(OSError:\s*\[Errno 28\][^$]*|No space left on device)/i
|
|
2400
|
+
);
|
|
2401
|
+
if (osDiskFullFailure) {
|
|
1635
2402
|
return {
|
|
1636
|
-
reason:
|
|
1637
|
-
|
|
2403
|
+
reason: buildClassifiedReason(
|
|
2404
|
+
"configuration",
|
|
2405
|
+
`disk full (${buildExcerptDetail(
|
|
2406
|
+
osDiskFullFailure[1] ?? normalized,
|
|
2407
|
+
"No space left on device"
|
|
2408
|
+
)})`
|
|
2409
|
+
),
|
|
2410
|
+
group: "test configuration failures"
|
|
1638
2411
|
};
|
|
1639
2412
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
2413
|
+
const osPermissionFailure = normalized.match(/OSError:\s*\[Errno 13\][^$]*/i);
|
|
2414
|
+
if (osPermissionFailure) {
|
|
2415
|
+
return {
|
|
2416
|
+
reason: buildClassifiedReason(
|
|
2417
|
+
"permission",
|
|
2418
|
+
buildExcerptDetail(osPermissionFailure[0] ?? normalized, "permission denied")
|
|
2419
|
+
),
|
|
2420
|
+
group: "permission or locked resource failures"
|
|
2421
|
+
};
|
|
1642
2422
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
2423
|
+
const xdistWorkerCrash = normalized.match(
|
|
2424
|
+
/(worker ['"][^'"]+['"] crashed|node down:\s*[^,;]+|WorkerLost[^,;]*|Worker exited unexpectedly[^,;]*|worker exited unexpectedly[^,;]*)/i
|
|
2425
|
+
);
|
|
2426
|
+
if (xdistWorkerCrash) {
|
|
2427
|
+
return {
|
|
2428
|
+
reason: buildClassifiedReason(
|
|
2429
|
+
"xdist worker crash",
|
|
2430
|
+
buildExcerptDetail(xdistWorkerCrash[1] ?? normalized, "pytest-xdist worker crashed")
|
|
2431
|
+
),
|
|
2432
|
+
group: "xdist worker crashes"
|
|
2433
|
+
};
|
|
1651
2434
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
2435
|
+
if (/Worker terminated due to reaching memory limit/i.test(normalized)) {
|
|
2436
|
+
return {
|
|
2437
|
+
reason: "memory: Worker terminated due to reaching memory limit",
|
|
2438
|
+
group: "memory exhaustion failures"
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
if (/Database access not allowed, use the ["']django_db["'] mark/i.test(normalized)) {
|
|
2442
|
+
return {
|
|
2443
|
+
reason: 'django db access: Database access not allowed, use the "django_db" mark',
|
|
2444
|
+
group: "django database marker failures"
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
const networkFailure = normalized.match(
|
|
2448
|
+
/(Max retries exceeded[^,;]*|gaierror[^,;]*|SSLCertVerificationError[^,;]*|Network is unreachable|ConnectionResetError[^,;]*|BrokenPipeError[^,;]*|HTTPError:\s*[45]\d\d[^,;]*)/i
|
|
2449
|
+
);
|
|
2450
|
+
if (networkFailure) {
|
|
2451
|
+
return {
|
|
2452
|
+
reason: buildClassifiedReason(
|
|
2453
|
+
"network",
|
|
2454
|
+
buildExcerptDetail(networkFailure[1] ?? normalized, "network dependency failure")
|
|
2455
|
+
),
|
|
2456
|
+
group: "network dependency failures"
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
const matcherAssertionFailure = normalized.match(
|
|
2460
|
+
/(expect\(received\)\.(?:toBe|toEqual|toStrictEqual|toMatchObject)\(expected\))/i
|
|
2461
|
+
);
|
|
2462
|
+
if (matcherAssertionFailure) {
|
|
2463
|
+
return {
|
|
2464
|
+
reason: `assertion failed: ${matcherAssertionFailure[1]}`.slice(0, 120),
|
|
2465
|
+
group: "assertion failures"
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
const relationMigration = normalized.match(/relation ["'`]([^"'`]+)["'`] does not exist/i);
|
|
2469
|
+
if (relationMigration) {
|
|
2470
|
+
return {
|
|
2471
|
+
reason: buildClassifiedReason("db migration", `relation ${relationMigration[1]} does not exist`),
|
|
2472
|
+
group: "database migration or schema failures"
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
const noSuchTable = normalized.match(/no such table(?::)?\s*([A-Za-z0-9_.-]+)/i);
|
|
2476
|
+
if (noSuchTable) {
|
|
2477
|
+
return {
|
|
2478
|
+
reason: buildClassifiedReason("db migration", `no such table ${noSuchTable[1]}`),
|
|
2479
|
+
group: "database migration or schema failures"
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
if (/InconsistentMigrationHistory/i.test(normalized)) {
|
|
2483
|
+
return {
|
|
2484
|
+
reason: "db migration: InconsistentMigrationHistory",
|
|
2485
|
+
group: "database migration or schema failures"
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
if (/(Segmentation fault|SIGSEGV|\bexit 139\b)/i.test(normalized)) {
|
|
2489
|
+
return {
|
|
2490
|
+
reason: buildClassifiedReason(
|
|
2491
|
+
"segfault",
|
|
2492
|
+
buildExcerptDetail(normalized, "subprocess crashed with SIGSEGV")
|
|
2493
|
+
),
|
|
2494
|
+
group: "subprocess crash failures"
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
if (/(MemoryError\b|\bexit 137\b|OOMKilled|OutOfMemory)/i.test(normalized)) {
|
|
2498
|
+
return {
|
|
2499
|
+
reason: buildClassifiedReason(
|
|
2500
|
+
"memory",
|
|
2501
|
+
buildExcerptDetail(normalized, "process exhausted available memory")
|
|
2502
|
+
),
|
|
2503
|
+
group: "memory exhaustion failures"
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
const propertySetterOverrideFailure = normalized.match(
|
|
2507
|
+
/AttributeError:\s*(property ['"][^'"]+['"] of ['"][^'"]+['"] object has no setter|can't set attribute|readonly attribute|read-only attribute)/i
|
|
2508
|
+
);
|
|
2509
|
+
if (propertySetterOverrideFailure) {
|
|
2510
|
+
return {
|
|
2511
|
+
reason: buildClassifiedReason(
|
|
2512
|
+
"configuration",
|
|
2513
|
+
`invalid test setup override (${buildExcerptDetail(
|
|
2514
|
+
`AttributeError: ${propertySetterOverrideFailure[1] ?? normalized}`,
|
|
2515
|
+
"AttributeError: can't set attribute"
|
|
2516
|
+
)})`
|
|
2517
|
+
),
|
|
2518
|
+
group: "test configuration failures"
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
const setupOverrideFailure = normalized.match(/\b(AttributeError|TypeError):\s*(.+)$/i);
|
|
2522
|
+
if (setupOverrideFailure && /(monkeypatch|patch|fixture|settings|conftest)/i.test(normalized)) {
|
|
2523
|
+
return {
|
|
2524
|
+
reason: buildClassifiedReason(
|
|
2525
|
+
"configuration",
|
|
2526
|
+
`invalid test setup override (${buildExcerptDetail(
|
|
2527
|
+
`${setupOverrideFailure[1]}: ${setupOverrideFailure[2] ?? ""}`,
|
|
2528
|
+
`${setupOverrideFailure[1]}`
|
|
2529
|
+
)})`
|
|
2530
|
+
),
|
|
2531
|
+
group: "test configuration failures"
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
const typeErrorFailure = normalized.match(/TypeError:\s*(.+)$/i);
|
|
2535
|
+
if (typeErrorFailure) {
|
|
2536
|
+
return {
|
|
2537
|
+
reason: buildClassifiedReason(
|
|
2538
|
+
"type error",
|
|
2539
|
+
buildExcerptDetail(typeErrorFailure[1] ?? normalized, "TypeError")
|
|
2540
|
+
),
|
|
2541
|
+
group: "type errors"
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
const serializationFailure = normalized.match(
|
|
2545
|
+
/\b(UnicodeDecodeError|JSONDecodeError|PicklingError):\s*(.+)$/i
|
|
2546
|
+
);
|
|
2547
|
+
if (serializationFailure) {
|
|
2548
|
+
return {
|
|
2549
|
+
reason: buildClassifiedReason(
|
|
2550
|
+
"serialization",
|
|
2551
|
+
`${serializationFailure[1]}: ${buildExcerptDetail(serializationFailure[2] ?? "", serializationFailure[1] ?? "serialization failure")}`
|
|
2552
|
+
),
|
|
2553
|
+
group: "serialization and encoding failures"
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
const fileNotFoundFailure = normalized.match(/FileNotFoundError:\s*(.+)$/i);
|
|
2557
|
+
if (fileNotFoundFailure) {
|
|
2558
|
+
return {
|
|
2559
|
+
reason: buildClassifiedReason(
|
|
2560
|
+
"file not found",
|
|
2561
|
+
buildExcerptDetail(fileNotFoundFailure[1] ?? normalized, "missing file during test execution")
|
|
2562
|
+
),
|
|
2563
|
+
group: "missing file failures"
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
const deprecationFailure = normalized.match(
|
|
2567
|
+
/\b(DeprecationWarning|FutureWarning|PytestRemovedIn9Warning):\s*(.+)$/i
|
|
2568
|
+
);
|
|
2569
|
+
if (deprecationFailure) {
|
|
2570
|
+
return {
|
|
2571
|
+
reason: buildClassifiedReason(
|
|
2572
|
+
"deprecation as error",
|
|
2573
|
+
`${deprecationFailure[1]}: ${buildExcerptDetail(deprecationFailure[2] ?? "", deprecationFailure[1] ?? "warning treated as error")}`
|
|
2574
|
+
),
|
|
2575
|
+
group: "warnings treated as errors"
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
const strictXfail = normalized.match(/XPASS\(strict\)\s*:?\s*(.+)?$/i);
|
|
2579
|
+
if (strictXfail) {
|
|
2580
|
+
return {
|
|
2581
|
+
reason: buildClassifiedReason(
|
|
2582
|
+
"xfail strict",
|
|
2583
|
+
buildExcerptDetail(strictXfail[1] ?? normalized, "strict xfail unexpectedly passed")
|
|
2584
|
+
),
|
|
2585
|
+
group: "strict xfail expectation failures"
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
const resourceLeak = normalized.match(
|
|
2589
|
+
/(PytestUnraisableExceptionWarning[^,;]*|ResourceWarning:\s*unclosed[^,;]*)/i
|
|
2590
|
+
);
|
|
2591
|
+
if (resourceLeak) {
|
|
2592
|
+
return {
|
|
2593
|
+
reason: buildClassifiedReason(
|
|
2594
|
+
"resource leak",
|
|
2595
|
+
buildExcerptDetail(resourceLeak[1] ?? normalized, "resource leak warning promoted to failure")
|
|
2596
|
+
),
|
|
2597
|
+
group: "resource leak warnings"
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
const flakyFailure = normalized.match(/\b(RERUN|[0-9]+\s+rerun|Flaky test passed)\b[^$]*/i);
|
|
2601
|
+
if (flakyFailure) {
|
|
2602
|
+
return {
|
|
2603
|
+
reason: buildClassifiedReason(
|
|
2604
|
+
"flaky",
|
|
2605
|
+
buildExcerptDetail(flakyFailure[0] ?? normalized, "test required reruns before passing")
|
|
2606
|
+
),
|
|
2607
|
+
group: "flaky test detections"
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
const teardownFailure = normalized.match(/ERROR at teardown of\s+(.+)$/i);
|
|
2611
|
+
if (teardownFailure) {
|
|
2612
|
+
return {
|
|
2613
|
+
reason: buildClassifiedReason(
|
|
2614
|
+
"fixture teardown",
|
|
2615
|
+
buildExcerptDetail(teardownFailure[1] ?? normalized, "fixture teardown failed")
|
|
2616
|
+
),
|
|
2617
|
+
group: "fixture teardown failures"
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
const configurationFailure = normalized.match(
|
|
2621
|
+
/(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
|
|
2622
|
+
);
|
|
2623
|
+
if (configurationFailure) {
|
|
2624
|
+
return {
|
|
2625
|
+
reason: buildClassifiedReason(
|
|
2626
|
+
"configuration",
|
|
2627
|
+
buildExcerptDetail(configurationFailure[1] ?? normalized, "test configuration error")
|
|
2628
|
+
),
|
|
2629
|
+
group: "test configuration failures"
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
const pythonMissingModule = normalized.match(
|
|
2633
|
+
/ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
|
|
2634
|
+
);
|
|
2635
|
+
if (pythonMissingModule) {
|
|
2636
|
+
return {
|
|
2637
|
+
reason: `missing module: ${pythonMissingModule[1]}`,
|
|
2638
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
const nodeMissingModule = normalized.match(/Cannot find module ['"]([^'"]+)['"]/i);
|
|
2642
|
+
if (nodeMissingModule) {
|
|
2643
|
+
return {
|
|
2644
|
+
reason: `missing module: ${nodeMissingModule[1]}`,
|
|
2645
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
const importResolutionFailure = normalized.match(/Failed to resolve import ['"]([^'"]+)['"]/i);
|
|
2649
|
+
if (importResolutionFailure) {
|
|
2650
|
+
return {
|
|
2651
|
+
reason: `missing module: ${importResolutionFailure[1]}`,
|
|
2652
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
const esmModuleFailure = normalized.match(/ERR_MODULE_NOT_FOUND[^'"`]*['"`]([^'"`]+)['"`]/i) ?? normalized.match(/Cannot find package ['"`]([^'"`]+)['"`]/i);
|
|
2656
|
+
if (esmModuleFailure) {
|
|
2657
|
+
return {
|
|
2658
|
+
reason: `missing module: ${esmModuleFailure[1]}`,
|
|
2659
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
|
|
2663
|
+
if (assertionFailure) {
|
|
2664
|
+
return {
|
|
2665
|
+
reason: `assertion failed: ${assertionFailure[1]}`.slice(0, 120),
|
|
2666
|
+
group: "assertion failures"
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
const vitestUnhandled = normalized.match(/Vitest caught\s+\d+\s+unhandled errors?/i);
|
|
2670
|
+
if (vitestUnhandled) {
|
|
2671
|
+
return {
|
|
2672
|
+
reason: `RuntimeError: ${buildExcerptDetail(vitestUnhandled[0] ?? normalized, "Vitest caught unhandled errors")}`.slice(
|
|
2673
|
+
0,
|
|
2674
|
+
120
|
|
2675
|
+
),
|
|
2676
|
+
group: "runtime failures"
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
|
|
2680
|
+
if (genericError) {
|
|
2681
|
+
const errorType = genericError[1];
|
|
2682
|
+
return {
|
|
2683
|
+
reason: `${errorType}: ${genericError[2]}`.slice(0, 120),
|
|
2684
|
+
group: options.duringCollection && errorType === "ImportError" ? "import/dependency errors during collection" : `${errorType} failures`
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
if (/ImportError while importing test module/i.test(normalized)) {
|
|
2688
|
+
return {
|
|
2689
|
+
reason: "import error during collection",
|
|
2690
|
+
group: "import/dependency errors during collection"
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
if (!/[A-Za-z]/.test(normalized)) {
|
|
2694
|
+
return null;
|
|
2695
|
+
}
|
|
2696
|
+
return {
|
|
2697
|
+
reason: normalized.slice(0, 120),
|
|
2698
|
+
group: options.duringCollection ? "collection/import errors" : "other failures"
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
function pushFocusedFailureItem(items, candidate) {
|
|
2702
|
+
if (items.some((item) => item.label === candidate.label && item.reason === candidate.reason)) {
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
items.push(candidate);
|
|
2706
|
+
}
|
|
2707
|
+
function chooseStrongestFailureItems(items) {
|
|
2708
|
+
const strongest = /* @__PURE__ */ new Map();
|
|
2709
|
+
const order = [];
|
|
1657
2710
|
for (const item of items) {
|
|
1658
2711
|
const existing = strongest.get(item.label);
|
|
1659
2712
|
if (!existing) {
|
|
@@ -1667,6 +2720,125 @@ function chooseStrongestFailureItems(items) {
|
|
|
1667
2720
|
}
|
|
1668
2721
|
return order.map((label) => strongest.get(label));
|
|
1669
2722
|
}
|
|
2723
|
+
function extractJsTestFile(value) {
|
|
2724
|
+
const match = value.match(/([A-Za-z0-9_./-]+\.(?:test|spec)\.[cm]?[jt]sx?)/i);
|
|
2725
|
+
return match ? normalizeAnchorFile(match[1]) : null;
|
|
2726
|
+
}
|
|
2727
|
+
function normalizeJsFailureLabel(label) {
|
|
2728
|
+
return cleanFailureLabel(label).replace(/^[❯×]\s*/, "").replace(/\s+\[[^\]]+\]\s*$/, "").replace(/\s+/g, " ").trim();
|
|
2729
|
+
}
|
|
2730
|
+
function classifyFailureLines(args) {
|
|
2731
|
+
let observedAnchor = null;
|
|
2732
|
+
let strongest = null;
|
|
2733
|
+
for (const line of args.lines) {
|
|
2734
|
+
observedAnchor = parseObservedAnchor(line) ?? observedAnchor;
|
|
2735
|
+
const classification = classifyFailureReason(line, {
|
|
2736
|
+
duringCollection: args.duringCollection
|
|
2737
|
+
});
|
|
2738
|
+
if (!classification) {
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
const score = scoreFailureReason(classification.reason);
|
|
2742
|
+
if (!strongest || score > strongest.score) {
|
|
2743
|
+
strongest = {
|
|
2744
|
+
classification,
|
|
2745
|
+
score,
|
|
2746
|
+
observedAnchor: parseObservedAnchor(line) ?? observedAnchor
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
if (!strongest) {
|
|
2751
|
+
return null;
|
|
2752
|
+
}
|
|
2753
|
+
return {
|
|
2754
|
+
classification: strongest.classification,
|
|
2755
|
+
observedAnchor: strongest.observedAnchor ?? observedAnchor
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
function collectJsFailureBlocks(input) {
|
|
2759
|
+
const blocks = [];
|
|
2760
|
+
let current = null;
|
|
2761
|
+
let section = null;
|
|
2762
|
+
let currentFile = null;
|
|
2763
|
+
const flushCurrent = () => {
|
|
2764
|
+
if (!current) {
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
blocks.push(current);
|
|
2768
|
+
current = null;
|
|
2769
|
+
};
|
|
2770
|
+
for (const rawLine of input.split("\n")) {
|
|
2771
|
+
const line = rawLine.trimEnd();
|
|
2772
|
+
const trimmed = line.trim();
|
|
2773
|
+
if (/⎯{2,}\s+Failed Tests?\s+\d+\s+⎯{2,}/.test(line)) {
|
|
2774
|
+
flushCurrent();
|
|
2775
|
+
section = "failed_tests";
|
|
2776
|
+
continue;
|
|
2777
|
+
}
|
|
2778
|
+
if (/⎯{2,}\s+Failed Suites?\s+\d+\s+⎯{2,}/.test(line)) {
|
|
2779
|
+
flushCurrent();
|
|
2780
|
+
section = "failed_suites";
|
|
2781
|
+
continue;
|
|
2782
|
+
}
|
|
2783
|
+
if (section && /^⎯{2,}.+⎯{2,}\s*$/.test(line)) {
|
|
2784
|
+
flushCurrent();
|
|
2785
|
+
section = null;
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
const progress = line.match(
|
|
2789
|
+
/^(.+?\.(?:test|spec)\.[cm]?[jt]sx?(?:\s+>.+?)?)\s+(FAILED|ERROR)\s+\[[^\]]+\]\s*$/
|
|
2790
|
+
);
|
|
2791
|
+
if (progress) {
|
|
2792
|
+
flushCurrent();
|
|
2793
|
+
const label = normalizeJsFailureLabel(progress[1]);
|
|
2794
|
+
current = {
|
|
2795
|
+
label,
|
|
2796
|
+
status: progress[2] === "ERROR" ? "error" : "failed",
|
|
2797
|
+
detailLines: []
|
|
2798
|
+
};
|
|
2799
|
+
currentFile = extractJsTestFile(label);
|
|
2800
|
+
continue;
|
|
2801
|
+
}
|
|
2802
|
+
const failHeader = line.match(/^\s*FAIL\s+(.+)$/);
|
|
2803
|
+
if (failHeader) {
|
|
2804
|
+
const label = normalizeJsFailureLabel(failHeader[1]);
|
|
2805
|
+
if (extractJsTestFile(label)) {
|
|
2806
|
+
flushCurrent();
|
|
2807
|
+
current = {
|
|
2808
|
+
label,
|
|
2809
|
+
status: section === "failed_suites" || !label.includes(" > ") ? "error" : "failed",
|
|
2810
|
+
detailLines: []
|
|
2811
|
+
};
|
|
2812
|
+
currentFile = extractJsTestFile(label);
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
const failedTest = line.match(/^\s*×\s+(.+)$/);
|
|
2817
|
+
if (failedTest && (section === "failed_tests" || extractJsTestFile(failedTest[1]))) {
|
|
2818
|
+
flushCurrent();
|
|
2819
|
+
const candidate = normalizeJsFailureLabel(failedTest[1]);
|
|
2820
|
+
const file = extractJsTestFile(candidate) ?? currentFile;
|
|
2821
|
+
const label = file && !extractJsTestFile(candidate) ? `${file} > ${candidate}` : candidate;
|
|
2822
|
+
current = {
|
|
2823
|
+
label,
|
|
2824
|
+
status: "failed",
|
|
2825
|
+
detailLines: []
|
|
2826
|
+
};
|
|
2827
|
+
currentFile = extractJsTestFile(label) ?? currentFile;
|
|
2828
|
+
continue;
|
|
2829
|
+
}
|
|
2830
|
+
if (/^\s*(?:Tests?|Snapshots?|Test Files?|Test Suites?)\b/.test(line)) {
|
|
2831
|
+
flushCurrent();
|
|
2832
|
+
section = null;
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
if (current && trimmed.length > 0) {
|
|
2836
|
+
current.detailLines.push(line);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
flushCurrent();
|
|
2840
|
+
return blocks;
|
|
2841
|
+
}
|
|
1670
2842
|
function collectCollectionFailureItems(input) {
|
|
1671
2843
|
const items = [];
|
|
1672
2844
|
const lines = input.split("\n");
|
|
@@ -1674,6 +2846,24 @@ function collectCollectionFailureItems(input) {
|
|
|
1674
2846
|
let pendingGenericReason = null;
|
|
1675
2847
|
let currentAnchor = null;
|
|
1676
2848
|
for (const line of lines) {
|
|
2849
|
+
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];
|
|
2850
|
+
if (standaloneCollectionLabel) {
|
|
2851
|
+
const classification2 = classifyFailureReason(line, {
|
|
2852
|
+
duringCollection: true
|
|
2853
|
+
});
|
|
2854
|
+
if (classification2) {
|
|
2855
|
+
pushFocusedFailureItem(items, {
|
|
2856
|
+
label: cleanFailureLabel(standaloneCollectionLabel),
|
|
2857
|
+
reason: classification2.reason,
|
|
2858
|
+
group: classification2.group,
|
|
2859
|
+
...resolveAnchorForLabel({
|
|
2860
|
+
label: cleanFailureLabel(standaloneCollectionLabel),
|
|
2861
|
+
observedAnchor: parseObservedAnchor(line)
|
|
2862
|
+
})
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
continue;
|
|
2866
|
+
}
|
|
1677
2867
|
const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
|
|
1678
2868
|
if (collecting) {
|
|
1679
2869
|
if (currentLabel && pendingGenericReason) {
|
|
@@ -1762,6 +2952,24 @@ function collectInlineFailureItems(input) {
|
|
|
1762
2952
|
})
|
|
1763
2953
|
});
|
|
1764
2954
|
}
|
|
2955
|
+
for (const block of collectJsFailureBlocks(input)) {
|
|
2956
|
+
const resolved = classifyFailureLines({
|
|
2957
|
+
lines: block.detailLines,
|
|
2958
|
+
duringCollection: block.status === "error"
|
|
2959
|
+
});
|
|
2960
|
+
if (!resolved) {
|
|
2961
|
+
continue;
|
|
2962
|
+
}
|
|
2963
|
+
pushFocusedFailureItem(items, {
|
|
2964
|
+
label: block.label,
|
|
2965
|
+
reason: resolved.classification.reason,
|
|
2966
|
+
group: resolved.classification.group,
|
|
2967
|
+
...resolveAnchorForLabel({
|
|
2968
|
+
label: block.label,
|
|
2969
|
+
observedAnchor: resolved.observedAnchor
|
|
2970
|
+
})
|
|
2971
|
+
});
|
|
2972
|
+
}
|
|
1765
2973
|
return items;
|
|
1766
2974
|
}
|
|
1767
2975
|
function collectInlineFailureItemsWithStatus(input) {
|
|
@@ -1796,16 +3004,42 @@ function collectInlineFailureItemsWithStatus(input) {
|
|
|
1796
3004
|
})
|
|
1797
3005
|
});
|
|
1798
3006
|
}
|
|
3007
|
+
for (const block of collectJsFailureBlocks(input)) {
|
|
3008
|
+
const resolved = classifyFailureLines({
|
|
3009
|
+
lines: block.detailLines,
|
|
3010
|
+
duringCollection: block.status === "error"
|
|
3011
|
+
});
|
|
3012
|
+
if (!resolved) {
|
|
3013
|
+
continue;
|
|
3014
|
+
}
|
|
3015
|
+
items.push({
|
|
3016
|
+
label: block.label,
|
|
3017
|
+
reason: resolved.classification.reason,
|
|
3018
|
+
group: resolved.classification.group,
|
|
3019
|
+
status: block.status,
|
|
3020
|
+
...resolveAnchorForLabel({
|
|
3021
|
+
label: block.label,
|
|
3022
|
+
observedAnchor: resolved.observedAnchor
|
|
3023
|
+
})
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
1799
3026
|
return items;
|
|
1800
3027
|
}
|
|
1801
3028
|
function collectStandaloneErrorClassifications(input) {
|
|
1802
3029
|
const classifications = [];
|
|
1803
3030
|
for (const line of input.split("\n")) {
|
|
3031
|
+
const trimmed = line.trim();
|
|
3032
|
+
if (!trimmed) {
|
|
3033
|
+
continue;
|
|
3034
|
+
}
|
|
1804
3035
|
const standalone = line.match(/^\s*E\s+(.+)$/);
|
|
1805
|
-
|
|
3036
|
+
const candidate = standalone?.[1] ?? (/^(INTERNALERROR>|ConftestImportFailure\b|UsageError:|ERROR:\s*usage:|pytest:\s*error:)/i.test(
|
|
3037
|
+
trimmed
|
|
3038
|
+
) ? trimmed : null);
|
|
3039
|
+
if (!candidate) {
|
|
1806
3040
|
continue;
|
|
1807
3041
|
}
|
|
1808
|
-
const classification = classifyFailureReason(
|
|
3042
|
+
const classification = classifyFailureReason(candidate, {
|
|
1809
3043
|
duringCollection: false
|
|
1810
3044
|
});
|
|
1811
3045
|
if (!classification || classification.reason === "import error during collection") {
|
|
@@ -1921,6 +3155,9 @@ function collectFailureLabels(input) {
|
|
|
1921
3155
|
pushLabel(summary[2], summary[1] === "FAILED" ? "failed" : "error");
|
|
1922
3156
|
}
|
|
1923
3157
|
}
|
|
3158
|
+
for (const block of collectJsFailureBlocks(input)) {
|
|
3159
|
+
pushLabel(block.label, block.status);
|
|
3160
|
+
}
|
|
1924
3161
|
return labels;
|
|
1925
3162
|
}
|
|
1926
3163
|
function classifyBucketTypeFromReason(reason) {
|
|
@@ -1930,6 +3167,60 @@ function classifyBucketTypeFromReason(reason) {
|
|
|
1930
3167
|
if (reason.startsWith("fixture guard:")) {
|
|
1931
3168
|
return "fixture_guard_failure";
|
|
1932
3169
|
}
|
|
3170
|
+
if (reason.startsWith("timeout:")) {
|
|
3171
|
+
return "timeout_failure";
|
|
3172
|
+
}
|
|
3173
|
+
if (reason.startsWith("permission:")) {
|
|
3174
|
+
return "permission_denied_failure";
|
|
3175
|
+
}
|
|
3176
|
+
if (reason.startsWith("async loop:")) {
|
|
3177
|
+
return "async_event_loop_failure";
|
|
3178
|
+
}
|
|
3179
|
+
if (reason.startsWith("fixture teardown:")) {
|
|
3180
|
+
return "fixture_teardown_failure";
|
|
3181
|
+
}
|
|
3182
|
+
if (reason.startsWith("db migration:")) {
|
|
3183
|
+
return "db_migration_failure";
|
|
3184
|
+
}
|
|
3185
|
+
if (reason.startsWith("configuration:")) {
|
|
3186
|
+
return "configuration_error";
|
|
3187
|
+
}
|
|
3188
|
+
if (reason.startsWith("xdist worker crash:")) {
|
|
3189
|
+
return "xdist_worker_crash";
|
|
3190
|
+
}
|
|
3191
|
+
if (reason.startsWith("type error:")) {
|
|
3192
|
+
return "type_error_failure";
|
|
3193
|
+
}
|
|
3194
|
+
if (reason.startsWith("resource leak:")) {
|
|
3195
|
+
return "resource_leak_warning";
|
|
3196
|
+
}
|
|
3197
|
+
if (reason.startsWith("django db access:")) {
|
|
3198
|
+
return "django_db_access_denied";
|
|
3199
|
+
}
|
|
3200
|
+
if (reason.startsWith("network:")) {
|
|
3201
|
+
return "network_failure";
|
|
3202
|
+
}
|
|
3203
|
+
if (reason.startsWith("segfault:")) {
|
|
3204
|
+
return "subprocess_crash_segfault";
|
|
3205
|
+
}
|
|
3206
|
+
if (reason.startsWith("flaky:")) {
|
|
3207
|
+
return "flaky_test_detected";
|
|
3208
|
+
}
|
|
3209
|
+
if (reason.startsWith("serialization:")) {
|
|
3210
|
+
return "serialization_encoding_failure";
|
|
3211
|
+
}
|
|
3212
|
+
if (reason.startsWith("file not found:")) {
|
|
3213
|
+
return "file_not_found_failure";
|
|
3214
|
+
}
|
|
3215
|
+
if (reason.startsWith("memory:")) {
|
|
3216
|
+
return "memory_error";
|
|
3217
|
+
}
|
|
3218
|
+
if (reason.startsWith("deprecation as error:")) {
|
|
3219
|
+
return "deprecation_warning_as_error";
|
|
3220
|
+
}
|
|
3221
|
+
if (reason.startsWith("xfail strict:")) {
|
|
3222
|
+
return "xfail_strict_unexpected_pass";
|
|
3223
|
+
}
|
|
1933
3224
|
if (reason.startsWith("service unavailable:")) {
|
|
1934
3225
|
return "service_unavailable";
|
|
1935
3226
|
}
|
|
@@ -1939,6 +3230,9 @@ function classifyBucketTypeFromReason(reason) {
|
|
|
1939
3230
|
if (reason.startsWith("auth bypass absent:")) {
|
|
1940
3231
|
return "auth_bypass_absent";
|
|
1941
3232
|
}
|
|
3233
|
+
if (reason.startsWith("snapshot mismatch:")) {
|
|
3234
|
+
return "snapshot_mismatch";
|
|
3235
|
+
}
|
|
1942
3236
|
if (reason.startsWith("missing module:")) {
|
|
1943
3237
|
return "import_dependency_failure";
|
|
1944
3238
|
}
|
|
@@ -1951,9 +3245,6 @@ function classifyBucketTypeFromReason(reason) {
|
|
|
1951
3245
|
return "unknown_failure";
|
|
1952
3246
|
}
|
|
1953
3247
|
function synthesizeSharedBlockerBucket(args) {
|
|
1954
|
-
if (args.errors === 0) {
|
|
1955
|
-
return null;
|
|
1956
|
-
}
|
|
1957
3248
|
const visibleReasonGroups = /* @__PURE__ */ new Map();
|
|
1958
3249
|
for (const item of args.visibleErrorItems) {
|
|
1959
3250
|
const entry = visibleReasonGroups.get(item.reason);
|
|
@@ -1968,7 +3259,7 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
1968
3259
|
items: [item]
|
|
1969
3260
|
});
|
|
1970
3261
|
}
|
|
1971
|
-
const top = [...visibleReasonGroups.entries()].filter(([, entry]) => entry.count >=
|
|
3262
|
+
const top = [...visibleReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
|
|
1972
3263
|
const standaloneReasonGroups = /* @__PURE__ */ new Map();
|
|
1973
3264
|
for (const classification of collectStandaloneErrorClassifications(args.input)) {
|
|
1974
3265
|
const entry = standaloneReasonGroups.get(classification.reason);
|
|
@@ -1981,7 +3272,7 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
1981
3272
|
group: classification.group
|
|
1982
3273
|
});
|
|
1983
3274
|
}
|
|
1984
|
-
const standaloneTop = [...standaloneReasonGroups.entries()].filter(([, entry]) => entry.count >=
|
|
3275
|
+
const standaloneTop = [...standaloneReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
|
|
1985
3276
|
const visibleTopReason = top?.[0];
|
|
1986
3277
|
const visibleTopStats = top?.[1];
|
|
1987
3278
|
const standaloneTopReason = standaloneTop?.[0];
|
|
@@ -2020,6 +3311,12 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
2020
3311
|
let hint;
|
|
2021
3312
|
if (envVar) {
|
|
2022
3313
|
hint = `Set ${envVar} (or pass --pgtest-dsn) before rerunning DB-isolated tests.`;
|
|
3314
|
+
} else if (effectiveReason.startsWith("configuration:")) {
|
|
3315
|
+
hint = "Fix the pytest configuration or conftest import error before rerunning the suite.";
|
|
3316
|
+
} else if (effectiveReason.startsWith("xdist worker crash:")) {
|
|
3317
|
+
hint = "Check shared state, worker startup, or resource contention between xdist workers before rerunning.";
|
|
3318
|
+
} else if (effectiveReason.startsWith("network:")) {
|
|
3319
|
+
hint = "Restore DNS, TLS, or outbound network access for the affected dependency before rerunning.";
|
|
2023
3320
|
} else if (effectiveReason.startsWith("fixture guard:")) {
|
|
2024
3321
|
hint = "Unblock the required fixture or setup guard before rerunning the affected tests.";
|
|
2025
3322
|
} else if (effectiveReason.startsWith("db refused:")) {
|
|
@@ -2034,6 +3331,12 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
2034
3331
|
let headline;
|
|
2035
3332
|
if (envVar) {
|
|
2036
3333
|
headline = `Shared blocker: ${atLeastPrefix}${countText} errors require ${envVar} for DB-isolated tests.`;
|
|
3334
|
+
} else if (effectiveReason.startsWith("configuration:")) {
|
|
3335
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} visible failure${countText === 1 ? "" : "s"} are caused by a pytest configuration error.`;
|
|
3336
|
+
} else if (effectiveReason.startsWith("xdist worker crash:")) {
|
|
3337
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by xdist worker crashes.`;
|
|
3338
|
+
} else if (effectiveReason.startsWith("network:")) {
|
|
3339
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by a network dependency failure.`;
|
|
2037
3340
|
} else if (effectiveReason.startsWith("fixture guard:")) {
|
|
2038
3341
|
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are gated by the same fixture/setup guard.`;
|
|
2039
3342
|
} else if (effectiveReason.startsWith("db refused:")) {
|
|
@@ -2064,11 +3367,17 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
2064
3367
|
};
|
|
2065
3368
|
}
|
|
2066
3369
|
function synthesizeImportDependencyBucket(args) {
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
const
|
|
2071
|
-
|
|
3370
|
+
const visibleImportItems = args.visibleErrorItems.filter(
|
|
3371
|
+
(item) => item.reason.startsWith("missing module:")
|
|
3372
|
+
);
|
|
3373
|
+
const inlineImportItems = chooseStrongestFailureItems(
|
|
3374
|
+
args.inlineItems.filter((item) => item.reason.startsWith("missing module:"))
|
|
3375
|
+
);
|
|
3376
|
+
const importItems = visibleImportItems.length > 0 ? visibleImportItems : inlineImportItems.map((item) => ({
|
|
3377
|
+
...item,
|
|
3378
|
+
status: "failed"
|
|
3379
|
+
}));
|
|
3380
|
+
if (importItems.length === 0) {
|
|
2072
3381
|
return null;
|
|
2073
3382
|
}
|
|
2074
3383
|
const allVisibleErrorsAreImportRelated = args.visibleErrorItems.length > 0 && args.visibleErrorItems.every((item) => item.reason.startsWith("missing module:"));
|
|
@@ -2079,7 +3388,7 @@ function synthesizeImportDependencyBucket(args) {
|
|
|
2079
3388
|
)
|
|
2080
3389
|
).slice(0, 6);
|
|
2081
3390
|
const headlineCount = countClaimed ?? importItems.length;
|
|
2082
|
-
const headline = countClaimed ? `Import/dependency blocker: ${headlineCount} errors are caused by missing dependencies during test collection.` : `Import/dependency blocker: at least ${headlineCount} visible
|
|
3391
|
+
const headline = countClaimed ? `Import/dependency blocker: ${headlineCount} errors are caused by missing dependencies during test collection.` : `Import/dependency blocker: at least ${headlineCount} visible failure${headlineCount === 1 ? "" : "s"} are caused by missing dependencies during test collection.`;
|
|
2083
3392
|
const summaryLines = [headline];
|
|
2084
3393
|
if (modules.length > 0) {
|
|
2085
3394
|
summaryLines.push(`Missing modules include ${modules.join(", ")}.`);
|
|
@@ -2089,7 +3398,7 @@ function synthesizeImportDependencyBucket(args) {
|
|
|
2089
3398
|
headline,
|
|
2090
3399
|
countVisible: importItems.length,
|
|
2091
3400
|
countClaimed,
|
|
2092
|
-
reason: "missing dependencies during test collection",
|
|
3401
|
+
reason: modules.length === 1 ? `missing module: ${modules[0]}` : "missing dependencies during test collection",
|
|
2093
3402
|
representativeItems: importItems.slice(0, 4).map((item) => ({
|
|
2094
3403
|
label: item.label,
|
|
2095
3404
|
reason: item.reason,
|
|
@@ -2108,7 +3417,7 @@ function synthesizeImportDependencyBucket(args) {
|
|
|
2108
3417
|
};
|
|
2109
3418
|
}
|
|
2110
3419
|
function isContractDriftLabel(label) {
|
|
2111
|
-
return /(freeze|
|
|
3420
|
+
return /(freeze|contract|manifest|openapi|golden)/i.test(label);
|
|
2112
3421
|
}
|
|
2113
3422
|
function looksLikeTaskKey(value) {
|
|
2114
3423
|
return /^[a-z]+(?:_[a-z0-9]+)+$/i.test(value) && !value.startsWith("/api/");
|
|
@@ -2239,23 +3548,81 @@ function synthesizeContractDriftBucket(args) {
|
|
|
2239
3548
|
overflowLabel: "changed entities"
|
|
2240
3549
|
};
|
|
2241
3550
|
}
|
|
3551
|
+
function synthesizeSnapshotMismatchBucket(args) {
|
|
3552
|
+
const snapshotItems = chooseStrongestFailureItems(
|
|
3553
|
+
args.inlineItems.filter((item) => item.reason.startsWith("snapshot mismatch:"))
|
|
3554
|
+
);
|
|
3555
|
+
if (snapshotItems.length === 0) {
|
|
3556
|
+
return null;
|
|
3557
|
+
}
|
|
3558
|
+
const countClaimed = args.snapshotFailures && args.snapshotFailures >= snapshotItems.length ? args.snapshotFailures : void 0;
|
|
3559
|
+
const countText = countClaimed ?? snapshotItems.length;
|
|
3560
|
+
const summaryLines = [
|
|
3561
|
+
`Snapshot mismatches: ${formatCount2(countText, "snapshot expectation")} ${countText === 1 ? "is" : "are"} out of date with current output.`
|
|
3562
|
+
];
|
|
3563
|
+
return {
|
|
3564
|
+
type: "snapshot_mismatch",
|
|
3565
|
+
headline: summaryLines[0],
|
|
3566
|
+
countVisible: snapshotItems.length,
|
|
3567
|
+
countClaimed,
|
|
3568
|
+
reason: "snapshot mismatch: snapshot expectations differ from current output",
|
|
3569
|
+
representativeItems: snapshotItems.slice(0, 4),
|
|
3570
|
+
entities: snapshotItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
|
|
3571
|
+
hint: "Update the snapshots if these output changes are intentional.",
|
|
3572
|
+
confidence: countClaimed ? 0.92 : 0.8,
|
|
3573
|
+
summaryLines,
|
|
3574
|
+
overflowCount: Math.max((countClaimed ?? snapshotItems.length) - Math.min(snapshotItems.length, 4), 0),
|
|
3575
|
+
overflowLabel: "snapshot failures"
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
function synthesizeTimeoutBucket(args) {
|
|
3579
|
+
const timeoutItems = chooseStrongestFailureItems(
|
|
3580
|
+
args.inlineItems.filter((item) => item.reason.startsWith("timeout:"))
|
|
3581
|
+
);
|
|
3582
|
+
if (timeoutItems.length === 0) {
|
|
3583
|
+
return null;
|
|
3584
|
+
}
|
|
3585
|
+
const summaryLines = [
|
|
3586
|
+
`Timeout failures: ${formatCount2(timeoutItems.length, "test")} exceeded the configured timeout threshold.`
|
|
3587
|
+
];
|
|
3588
|
+
return {
|
|
3589
|
+
type: "timeout_failure",
|
|
3590
|
+
headline: summaryLines[0],
|
|
3591
|
+
countVisible: timeoutItems.length,
|
|
3592
|
+
countClaimed: timeoutItems.length,
|
|
3593
|
+
reason: timeoutItems.length === 1 ? timeoutItems[0].reason : "timeout: tests exceeded the configured timeout threshold",
|
|
3594
|
+
representativeItems: timeoutItems.slice(0, 4),
|
|
3595
|
+
entities: timeoutItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
|
|
3596
|
+
hint: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning.",
|
|
3597
|
+
confidence: 0.84,
|
|
3598
|
+
summaryLines,
|
|
3599
|
+
overflowCount: Math.max(timeoutItems.length - Math.min(timeoutItems.length, 4), 0),
|
|
3600
|
+
overflowLabel: "timeout failures"
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
2242
3603
|
function analyzeTestStatus(input) {
|
|
2243
|
-
const
|
|
2244
|
-
const
|
|
2245
|
-
const
|
|
2246
|
-
const
|
|
3604
|
+
const runner = detectTestRunner(input);
|
|
3605
|
+
const counts = extractTestStatusCounts(input, runner);
|
|
3606
|
+
const passed = counts.passed;
|
|
3607
|
+
const failed = counts.failed;
|
|
3608
|
+
const errors = counts.errors;
|
|
3609
|
+
const skipped = counts.skipped;
|
|
2247
3610
|
const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
|
|
2248
|
-
const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
|
|
3611
|
+
const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input) || /No test suite found in file/i.test(input) || /No test found in suite/i.test(input);
|
|
2249
3612
|
const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
|
|
2250
3613
|
const collectionItems = chooseStrongestFailureItems(collectCollectionFailureItems(input));
|
|
2251
3614
|
const inlineItems = chooseStrongestFailureItems(collectInlineFailureItems(input));
|
|
3615
|
+
const statusItems = collectInlineFailureItemsWithStatus(input);
|
|
2252
3616
|
const visibleErrorItems = chooseStrongestStatusFailureItems([
|
|
2253
3617
|
...collectionItems.map((item) => ({
|
|
2254
3618
|
...item,
|
|
2255
3619
|
status: "error"
|
|
2256
3620
|
})),
|
|
2257
|
-
...
|
|
3621
|
+
...statusItems.filter((item) => item.status === "error")
|
|
2258
3622
|
]);
|
|
3623
|
+
const visibleFailedItems = chooseStrongestStatusFailureItems(
|
|
3624
|
+
statusItems.filter((item) => item.status === "failed")
|
|
3625
|
+
);
|
|
2259
3626
|
const labels = collectFailureLabels(input);
|
|
2260
3627
|
const visibleErrorLabels = labels.filter((item) => item.status === "error").map((item) => item.label);
|
|
2261
3628
|
const visibleFailedLabels = labels.filter((item) => item.status === "failed").map((item) => item.label);
|
|
@@ -2272,7 +3639,8 @@ function analyzeTestStatus(input) {
|
|
|
2272
3639
|
if (!sharedBlocker) {
|
|
2273
3640
|
const importDependencyBucket = synthesizeImportDependencyBucket({
|
|
2274
3641
|
errors,
|
|
2275
|
-
visibleErrorItems
|
|
3642
|
+
visibleErrorItems,
|
|
3643
|
+
inlineItems
|
|
2276
3644
|
});
|
|
2277
3645
|
if (importDependencyBucket) {
|
|
2278
3646
|
buckets.push(importDependencyBucket);
|
|
@@ -2285,11 +3653,26 @@ function analyzeTestStatus(input) {
|
|
|
2285
3653
|
if (contractDrift) {
|
|
2286
3654
|
buckets.push(contractDrift);
|
|
2287
3655
|
}
|
|
3656
|
+
const snapshotMismatch = synthesizeSnapshotMismatchBucket({
|
|
3657
|
+
inlineItems,
|
|
3658
|
+
snapshotFailures: counts.snapshotFailures
|
|
3659
|
+
});
|
|
3660
|
+
if (snapshotMismatch) {
|
|
3661
|
+
buckets.push(snapshotMismatch);
|
|
3662
|
+
}
|
|
3663
|
+
const timeoutBucket = synthesizeTimeoutBucket({
|
|
3664
|
+
inlineItems
|
|
3665
|
+
});
|
|
3666
|
+
if (timeoutBucket) {
|
|
3667
|
+
buckets.push(timeoutBucket);
|
|
3668
|
+
}
|
|
2288
3669
|
return {
|
|
3670
|
+
runner,
|
|
2289
3671
|
passed,
|
|
2290
3672
|
failed,
|
|
2291
3673
|
errors,
|
|
2292
3674
|
skipped,
|
|
3675
|
+
snapshotFailures: counts.snapshotFailures,
|
|
2293
3676
|
noTestsCollected,
|
|
2294
3677
|
interrupted,
|
|
2295
3678
|
collectionErrorCount: collectionErrors ? Number(collectionErrors[1]) : void 0,
|
|
@@ -2298,6 +3681,7 @@ function analyzeTestStatus(input) {
|
|
|
2298
3681
|
visibleErrorLabels,
|
|
2299
3682
|
visibleFailedLabels,
|
|
2300
3683
|
visibleErrorItems,
|
|
3684
|
+
visibleFailedItems,
|
|
2301
3685
|
buckets
|
|
2302
3686
|
};
|
|
2303
3687
|
}
|
|
@@ -2319,114 +3703,666 @@ function testStatusHeuristic(input, detail = "standard") {
|
|
|
2319
3703
|
if (detail === "focused") {
|
|
2320
3704
|
return decision.focusedText;
|
|
2321
3705
|
}
|
|
2322
|
-
return decision.standardText;
|
|
3706
|
+
return decision.standardText;
|
|
3707
|
+
}
|
|
3708
|
+
return [
|
|
3709
|
+
"- Tests did not complete.",
|
|
3710
|
+
`- ${formatCount2(analysis.collectionErrorCount, "error")} occurred during collection.`,
|
|
3711
|
+
...summarizeRepeatedTestCauses(input, {
|
|
3712
|
+
duringCollection: true
|
|
3713
|
+
})
|
|
3714
|
+
].join("\n");
|
|
3715
|
+
}
|
|
3716
|
+
if (analysis.noTestsCollected) {
|
|
3717
|
+
return ["- Tests did not run.", "- Collected 0 items."].join("\n");
|
|
3718
|
+
}
|
|
3719
|
+
if (analysis.interrupted && analysis.failed === 0 && analysis.errors === 0) {
|
|
3720
|
+
return "- Test run was interrupted.";
|
|
3721
|
+
}
|
|
3722
|
+
if (analysis.failed === 0 && analysis.errors === 0 && analysis.passed > 0) {
|
|
3723
|
+
const details = [formatCount2(analysis.passed, "test")];
|
|
3724
|
+
if (analysis.skipped > 0) {
|
|
3725
|
+
details.push(formatCount2(analysis.skipped, "skip"));
|
|
3726
|
+
}
|
|
3727
|
+
return ["- Tests passed.", `- ${details.join(", ")}.`].join("\n");
|
|
3728
|
+
}
|
|
3729
|
+
if (analysis.failed > 0 || analysis.errors > 0 || analysis.inlineItems.length > 0 || analysis.buckets.length > 0) {
|
|
3730
|
+
const decision = buildTestStatusDiagnoseContract({
|
|
3731
|
+
input,
|
|
3732
|
+
analysis
|
|
3733
|
+
});
|
|
3734
|
+
if (detail === "verbose") {
|
|
3735
|
+
return decision.verboseText;
|
|
3736
|
+
}
|
|
3737
|
+
if (detail === "focused") {
|
|
3738
|
+
return decision.focusedText;
|
|
3739
|
+
}
|
|
3740
|
+
return decision.standardText;
|
|
3741
|
+
}
|
|
3742
|
+
return null;
|
|
3743
|
+
}
|
|
3744
|
+
function auditCriticalHeuristic(input) {
|
|
3745
|
+
if (/\bfound\s+0\s+vulnerabilities\b/i.test(input) || /\b0\s+vulnerabilities\b/i.test(input)) {
|
|
3746
|
+
return JSON.stringify(
|
|
3747
|
+
{
|
|
3748
|
+
status: "ok",
|
|
3749
|
+
vulnerabilities: [],
|
|
3750
|
+
summary: "No high or critical vulnerabilities found in the provided input."
|
|
3751
|
+
},
|
|
3752
|
+
null,
|
|
3753
|
+
2
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3756
|
+
const vulnerabilities = collectAuditCriticalVulnerabilities(input);
|
|
3757
|
+
if (vulnerabilities.length === 0) {
|
|
3758
|
+
return null;
|
|
3759
|
+
}
|
|
3760
|
+
const firstVulnerability = vulnerabilities[0];
|
|
3761
|
+
return JSON.stringify(
|
|
3762
|
+
{
|
|
3763
|
+
status: "ok",
|
|
3764
|
+
vulnerabilities,
|
|
3765
|
+
summary: vulnerabilities.length === 1 ? `One ${firstVulnerability.severity} vulnerability found in ${firstVulnerability.package}.` : `${vulnerabilities.length} high or critical vulnerabilities found in the provided input.`
|
|
3766
|
+
},
|
|
3767
|
+
null,
|
|
3768
|
+
2
|
|
3769
|
+
);
|
|
3770
|
+
}
|
|
3771
|
+
function infraRiskHeuristic(input) {
|
|
3772
|
+
const destroyTargets = collectInfraDestroyTargets(input);
|
|
3773
|
+
const blockers = collectInfraBlockers(input);
|
|
3774
|
+
const zeroDestructiveEvidence = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)).slice(0, 3);
|
|
3775
|
+
const riskEvidence = collectInfraRiskEvidence(input);
|
|
3776
|
+
if (riskEvidence.length > 0) {
|
|
3777
|
+
return JSON.stringify(
|
|
3778
|
+
{
|
|
3779
|
+
verdict: "fail",
|
|
3780
|
+
reason: "Destructive or clearly risky infrastructure change signals are present.",
|
|
3781
|
+
evidence: riskEvidence,
|
|
3782
|
+
destroy_count: inferInfraDestroyCount(input, destroyTargets),
|
|
3783
|
+
destroy_targets: destroyTargets,
|
|
3784
|
+
blockers
|
|
3785
|
+
},
|
|
3786
|
+
null,
|
|
3787
|
+
2
|
|
3788
|
+
);
|
|
3789
|
+
}
|
|
3790
|
+
if (zeroDestructiveEvidence.length > 0) {
|
|
3791
|
+
return JSON.stringify(
|
|
3792
|
+
{
|
|
3793
|
+
verdict: "pass",
|
|
3794
|
+
reason: "The provided input explicitly indicates zero destructive changes.",
|
|
3795
|
+
evidence: zeroDestructiveEvidence,
|
|
3796
|
+
destroy_count: 0,
|
|
3797
|
+
destroy_targets: [],
|
|
3798
|
+
blockers: []
|
|
3799
|
+
},
|
|
3800
|
+
null,
|
|
3801
|
+
2
|
|
3802
|
+
);
|
|
3803
|
+
}
|
|
3804
|
+
const safeEvidence = collectEvidence(input, SAFE_LINE_PATTERN);
|
|
3805
|
+
if (safeEvidence.length > 0) {
|
|
3806
|
+
return JSON.stringify(
|
|
3807
|
+
{
|
|
3808
|
+
verdict: "pass",
|
|
3809
|
+
reason: "The provided input explicitly indicates no risky infrastructure changes.",
|
|
3810
|
+
evidence: safeEvidence,
|
|
3811
|
+
destroy_count: 0,
|
|
3812
|
+
destroy_targets: [],
|
|
3813
|
+
blockers: []
|
|
3814
|
+
},
|
|
3815
|
+
null,
|
|
3816
|
+
2
|
|
3817
|
+
);
|
|
3818
|
+
}
|
|
3819
|
+
return null;
|
|
3820
|
+
}
|
|
3821
|
+
function parseTscErrors(input) {
|
|
3822
|
+
const diagnostics = [];
|
|
3823
|
+
for (const rawLine of input.split("\n")) {
|
|
3824
|
+
const line = rawLine.replace(/\u001b\[[0-9;]*m/g, "").trimEnd();
|
|
3825
|
+
if (!line.trim()) {
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3828
|
+
let match = line.match(/^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/);
|
|
3829
|
+
if (match) {
|
|
3830
|
+
diagnostics.push({
|
|
3831
|
+
file: match[1].replace(/\\/g, "/").trim(),
|
|
3832
|
+
line: Number(match[2]),
|
|
3833
|
+
column: Number(match[3]),
|
|
3834
|
+
code: match[4],
|
|
3835
|
+
message: match[5].trim()
|
|
3836
|
+
});
|
|
3837
|
+
continue;
|
|
3838
|
+
}
|
|
3839
|
+
match = line.match(/^(.+):(\d+):(\d+)\s+-\s+error\s+(TS\d+):\s+(.+)$/);
|
|
3840
|
+
if (match) {
|
|
3841
|
+
diagnostics.push({
|
|
3842
|
+
file: match[1].replace(/\\/g, "/").trim(),
|
|
3843
|
+
line: Number(match[2]),
|
|
3844
|
+
column: Number(match[3]),
|
|
3845
|
+
code: match[4],
|
|
3846
|
+
message: match[5].trim()
|
|
3847
|
+
});
|
|
3848
|
+
continue;
|
|
3849
|
+
}
|
|
3850
|
+
match = line.match(/^\s*error\s+(TS\d+):\s+(.+)$/);
|
|
3851
|
+
if (match) {
|
|
3852
|
+
diagnostics.push({
|
|
3853
|
+
file: null,
|
|
3854
|
+
line: null,
|
|
3855
|
+
column: null,
|
|
3856
|
+
code: match[1],
|
|
3857
|
+
message: match[2].trim()
|
|
3858
|
+
});
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
return diagnostics;
|
|
3862
|
+
}
|
|
3863
|
+
function extractTscSummary(input) {
|
|
3864
|
+
const matches = [
|
|
3865
|
+
...input.matchAll(/\bFound\s+(\d+)\s+errors?\b(?:\s+in\s+(\d+)\s+files?)?\.?/gi)
|
|
3866
|
+
];
|
|
3867
|
+
const summary = matches.at(-1);
|
|
3868
|
+
if (!summary) {
|
|
3869
|
+
return null;
|
|
3870
|
+
}
|
|
3871
|
+
return {
|
|
3872
|
+
errorCount: Number(summary[1]),
|
|
3873
|
+
fileCount: summary[2] ? Number(summary[2]) : null
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3876
|
+
function formatTscGroup(args) {
|
|
3877
|
+
const label = TSC_CODE_LABELS[args.code];
|
|
3878
|
+
const displayFiles = formatDisplayedFiles(args.files);
|
|
3879
|
+
let line = `- ${args.code}`;
|
|
3880
|
+
if (label) {
|
|
3881
|
+
line += ` (${label})`;
|
|
3882
|
+
}
|
|
3883
|
+
line += `: ${formatCount2(args.count, "occurrence")}`;
|
|
3884
|
+
if (displayFiles.length > 0) {
|
|
3885
|
+
line += ` across ${displayFiles.join(", ")}`;
|
|
3886
|
+
}
|
|
3887
|
+
return `${line}.`;
|
|
3888
|
+
}
|
|
3889
|
+
function typecheckSummaryHeuristic(input) {
|
|
3890
|
+
if (input.trim().length === 0) {
|
|
3891
|
+
return null;
|
|
3892
|
+
}
|
|
3893
|
+
const diagnostics = parseTscErrors(input);
|
|
3894
|
+
const summary = extractTscSummary(input);
|
|
3895
|
+
const hasTscSignal = diagnostics.length > 0 || summary !== null || /\berror\s+TS\d+:/m.test(input);
|
|
3896
|
+
if (!hasTscSignal) {
|
|
3897
|
+
return null;
|
|
3898
|
+
}
|
|
3899
|
+
if (summary?.errorCount === 0) {
|
|
3900
|
+
return "No type errors.";
|
|
3901
|
+
}
|
|
3902
|
+
if (diagnostics.length === 0 && summary === null) {
|
|
3903
|
+
return null;
|
|
3904
|
+
}
|
|
3905
|
+
const errorCount = summary?.errorCount ?? diagnostics.length;
|
|
3906
|
+
const allFiles = new Set(
|
|
3907
|
+
diagnostics.map((diagnostic) => diagnostic.file).filter((file) => Boolean(file))
|
|
3908
|
+
);
|
|
3909
|
+
const fileCount = summary?.fileCount ?? (allFiles.size > 0 ? allFiles.size : null);
|
|
3910
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3911
|
+
for (const diagnostic of diagnostics) {
|
|
3912
|
+
const group = groups.get(diagnostic.code) ?? {
|
|
3913
|
+
count: 0,
|
|
3914
|
+
files: /* @__PURE__ */ new Set()
|
|
3915
|
+
};
|
|
3916
|
+
group.count += 1;
|
|
3917
|
+
if (diagnostic.file) {
|
|
3918
|
+
group.files.add(diagnostic.file);
|
|
3919
|
+
}
|
|
3920
|
+
groups.set(diagnostic.code, group);
|
|
3921
|
+
}
|
|
3922
|
+
const bullets = [
|
|
3923
|
+
`- Typecheck failed: ${formatCount2(errorCount, "error")}${fileCount ? ` in ${formatCount2(fileCount, "file")}` : ""}.`
|
|
3924
|
+
];
|
|
3925
|
+
const sortedGroups = [...groups.entries()].map(([code, group]) => ({
|
|
3926
|
+
code,
|
|
3927
|
+
count: group.count,
|
|
3928
|
+
files: group.files
|
|
3929
|
+
})).sort((left, right) => right.count - left.count || left.code.localeCompare(right.code));
|
|
3930
|
+
for (const group of sortedGroups.slice(0, 3)) {
|
|
3931
|
+
bullets.push(formatTscGroup(group));
|
|
3932
|
+
}
|
|
3933
|
+
if (sortedGroups.length > 3) {
|
|
3934
|
+
const overflowFiles = /* @__PURE__ */ new Set();
|
|
3935
|
+
for (const group of sortedGroups.slice(3)) {
|
|
3936
|
+
for (const file of group.files) {
|
|
3937
|
+
overflowFiles.add(file);
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
let overflow = `- ${formatCount2(sortedGroups.length - 3, "more error code")}`;
|
|
3941
|
+
if (overflowFiles.size > 0) {
|
|
3942
|
+
overflow += ` across ${formatCount2(overflowFiles.size, "file")}`;
|
|
3943
|
+
}
|
|
3944
|
+
bullets.push(`${overflow}.`);
|
|
3945
|
+
}
|
|
3946
|
+
return bullets.join("\n");
|
|
3947
|
+
}
|
|
3948
|
+
function looksLikeEslintFileHeader(line) {
|
|
3949
|
+
if (line.trim().length === 0 || line.trim() !== line) {
|
|
3950
|
+
return false;
|
|
3951
|
+
}
|
|
3952
|
+
if (/^\s*[✖×x]\s+\d+\s+problems?\b/i.test(line) || /potentially\s+fixable/i.test(line) || /^\d+\s+problems?\b/i.test(line)) {
|
|
3953
|
+
return false;
|
|
3954
|
+
}
|
|
3955
|
+
const normalized = line.replace(/\\/g, "/");
|
|
3956
|
+
const pathLike = normalized.startsWith("/") || normalized.startsWith("./") || normalized.startsWith("../") || /^[A-Za-z]:\//.test(normalized) || /^[A-Za-z0-9_.-]+\//.test(normalized);
|
|
3957
|
+
return pathLike && /\.[A-Za-z0-9]+$/.test(normalized);
|
|
3958
|
+
}
|
|
3959
|
+
function normalizeEslintRule(rule, message) {
|
|
3960
|
+
if (rule && rule.trim().length > 0) {
|
|
3961
|
+
return rule.trim();
|
|
3962
|
+
}
|
|
3963
|
+
if (/parsing error/i.test(message)) {
|
|
3964
|
+
return "parsing error";
|
|
3965
|
+
}
|
|
3966
|
+
if (/fatal/i.test(message)) {
|
|
3967
|
+
return "fatal error";
|
|
3968
|
+
}
|
|
3969
|
+
return "unclassified lint error";
|
|
3970
|
+
}
|
|
3971
|
+
function parseEslintStylish(input) {
|
|
3972
|
+
const violations = [];
|
|
3973
|
+
let currentFile = null;
|
|
3974
|
+
for (const rawLine of input.split("\n")) {
|
|
3975
|
+
const line = rawLine.replace(/\u001b\[[0-9;]*m/g, "").replace(/\r$/, "");
|
|
3976
|
+
if (looksLikeEslintFileHeader(line.trim())) {
|
|
3977
|
+
currentFile = line.trim().replace(/\\/g, "/");
|
|
3978
|
+
continue;
|
|
3979
|
+
}
|
|
3980
|
+
let match = line.match(/^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s{2,}(\S+)\s*$/);
|
|
3981
|
+
if (match) {
|
|
3982
|
+
violations.push({
|
|
3983
|
+
file: currentFile ?? "(unknown file)",
|
|
3984
|
+
line: Number(match[1]),
|
|
3985
|
+
column: Number(match[2]),
|
|
3986
|
+
severity: match[3],
|
|
3987
|
+
message: match[4].trim(),
|
|
3988
|
+
rule: normalizeEslintRule(match[5], match[4])
|
|
3989
|
+
});
|
|
3990
|
+
continue;
|
|
3991
|
+
}
|
|
3992
|
+
match = line.match(/^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s*$/);
|
|
3993
|
+
if (match) {
|
|
3994
|
+
violations.push({
|
|
3995
|
+
file: currentFile ?? "(unknown file)",
|
|
3996
|
+
line: Number(match[1]),
|
|
3997
|
+
column: Number(match[2]),
|
|
3998
|
+
severity: match[3],
|
|
3999
|
+
message: match[4].trim(),
|
|
4000
|
+
rule: normalizeEslintRule(null, match[4])
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
return violations;
|
|
4005
|
+
}
|
|
4006
|
+
function extractEslintSummary(input) {
|
|
4007
|
+
const summaryMatches = [
|
|
4008
|
+
...input.matchAll(
|
|
4009
|
+
/^\s*[✖×x]?\s*(\d+)\s+problems?\s+\((\d+)\s+errors?,\s+(\d+)\s+warnings?\)/gim
|
|
4010
|
+
)
|
|
4011
|
+
];
|
|
4012
|
+
const summary = summaryMatches.at(-1);
|
|
4013
|
+
if (!summary) {
|
|
4014
|
+
return null;
|
|
4015
|
+
}
|
|
4016
|
+
const fixableMatch = input.match(
|
|
4017
|
+
/(\d+)\s+errors?\s+and\s+(\d+)\s+warnings?\s+(?:are|is)\s+potentially\s+fixable/i
|
|
4018
|
+
);
|
|
4019
|
+
return {
|
|
4020
|
+
problems: Number(summary[1]),
|
|
4021
|
+
errors: Number(summary[2]),
|
|
4022
|
+
warnings: Number(summary[3]),
|
|
4023
|
+
fixableProblems: fixableMatch ? Number(fixableMatch[1]) + Number(fixableMatch[2]) : null
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
function formatLintGroup(args) {
|
|
4027
|
+
const totalErrors = args.errors;
|
|
4028
|
+
const totalWarnings = args.warnings;
|
|
4029
|
+
const displayFiles = formatDisplayedFiles(args.files);
|
|
4030
|
+
let detail = "";
|
|
4031
|
+
if (totalErrors > 0 && totalWarnings > 0) {
|
|
4032
|
+
detail = `${formatCount2(totalErrors, "error")}, ${formatCount2(totalWarnings, "warning")}`;
|
|
4033
|
+
} else if (totalErrors > 0) {
|
|
4034
|
+
detail = formatCount2(totalErrors, "error");
|
|
4035
|
+
} else {
|
|
4036
|
+
detail = formatCount2(totalWarnings, "warning");
|
|
4037
|
+
}
|
|
4038
|
+
let line = `- ${args.rule}: ${detail}`;
|
|
4039
|
+
if (displayFiles.length > 0) {
|
|
4040
|
+
line += ` across ${displayFiles.join(", ")}`;
|
|
4041
|
+
}
|
|
4042
|
+
return `${line}.`;
|
|
4043
|
+
}
|
|
4044
|
+
function lintFailuresHeuristic(input) {
|
|
4045
|
+
const trimmed = input.trim();
|
|
4046
|
+
if (trimmed.length === 0 || trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
4047
|
+
return null;
|
|
4048
|
+
}
|
|
4049
|
+
const summary = extractEslintSummary(input);
|
|
4050
|
+
const violations = parseEslintStylish(input);
|
|
4051
|
+
if (summary === null && violations.length === 0) {
|
|
4052
|
+
return null;
|
|
4053
|
+
}
|
|
4054
|
+
if (summary?.problems === 0) {
|
|
4055
|
+
return "No lint failures.";
|
|
4056
|
+
}
|
|
4057
|
+
const problems = summary?.problems ?? violations.length;
|
|
4058
|
+
const errors = summary?.errors ?? countPattern(input, /^\s*\d+:\d+\s+error\b/gm);
|
|
4059
|
+
const warnings = summary?.warnings ?? countPattern(input, /^\s*\d+:\d+\s+warning\b/gm);
|
|
4060
|
+
const bullets = [];
|
|
4061
|
+
if (errors > 0) {
|
|
4062
|
+
let headline = `- Lint failed: ${formatCount2(problems, "problem")} (${formatCount2(errors, "error")}, ${formatCount2(warnings, "warning")}).`;
|
|
4063
|
+
if ((summary?.fixableProblems ?? 0) > 0) {
|
|
4064
|
+
headline += ` ${formatCount2(summary.fixableProblems, "problem")} potentially fixable with --fix.`;
|
|
4065
|
+
}
|
|
4066
|
+
bullets.push(headline);
|
|
4067
|
+
} else {
|
|
4068
|
+
bullets.push(`- No lint errors visible: ${formatCount2(warnings, "warning")}.`);
|
|
4069
|
+
}
|
|
4070
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4071
|
+
for (const violation of violations) {
|
|
4072
|
+
const group = groups.get(violation.rule) ?? {
|
|
4073
|
+
errors: 0,
|
|
4074
|
+
warnings: 0,
|
|
4075
|
+
files: /* @__PURE__ */ new Set()
|
|
4076
|
+
};
|
|
4077
|
+
if (violation.severity === "error") {
|
|
4078
|
+
group.errors += 1;
|
|
4079
|
+
} else {
|
|
4080
|
+
group.warnings += 1;
|
|
4081
|
+
}
|
|
4082
|
+
group.files.add(violation.file);
|
|
4083
|
+
groups.set(violation.rule, group);
|
|
4084
|
+
}
|
|
4085
|
+
const sortedGroups = [...groups.entries()].map(([rule, group]) => ({
|
|
4086
|
+
rule,
|
|
4087
|
+
errors: group.errors,
|
|
4088
|
+
warnings: group.warnings,
|
|
4089
|
+
total: group.errors + group.warnings,
|
|
4090
|
+
files: group.files
|
|
4091
|
+
})).sort((left, right) => {
|
|
4092
|
+
const leftHasErrors = left.errors > 0 ? 1 : 0;
|
|
4093
|
+
const rightHasErrors = right.errors > 0 ? 1 : 0;
|
|
4094
|
+
return rightHasErrors - leftHasErrors || right.total - left.total || left.rule.localeCompare(right.rule);
|
|
4095
|
+
});
|
|
4096
|
+
for (const group of sortedGroups.slice(0, 3)) {
|
|
4097
|
+
bullets.push(formatLintGroup(group));
|
|
4098
|
+
}
|
|
4099
|
+
if (sortedGroups.length > 3) {
|
|
4100
|
+
const overflowFiles = /* @__PURE__ */ new Set();
|
|
4101
|
+
for (const group of sortedGroups.slice(3)) {
|
|
4102
|
+
for (const file of group.files) {
|
|
4103
|
+
overflowFiles.add(file);
|
|
4104
|
+
}
|
|
2323
4105
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
})
|
|
2330
|
-
].join("\n");
|
|
4106
|
+
let overflow = `- ${formatCount2(sortedGroups.length - 3, "more rule")}`;
|
|
4107
|
+
if (overflowFiles.size > 0) {
|
|
4108
|
+
overflow += ` across ${formatCount2(overflowFiles.size, "file")}`;
|
|
4109
|
+
}
|
|
4110
|
+
bullets.push(`${overflow}.`);
|
|
2331
4111
|
}
|
|
2332
|
-
|
|
2333
|
-
|
|
4112
|
+
return bullets.join("\n");
|
|
4113
|
+
}
|
|
4114
|
+
function stripAnsiText(input) {
|
|
4115
|
+
return input.replace(/\u001b\[[0-9;]*m/g, "");
|
|
4116
|
+
}
|
|
4117
|
+
function normalizeBuildPath(file) {
|
|
4118
|
+
return file.replace(/\\/g, "/").replace(/^\.\//, "").trim();
|
|
4119
|
+
}
|
|
4120
|
+
function trimTrailingSentencePunctuation(input) {
|
|
4121
|
+
return input.replace(/[.:]+$/, "").trim();
|
|
4122
|
+
}
|
|
4123
|
+
function containsKnownBuildFailureSignal(input) {
|
|
4124
|
+
return /^ERROR in /m.test(input) || /^(?:[✘✗]\s*)?\[ERROR\]\s+/m.test(input) || /^error(?:\[E\d+\])?:\s+/m.test(input) || /^.+?\.go:\d+:\d+:\s+\S+/m.test(input) || /^.+?\.(?:c|cc|cpp|cxx|h|hpp|m|mm):\d+:\d+:\s*error:\s+/m.test(input) || /\berror\s+TS\d+:/m.test(input) || /^\s*npm ERR!/m.test(input) || /\bERR_PNPM_/m.test(input) || /^\s*error Command failed/m.test(input);
|
|
4125
|
+
}
|
|
4126
|
+
function detectExplicitBuildSuccess(input) {
|
|
4127
|
+
if (containsKnownBuildFailureSignal(input)) {
|
|
4128
|
+
return false;
|
|
2334
4129
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
4130
|
+
return /\bcompiled successfully\b/i.test(input) || /^\s*Build succeeded\.?\s*$/im.test(input) || /\bcompiled with 0 errors?\b/i.test(input);
|
|
4131
|
+
}
|
|
4132
|
+
function inferBuildFailureCategory(message) {
|
|
4133
|
+
if (/module not found|can't resolve|could not resolve|cannot find module|no required module provides package/i.test(
|
|
4134
|
+
message
|
|
4135
|
+
)) {
|
|
4136
|
+
return "module-resolution";
|
|
2337
4137
|
}
|
|
2338
|
-
if (
|
|
2339
|
-
|
|
2340
|
-
if (analysis.skipped > 0) {
|
|
2341
|
-
details.push(formatCount2(analysis.skipped, "skip"));
|
|
2342
|
-
}
|
|
2343
|
-
return ["- Tests passed.", `- ${details.join(", ")}.`].join("\n");
|
|
4138
|
+
if (/no matching export|does not provide an export named|missing export/i.test(message)) {
|
|
4139
|
+
return "missing-export";
|
|
2344
4140
|
}
|
|
2345
|
-
if (
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
4141
|
+
if (/cannot find name|cannot find value|not found in this scope|undefined:|undeclared identifier/i.test(
|
|
4142
|
+
message
|
|
4143
|
+
)) {
|
|
4144
|
+
return "undefined-identifier";
|
|
4145
|
+
}
|
|
4146
|
+
if (/syntax error|unexpected token|expected ['"`;)]|expected .* after expression/i.test(message)) {
|
|
4147
|
+
return "syntax";
|
|
4148
|
+
}
|
|
4149
|
+
if (/\bTS\d+\b/.test(message) || /type .* is not assignable|type error|no matching overload/i.test(message)) {
|
|
4150
|
+
return "type";
|
|
4151
|
+
}
|
|
4152
|
+
return "generic";
|
|
4153
|
+
}
|
|
4154
|
+
function buildFailureSuggestion(category) {
|
|
4155
|
+
switch (category) {
|
|
4156
|
+
case "module-resolution":
|
|
4157
|
+
return "Install the missing package or fix the import path.";
|
|
4158
|
+
case "missing-export":
|
|
4159
|
+
return "Check the export name in the source module.";
|
|
4160
|
+
case "undefined-identifier":
|
|
4161
|
+
return "Define or import the missing identifier.";
|
|
4162
|
+
case "syntax":
|
|
4163
|
+
return "Fix the syntax error at the indicated location.";
|
|
4164
|
+
case "type":
|
|
4165
|
+
return "Fix the type error at the indicated location.";
|
|
4166
|
+
case "wrapper":
|
|
4167
|
+
return "Check the underlying build tool output above.";
|
|
4168
|
+
default:
|
|
4169
|
+
return "Fix the first reported error and rebuild.";
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
function formatBuildFailureOutput(match) {
|
|
4173
|
+
const message = trimTrailingSentencePunctuation(match.message);
|
|
4174
|
+
const suggestion = buildFailureSuggestion(match.category);
|
|
4175
|
+
const displayFile = match.file ? compactDisplayFile(match.file) : null;
|
|
4176
|
+
if (displayFile && match.line !== null) {
|
|
4177
|
+
return `Build failed: ${message} in ${displayFile}:${match.line}. Fix: ${suggestion}`;
|
|
4178
|
+
}
|
|
4179
|
+
if (displayFile) {
|
|
4180
|
+
return `Build failed: ${message} in ${displayFile}. Fix: ${suggestion}`;
|
|
4181
|
+
}
|
|
4182
|
+
return `Build failed: ${message}. Fix: ${suggestion}`;
|
|
4183
|
+
}
|
|
4184
|
+
function extractWebpackBuildFailure(input) {
|
|
4185
|
+
const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
|
|
4186
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
4187
|
+
const match = lines[index]?.match(/^ERROR in (.+?)(?:\s+(\d+):(\d+))?$/);
|
|
4188
|
+
if (!match) {
|
|
4189
|
+
continue;
|
|
2352
4190
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
4191
|
+
const candidates = [];
|
|
4192
|
+
for (let cursor = index + 1; cursor < Math.min(lines.length, index + 6); cursor += 1) {
|
|
4193
|
+
const candidate = lines[cursor]?.trim();
|
|
4194
|
+
if (!candidate) {
|
|
4195
|
+
continue;
|
|
4196
|
+
}
|
|
4197
|
+
if (/^ERROR in /.test(candidate) || /compiled with \d+ errors?/i.test(candidate)) {
|
|
4198
|
+
break;
|
|
4199
|
+
}
|
|
4200
|
+
if (/^(?:>|\|)|^\d+\s+\|/.test(candidate)) {
|
|
4201
|
+
continue;
|
|
4202
|
+
}
|
|
4203
|
+
candidates.push(candidate);
|
|
2355
4204
|
}
|
|
2356
|
-
|
|
4205
|
+
let message = "Compilation error";
|
|
4206
|
+
if (candidates.length > 0) {
|
|
4207
|
+
const preferred = candidates.find(
|
|
4208
|
+
(candidate) => !/^Module build failed\b/i.test(candidate) && !/^Error:\s+TypeScript compilation failed\b/i.test(candidate) && inferBuildFailureCategory(candidate) !== "generic"
|
|
4209
|
+
) ?? candidates.find(
|
|
4210
|
+
(candidate) => !/^Module build failed\b/i.test(candidate) && !/^Error:\s+TypeScript compilation failed\b/i.test(candidate)
|
|
4211
|
+
) ?? candidates[0];
|
|
4212
|
+
message = preferred ?? message;
|
|
4213
|
+
}
|
|
4214
|
+
return {
|
|
4215
|
+
message,
|
|
4216
|
+
file: normalizeBuildPath(match[1]),
|
|
4217
|
+
line: match[2] ? Number(match[2]) : null,
|
|
4218
|
+
column: match[3] ? Number(match[3]) : null,
|
|
4219
|
+
category: inferBuildFailureCategory(message)
|
|
4220
|
+
};
|
|
2357
4221
|
}
|
|
2358
4222
|
return null;
|
|
2359
4223
|
}
|
|
2360
|
-
function
|
|
2361
|
-
const
|
|
2362
|
-
|
|
2363
|
-
|
|
4224
|
+
function extractViteImportAnalysisBuildFailure(input) {
|
|
4225
|
+
const lines = stripAnsiText(input).split("\n").map((line) => line.trim());
|
|
4226
|
+
for (const line of lines) {
|
|
4227
|
+
const match = line.match(
|
|
4228
|
+
/^\[plugin:vite:import-analysis\]\s+Failed to resolve import\s+"([^"]+)"\s+from\s+"([^"]+)"/i
|
|
4229
|
+
);
|
|
4230
|
+
if (!match) {
|
|
4231
|
+
continue;
|
|
2364
4232
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
4233
|
+
return {
|
|
4234
|
+
message: `Failed to resolve import "${match[1]}"`,
|
|
4235
|
+
file: normalizeBuildPath(match[2]),
|
|
4236
|
+
line: null,
|
|
4237
|
+
column: null,
|
|
4238
|
+
category: "module-resolution"
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
return null;
|
|
4242
|
+
}
|
|
4243
|
+
function extractEsbuildBuildFailure(input) {
|
|
4244
|
+
const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
|
|
4245
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
4246
|
+
const match = lines[index]?.match(/^(?:[✘✗]\s*)?\[ERROR\]\s*(.+)$/);
|
|
4247
|
+
if (!match) {
|
|
4248
|
+
continue;
|
|
4249
|
+
}
|
|
4250
|
+
const message = match[1].replace(/^\[vite\]\s*/i, "").trim();
|
|
4251
|
+
let file = null;
|
|
4252
|
+
let line = null;
|
|
4253
|
+
let column = null;
|
|
4254
|
+
for (let cursor = index + 1; cursor < Math.min(lines.length, index + 6); cursor += 1) {
|
|
4255
|
+
const locationMatch = lines[cursor]?.trim().match(/^(.+?):(\d+):(\d+):$/);
|
|
4256
|
+
if (!locationMatch) {
|
|
4257
|
+
continue;
|
|
4258
|
+
}
|
|
4259
|
+
file = normalizeBuildPath(locationMatch[1]);
|
|
4260
|
+
line = Number(locationMatch[2]);
|
|
4261
|
+
column = Number(locationMatch[3]);
|
|
4262
|
+
break;
|
|
2368
4263
|
}
|
|
2369
4264
|
return {
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
4265
|
+
message,
|
|
4266
|
+
file,
|
|
4267
|
+
line,
|
|
4268
|
+
column,
|
|
4269
|
+
category: inferBuildFailureCategory(message)
|
|
2373
4270
|
};
|
|
2374
|
-
}).filter((item) => item !== null);
|
|
2375
|
-
if (vulnerabilities.length === 0) {
|
|
2376
|
-
return null;
|
|
2377
4271
|
}
|
|
2378
|
-
|
|
2379
|
-
return JSON.stringify(
|
|
2380
|
-
{
|
|
2381
|
-
status: "ok",
|
|
2382
|
-
vulnerabilities,
|
|
2383
|
-
summary: vulnerabilities.length === 1 ? `One ${firstVulnerability.severity} vulnerability found in ${firstVulnerability.package}.` : `${vulnerabilities.length} high or critical vulnerabilities found in the provided input.`
|
|
2384
|
-
},
|
|
2385
|
-
null,
|
|
2386
|
-
2
|
|
2387
|
-
);
|
|
4272
|
+
return null;
|
|
2388
4273
|
}
|
|
2389
|
-
function
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
(line) => line.length > 0 && RISK_LINE_PATTERN.test(line) && !ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)
|
|
2393
|
-
).slice(0, 3);
|
|
2394
|
-
if (riskEvidence.length > 0) {
|
|
2395
|
-
return JSON.stringify(
|
|
2396
|
-
{
|
|
2397
|
-
verdict: "fail",
|
|
2398
|
-
reason: "Destructive or clearly risky infrastructure change signals are present.",
|
|
2399
|
-
evidence: riskEvidence
|
|
2400
|
-
},
|
|
2401
|
-
null,
|
|
2402
|
-
2
|
|
2403
|
-
);
|
|
4274
|
+
function extractCargoBuildFailure(input) {
|
|
4275
|
+
if (!/^error(?:\[E\d+\])?:\s+/m.test(input) || !(/^\s*-->\s+/m.test(input) || /could not compile/i.test(input))) {
|
|
4276
|
+
return null;
|
|
2404
4277
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
4278
|
+
const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
|
|
4279
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
4280
|
+
const match = lines[index]?.match(/^error(?:\[(E\d+)\])?:\s+(.+)$/);
|
|
4281
|
+
if (!match) {
|
|
4282
|
+
continue;
|
|
4283
|
+
}
|
|
4284
|
+
const code = match[1];
|
|
4285
|
+
const locationMatch = lines.slice(index + 1, index + 7).join("\n").match(/^\s*-->\s+(.+?):(\d+):(\d+)/m);
|
|
4286
|
+
return {
|
|
4287
|
+
message: code ? `${code}: ${match[2].trim()}` : match[2].trim(),
|
|
4288
|
+
file: locationMatch ? normalizeBuildPath(locationMatch[1]) : null,
|
|
4289
|
+
line: locationMatch ? Number(locationMatch[2]) : null,
|
|
4290
|
+
column: locationMatch ? Number(locationMatch[3]) : null,
|
|
4291
|
+
category: inferBuildFailureCategory(match[2])
|
|
4292
|
+
};
|
|
2415
4293
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
},
|
|
2424
|
-
null,
|
|
2425
|
-
2
|
|
4294
|
+
return null;
|
|
4295
|
+
}
|
|
4296
|
+
function extractCompilerStyleBuildFailure(input) {
|
|
4297
|
+
const lines = stripAnsiText(input).split("\n").map((line) => line.trimEnd());
|
|
4298
|
+
for (const rawLine of lines) {
|
|
4299
|
+
let match = rawLine.match(
|
|
4300
|
+
/^(.+?\.(?:c|cc|cpp|cxx|h|hpp|m|mm)):([0-9]+):([0-9]+):\s*error:\s+(.+)$/
|
|
2426
4301
|
);
|
|
4302
|
+
if (match) {
|
|
4303
|
+
return {
|
|
4304
|
+
message: match[4].trim(),
|
|
4305
|
+
file: normalizeBuildPath(match[1]),
|
|
4306
|
+
line: Number(match[2]),
|
|
4307
|
+
column: Number(match[3]),
|
|
4308
|
+
category: inferBuildFailureCategory(match[4])
|
|
4309
|
+
};
|
|
4310
|
+
}
|
|
4311
|
+
match = rawLine.match(/^(.+?\.go):([0-9]+):([0-9]+):\s+(.+)$/);
|
|
4312
|
+
if (match && !/^\s*warning:/i.test(match[4])) {
|
|
4313
|
+
return {
|
|
4314
|
+
message: match[4].trim(),
|
|
4315
|
+
file: normalizeBuildPath(match[1]),
|
|
4316
|
+
line: Number(match[2]),
|
|
4317
|
+
column: Number(match[3]),
|
|
4318
|
+
category: inferBuildFailureCategory(match[4])
|
|
4319
|
+
};
|
|
4320
|
+
}
|
|
2427
4321
|
}
|
|
2428
4322
|
return null;
|
|
2429
4323
|
}
|
|
4324
|
+
function extractTscBuildFailure(input) {
|
|
4325
|
+
const diagnostics = parseTscErrors(input);
|
|
4326
|
+
const first = diagnostics[0];
|
|
4327
|
+
if (!first) {
|
|
4328
|
+
return null;
|
|
4329
|
+
}
|
|
4330
|
+
return {
|
|
4331
|
+
message: `${first.code}: ${first.message}`,
|
|
4332
|
+
file: first.file,
|
|
4333
|
+
line: first.line,
|
|
4334
|
+
column: first.column,
|
|
4335
|
+
category: inferBuildFailureCategory(`${first.code}: ${first.message}`)
|
|
4336
|
+
};
|
|
4337
|
+
}
|
|
4338
|
+
function extractWrapperBuildFailure(input) {
|
|
4339
|
+
if (!/^\s*npm ERR!|\bERR_PNPM_|^\s*error Command failed/m.test(input)) {
|
|
4340
|
+
return null;
|
|
4341
|
+
}
|
|
4342
|
+
const npmCommandMatch = input.match(/^\s*npm ERR!\s+.*?\bbuild:\s+`([^`]+)`/m);
|
|
4343
|
+
const genericCommandMatch = input.match(/^\s*.+?\s+build:\s+`([^`]+)`/m);
|
|
4344
|
+
const command = npmCommandMatch?.[1] ?? genericCommandMatch?.[1] ?? null;
|
|
4345
|
+
return {
|
|
4346
|
+
message: command ? `build script \`${command}\` failed` : "the build script failed",
|
|
4347
|
+
file: null,
|
|
4348
|
+
line: null,
|
|
4349
|
+
column: null,
|
|
4350
|
+
category: "wrapper"
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
4353
|
+
function buildFailureHeuristic(input) {
|
|
4354
|
+
if (input.trim().length === 0) {
|
|
4355
|
+
return null;
|
|
4356
|
+
}
|
|
4357
|
+
if (detectExplicitBuildSuccess(input)) {
|
|
4358
|
+
return "Build succeeded.";
|
|
4359
|
+
}
|
|
4360
|
+
const match = extractViteImportAnalysisBuildFailure(input) ?? extractWebpackBuildFailure(input) ?? extractEsbuildBuildFailure(input) ?? extractCargoBuildFailure(input) ?? extractCompilerStyleBuildFailure(input) ?? extractTscBuildFailure(input) ?? extractWrapperBuildFailure(input);
|
|
4361
|
+
if (!match) {
|
|
4362
|
+
return null;
|
|
4363
|
+
}
|
|
4364
|
+
return formatBuildFailureOutput(match);
|
|
4365
|
+
}
|
|
2430
4366
|
function applyHeuristicPolicy(policyName, input, detail) {
|
|
2431
4367
|
if (!policyName) {
|
|
2432
4368
|
return null;
|
|
@@ -2440,6 +4376,15 @@ function applyHeuristicPolicy(policyName, input, detail) {
|
|
|
2440
4376
|
if (policyName === "test-status") {
|
|
2441
4377
|
return testStatusHeuristic(input, detail);
|
|
2442
4378
|
}
|
|
4379
|
+
if (policyName === "typecheck-summary") {
|
|
4380
|
+
return typecheckSummaryHeuristic(input);
|
|
4381
|
+
}
|
|
4382
|
+
if (policyName === "lint-failures") {
|
|
4383
|
+
return lintFailuresHeuristic(input);
|
|
4384
|
+
}
|
|
4385
|
+
if (policyName === "build-failure") {
|
|
4386
|
+
return buildFailureHeuristic(input);
|
|
4387
|
+
}
|
|
2443
4388
|
return null;
|
|
2444
4389
|
}
|
|
2445
4390
|
|
|
@@ -2462,8 +4407,8 @@ function buildInsufficientSignalOutput(input) {
|
|
|
2462
4407
|
} else {
|
|
2463
4408
|
hint = "Hint: the captured output did not contain a clear answer for this preset.";
|
|
2464
4409
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
4410
|
+
const presetSuggestion = input.recognizedRunner && input.recognizedRunner !== "unknown" && input.presetName !== "test-status" ? `Hint: captured output looks like ${input.recognizedRunner} test output; try --preset test-status.` : null;
|
|
4411
|
+
return [INSUFFICIENT_SIGNAL_TEXT, hint, presetSuggestion].filter((value) => Boolean(value)).join("\n");
|
|
2467
4412
|
}
|
|
2468
4413
|
|
|
2469
4414
|
// src/core/run.ts
|
|
@@ -3127,6 +5072,138 @@ function escapeRegExp(value) {
|
|
|
3127
5072
|
function unique2(values) {
|
|
3128
5073
|
return [...new Set(values)];
|
|
3129
5074
|
}
|
|
5075
|
+
var genericBucketSearchTerms = /* @__PURE__ */ new Set([
|
|
5076
|
+
"runtimeerror",
|
|
5077
|
+
"typeerror",
|
|
5078
|
+
"error",
|
|
5079
|
+
"exception",
|
|
5080
|
+
"failed",
|
|
5081
|
+
"failure",
|
|
5082
|
+
"visible failure",
|
|
5083
|
+
"failing tests",
|
|
5084
|
+
"setup failures",
|
|
5085
|
+
"runtime failure",
|
|
5086
|
+
"assertion failed",
|
|
5087
|
+
"network",
|
|
5088
|
+
"permission",
|
|
5089
|
+
"configuration"
|
|
5090
|
+
]);
|
|
5091
|
+
function normalizeSearchTerm(value) {
|
|
5092
|
+
return value.replace(/^['"`]+|['"`]+$/g, "").trim();
|
|
5093
|
+
}
|
|
5094
|
+
function isHighSignalSearchTerm(term) {
|
|
5095
|
+
const normalized = normalizeSearchTerm(term);
|
|
5096
|
+
if (normalized.length < 4) {
|
|
5097
|
+
return false;
|
|
5098
|
+
}
|
|
5099
|
+
const lower = normalized.toLowerCase();
|
|
5100
|
+
if (genericBucketSearchTerms.has(lower)) {
|
|
5101
|
+
return false;
|
|
5102
|
+
}
|
|
5103
|
+
if (/^(runtime|type|assertion|network|permission|configuration)\b/i.test(normalized)) {
|
|
5104
|
+
return false;
|
|
5105
|
+
}
|
|
5106
|
+
return true;
|
|
5107
|
+
}
|
|
5108
|
+
function scoreSearchTerm(term) {
|
|
5109
|
+
const normalized = normalizeSearchTerm(term);
|
|
5110
|
+
let score = normalized.length;
|
|
5111
|
+
if (/^[A-Z][A-Z0-9_]{2,}$/.test(normalized)) {
|
|
5112
|
+
score += 80;
|
|
5113
|
+
}
|
|
5114
|
+
if (/^TS\d+$/.test(normalized)) {
|
|
5115
|
+
score += 70;
|
|
5116
|
+
}
|
|
5117
|
+
if (/^[45]\d\d\b/.test(normalized) || /\bHTTPError:\s*[45]\d\d\b/i.test(normalized)) {
|
|
5118
|
+
score += 60;
|
|
5119
|
+
}
|
|
5120
|
+
if (normalized.includes("/") || normalized.includes("\\")) {
|
|
5121
|
+
score += 50;
|
|
5122
|
+
}
|
|
5123
|
+
if (/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/.test(normalized)) {
|
|
5124
|
+
score += 40;
|
|
5125
|
+
}
|
|
5126
|
+
if (/['"`]/.test(term)) {
|
|
5127
|
+
score += 30;
|
|
5128
|
+
}
|
|
5129
|
+
if (normalized.includes("::")) {
|
|
5130
|
+
score += 25;
|
|
5131
|
+
}
|
|
5132
|
+
return score;
|
|
5133
|
+
}
|
|
5134
|
+
function collectCandidateSearchTerms(value) {
|
|
5135
|
+
const candidates = [];
|
|
5136
|
+
const normalized = value.trim();
|
|
5137
|
+
if (!normalized) {
|
|
5138
|
+
return candidates;
|
|
5139
|
+
}
|
|
5140
|
+
for (const match of normalized.matchAll(/['"`]([^'"`]{4,})['"`]/g)) {
|
|
5141
|
+
candidates.push(match[1]);
|
|
5142
|
+
}
|
|
5143
|
+
for (const match of normalized.matchAll(/\b[A-Z][A-Z0-9_]{2,}\b/g)) {
|
|
5144
|
+
candidates.push(match[0]);
|
|
5145
|
+
}
|
|
5146
|
+
for (const match of normalized.matchAll(/\bTS\d+\b/g)) {
|
|
5147
|
+
candidates.push(match[0]);
|
|
5148
|
+
}
|
|
5149
|
+
for (const match of normalized.matchAll(/\bHTTPError:\s*[45]\d\d\b/gi)) {
|
|
5150
|
+
candidates.push(match[0]);
|
|
5151
|
+
}
|
|
5152
|
+
for (const match of normalized.matchAll(/\/[A-Za-z0-9_./:{}-]{4,}/g)) {
|
|
5153
|
+
candidates.push(match[0]);
|
|
5154
|
+
}
|
|
5155
|
+
for (const match of normalized.matchAll(/\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\b/g)) {
|
|
5156
|
+
candidates.push(match[0]);
|
|
5157
|
+
}
|
|
5158
|
+
for (const match of normalized.matchAll(/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9_.-]+\b/g)) {
|
|
5159
|
+
candidates.push(match[0]);
|
|
5160
|
+
}
|
|
5161
|
+
const detail = normalized.split(":").slice(1).join(":").trim();
|
|
5162
|
+
if (detail.length >= 8) {
|
|
5163
|
+
candidates.push(detail);
|
|
5164
|
+
}
|
|
5165
|
+
return candidates;
|
|
5166
|
+
}
|
|
5167
|
+
function extractBucketSearchTerms(args) {
|
|
5168
|
+
const sources = [
|
|
5169
|
+
args.bucket.root_cause,
|
|
5170
|
+
...args.bucket.evidence,
|
|
5171
|
+
...args.readTargets.filter((target) => target.bucket_index === args.bucket.bucket_index).flatMap((target) => [target.context_hint.search_hint ?? "", target.file])
|
|
5172
|
+
];
|
|
5173
|
+
const prioritized = unique2(
|
|
5174
|
+
sources.flatMap((value) => collectCandidateSearchTerms(value)).filter(isHighSignalSearchTerm)
|
|
5175
|
+
).sort((left, right) => {
|
|
5176
|
+
const delta = scoreSearchTerm(right) - scoreSearchTerm(left);
|
|
5177
|
+
if (delta !== 0) {
|
|
5178
|
+
return delta;
|
|
5179
|
+
}
|
|
5180
|
+
return left.localeCompare(right);
|
|
5181
|
+
});
|
|
5182
|
+
if (prioritized.length > 0) {
|
|
5183
|
+
return prioritized.slice(0, 6);
|
|
5184
|
+
}
|
|
5185
|
+
const fallbackTerms = unique2(
|
|
5186
|
+
[...args.bucket.evidence, args.bucket.root_cause].flatMap((value) => value.split(/->|:/).map((part) => normalizeSearchTerm(part))).filter(isHighSignalSearchTerm)
|
|
5187
|
+
);
|
|
5188
|
+
return fallbackTerms.slice(0, 4);
|
|
5189
|
+
}
|
|
5190
|
+
function clusterIndexes(indexes, maxGap = 12) {
|
|
5191
|
+
if (indexes.length === 0) {
|
|
5192
|
+
return [];
|
|
5193
|
+
}
|
|
5194
|
+
const clusters = [];
|
|
5195
|
+
let currentCluster = [indexes[0]];
|
|
5196
|
+
for (const index of indexes.slice(1)) {
|
|
5197
|
+
if (index - currentCluster[currentCluster.length - 1] <= maxGap) {
|
|
5198
|
+
currentCluster.push(index);
|
|
5199
|
+
continue;
|
|
5200
|
+
}
|
|
5201
|
+
clusters.push(currentCluster);
|
|
5202
|
+
currentCluster = [index];
|
|
5203
|
+
}
|
|
5204
|
+
clusters.push(currentCluster);
|
|
5205
|
+
return clusters;
|
|
5206
|
+
}
|
|
3130
5207
|
function buildLineWindows(args) {
|
|
3131
5208
|
const selected = /* @__PURE__ */ new Set();
|
|
3132
5209
|
for (const index of args.indexes) {
|
|
@@ -3142,6 +5219,12 @@ function buildLineWindows(args) {
|
|
|
3142
5219
|
}
|
|
3143
5220
|
return [...selected].sort((left, right) => left - right).map((index) => args.lines[index]);
|
|
3144
5221
|
}
|
|
5222
|
+
function buildPriorityLineGroup(args) {
|
|
5223
|
+
return unique2([
|
|
5224
|
+
...args.indexes.map((index) => args.lines[index]).filter(Boolean),
|
|
5225
|
+
...buildLineWindows(args)
|
|
5226
|
+
]);
|
|
5227
|
+
}
|
|
3145
5228
|
function collapseSelectedLines(args) {
|
|
3146
5229
|
if (args.lines.length === 0) {
|
|
3147
5230
|
return args.fallback();
|
|
@@ -3290,15 +5373,16 @@ function buildTestStatusRawSlice(args) {
|
|
|
3290
5373
|
) ? index : -1
|
|
3291
5374
|
).filter((index) => index >= 0);
|
|
3292
5375
|
const bucketGroups = args.contract.main_buckets.map((bucket) => {
|
|
3293
|
-
const bucketTerms =
|
|
3294
|
-
|
|
3295
|
-
|
|
5376
|
+
const bucketTerms = extractBucketSearchTerms({
|
|
5377
|
+
bucket,
|
|
5378
|
+
readTargets: args.contract.read_targets
|
|
5379
|
+
});
|
|
3296
5380
|
const indexes = lines.map(
|
|
3297
5381
|
(line, index) => bucketTerms.some((term) => new RegExp(escapeRegExp(term), "i").test(line)) ? index : -1
|
|
3298
5382
|
).filter((index) => index >= 0);
|
|
3299
5383
|
return unique2([
|
|
3300
5384
|
...indexes.map((index) => lines[index]).filter(Boolean),
|
|
3301
|
-
...
|
|
5385
|
+
...buildPriorityLineGroup({
|
|
3302
5386
|
lines,
|
|
3303
5387
|
indexes,
|
|
3304
5388
|
radius: 2,
|
|
@@ -3306,26 +5390,55 @@ function buildTestStatusRawSlice(args) {
|
|
|
3306
5390
|
})
|
|
3307
5391
|
]);
|
|
3308
5392
|
});
|
|
3309
|
-
const targetGroups = args.contract.read_targets.
|
|
3310
|
-
|
|
5393
|
+
const targetGroups = args.contract.read_targets.flatMap((target) => {
|
|
5394
|
+
const searchHintIndexes = findSearchHintIndexes({
|
|
3311
5395
|
lines,
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
5396
|
+
searchHint: target.context_hint.search_hint
|
|
5397
|
+
});
|
|
5398
|
+
const fileIndexes = findReadTargetIndexes({
|
|
5399
|
+
lines,
|
|
5400
|
+
file: target.file,
|
|
5401
|
+
line: target.line,
|
|
5402
|
+
contextHint: target.context_hint
|
|
5403
|
+
});
|
|
5404
|
+
const radius = target.line === null ? 1 : 2;
|
|
5405
|
+
const maxLines = target.line === null ? 6 : 8;
|
|
5406
|
+
const groups = [
|
|
5407
|
+
searchHintIndexes.length > 0 ? buildPriorityLineGroup({
|
|
5408
|
+
lines,
|
|
5409
|
+
indexes: searchHintIndexes,
|
|
5410
|
+
radius,
|
|
5411
|
+
maxLines
|
|
5412
|
+
}) : null,
|
|
5413
|
+
fileIndexes.length > 0 ? buildPriorityLineGroup({
|
|
5414
|
+
lines,
|
|
5415
|
+
indexes: fileIndexes,
|
|
5416
|
+
radius,
|
|
5417
|
+
maxLines
|
|
5418
|
+
}) : null
|
|
5419
|
+
].filter((group) => group !== null && group.length > 0);
|
|
5420
|
+
if (groups.length > 0) {
|
|
5421
|
+
return groups;
|
|
5422
|
+
}
|
|
5423
|
+
return [
|
|
5424
|
+
buildPriorityLineGroup({
|
|
5425
|
+
lines,
|
|
5426
|
+
indexes: unique2([...searchHintIndexes, ...fileIndexes]),
|
|
5427
|
+
radius,
|
|
5428
|
+
maxLines
|
|
5429
|
+
})
|
|
5430
|
+
];
|
|
5431
|
+
});
|
|
5432
|
+
const failureHeaderIndexes = lines.map((line, index) => /\b(FAILED|ERROR)\b/.test(line) ? index : -1).filter((index) => index >= 0);
|
|
5433
|
+
const failureIndexes = (failureHeaderIndexes.length > 0 ? failureHeaderIndexes : lines.map((line, index) => /^E\s/.test(line) ? index : -1).filter((index) => index >= 0)).filter((index) => index >= 0);
|
|
5434
|
+
const failureHeaderGroups = clusterIndexes(failureIndexes).slice(0, 8).map(
|
|
5435
|
+
(cluster) => buildPriorityLineGroup({
|
|
5436
|
+
lines,
|
|
5437
|
+
indexes: cluster,
|
|
5438
|
+
radius: 1,
|
|
5439
|
+
maxLines: 8
|
|
3326
5440
|
})
|
|
3327
|
-
);
|
|
3328
|
-
const failureIndexes = lines.map((line, index) => /\b(FAILED|ERROR)\b/.test(line) || /^E\s/.test(line) ? index : -1).filter((index) => index >= 0);
|
|
5441
|
+
).filter((group) => group.length > 0);
|
|
3329
5442
|
const selected = collapseSelectedLineGroups({
|
|
3330
5443
|
groups: [
|
|
3331
5444
|
...targetGroups,
|
|
@@ -3339,12 +5452,14 @@ function buildTestStatusRawSlice(args) {
|
|
|
3339
5452
|
})
|
|
3340
5453
|
]),
|
|
3341
5454
|
...bucketGroups,
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
5455
|
+
...failureHeaderGroups.length > 0 ? failureHeaderGroups : [
|
|
5456
|
+
buildLineWindows({
|
|
5457
|
+
lines,
|
|
5458
|
+
indexes: failureIndexes,
|
|
5459
|
+
radius: 1,
|
|
5460
|
+
maxLines: 24
|
|
5461
|
+
})
|
|
5462
|
+
]
|
|
3348
5463
|
],
|
|
3349
5464
|
maxInputChars: args.config.maxInputChars,
|
|
3350
5465
|
fallback: () => truncateInput(args.input, {
|
|
@@ -3485,7 +5600,8 @@ function withInsufficientHint(args) {
|
|
|
3485
5600
|
return buildInsufficientSignalOutput({
|
|
3486
5601
|
presetName: args.request.presetName,
|
|
3487
5602
|
originalLength: args.prepared.meta.originalLength,
|
|
3488
|
-
truncatedApplied: args.prepared.meta.truncatedApplied
|
|
5603
|
+
truncatedApplied: args.prepared.meta.truncatedApplied,
|
|
5604
|
+
recognizedRunner: detectTestRunner(args.prepared.redacted)
|
|
3489
5605
|
});
|
|
3490
5606
|
}
|
|
3491
5607
|
async function generateWithRetry(args) {
|
|
@@ -3545,6 +5661,38 @@ function renderTestStatusDecisionOutput(args) {
|
|
|
3545
5661
|
return args.decision.standardText;
|
|
3546
5662
|
}
|
|
3547
5663
|
function buildTestStatusProviderFailureDecision(args) {
|
|
5664
|
+
const concreteReadTarget = args.baseDecision.contract.read_targets.find(
|
|
5665
|
+
(target) => Boolean(target.file)
|
|
5666
|
+
);
|
|
5667
|
+
const hasUnknownBucket = args.baseDecision.contract.main_buckets.some(
|
|
5668
|
+
(bucket) => bucket.root_cause.startsWith("unknown ")
|
|
5669
|
+
);
|
|
5670
|
+
if (concreteReadTarget && !hasUnknownBucket) {
|
|
5671
|
+
return buildTestStatusDiagnoseContract({
|
|
5672
|
+
input: args.input,
|
|
5673
|
+
analysis: args.analysis,
|
|
5674
|
+
resolvedTests: args.baseDecision.contract.resolved_tests,
|
|
5675
|
+
remainingTests: args.baseDecision.contract.remaining_tests,
|
|
5676
|
+
contractOverrides: {
|
|
5677
|
+
...args.baseDecision.contract,
|
|
5678
|
+
diagnosis_complete: false,
|
|
5679
|
+
raw_needed: false,
|
|
5680
|
+
additional_source_read_likely_low_value: false,
|
|
5681
|
+
read_raw_only_if: null,
|
|
5682
|
+
decision: "read_source",
|
|
5683
|
+
provider_used: true,
|
|
5684
|
+
provider_confidence: null,
|
|
5685
|
+
provider_failed: true,
|
|
5686
|
+
raw_slice_used: args.rawSliceUsed,
|
|
5687
|
+
raw_slice_strategy: args.rawSliceStrategy,
|
|
5688
|
+
next_best_action: {
|
|
5689
|
+
code: "read_source_for_bucket",
|
|
5690
|
+
bucket_index: args.baseDecision.contract.dominant_blocker_bucket_index ?? concreteReadTarget.bucket_index,
|
|
5691
|
+
note: `Provider follow-up failed (${args.reason}). The heuristic anchor is concrete enough to inspect source for the current bucket before reading raw traceback.`
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
});
|
|
5695
|
+
}
|
|
3548
5696
|
const shouldZoomFirst = args.request.detail !== "verbose";
|
|
3549
5697
|
return buildTestStatusDiagnoseContract({
|
|
3550
5698
|
input: args.input,
|
|
@@ -3571,7 +5719,7 @@ function buildTestStatusProviderFailureDecision(args) {
|
|
|
3571
5719
|
}
|
|
3572
5720
|
});
|
|
3573
5721
|
}
|
|
3574
|
-
async function
|
|
5722
|
+
async function runSiftCore(request, recorder) {
|
|
3575
5723
|
const prepared = prepareInput(request.stdin, request.config.input);
|
|
3576
5724
|
const heuristicInput = prepared.redacted;
|
|
3577
5725
|
const heuristicInputTruncated = false;
|
|
@@ -3657,6 +5805,7 @@ async function runSift(request) {
|
|
|
3657
5805
|
finalOutput
|
|
3658
5806
|
});
|
|
3659
5807
|
}
|
|
5808
|
+
recorder?.heuristic();
|
|
3660
5809
|
return finalOutput;
|
|
3661
5810
|
}
|
|
3662
5811
|
if (testStatusDecision && testStatusAnalysis) {
|
|
@@ -3756,6 +5905,7 @@ async function runSift(request) {
|
|
|
3756
5905
|
providerInputChars: providerPrepared2.truncated.length,
|
|
3757
5906
|
providerOutputChars: result.text.length
|
|
3758
5907
|
});
|
|
5908
|
+
recorder?.provider(result.usage);
|
|
3759
5909
|
return finalOutput;
|
|
3760
5910
|
} catch (error) {
|
|
3761
5911
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
@@ -3790,6 +5940,7 @@ async function runSift(request) {
|
|
|
3790
5940
|
rawSliceChars: rawSlice.text.length,
|
|
3791
5941
|
providerInputChars: providerPrepared2.truncated.length
|
|
3792
5942
|
});
|
|
5943
|
+
recorder?.fallback();
|
|
3793
5944
|
return finalOutput;
|
|
3794
5945
|
}
|
|
3795
5946
|
}
|
|
@@ -3846,6 +5997,7 @@ async function runSift(request) {
|
|
|
3846
5997
|
})) {
|
|
3847
5998
|
throw new Error("Model output rejected by quality gate");
|
|
3848
5999
|
}
|
|
6000
|
+
recorder?.provider(result.usage);
|
|
3849
6001
|
return withInsufficientHint({
|
|
3850
6002
|
output: normalizeOutput(result.text, providerPrompt.responseMode),
|
|
3851
6003
|
request,
|
|
@@ -3853,6 +6005,7 @@ async function runSift(request) {
|
|
|
3853
6005
|
});
|
|
3854
6006
|
} catch (error) {
|
|
3855
6007
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
6008
|
+
recorder?.fallback();
|
|
3856
6009
|
return withInsufficientHint({
|
|
3857
6010
|
output: buildFallbackOutput({
|
|
3858
6011
|
format: request.format,
|
|
@@ -3866,6 +6019,72 @@ async function runSift(request) {
|
|
|
3866
6019
|
});
|
|
3867
6020
|
}
|
|
3868
6021
|
}
|
|
6022
|
+
async function runSift(request) {
|
|
6023
|
+
return runSiftCore(request);
|
|
6024
|
+
}
|
|
6025
|
+
async function runSiftWithStats(request) {
|
|
6026
|
+
if (request.dryRun) {
|
|
6027
|
+
return {
|
|
6028
|
+
output: await runSiftCore(request),
|
|
6029
|
+
stats: null
|
|
6030
|
+
};
|
|
6031
|
+
}
|
|
6032
|
+
const startedAt = Date.now();
|
|
6033
|
+
let layer = "fallback";
|
|
6034
|
+
let providerCalled = false;
|
|
6035
|
+
let totalTokens = null;
|
|
6036
|
+
const output = await runSiftCore(request, {
|
|
6037
|
+
heuristic() {
|
|
6038
|
+
layer = "heuristic";
|
|
6039
|
+
providerCalled = false;
|
|
6040
|
+
totalTokens = null;
|
|
6041
|
+
},
|
|
6042
|
+
provider(usage) {
|
|
6043
|
+
layer = "provider";
|
|
6044
|
+
providerCalled = true;
|
|
6045
|
+
totalTokens = usage?.totalTokens ?? null;
|
|
6046
|
+
},
|
|
6047
|
+
fallback() {
|
|
6048
|
+
layer = "fallback";
|
|
6049
|
+
providerCalled = true;
|
|
6050
|
+
totalTokens = null;
|
|
6051
|
+
}
|
|
6052
|
+
});
|
|
6053
|
+
return {
|
|
6054
|
+
output,
|
|
6055
|
+
stats: {
|
|
6056
|
+
layer,
|
|
6057
|
+
providerCalled,
|
|
6058
|
+
totalTokens,
|
|
6059
|
+
durationMs: Date.now() - startedAt,
|
|
6060
|
+
presetName: request.presetName
|
|
6061
|
+
}
|
|
6062
|
+
};
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
// src/core/stats.ts
|
|
6066
|
+
import pc2 from "picocolors";
|
|
6067
|
+
function formatDuration(durationMs) {
|
|
6068
|
+
return durationMs >= 1e3 ? `${(durationMs / 1e3).toFixed(1)}s` : `${durationMs}ms`;
|
|
6069
|
+
}
|
|
6070
|
+
function formatStatsFooter(stats) {
|
|
6071
|
+
const duration = formatDuration(stats.durationMs);
|
|
6072
|
+
if (stats.layer === "heuristic") {
|
|
6073
|
+
return `[sift: heuristic \u2022 LLM skipped \u2022 summary ${duration}]`;
|
|
6074
|
+
}
|
|
6075
|
+
if (stats.layer === "provider") {
|
|
6076
|
+
const tokenSegment = stats.totalTokens !== null ? ` \u2022 ${stats.totalTokens} tokens` : "";
|
|
6077
|
+
return `[sift: provider \u2022 LLM used${tokenSegment} \u2022 summary ${duration}]`;
|
|
6078
|
+
}
|
|
6079
|
+
return `[sift: fallback \u2022 provider failed \u2022 summary ${duration}]`;
|
|
6080
|
+
}
|
|
6081
|
+
function emitStatsFooter(args) {
|
|
6082
|
+
if (args.quiet || !args.stats || !process.stderr.isTTY) {
|
|
6083
|
+
return;
|
|
6084
|
+
}
|
|
6085
|
+
process.stderr.write(`${pc2.dim(formatStatsFooter(args.stats))}
|
|
6086
|
+
`);
|
|
6087
|
+
}
|
|
3869
6088
|
|
|
3870
6089
|
// src/core/testStatusState.ts
|
|
3871
6090
|
import fs from "fs";
|
|
@@ -3875,10 +6094,29 @@ var detailSchema = z2.enum(["standard", "focused", "verbose"]);
|
|
|
3875
6094
|
var failureBucketTypeSchema = z2.enum([
|
|
3876
6095
|
"shared_environment_blocker",
|
|
3877
6096
|
"fixture_guard_failure",
|
|
6097
|
+
"timeout_failure",
|
|
6098
|
+
"permission_denied_failure",
|
|
6099
|
+
"async_event_loop_failure",
|
|
6100
|
+
"fixture_teardown_failure",
|
|
6101
|
+
"db_migration_failure",
|
|
6102
|
+
"configuration_error",
|
|
6103
|
+
"xdist_worker_crash",
|
|
6104
|
+
"type_error_failure",
|
|
6105
|
+
"resource_leak_warning",
|
|
6106
|
+
"django_db_access_denied",
|
|
6107
|
+
"network_failure",
|
|
6108
|
+
"subprocess_crash_segfault",
|
|
6109
|
+
"flaky_test_detected",
|
|
6110
|
+
"serialization_encoding_failure",
|
|
6111
|
+
"file_not_found_failure",
|
|
6112
|
+
"memory_error",
|
|
6113
|
+
"deprecation_warning_as_error",
|
|
6114
|
+
"xfail_strict_unexpected_pass",
|
|
3878
6115
|
"service_unavailable",
|
|
3879
6116
|
"db_connection_failure",
|
|
3880
6117
|
"auth_bypass_absent",
|
|
3881
6118
|
"contract_snapshot_drift",
|
|
6119
|
+
"snapshot_mismatch",
|
|
3882
6120
|
"import_dependency_failure",
|
|
3883
6121
|
"collection_failure",
|
|
3884
6122
|
"assertion_failure",
|
|
@@ -4548,7 +6786,7 @@ async function runExec(request) {
|
|
|
4548
6786
|
const previousCachedRun = shouldCacheTestStatusBase ? tryReadCachedTestStatusRun() : null;
|
|
4549
6787
|
if (request.config.runtime.verbose) {
|
|
4550
6788
|
process.stderr.write(
|
|
4551
|
-
`${
|
|
6789
|
+
`${pc3.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${commandPreview}
|
|
4552
6790
|
`
|
|
4553
6791
|
);
|
|
4554
6792
|
}
|
|
@@ -4577,7 +6815,7 @@ async function runExec(request) {
|
|
|
4577
6815
|
}
|
|
4578
6816
|
bypassed = true;
|
|
4579
6817
|
if (request.config.runtime.verbose) {
|
|
4580
|
-
process.stderr.write(`${
|
|
6818
|
+
process.stderr.write(`${pc3.dim("sift")} bypass=interactive-prompt
|
|
4581
6819
|
`);
|
|
4582
6820
|
}
|
|
4583
6821
|
process.stderr.write(capture.render());
|
|
@@ -4606,15 +6844,16 @@ async function runExec(request) {
|
|
|
4606
6844
|
const shouldCacheTestStatus = shouldCacheTestStatusBase && !useWatchFlow;
|
|
4607
6845
|
if (request.config.runtime.verbose) {
|
|
4608
6846
|
process.stderr.write(
|
|
4609
|
-
`${
|
|
6847
|
+
`${pc3.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
|
|
4610
6848
|
`
|
|
4611
6849
|
);
|
|
4612
6850
|
}
|
|
4613
6851
|
if (autoWatchDetected) {
|
|
4614
|
-
process.stderr.write(`${
|
|
6852
|
+
process.stderr.write(`${pc3.dim("sift")} auto-watch=detected
|
|
4615
6853
|
`);
|
|
4616
6854
|
}
|
|
4617
6855
|
if (!bypassed) {
|
|
6856
|
+
const reductionStartedAt = Date.now();
|
|
4618
6857
|
if (request.showRaw && capturedOutput.length > 0) {
|
|
4619
6858
|
process.stderr.write(capturedOutput);
|
|
4620
6859
|
if (!capturedOutput.endsWith("\n")) {
|
|
@@ -4629,12 +6868,22 @@ async function runExec(request) {
|
|
|
4629
6868
|
if (execSuccessShortcut && !request.dryRun) {
|
|
4630
6869
|
if (request.config.runtime.verbose) {
|
|
4631
6870
|
process.stderr.write(
|
|
4632
|
-
`${
|
|
6871
|
+
`${pc3.dim("sift")} exec_shortcut=${request.presetName}
|
|
4633
6872
|
`
|
|
4634
6873
|
);
|
|
4635
6874
|
}
|
|
4636
6875
|
process.stdout.write(`${execSuccessShortcut}
|
|
4637
6876
|
`);
|
|
6877
|
+
emitStatsFooter({
|
|
6878
|
+
stats: {
|
|
6879
|
+
layer: "heuristic",
|
|
6880
|
+
providerCalled: false,
|
|
6881
|
+
totalTokens: null,
|
|
6882
|
+
durationMs: Date.now() - reductionStartedAt,
|
|
6883
|
+
presetName: request.presetName
|
|
6884
|
+
},
|
|
6885
|
+
quiet: Boolean(request.quiet)
|
|
6886
|
+
});
|
|
4638
6887
|
return exitCode;
|
|
4639
6888
|
}
|
|
4640
6889
|
if (useWatchFlow) {
|
|
@@ -4647,7 +6896,8 @@ async function runExec(request) {
|
|
|
4647
6896
|
presetName: request.presetName,
|
|
4648
6897
|
originalLength: capture.getTotalChars(),
|
|
4649
6898
|
truncatedApplied: capture.wasTruncated(),
|
|
4650
|
-
exitCode
|
|
6899
|
+
exitCode,
|
|
6900
|
+
recognizedRunner: detectTestRunner(capturedOutput)
|
|
4651
6901
|
});
|
|
4652
6902
|
}
|
|
4653
6903
|
process.stdout.write(`${output2}
|
|
@@ -4675,7 +6925,7 @@ async function runExec(request) {
|
|
|
4675
6925
|
previous: previousCachedRun,
|
|
4676
6926
|
current: currentCachedRun
|
|
4677
6927
|
}) : null;
|
|
4678
|
-
|
|
6928
|
+
const result = await runSiftWithStats({
|
|
4679
6929
|
...request,
|
|
4680
6930
|
stdin: capturedOutput,
|
|
4681
6931
|
analysisContext: request.skipCacheWrite && request.presetName === "test-status" ? [
|
|
@@ -4694,13 +6944,15 @@ async function runExec(request) {
|
|
|
4694
6944
|
)
|
|
4695
6945
|
} : request.testStatusContext
|
|
4696
6946
|
});
|
|
6947
|
+
let output = result.output;
|
|
4697
6948
|
if (shouldCacheTestStatus) {
|
|
4698
6949
|
if (isInsufficientSignalOutput(output)) {
|
|
4699
6950
|
output = buildInsufficientSignalOutput({
|
|
4700
6951
|
presetName: request.presetName,
|
|
4701
6952
|
originalLength: capture.getTotalChars(),
|
|
4702
6953
|
truncatedApplied: capture.wasTruncated(),
|
|
4703
|
-
exitCode
|
|
6954
|
+
exitCode,
|
|
6955
|
+
recognizedRunner: detectTestRunner(capturedOutput)
|
|
4704
6956
|
});
|
|
4705
6957
|
}
|
|
4706
6958
|
if (request.diff && !request.dryRun && previousCachedRun && currentCachedRun) {
|
|
@@ -4733,7 +6985,7 @@ ${output}`;
|
|
|
4733
6985
|
} catch (error) {
|
|
4734
6986
|
if (request.config.runtime.verbose) {
|
|
4735
6987
|
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
4736
|
-
process.stderr.write(`${
|
|
6988
|
+
process.stderr.write(`${pc3.dim("sift")} cache_write=failed reason=${reason}
|
|
4737
6989
|
`);
|
|
4738
6990
|
}
|
|
4739
6991
|
}
|
|
@@ -4743,11 +6995,16 @@ ${output}`;
|
|
|
4743
6995
|
presetName: request.presetName,
|
|
4744
6996
|
originalLength: capture.getTotalChars(),
|
|
4745
6997
|
truncatedApplied: capture.wasTruncated(),
|
|
4746
|
-
exitCode
|
|
6998
|
+
exitCode,
|
|
6999
|
+
recognizedRunner: detectTestRunner(capturedOutput)
|
|
4747
7000
|
});
|
|
4748
7001
|
}
|
|
4749
7002
|
process.stdout.write(`${output}
|
|
4750
7003
|
`);
|
|
7004
|
+
emitStatsFooter({
|
|
7005
|
+
stats: result.stats,
|
|
7006
|
+
quiet: Boolean(request.quiet)
|
|
7007
|
+
});
|
|
4751
7008
|
if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
|
|
4752
7009
|
presetName: request.presetName,
|
|
4753
7010
|
output
|
|
@@ -5107,5 +7364,6 @@ export {
|
|
|
5107
7364
|
normalizeChildExitCode,
|
|
5108
7365
|
resolveConfig,
|
|
5109
7366
|
runExec,
|
|
5110
|
-
runSift
|
|
7367
|
+
runSift,
|
|
7368
|
+
runSiftWithStats
|
|
5111
7369
|
};
|