@bilalimamoglu/sift 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -312
- package/dist/cli.js +1939 -194
- package/dist/index.d.ts +11 -2
- package/dist/index.js +1528 -68
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -136,6 +136,78 @@ var defaultConfig = {
|
|
|
136
136
|
}
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
+
// src/config/native-provider.ts
|
|
140
|
+
function getNativeProviderDefaults(provider) {
|
|
141
|
+
if (provider === "openrouter") {
|
|
142
|
+
return {
|
|
143
|
+
provider,
|
|
144
|
+
model: "openrouter/free",
|
|
145
|
+
baseUrl: "https://openrouter.ai/api/v1"
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
provider,
|
|
150
|
+
model: defaultConfig.provider.model,
|
|
151
|
+
baseUrl: defaultConfig.provider.baseUrl
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function getProfileProviderState(provider, profile) {
|
|
155
|
+
const defaults = getNativeProviderDefaults(provider);
|
|
156
|
+
return {
|
|
157
|
+
provider,
|
|
158
|
+
model: profile?.model ?? defaults.model,
|
|
159
|
+
baseUrl: profile?.baseUrl ?? defaults.baseUrl
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function getStoredProviderProfile(config, provider) {
|
|
163
|
+
const existingProfile = config.providerProfiles?.[provider];
|
|
164
|
+
if (existingProfile) {
|
|
165
|
+
return existingProfile;
|
|
166
|
+
}
|
|
167
|
+
if (config.provider.provider !== provider) {
|
|
168
|
+
return void 0;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
model: config.provider.model,
|
|
172
|
+
baseUrl: config.provider.baseUrl,
|
|
173
|
+
apiKey: config.provider.apiKey || void 0
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function setStoredProviderProfile(config, provider, profile) {
|
|
177
|
+
const providerProfiles = {
|
|
178
|
+
...config.providerProfiles ?? {},
|
|
179
|
+
[provider]: profile
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
...config,
|
|
183
|
+
providerProfiles
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function preserveActiveNativeProviderProfile(config) {
|
|
187
|
+
const provider = config.provider.provider;
|
|
188
|
+
if (provider !== "openai" && provider !== "openrouter") {
|
|
189
|
+
return config;
|
|
190
|
+
}
|
|
191
|
+
if (config.providerProfiles?.[provider]) {
|
|
192
|
+
return config;
|
|
193
|
+
}
|
|
194
|
+
return setStoredProviderProfile(config, provider, {
|
|
195
|
+
model: config.provider.model,
|
|
196
|
+
baseUrl: config.provider.baseUrl,
|
|
197
|
+
apiKey: config.provider.apiKey || void 0
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function applyActiveProvider(config, provider, profile, apiKey) {
|
|
201
|
+
return {
|
|
202
|
+
...config,
|
|
203
|
+
provider: {
|
|
204
|
+
...config.provider,
|
|
205
|
+
...getProfileProviderState(provider, profile),
|
|
206
|
+
apiKey
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
139
211
|
// src/config/provider-api-key.ts
|
|
140
212
|
var OPENAI_COMPATIBLE_BASE_URL_ENV = [
|
|
141
213
|
{ prefix: "https://api.openai.com/", envName: "OPENAI_API_KEY" },
|
|
@@ -143,13 +215,16 @@ var OPENAI_COMPATIBLE_BASE_URL_ENV = [
|
|
|
143
215
|
{ prefix: "https://api.together.xyz/", envName: "TOGETHER_API_KEY" },
|
|
144
216
|
{ prefix: "https://api.groq.com/openai/", envName: "GROQ_API_KEY" }
|
|
145
217
|
];
|
|
218
|
+
var NATIVE_PROVIDER_API_KEY_ENV = {
|
|
219
|
+
openai: "OPENAI_API_KEY",
|
|
220
|
+
openrouter: "OPENROUTER_API_KEY"
|
|
221
|
+
};
|
|
146
222
|
var PROVIDER_API_KEY_ENV = {
|
|
147
223
|
anthropic: "ANTHROPIC_API_KEY",
|
|
148
224
|
claude: "ANTHROPIC_API_KEY",
|
|
149
225
|
groq: "GROQ_API_KEY",
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
together: "TOGETHER_API_KEY"
|
|
226
|
+
together: "TOGETHER_API_KEY",
|
|
227
|
+
...NATIVE_PROVIDER_API_KEY_ENV
|
|
153
228
|
};
|
|
154
229
|
function normalizeBaseUrl(baseUrl) {
|
|
155
230
|
if (!baseUrl) {
|
|
@@ -184,6 +259,9 @@ function resolveProviderApiKey(provider, baseUrl, env) {
|
|
|
184
259
|
}
|
|
185
260
|
return env.SIFT_PROVIDER_API_KEY;
|
|
186
261
|
}
|
|
262
|
+
function getNativeProviderApiKeyEnvName(provider) {
|
|
263
|
+
return NATIVE_PROVIDER_API_KEY_ENV[provider];
|
|
264
|
+
}
|
|
187
265
|
function getProviderApiKeyEnvNames(provider, baseUrl) {
|
|
188
266
|
const envNames = ["SIFT_PROVIDER_API_KEY"];
|
|
189
267
|
if (provider === "openai-compatible") {
|
|
@@ -205,7 +283,11 @@ function getProviderApiKeyEnvNames(provider, baseUrl) {
|
|
|
205
283
|
|
|
206
284
|
// src/config/schema.ts
|
|
207
285
|
import { z } from "zod";
|
|
208
|
-
var providerNameSchema = z.enum([
|
|
286
|
+
var providerNameSchema = z.enum([
|
|
287
|
+
"openai",
|
|
288
|
+
"openai-compatible",
|
|
289
|
+
"openrouter"
|
|
290
|
+
]);
|
|
209
291
|
var outputFormatSchema = z.enum([
|
|
210
292
|
"brief",
|
|
211
293
|
"bullets",
|
|
@@ -234,6 +316,15 @@ var providerConfigSchema = z.object({
|
|
|
234
316
|
temperature: z.number().min(0).max(2),
|
|
235
317
|
maxOutputTokens: z.number().int().positive()
|
|
236
318
|
});
|
|
319
|
+
var providerProfileSchema = z.object({
|
|
320
|
+
model: z.string().min(1).optional(),
|
|
321
|
+
baseUrl: z.string().url().optional(),
|
|
322
|
+
apiKey: z.string().optional()
|
|
323
|
+
});
|
|
324
|
+
var providerProfilesSchema = z.object({
|
|
325
|
+
openai: providerProfileSchema.optional(),
|
|
326
|
+
openrouter: providerProfileSchema.optional()
|
|
327
|
+
}).optional();
|
|
237
328
|
var inputConfigSchema = z.object({
|
|
238
329
|
stripAnsi: z.boolean(),
|
|
239
330
|
redact: z.boolean(),
|
|
@@ -258,10 +349,19 @@ var siftConfigSchema = z.object({
|
|
|
258
349
|
provider: providerConfigSchema,
|
|
259
350
|
input: inputConfigSchema,
|
|
260
351
|
runtime: runtimeConfigSchema,
|
|
261
|
-
presets: z.record(presetDefinitionSchema)
|
|
352
|
+
presets: z.record(presetDefinitionSchema),
|
|
353
|
+
providerProfiles: providerProfilesSchema
|
|
262
354
|
});
|
|
263
355
|
|
|
264
356
|
// src/config/resolve.ts
|
|
357
|
+
var PROVIDER_DEFAULT_OVERRIDES = {
|
|
358
|
+
openrouter: {
|
|
359
|
+
provider: {
|
|
360
|
+
model: "openrouter/free",
|
|
361
|
+
baseUrl: "https://openrouter.ai/api/v1"
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
265
365
|
function isRecord(value) {
|
|
266
366
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
267
367
|
}
|
|
@@ -324,13 +424,32 @@ function buildCredentialEnvOverrides(env, context) {
|
|
|
324
424
|
}
|
|
325
425
|
};
|
|
326
426
|
}
|
|
427
|
+
function getBaseConfigForProvider(provider) {
|
|
428
|
+
return mergeDefined(defaultConfig, provider ? PROVIDER_DEFAULT_OVERRIDES[provider] : {});
|
|
429
|
+
}
|
|
430
|
+
function resolveProvisionalProvider(args) {
|
|
431
|
+
const provisional = mergeDefined(
|
|
432
|
+
mergeDefined(
|
|
433
|
+
mergeDefined(defaultConfig, args.fileConfig),
|
|
434
|
+
args.nonCredentialEnvConfig
|
|
435
|
+
),
|
|
436
|
+
stripApiKey(args.cliOverrides) ?? {}
|
|
437
|
+
);
|
|
438
|
+
return provisional.provider.provider;
|
|
439
|
+
}
|
|
327
440
|
function resolveConfig(options = {}) {
|
|
328
441
|
const env = options.env ?? process.env;
|
|
329
442
|
const fileConfig = loadRawConfig(options.configPath);
|
|
330
443
|
const nonCredentialEnvConfig = buildNonCredentialEnvOverrides(env);
|
|
444
|
+
const provisionalProvider = resolveProvisionalProvider({
|
|
445
|
+
fileConfig,
|
|
446
|
+
nonCredentialEnvConfig,
|
|
447
|
+
cliOverrides: options.cliOverrides
|
|
448
|
+
});
|
|
449
|
+
const baseConfig = getBaseConfigForProvider(provisionalProvider);
|
|
331
450
|
const contextConfig = mergeDefined(
|
|
332
451
|
mergeDefined(
|
|
333
|
-
mergeDefined(
|
|
452
|
+
mergeDefined(baseConfig, fileConfig),
|
|
334
453
|
nonCredentialEnvConfig
|
|
335
454
|
),
|
|
336
455
|
stripApiKey(options.cliOverrides) ?? {}
|
|
@@ -342,7 +461,7 @@ function resolveConfig(options = {}) {
|
|
|
342
461
|
const merged = mergeDefined(
|
|
343
462
|
mergeDefined(
|
|
344
463
|
mergeDefined(
|
|
345
|
-
mergeDefined(
|
|
464
|
+
mergeDefined(baseConfig, fileConfig),
|
|
346
465
|
nonCredentialEnvConfig
|
|
347
466
|
),
|
|
348
467
|
credentialEnvConfig
|
|
@@ -387,6 +506,32 @@ function writeConfigFile(options) {
|
|
|
387
506
|
return resolved;
|
|
388
507
|
}
|
|
389
508
|
|
|
509
|
+
// src/config/editable.ts
|
|
510
|
+
import fs3 from "fs";
|
|
511
|
+
import path4 from "path";
|
|
512
|
+
function isRecord2(value) {
|
|
513
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
514
|
+
}
|
|
515
|
+
function resolveEditableConfigPath(explicitPath) {
|
|
516
|
+
if (explicitPath) {
|
|
517
|
+
return path4.resolve(explicitPath);
|
|
518
|
+
}
|
|
519
|
+
return findConfigPath() ?? getDefaultGlobalConfigPath();
|
|
520
|
+
}
|
|
521
|
+
function loadEditableConfig(explicitPath) {
|
|
522
|
+
const resolvedPath = resolveEditableConfigPath(explicitPath);
|
|
523
|
+
const existed = fs3.existsSync(resolvedPath);
|
|
524
|
+
const rawConfig = existed ? loadRawConfig(resolvedPath) : {};
|
|
525
|
+
const config = siftConfigSchema.parse(
|
|
526
|
+
mergeDefined(defaultConfig, isRecord2(rawConfig) ? rawConfig : {})
|
|
527
|
+
);
|
|
528
|
+
return {
|
|
529
|
+
config,
|
|
530
|
+
existed,
|
|
531
|
+
resolvedPath
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
390
535
|
// src/ui/presentation.ts
|
|
391
536
|
import pc from "picocolors";
|
|
392
537
|
function applyColor(enabled, formatter, value) {
|
|
@@ -437,8 +582,7 @@ ${tagline}`;
|
|
|
437
582
|
}
|
|
438
583
|
|
|
439
584
|
// src/commands/config-setup.ts
|
|
440
|
-
import
|
|
441
|
-
import path4 from "path";
|
|
585
|
+
import path5 from "path";
|
|
442
586
|
import { emitKeypressEvents } from "readline";
|
|
443
587
|
import { createInterface } from "readline/promises";
|
|
444
588
|
import { stderr as defaultStderr, stdin as defaultStdin2, stdout as defaultStdout } from "process";
|
|
@@ -636,59 +780,102 @@ function createTerminalIO() {
|
|
|
636
780
|
};
|
|
637
781
|
}
|
|
638
782
|
function resolveSetupPath(targetPath) {
|
|
639
|
-
return targetPath ?
|
|
640
|
-
}
|
|
641
|
-
function buildOpenAISetupConfig(apiKey) {
|
|
642
|
-
return {
|
|
643
|
-
...defaultConfig,
|
|
644
|
-
provider: {
|
|
645
|
-
...defaultConfig.provider,
|
|
646
|
-
provider: "openai",
|
|
647
|
-
model: "gpt-5-nano",
|
|
648
|
-
baseUrl: "https://api.openai.com/v1",
|
|
649
|
-
apiKey
|
|
650
|
-
}
|
|
651
|
-
};
|
|
783
|
+
return targetPath ? path5.resolve(targetPath) : getDefaultGlobalConfigPath();
|
|
652
784
|
}
|
|
653
785
|
function getSetupPresenter(io) {
|
|
654
786
|
return createPresentation(io.stdoutIsTTY);
|
|
655
787
|
}
|
|
788
|
+
function getProviderLabel(provider) {
|
|
789
|
+
return provider === "openrouter" ? "OpenRouter" : "OpenAI";
|
|
790
|
+
}
|
|
656
791
|
async function promptForProvider(io) {
|
|
657
792
|
if (io.select) {
|
|
658
|
-
const choice = await io.select("Select provider for this machine", [
|
|
793
|
+
const choice = await io.select("Select provider for this machine", [
|
|
794
|
+
"OpenAI",
|
|
795
|
+
"OpenRouter"
|
|
796
|
+
]);
|
|
659
797
|
if (choice === "OpenAI") {
|
|
660
798
|
return "openai";
|
|
661
799
|
}
|
|
800
|
+
if (choice === "OpenRouter") {
|
|
801
|
+
return "openrouter";
|
|
802
|
+
}
|
|
662
803
|
}
|
|
663
804
|
while (true) {
|
|
664
|
-
const answer = (await io.ask("Provider [OpenAI]: ")).trim().toLowerCase();
|
|
805
|
+
const answer = (await io.ask("Provider [OpenAI/OpenRouter]: ")).trim().toLowerCase();
|
|
665
806
|
if (answer === "" || answer === "openai") {
|
|
666
807
|
return "openai";
|
|
667
808
|
}
|
|
668
|
-
|
|
809
|
+
if (answer === "openrouter") {
|
|
810
|
+
return "openrouter";
|
|
811
|
+
}
|
|
812
|
+
io.error("Only OpenAI and OpenRouter are supported in guided setup right now.\n");
|
|
669
813
|
}
|
|
670
814
|
}
|
|
671
|
-
async function promptForApiKey(io) {
|
|
815
|
+
async function promptForApiKey(io, provider) {
|
|
816
|
+
const providerLabel = getProviderLabel(provider);
|
|
817
|
+
const promptText = `Enter your ${providerLabel} API key (input hidden): `;
|
|
818
|
+
const visiblePromptText = `Enter your ${providerLabel} API key: `;
|
|
672
819
|
while (true) {
|
|
673
|
-
const answer = (await (io.secret ? io.secret(
|
|
820
|
+
const answer = (await (io.secret ? io.secret(promptText) : io.ask(visiblePromptText))).trim();
|
|
674
821
|
if (answer.length > 0) {
|
|
675
822
|
return answer;
|
|
676
823
|
}
|
|
677
824
|
io.error("API key cannot be empty.\n");
|
|
678
825
|
}
|
|
679
826
|
}
|
|
680
|
-
async function
|
|
827
|
+
async function promptForApiKeyChoice(args) {
|
|
828
|
+
const providerLabel = getProviderLabel(args.provider);
|
|
829
|
+
if (!args.hasSavedKey && !args.hasEnvKey) {
|
|
830
|
+
return "override";
|
|
831
|
+
}
|
|
832
|
+
if (args.hasSavedKey && args.hasEnvKey) {
|
|
833
|
+
if (args.io.select) {
|
|
834
|
+
const choice = await args.io.select(
|
|
835
|
+
`Found both a saved ${providerLabel} API key and ${args.envName} in your environment`,
|
|
836
|
+
["Use saved key", "Use env key", "Override"]
|
|
837
|
+
);
|
|
838
|
+
if (choice === "Use saved key") {
|
|
839
|
+
return "saved";
|
|
840
|
+
}
|
|
841
|
+
if (choice === "Use env key") {
|
|
842
|
+
return "env";
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
while (true) {
|
|
846
|
+
const answer = (await args.io.ask("API key choice [saved/env/override]: ")).trim().toLowerCase();
|
|
847
|
+
if (answer === "" || answer === "saved") {
|
|
848
|
+
return "saved";
|
|
849
|
+
}
|
|
850
|
+
if (answer === "env") {
|
|
851
|
+
return "env";
|
|
852
|
+
}
|
|
853
|
+
if (answer === "override") {
|
|
854
|
+
return "override";
|
|
855
|
+
}
|
|
856
|
+
args.io.error("Please answer saved, env, or override.\n");
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const sourceLabel = args.hasSavedKey ? "saved key" : `${args.envName} from your environment`;
|
|
860
|
+
if (args.io.select) {
|
|
861
|
+
const choice = await args.io.select(
|
|
862
|
+
`Found an existing ${providerLabel} API key via ${sourceLabel}`,
|
|
863
|
+
["Use existing key", "Override"]
|
|
864
|
+
);
|
|
865
|
+
if (choice === "Override") {
|
|
866
|
+
return "override";
|
|
867
|
+
}
|
|
868
|
+
return args.hasSavedKey ? "saved" : "env";
|
|
869
|
+
}
|
|
681
870
|
while (true) {
|
|
682
|
-
const answer = (await io.ask(
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (answer === "" || answer === "n" || answer === "no") {
|
|
686
|
-
return false;
|
|
871
|
+
const answer = (await args.io.ask("API key choice [existing/override]: ")).trim().toLowerCase();
|
|
872
|
+
if (answer === "" || answer === "existing") {
|
|
873
|
+
return args.hasSavedKey ? "saved" : "env";
|
|
687
874
|
}
|
|
688
|
-
if (answer === "
|
|
689
|
-
return
|
|
875
|
+
if (answer === "override") {
|
|
876
|
+
return "override";
|
|
690
877
|
}
|
|
691
|
-
io.error("Please answer
|
|
878
|
+
args.io.error("Please answer existing or override.\n");
|
|
692
879
|
}
|
|
693
880
|
}
|
|
694
881
|
function writeSetupSuccess(io, writtenPath) {
|
|
@@ -722,10 +909,86 @@ ${ui.section("Try next")}
|
|
|
722
909
|
io.write(` ${ui.command("sift exec --preset test-status -- npm test")}
|
|
723
910
|
`);
|
|
724
911
|
}
|
|
912
|
+
function writeProviderDefaults(io, provider) {
|
|
913
|
+
const ui = getSetupPresenter(io);
|
|
914
|
+
if (provider === "openrouter") {
|
|
915
|
+
io.write(`${ui.info("Using OpenRouter defaults for your first run.")}
|
|
916
|
+
`);
|
|
917
|
+
io.write(`${ui.labelValue("Default model", "openrouter/free")}
|
|
918
|
+
`);
|
|
919
|
+
io.write(`${ui.labelValue("Default base URL", "https://openrouter.ai/api/v1")}
|
|
920
|
+
`);
|
|
921
|
+
} else {
|
|
922
|
+
io.write(`${ui.info("Using OpenAI defaults for your first run.")}
|
|
923
|
+
`);
|
|
924
|
+
io.write(`${ui.labelValue("Default model", "gpt-5-nano")}
|
|
925
|
+
`);
|
|
926
|
+
io.write(`${ui.labelValue("Default base URL", "https://api.openai.com/v1")}
|
|
927
|
+
`);
|
|
928
|
+
}
|
|
929
|
+
io.write(
|
|
930
|
+
`${ui.note("Want to switch providers later? Run 'sift config use openai' or 'sift config use openrouter'.")}
|
|
931
|
+
`
|
|
932
|
+
);
|
|
933
|
+
io.write(
|
|
934
|
+
`${ui.note("Want to inspect the active values first? Run 'sift config show --show-secrets'.")}
|
|
935
|
+
`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
function materializeProfile(provider, profile, apiKey) {
|
|
939
|
+
return {
|
|
940
|
+
...getProfileProviderState(provider, profile),
|
|
941
|
+
...apiKey !== void 0 ? { apiKey } : {}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function buildSetupConfig(args) {
|
|
945
|
+
const preservedConfig = preserveActiveNativeProviderProfile(args.config);
|
|
946
|
+
const storedProfile = getStoredProviderProfile(preservedConfig, args.provider);
|
|
947
|
+
if (args.apiKeyChoice === "saved") {
|
|
948
|
+
const profile2 = materializeProfile(
|
|
949
|
+
args.provider,
|
|
950
|
+
storedProfile,
|
|
951
|
+
storedProfile?.apiKey ?? ""
|
|
952
|
+
);
|
|
953
|
+
const configWithProfile2 = setStoredProviderProfile(
|
|
954
|
+
preservedConfig,
|
|
955
|
+
args.provider,
|
|
956
|
+
profile2
|
|
957
|
+
);
|
|
958
|
+
return applyActiveProvider(
|
|
959
|
+
configWithProfile2,
|
|
960
|
+
args.provider,
|
|
961
|
+
profile2,
|
|
962
|
+
profile2.apiKey ?? ""
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
if (args.apiKeyChoice === "env") {
|
|
966
|
+
const profile2 = storedProfile ? storedProfile : materializeProfile(args.provider, void 0);
|
|
967
|
+
const configWithProfile2 = storedProfile ? preservedConfig : setStoredProviderProfile(preservedConfig, args.provider, profile2);
|
|
968
|
+
return applyActiveProvider(configWithProfile2, args.provider, profile2, "");
|
|
969
|
+
}
|
|
970
|
+
const profile = materializeProfile(
|
|
971
|
+
args.provider,
|
|
972
|
+
storedProfile,
|
|
973
|
+
args.nextApiKey ?? ""
|
|
974
|
+
);
|
|
975
|
+
const configWithProfile = setStoredProviderProfile(
|
|
976
|
+
preservedConfig,
|
|
977
|
+
args.provider,
|
|
978
|
+
profile
|
|
979
|
+
);
|
|
980
|
+
return applyActiveProvider(
|
|
981
|
+
configWithProfile,
|
|
982
|
+
args.provider,
|
|
983
|
+
profile,
|
|
984
|
+
args.nextApiKey ?? ""
|
|
985
|
+
);
|
|
986
|
+
}
|
|
725
987
|
async function configSetup(options = {}) {
|
|
726
988
|
void options.global;
|
|
727
989
|
const io = options.io ?? createTerminalIO();
|
|
728
990
|
const ui = getSetupPresenter(io);
|
|
991
|
+
const env = options.env ?? process.env;
|
|
729
992
|
try {
|
|
730
993
|
if (!io.stdinIsTTY || !io.stdoutIsTTY) {
|
|
731
994
|
io.error(
|
|
@@ -736,39 +999,43 @@ async function configSetup(options = {}) {
|
|
|
736
999
|
io.write(`${ui.welcome("Let's keep the expensive model for the interesting bits.")}
|
|
737
1000
|
`);
|
|
738
1001
|
const resolvedPath = resolveSetupPath(options.targetPath);
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
io.write(`${ui.note("Aborted.")}
|
|
1002
|
+
const { config: existingConfig, existed } = loadEditableConfig(resolvedPath);
|
|
1003
|
+
if (existed) {
|
|
1004
|
+
io.write(`${ui.info(`Updating existing config at ${resolvedPath}.`)}
|
|
743
1005
|
`);
|
|
744
|
-
return 1;
|
|
745
|
-
}
|
|
746
1006
|
}
|
|
747
|
-
await promptForProvider(io);
|
|
748
|
-
io
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
);
|
|
758
|
-
io
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1007
|
+
const provider = await promptForProvider(io);
|
|
1008
|
+
writeProviderDefaults(io, provider);
|
|
1009
|
+
const storedProfile = getStoredProviderProfile(existingConfig, provider);
|
|
1010
|
+
const envName = getNativeProviderApiKeyEnvName(provider);
|
|
1011
|
+
const apiKeyChoice = await promptForApiKeyChoice({
|
|
1012
|
+
io,
|
|
1013
|
+
provider,
|
|
1014
|
+
envName,
|
|
1015
|
+
hasSavedKey: Boolean(storedProfile?.apiKey),
|
|
1016
|
+
hasEnvKey: Boolean(env[envName])
|
|
1017
|
+
});
|
|
1018
|
+
const nextApiKey = apiKeyChoice === "override" ? await promptForApiKey(io, provider) : void 0;
|
|
1019
|
+
const config = buildSetupConfig({
|
|
1020
|
+
config: existingConfig,
|
|
1021
|
+
provider,
|
|
1022
|
+
apiKeyChoice,
|
|
1023
|
+
nextApiKey
|
|
1024
|
+
});
|
|
764
1025
|
const writtenPath = writeConfigFile({
|
|
765
1026
|
targetPath: resolvedPath,
|
|
766
1027
|
config,
|
|
767
|
-
overwrite:
|
|
1028
|
+
overwrite: existed
|
|
768
1029
|
});
|
|
1030
|
+
if (apiKeyChoice === "env") {
|
|
1031
|
+
io.write(
|
|
1032
|
+
`${ui.note(`Using ${envName} from the environment. No API key was written to config.`)}
|
|
1033
|
+
`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
769
1036
|
writeSetupSuccess(io, writtenPath);
|
|
770
1037
|
const activeConfigPath = findConfigPath();
|
|
771
|
-
if (activeConfigPath &&
|
|
1038
|
+
if (activeConfigPath && path5.resolve(activeConfigPath) !== path5.resolve(writtenPath)) {
|
|
772
1039
|
writeOverrideWarning(io, activeConfigPath);
|
|
773
1040
|
}
|
|
774
1041
|
writeNextSteps(io);
|
|
@@ -798,18 +1065,18 @@ function maskConfigSecrets(value) {
|
|
|
798
1065
|
return output;
|
|
799
1066
|
}
|
|
800
1067
|
function configInit(targetPath, global = false) {
|
|
801
|
-
const
|
|
1068
|
+
const path8 = writeExampleConfig({
|
|
802
1069
|
targetPath,
|
|
803
1070
|
global
|
|
804
1071
|
});
|
|
805
1072
|
if (!process.stdout.isTTY) {
|
|
806
|
-
process.stdout.write(`${
|
|
1073
|
+
process.stdout.write(`${path8}
|
|
807
1074
|
`);
|
|
808
1075
|
return;
|
|
809
1076
|
}
|
|
810
1077
|
const ui = createPresentation(true);
|
|
811
1078
|
process.stdout.write(
|
|
812
|
-
`${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${
|
|
1079
|
+
`${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path8}`)}
|
|
813
1080
|
`
|
|
814
1081
|
);
|
|
815
1082
|
}
|
|
@@ -838,11 +1105,61 @@ function configValidate(configPath) {
|
|
|
838
1105
|
process.stdout.write(`${ui.success(message)}
|
|
839
1106
|
`);
|
|
840
1107
|
}
|
|
1108
|
+
function isNativeProviderName(value) {
|
|
1109
|
+
return value === "openai" || value === "openrouter";
|
|
1110
|
+
}
|
|
1111
|
+
function configUse(provider, configPath, env = process.env) {
|
|
1112
|
+
if (!isNativeProviderName(provider)) {
|
|
1113
|
+
throw new Error(`Unsupported config provider: ${provider}`);
|
|
1114
|
+
}
|
|
1115
|
+
const { config, existed, resolvedPath } = loadEditableConfig(configPath);
|
|
1116
|
+
const preservedConfig = preserveActiveNativeProviderProfile(config);
|
|
1117
|
+
const storedProfile = getStoredProviderProfile(preservedConfig, provider);
|
|
1118
|
+
const envName = getNativeProviderApiKeyEnvName(provider);
|
|
1119
|
+
const envKey = env[envName];
|
|
1120
|
+
if (!storedProfile?.apiKey && !envKey) {
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
`No saved ${provider} API key or ${envName} found. Run 'sift config setup' first.`
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
const nextConfig = applyActiveProvider(
|
|
1126
|
+
preservedConfig,
|
|
1127
|
+
provider,
|
|
1128
|
+
storedProfile,
|
|
1129
|
+
storedProfile?.apiKey ?? ""
|
|
1130
|
+
);
|
|
1131
|
+
writeConfigFile({
|
|
1132
|
+
targetPath: resolvedPath,
|
|
1133
|
+
config: nextConfig,
|
|
1134
|
+
overwrite: existed
|
|
1135
|
+
});
|
|
1136
|
+
const message = `Switched active provider to ${provider} (${resolvedPath}).`;
|
|
1137
|
+
if (!process.stdout.isTTY) {
|
|
1138
|
+
process.stdout.write(`${message}
|
|
1139
|
+
`);
|
|
1140
|
+
if (!storedProfile?.apiKey && envKey) {
|
|
1141
|
+
process.stdout.write(
|
|
1142
|
+
`Using ${envName} from the environment. No API key was written to config.
|
|
1143
|
+
`
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const ui = createPresentation(true);
|
|
1149
|
+
process.stdout.write(`${ui.success(message)}
|
|
1150
|
+
`);
|
|
1151
|
+
if (!storedProfile?.apiKey && envKey) {
|
|
1152
|
+
process.stdout.write(
|
|
1153
|
+
`${ui.note(`Using ${envName} from the environment. No API key was written to config.`)}
|
|
1154
|
+
`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
841
1158
|
|
|
842
1159
|
// src/commands/agent.ts
|
|
843
1160
|
import fs4 from "fs";
|
|
844
1161
|
import os2 from "os";
|
|
845
|
-
import
|
|
1162
|
+
import path6 from "path";
|
|
846
1163
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
847
1164
|
import { stderr as defaultStderr2, stdin as defaultStdin3, stdout as defaultStdout2 } from "process";
|
|
848
1165
|
var AGENT_FILENAMES = {
|
|
@@ -907,14 +1224,14 @@ function normalizeAgentScope(value) {
|
|
|
907
1224
|
}
|
|
908
1225
|
function resolveAgentTargetPath(args) {
|
|
909
1226
|
if (args.targetPath) {
|
|
910
|
-
return
|
|
1227
|
+
return path6.resolve(args.cwd ?? process.cwd(), args.targetPath);
|
|
911
1228
|
}
|
|
912
1229
|
const scope = args.scope ?? "repo";
|
|
913
1230
|
if (scope === "global") {
|
|
914
1231
|
const homeDir = args.homeDir ?? os2.homedir();
|
|
915
1232
|
return args.agent === "codex" ? getDefaultCodexGlobalInstructionsPath(homeDir) : getDefaultClaudeGlobalInstructionsPath(homeDir);
|
|
916
1233
|
}
|
|
917
|
-
return
|
|
1234
|
+
return path6.resolve(args.cwd ?? process.cwd(), AGENT_FILENAMES[args.agent]);
|
|
918
1235
|
}
|
|
919
1236
|
function getManagedBlockMarkers(agent) {
|
|
920
1237
|
return {
|
|
@@ -949,6 +1266,7 @@ function renderInstructionBody() {
|
|
|
949
1266
|
"- Start with `standard` text. Use diagnose JSON only when automation or machine branching truly needs it.",
|
|
950
1267
|
"- If `standard` already shows bucket-level root cause, anchor, and fix lines, trust it and report from it directly.",
|
|
951
1268
|
"- In that case, do not re-verify the same bucket with raw pytest; at most do one targeted source read before you edit.",
|
|
1269
|
+
"- If `standard` still contains an unknown bucket or ends with `Decision: zoom`, do one deeper sift pass before raw traceback.",
|
|
952
1270
|
"- If you need a machine-readable diagnosis, use `sift exec --preset test-status --goal diagnose --format json -- <test command>` or the same shape with `sift rerun` / `sift watch --preset test-status`.",
|
|
953
1271
|
"- Diagnose JSON is summary-first by default. Add `--include-test-ids` only when you truly need the raw failing test IDs.",
|
|
954
1272
|
"- If diagnose JSON returns `read_targets.context_hint.start_line/end_line`, read only that small line range first.",
|
|
@@ -1478,7 +1796,7 @@ function readOptionalFile(targetPath) {
|
|
|
1478
1796
|
return fs4.readFileSync(targetPath, "utf8");
|
|
1479
1797
|
}
|
|
1480
1798
|
function writeTextFileAtomic(targetPath, content) {
|
|
1481
|
-
fs4.mkdirSync(
|
|
1799
|
+
fs4.mkdirSync(path6.dirname(targetPath), { recursive: true });
|
|
1482
1800
|
const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
1483
1801
|
fs4.writeFileSync(tempPath, content, "utf8");
|
|
1484
1802
|
fs4.renameSync(tempPath, targetPath);
|
|
@@ -1512,7 +1830,7 @@ function runDoctor(config, configPath) {
|
|
|
1512
1830
|
if (!config.provider.model) {
|
|
1513
1831
|
problems.push("Missing provider.model");
|
|
1514
1832
|
}
|
|
1515
|
-
if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible") && !config.provider.apiKey) {
|
|
1833
|
+
if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible" || config.provider.provider === "openrouter") && !config.provider.apiKey) {
|
|
1516
1834
|
problems.push("Missing provider.apiKey");
|
|
1517
1835
|
problems.push(
|
|
1518
1836
|
`Set one of: ${getProviderApiKeyEnvNames(
|
|
@@ -1714,10 +2032,11 @@ async function buildOpenAICompatibleError(response) {
|
|
|
1714
2032
|
return new Error(detail);
|
|
1715
2033
|
}
|
|
1716
2034
|
var OpenAICompatibleProvider = class {
|
|
1717
|
-
name
|
|
2035
|
+
name;
|
|
1718
2036
|
baseUrl;
|
|
1719
2037
|
apiKey;
|
|
1720
2038
|
constructor(options) {
|
|
2039
|
+
this.name = options.name ?? "openai-compatible";
|
|
1721
2040
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
1722
2041
|
this.apiKey = options.apiKey;
|
|
1723
2042
|
}
|
|
@@ -1793,13 +2112,20 @@ function createProvider(config) {
|
|
|
1793
2112
|
apiKey: config.provider.apiKey
|
|
1794
2113
|
});
|
|
1795
2114
|
}
|
|
2115
|
+
if (config.provider.provider === "openrouter") {
|
|
2116
|
+
return new OpenAICompatibleProvider({
|
|
2117
|
+
baseUrl: config.provider.baseUrl,
|
|
2118
|
+
apiKey: config.provider.apiKey,
|
|
2119
|
+
name: "openrouter"
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
1796
2122
|
throw new Error(`Unsupported provider: ${config.provider.provider}`);
|
|
1797
2123
|
}
|
|
1798
2124
|
|
|
1799
2125
|
// src/core/testStatusDecision.ts
|
|
1800
2126
|
import { z as z2 } from "zod";
|
|
1801
2127
|
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[]}';
|
|
1802
|
-
var TEST_STATUS_PROVIDER_SUPPLEMENT_JSON_CONTRACT = '{"diagnosis_complete":boolean,"raw_needed":boolean,"additional_source_read_likely_low_value":boolean,"read_raw_only_if":string|null,"decision":"stop|zoom|read_source|read_raw","provider_confidence":number|null,"next_best_action":{"code":"fix_dominant_blocker|read_source_for_bucket|read_raw_for_exact_traceback|insufficient_signal","bucket_index":number|null,"note":string}}';
|
|
2128
|
+
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}}';
|
|
1803
2129
|
var nextBestActionSchema = z2.object({
|
|
1804
2130
|
code: z2.enum([
|
|
1805
2131
|
"fix_dominant_blocker",
|
|
@@ -1817,6 +2143,20 @@ var testStatusProviderSupplementSchema = z2.object({
|
|
|
1817
2143
|
read_raw_only_if: z2.string().nullable(),
|
|
1818
2144
|
decision: z2.enum(["stop", "zoom", "read_source", "read_raw"]),
|
|
1819
2145
|
provider_confidence: z2.number().min(0).max(1).nullable(),
|
|
2146
|
+
bucket_supplements: z2.array(
|
|
2147
|
+
z2.object({
|
|
2148
|
+
label: z2.string().min(1),
|
|
2149
|
+
count: z2.number().int().positive(),
|
|
2150
|
+
root_cause: z2.string().min(1),
|
|
2151
|
+
anchor: z2.object({
|
|
2152
|
+
file: z2.string().nullable(),
|
|
2153
|
+
line: z2.number().int().nullable(),
|
|
2154
|
+
search_hint: z2.string().nullable()
|
|
2155
|
+
}),
|
|
2156
|
+
fix_hint: z2.string().nullable(),
|
|
2157
|
+
confidence: z2.number().min(0).max(1)
|
|
2158
|
+
})
|
|
2159
|
+
).max(2),
|
|
1820
2160
|
next_best_action: nextBestActionSchema
|
|
1821
2161
|
});
|
|
1822
2162
|
var testStatusDiagnoseContractSchema = z2.object({
|
|
@@ -1889,6 +2229,209 @@ var testStatusPublicDiagnoseContractSchema = testStatusDiagnoseContractSchema.om
|
|
|
1889
2229
|
function parseTestStatusProviderSupplement(input) {
|
|
1890
2230
|
return testStatusProviderSupplementSchema.parse(JSON.parse(input));
|
|
1891
2231
|
}
|
|
2232
|
+
var extendedBucketSpecs = [
|
|
2233
|
+
{
|
|
2234
|
+
prefix: "snapshot mismatch:",
|
|
2235
|
+
type: "snapshot_mismatch",
|
|
2236
|
+
label: "snapshot mismatch",
|
|
2237
|
+
genericTitle: "Snapshot mismatches",
|
|
2238
|
+
defaultCoverage: "failed",
|
|
2239
|
+
rootCauseConfidence: 0.84,
|
|
2240
|
+
why: "it contains the failing snapshot expectation behind this bucket",
|
|
2241
|
+
fix: "Update the snapshots if these output changes are intentional, then rerun the suite."
|
|
2242
|
+
},
|
|
2243
|
+
{
|
|
2244
|
+
prefix: "timeout:",
|
|
2245
|
+
type: "timeout_failure",
|
|
2246
|
+
label: "timeout",
|
|
2247
|
+
genericTitle: "Timeout failures",
|
|
2248
|
+
defaultCoverage: "mixed",
|
|
2249
|
+
rootCauseConfidence: 0.9,
|
|
2250
|
+
why: "it contains the test or fixture that exceeded the timeout threshold",
|
|
2251
|
+
fix: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning."
|
|
2252
|
+
},
|
|
2253
|
+
{
|
|
2254
|
+
prefix: "permission:",
|
|
2255
|
+
type: "permission_denied_failure",
|
|
2256
|
+
label: "permission denied",
|
|
2257
|
+
genericTitle: "Permission failures",
|
|
2258
|
+
defaultCoverage: "error",
|
|
2259
|
+
rootCauseConfidence: 0.85,
|
|
2260
|
+
why: "it contains the file, socket, or port access that was denied",
|
|
2261
|
+
fix: "Check file or port permissions in the CI environment before rerunning."
|
|
2262
|
+
},
|
|
2263
|
+
{
|
|
2264
|
+
prefix: "async loop:",
|
|
2265
|
+
type: "async_event_loop_failure",
|
|
2266
|
+
label: "async event loop",
|
|
2267
|
+
genericTitle: "Async event loop failures",
|
|
2268
|
+
defaultCoverage: "mixed",
|
|
2269
|
+
rootCauseConfidence: 0.88,
|
|
2270
|
+
why: "it contains the async setup or coroutine that caused the event loop error",
|
|
2271
|
+
fix: "Check event loop scope and pytest-asyncio configuration before rerunning."
|
|
2272
|
+
},
|
|
2273
|
+
{
|
|
2274
|
+
prefix: "fixture teardown:",
|
|
2275
|
+
type: "fixture_teardown_failure",
|
|
2276
|
+
label: "fixture teardown",
|
|
2277
|
+
genericTitle: "Fixture teardown failures",
|
|
2278
|
+
defaultCoverage: "error",
|
|
2279
|
+
rootCauseConfidence: 0.85,
|
|
2280
|
+
why: "it contains the fixture teardown path that failed after the test body completed",
|
|
2281
|
+
fix: "Inspect the teardown cleanup path and restore idempotent fixture cleanup before rerunning."
|
|
2282
|
+
},
|
|
2283
|
+
{
|
|
2284
|
+
prefix: "db migration:",
|
|
2285
|
+
type: "db_migration_failure",
|
|
2286
|
+
label: "db migration",
|
|
2287
|
+
genericTitle: "DB migration failures",
|
|
2288
|
+
defaultCoverage: "error",
|
|
2289
|
+
rootCauseConfidence: 0.9,
|
|
2290
|
+
why: "it contains the migration or model definition behind the missing table or relation",
|
|
2291
|
+
fix: "Run pending migrations or fix the expected model schema before rerunning."
|
|
2292
|
+
},
|
|
2293
|
+
{
|
|
2294
|
+
prefix: "configuration:",
|
|
2295
|
+
type: "configuration_error",
|
|
2296
|
+
label: "configuration error",
|
|
2297
|
+
genericTitle: "Configuration errors",
|
|
2298
|
+
defaultCoverage: "error",
|
|
2299
|
+
rootCauseConfidence: 0.95,
|
|
2300
|
+
dominantPriority: 4,
|
|
2301
|
+
dominantBlocker: true,
|
|
2302
|
+
why: "it contains the pytest configuration or conftest setup error that blocks the run",
|
|
2303
|
+
fix: "Fix the pytest configuration, CLI usage, or conftest import error before rerunning."
|
|
2304
|
+
},
|
|
2305
|
+
{
|
|
2306
|
+
prefix: "xdist worker crash:",
|
|
2307
|
+
type: "xdist_worker_crash",
|
|
2308
|
+
label: "xdist worker crash",
|
|
2309
|
+
genericTitle: "xdist worker crashes",
|
|
2310
|
+
defaultCoverage: "error",
|
|
2311
|
+
rootCauseConfidence: 0.92,
|
|
2312
|
+
dominantPriority: 3,
|
|
2313
|
+
why: "it contains the worker startup or shared-state path that crashed an xdist worker",
|
|
2314
|
+
fix: "Check shared state, worker startup hooks, or resource contention between workers before rerunning."
|
|
2315
|
+
},
|
|
2316
|
+
{
|
|
2317
|
+
prefix: "type error:",
|
|
2318
|
+
type: "type_error_failure",
|
|
2319
|
+
label: "type error",
|
|
2320
|
+
genericTitle: "Type errors",
|
|
2321
|
+
defaultCoverage: "mixed",
|
|
2322
|
+
rootCauseConfidence: 0.8,
|
|
2323
|
+
why: "it contains the call site or fixture value that triggered the type error",
|
|
2324
|
+
fix: "Inspect the mismatched argument or object shape and rerun the full suite at standard."
|
|
2325
|
+
},
|
|
2326
|
+
{
|
|
2327
|
+
prefix: "resource leak:",
|
|
2328
|
+
type: "resource_leak_warning",
|
|
2329
|
+
label: "resource leak",
|
|
2330
|
+
genericTitle: "Resource leak warnings",
|
|
2331
|
+
defaultCoverage: "mixed",
|
|
2332
|
+
rootCauseConfidence: 0.74,
|
|
2333
|
+
why: "it contains the warning source behind the leaked file, socket, or coroutine",
|
|
2334
|
+
fix: "Close the leaked resource or suppress the warning only if the cleanup is intentional."
|
|
2335
|
+
},
|
|
2336
|
+
{
|
|
2337
|
+
prefix: "django db access:",
|
|
2338
|
+
type: "django_db_access_denied",
|
|
2339
|
+
label: "django db access",
|
|
2340
|
+
genericTitle: "Django DB access failures",
|
|
2341
|
+
defaultCoverage: "error",
|
|
2342
|
+
rootCauseConfidence: 0.95,
|
|
2343
|
+
why: "it needs the @pytest.mark.django_db decorator or fixture permission to access the database",
|
|
2344
|
+
fix: "Add @pytest.mark.django_db to the test or class before rerunning."
|
|
2345
|
+
},
|
|
2346
|
+
{
|
|
2347
|
+
prefix: "network:",
|
|
2348
|
+
type: "network_failure",
|
|
2349
|
+
label: "network failure",
|
|
2350
|
+
genericTitle: "Network failures",
|
|
2351
|
+
defaultCoverage: "error",
|
|
2352
|
+
rootCauseConfidence: 0.88,
|
|
2353
|
+
dominantPriority: 2,
|
|
2354
|
+
why: "it contains the host, URL, or TLS path behind the network failure",
|
|
2355
|
+
fix: "Check DNS, outbound network access, retries, or TLS trust before rerunning."
|
|
2356
|
+
},
|
|
2357
|
+
{
|
|
2358
|
+
prefix: "segfault:",
|
|
2359
|
+
type: "subprocess_crash_segfault",
|
|
2360
|
+
label: "segfault",
|
|
2361
|
+
genericTitle: "Segfault crashes",
|
|
2362
|
+
defaultCoverage: "mixed",
|
|
2363
|
+
rootCauseConfidence: 0.8,
|
|
2364
|
+
why: "it contains the subprocess or native extension path that crashed with SIGSEGV",
|
|
2365
|
+
fix: "Inspect the native extension, subprocess boundary, or incompatible binary before rerunning."
|
|
2366
|
+
},
|
|
2367
|
+
{
|
|
2368
|
+
prefix: "flaky:",
|
|
2369
|
+
type: "flaky_test_detected",
|
|
2370
|
+
label: "flaky test",
|
|
2371
|
+
genericTitle: "Flaky test detections",
|
|
2372
|
+
defaultCoverage: "mixed",
|
|
2373
|
+
rootCauseConfidence: 0.72,
|
|
2374
|
+
why: "it contains the rerun-prone test that behaved inconsistently across attempts",
|
|
2375
|
+
fix: "Stabilize the nondeterministic test or fixture before relying on reruns."
|
|
2376
|
+
},
|
|
2377
|
+
{
|
|
2378
|
+
prefix: "serialization:",
|
|
2379
|
+
type: "serialization_encoding_failure",
|
|
2380
|
+
label: "serialization or encoding",
|
|
2381
|
+
genericTitle: "Serialization or encoding failures",
|
|
2382
|
+
defaultCoverage: "mixed",
|
|
2383
|
+
rootCauseConfidence: 0.78,
|
|
2384
|
+
why: "it contains the serialization or decoding path behind the malformed payload",
|
|
2385
|
+
fix: "Inspect the encoded payload, serializer, or fixture data before rerunning."
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
prefix: "file not found:",
|
|
2389
|
+
type: "file_not_found_failure",
|
|
2390
|
+
label: "file not found",
|
|
2391
|
+
genericTitle: "Missing file failures",
|
|
2392
|
+
defaultCoverage: "mixed",
|
|
2393
|
+
rootCauseConfidence: 0.82,
|
|
2394
|
+
why: "it contains the missing file path or fixture artifact required by the test",
|
|
2395
|
+
fix: "Restore the missing file, fixture artifact, or working-directory assumption before rerunning."
|
|
2396
|
+
},
|
|
2397
|
+
{
|
|
2398
|
+
prefix: "memory:",
|
|
2399
|
+
type: "memory_error",
|
|
2400
|
+
label: "memory error",
|
|
2401
|
+
genericTitle: "Memory failures",
|
|
2402
|
+
defaultCoverage: "mixed",
|
|
2403
|
+
rootCauseConfidence: 0.78,
|
|
2404
|
+
why: "it contains the allocation path that exhausted available memory",
|
|
2405
|
+
fix: "Reduce memory pressure or investigate the large allocation before rerunning."
|
|
2406
|
+
},
|
|
2407
|
+
{
|
|
2408
|
+
prefix: "deprecation as error:",
|
|
2409
|
+
type: "deprecation_warning_as_error",
|
|
2410
|
+
label: "deprecation as error",
|
|
2411
|
+
genericTitle: "Deprecation warnings as errors",
|
|
2412
|
+
defaultCoverage: "mixed",
|
|
2413
|
+
rootCauseConfidence: 0.74,
|
|
2414
|
+
why: "it contains the deprecated API or warning filter that is failing the test run",
|
|
2415
|
+
fix: "Update the deprecated call site or relax the warning policy only if that is intentional."
|
|
2416
|
+
},
|
|
2417
|
+
{
|
|
2418
|
+
prefix: "xfail strict:",
|
|
2419
|
+
type: "xfail_strict_unexpected_pass",
|
|
2420
|
+
label: "strict xfail unexpected pass",
|
|
2421
|
+
genericTitle: "Strict xfail unexpected passes",
|
|
2422
|
+
defaultCoverage: "failed",
|
|
2423
|
+
rootCauseConfidence: 0.78,
|
|
2424
|
+
why: "it contains the strict xfail case that unexpectedly passed",
|
|
2425
|
+
fix: "Remove or update the strict xfail expectation if the test is now passing intentionally."
|
|
2426
|
+
}
|
|
2427
|
+
];
|
|
2428
|
+
function findExtendedBucketSpec(reason) {
|
|
2429
|
+
return extendedBucketSpecs.find((spec) => reason.startsWith(spec.prefix)) ?? null;
|
|
2430
|
+
}
|
|
2431
|
+
function extractReasonDetail(reason, prefix) {
|
|
2432
|
+
const detail = reason.slice(prefix.length).trim();
|
|
2433
|
+
return detail.length > 0 ? detail : null;
|
|
2434
|
+
}
|
|
1892
2435
|
function formatCount(count, singular, plural = `${singular}s`) {
|
|
1893
2436
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
1894
2437
|
}
|
|
@@ -1942,6 +2485,10 @@ function formatTargetSummary(summary) {
|
|
|
1942
2485
|
return `count=${summary.count}; families=${families}`;
|
|
1943
2486
|
}
|
|
1944
2487
|
function classifyGenericBucketType(reason) {
|
|
2488
|
+
const extended = findExtendedBucketSpec(reason);
|
|
2489
|
+
if (extended) {
|
|
2490
|
+
return extended.type;
|
|
2491
|
+
}
|
|
1945
2492
|
if (reason.startsWith("missing test env:")) {
|
|
1946
2493
|
return "shared_environment_blocker";
|
|
1947
2494
|
}
|
|
@@ -1968,14 +2515,77 @@ function classifyGenericBucketType(reason) {
|
|
|
1968
2515
|
}
|
|
1969
2516
|
return "unknown_failure";
|
|
1970
2517
|
}
|
|
2518
|
+
function isUnknownBucket(bucket) {
|
|
2519
|
+
return bucket.source === "unknown" || bucket.reason.startsWith("unknown ");
|
|
2520
|
+
}
|
|
2521
|
+
function classifyVisibleStatusForLabel(args) {
|
|
2522
|
+
const isError = args.errorLabels.has(args.label);
|
|
2523
|
+
const isFailed = args.failedLabels.has(args.label);
|
|
2524
|
+
if (isError && isFailed) {
|
|
2525
|
+
return "mixed";
|
|
2526
|
+
}
|
|
2527
|
+
if (isError) {
|
|
2528
|
+
return "error";
|
|
2529
|
+
}
|
|
2530
|
+
if (isFailed) {
|
|
2531
|
+
return "failed";
|
|
2532
|
+
}
|
|
2533
|
+
return "unknown";
|
|
2534
|
+
}
|
|
2535
|
+
function inferCoverageFromReason(reason) {
|
|
2536
|
+
const extended = findExtendedBucketSpec(reason);
|
|
2537
|
+
if (extended) {
|
|
2538
|
+
return extended.defaultCoverage;
|
|
2539
|
+
}
|
|
2540
|
+
if (reason.startsWith("missing test env:") || reason.startsWith("fixture guard:") || reason.startsWith("service unavailable:") || reason.startsWith("db refused:") || reason.startsWith("auth bypass absent:") || reason.startsWith("missing module:")) {
|
|
2541
|
+
return "error";
|
|
2542
|
+
}
|
|
2543
|
+
if (reason.startsWith("assertion failed:")) {
|
|
2544
|
+
return "failed";
|
|
2545
|
+
}
|
|
2546
|
+
return "mixed";
|
|
2547
|
+
}
|
|
2548
|
+
function buildCoverageCounts(args) {
|
|
2549
|
+
if (args.coverageKind === "error") {
|
|
2550
|
+
return {
|
|
2551
|
+
error: args.count,
|
|
2552
|
+
failed: 0
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
if (args.coverageKind === "failed") {
|
|
2556
|
+
return {
|
|
2557
|
+
error: 0,
|
|
2558
|
+
failed: args.count
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
return {
|
|
2562
|
+
error: 0,
|
|
2563
|
+
failed: 0
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
1971
2566
|
function buildGenericBuckets(analysis) {
|
|
1972
2567
|
const buckets = [];
|
|
1973
2568
|
const grouped = /* @__PURE__ */ new Map();
|
|
2569
|
+
const errorLabels = new Set(analysis.visibleErrorLabels);
|
|
2570
|
+
const failedLabels = new Set(analysis.visibleFailedLabels);
|
|
1974
2571
|
const push = (reason, item) => {
|
|
1975
|
-
const
|
|
2572
|
+
const coverageKind = (() => {
|
|
2573
|
+
const status = classifyVisibleStatusForLabel({
|
|
2574
|
+
label: item.label,
|
|
2575
|
+
errorLabels,
|
|
2576
|
+
failedLabels
|
|
2577
|
+
});
|
|
2578
|
+
return status === "unknown" ? inferCoverageFromReason(reason) : status;
|
|
2579
|
+
})();
|
|
2580
|
+
const key = `${classifyGenericBucketType(reason)}:${coverageKind}:${reason}`;
|
|
1976
2581
|
const existing = grouped.get(key);
|
|
1977
2582
|
if (existing) {
|
|
1978
2583
|
existing.count += 1;
|
|
2584
|
+
if (coverageKind === "error") {
|
|
2585
|
+
existing.coverage.error += 1;
|
|
2586
|
+
} else if (coverageKind === "failed") {
|
|
2587
|
+
existing.coverage.failed += 1;
|
|
2588
|
+
}
|
|
1979
2589
|
if (!existing.representativeItems.some((entry) => entry.label === item.label) && existing.representativeItems.length < 6) {
|
|
1980
2590
|
existing.representativeItems.push(item);
|
|
1981
2591
|
}
|
|
@@ -1987,19 +2597,30 @@ function buildGenericBuckets(analysis) {
|
|
|
1987
2597
|
summaryLines: [],
|
|
1988
2598
|
reason,
|
|
1989
2599
|
count: 1,
|
|
1990
|
-
confidence:
|
|
2600
|
+
confidence: (() => {
|
|
2601
|
+
const extended = findExtendedBucketSpec(reason);
|
|
2602
|
+
if (extended) {
|
|
2603
|
+
return Math.max(0.72, Math.min(extended.rootCauseConfidence, 0.82));
|
|
2604
|
+
}
|
|
2605
|
+
return reason.startsWith("assertion failed:") || /^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason) ? 0.74 : 0.62;
|
|
2606
|
+
})(),
|
|
1991
2607
|
representativeItems: [item],
|
|
1992
2608
|
entities: [],
|
|
1993
2609
|
hint: void 0,
|
|
1994
2610
|
overflowCount: 0,
|
|
1995
|
-
overflowLabel: "failing tests/modules"
|
|
2611
|
+
overflowLabel: "failing tests/modules",
|
|
2612
|
+
coverage: buildCoverageCounts({
|
|
2613
|
+
count: 1,
|
|
2614
|
+
coverageKind
|
|
2615
|
+
}),
|
|
2616
|
+
source: "heuristic"
|
|
1996
2617
|
});
|
|
1997
2618
|
};
|
|
1998
2619
|
for (const item of [...analysis.collectionItems, ...analysis.inlineItems]) {
|
|
1999
2620
|
push(item.reason, item);
|
|
2000
2621
|
}
|
|
2001
2622
|
for (const bucket of grouped.values()) {
|
|
2002
|
-
const title = bucket.type === "assertion_failure" ? "Assertion failures" : bucket.type === "import_dependency_failure" ? "Import/dependency failures" : bucket.type === "collection_failure" ? "Collection or fixture failures" : "Runtime failures";
|
|
2623
|
+
const title = findExtendedBucketSpec(bucket.reason)?.genericTitle ?? (bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch" ? "Assertion failures" : bucket.type === "import_dependency_failure" ? "Import/dependency failures" : bucket.type === "collection_failure" ? "Collection or fixture failures" : "Runtime failures");
|
|
2003
2624
|
bucket.headline = `${title}: ${formatCount(bucket.count, "visible failure")} share ${bucket.reason}.`;
|
|
2004
2625
|
bucket.summaryLines = [bucket.headline];
|
|
2005
2626
|
bucket.overflowCount = Math.max(bucket.count - bucket.representativeItems.length, 0);
|
|
@@ -2045,10 +2666,51 @@ function mergeBucketDetails(existing, incoming) {
|
|
|
2045
2666
|
incoming.overflowCount,
|
|
2046
2667
|
count - representativeItems.length
|
|
2047
2668
|
),
|
|
2048
|
-
overflowLabel: existing.overflowLabel || incoming.overflowLabel
|
|
2669
|
+
overflowLabel: existing.overflowLabel || incoming.overflowLabel,
|
|
2670
|
+
labelOverride: existing.labelOverride ?? incoming.labelOverride,
|
|
2671
|
+
coverage: {
|
|
2672
|
+
error: Math.max(existing.coverage.error, incoming.coverage.error),
|
|
2673
|
+
failed: Math.max(existing.coverage.failed, incoming.coverage.failed)
|
|
2674
|
+
},
|
|
2675
|
+
source: existing.source
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
function inferFailureBucketCoverage(bucket, analysis) {
|
|
2679
|
+
const errorLabels = new Set(analysis.visibleErrorLabels);
|
|
2680
|
+
const failedLabels = new Set(analysis.visibleFailedLabels);
|
|
2681
|
+
let error = 0;
|
|
2682
|
+
let failed = 0;
|
|
2683
|
+
for (const item of bucket.representativeItems) {
|
|
2684
|
+
const status = classifyVisibleStatusForLabel({
|
|
2685
|
+
label: item.label,
|
|
2686
|
+
errorLabels,
|
|
2687
|
+
failedLabels
|
|
2688
|
+
});
|
|
2689
|
+
if (status === "error") {
|
|
2690
|
+
error += 1;
|
|
2691
|
+
} else if (status === "failed") {
|
|
2692
|
+
failed += 1;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
const claimed = bucket.countClaimed ?? bucket.countVisible;
|
|
2696
|
+
if (bucket.type === "contract_snapshot_drift" || bucket.type === "assertion_failure" || bucket.type === "snapshot_mismatch") {
|
|
2697
|
+
return {
|
|
2698
|
+
error,
|
|
2699
|
+
failed: Math.max(failed, claimed)
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
if (bucket.type === "shared_environment_blocker" || bucket.type === "import_dependency_failure" || bucket.type === "collection_failure" || bucket.type === "fixture_guard_failure" || bucket.type === "permission_denied_failure" || bucket.type === "fixture_teardown_failure" || bucket.type === "db_migration_failure" || bucket.type === "configuration_error" || bucket.type === "xdist_worker_crash" || bucket.type === "django_db_access_denied" || bucket.type === "network_failure" || bucket.type === "service_unavailable" || bucket.type === "db_connection_failure" || bucket.type === "auth_bypass_absent") {
|
|
2703
|
+
return {
|
|
2704
|
+
error: Math.max(error, claimed),
|
|
2705
|
+
failed
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
return {
|
|
2709
|
+
error,
|
|
2710
|
+
failed
|
|
2049
2711
|
};
|
|
2050
2712
|
}
|
|
2051
|
-
function mergeBuckets(analysis) {
|
|
2713
|
+
function mergeBuckets(analysis, extraBuckets = []) {
|
|
2052
2714
|
const mergedByIdentity = /* @__PURE__ */ new Map();
|
|
2053
2715
|
const merged = [];
|
|
2054
2716
|
const pushBucket = (bucket) => {
|
|
@@ -2077,7 +2739,9 @@ function mergeBuckets(analysis) {
|
|
|
2077
2739
|
entities: [...bucket2.entities],
|
|
2078
2740
|
hint: bucket2.hint,
|
|
2079
2741
|
overflowCount: bucket2.overflowCount,
|
|
2080
|
-
overflowLabel: bucket2.overflowLabel
|
|
2742
|
+
overflowLabel: bucket2.overflowLabel,
|
|
2743
|
+
coverage: inferFailureBucketCoverage(bucket2, analysis),
|
|
2744
|
+
source: "heuristic"
|
|
2081
2745
|
}))) {
|
|
2082
2746
|
pushBucket(bucket);
|
|
2083
2747
|
}
|
|
@@ -2101,12 +2765,19 @@ function mergeBuckets(analysis) {
|
|
|
2101
2765
|
coveredLabels.add(item.label);
|
|
2102
2766
|
}
|
|
2103
2767
|
}
|
|
2768
|
+
for (const bucket of extraBuckets) {
|
|
2769
|
+
pushBucket(bucket);
|
|
2770
|
+
}
|
|
2104
2771
|
return merged;
|
|
2105
2772
|
}
|
|
2106
2773
|
function dominantBucketPriority(bucket) {
|
|
2107
2774
|
if (bucket.reason.startsWith("missing test env:")) {
|
|
2108
2775
|
return 5;
|
|
2109
2776
|
}
|
|
2777
|
+
const extended = findExtendedBucketSpec(bucket.reason);
|
|
2778
|
+
if (extended?.dominantPriority !== void 0) {
|
|
2779
|
+
return extended.dominantPriority;
|
|
2780
|
+
}
|
|
2110
2781
|
if (bucket.type === "shared_environment_blocker") {
|
|
2111
2782
|
return 4;
|
|
2112
2783
|
}
|
|
@@ -2116,6 +2787,9 @@ function dominantBucketPriority(bucket) {
|
|
|
2116
2787
|
if (bucket.type === "collection_failure") {
|
|
2117
2788
|
return 2;
|
|
2118
2789
|
}
|
|
2790
|
+
if (isUnknownBucket(bucket)) {
|
|
2791
|
+
return 2;
|
|
2792
|
+
}
|
|
2119
2793
|
if (bucket.type === "contract_snapshot_drift") {
|
|
2120
2794
|
return 1;
|
|
2121
2795
|
}
|
|
@@ -2137,9 +2811,16 @@ function prioritizeBuckets(buckets) {
|
|
|
2137
2811
|
});
|
|
2138
2812
|
}
|
|
2139
2813
|
function isDominantBlockerType(type) {
|
|
2140
|
-
return type === "shared_environment_blocker" || type === "import_dependency_failure" || type === "collection_failure";
|
|
2814
|
+
return type === "shared_environment_blocker" || type === "configuration_error" || type === "import_dependency_failure" || type === "collection_failure";
|
|
2141
2815
|
}
|
|
2142
2816
|
function labelForBucket(bucket) {
|
|
2817
|
+
if (bucket.labelOverride) {
|
|
2818
|
+
return bucket.labelOverride;
|
|
2819
|
+
}
|
|
2820
|
+
const extended = findExtendedBucketSpec(bucket.reason);
|
|
2821
|
+
if (extended) {
|
|
2822
|
+
return extended.label;
|
|
2823
|
+
}
|
|
2143
2824
|
if (bucket.reason.startsWith("missing test env:")) {
|
|
2144
2825
|
return "missing test env";
|
|
2145
2826
|
}
|
|
@@ -2173,21 +2854,40 @@ function labelForBucket(bucket) {
|
|
|
2173
2854
|
if (bucket.type === "assertion_failure") {
|
|
2174
2855
|
return "assertion failure";
|
|
2175
2856
|
}
|
|
2857
|
+
if (bucket.type === "snapshot_mismatch") {
|
|
2858
|
+
return "snapshot mismatch";
|
|
2859
|
+
}
|
|
2176
2860
|
if (bucket.type === "collection_failure") {
|
|
2177
2861
|
return "collection failure";
|
|
2178
2862
|
}
|
|
2179
2863
|
if (bucket.type === "runtime_failure") {
|
|
2180
2864
|
return "runtime failure";
|
|
2181
2865
|
}
|
|
2866
|
+
if (bucket.reason.startsWith("unknown setup blocker:")) {
|
|
2867
|
+
return "unknown setup blocker";
|
|
2868
|
+
}
|
|
2869
|
+
if (bucket.reason.startsWith("unknown failure family:")) {
|
|
2870
|
+
return "unknown failure family";
|
|
2871
|
+
}
|
|
2182
2872
|
return "unknown failure";
|
|
2183
2873
|
}
|
|
2184
2874
|
function rootCauseConfidenceFor(bucket) {
|
|
2875
|
+
if (isUnknownBucket(bucket)) {
|
|
2876
|
+
return 0.52;
|
|
2877
|
+
}
|
|
2878
|
+
const extended = findExtendedBucketSpec(bucket.reason);
|
|
2879
|
+
if (extended) {
|
|
2880
|
+
return extended.rootCauseConfidence;
|
|
2881
|
+
}
|
|
2185
2882
|
if (bucket.reason.startsWith("missing test env:") || bucket.reason.startsWith("missing module:") || bucket.reason.startsWith("db refused:") || bucket.reason.startsWith("service unavailable:") || bucket.reason.startsWith("auth bypass absent:")) {
|
|
2186
2883
|
return 0.95;
|
|
2187
2884
|
}
|
|
2188
2885
|
if (bucket.type === "contract_snapshot_drift") {
|
|
2189
2886
|
return bucket.entities.length > 0 ? 0.92 : 0.76;
|
|
2190
2887
|
}
|
|
2888
|
+
if (bucket.source === "provider") {
|
|
2889
|
+
return Math.max(0.6, Math.min(bucket.confidence, 0.82));
|
|
2890
|
+
}
|
|
2191
2891
|
return Math.max(0.6, Math.min(bucket.confidence, 0.88));
|
|
2192
2892
|
}
|
|
2193
2893
|
function buildBucketEvidence(bucket) {
|
|
@@ -2219,6 +2919,10 @@ function buildReadTargetWhy(args) {
|
|
|
2219
2919
|
if (envVar) {
|
|
2220
2920
|
return `it contains the ${envVar} setup guard`;
|
|
2221
2921
|
}
|
|
2922
|
+
const extended = findExtendedBucketSpec(args.bucket.reason);
|
|
2923
|
+
if (extended) {
|
|
2924
|
+
return extended.why;
|
|
2925
|
+
}
|
|
2222
2926
|
if (args.bucket.reason.startsWith("fixture guard:")) {
|
|
2223
2927
|
return "it contains the fixture/setup guard behind this bucket";
|
|
2224
2928
|
}
|
|
@@ -2231,6 +2935,12 @@ function buildReadTargetWhy(args) {
|
|
|
2231
2935
|
if (args.bucket.reason.startsWith("auth bypass absent:")) {
|
|
2232
2936
|
return "it contains the auth bypass setup behind this bucket";
|
|
2233
2937
|
}
|
|
2938
|
+
if (args.bucket.reason.startsWith("unknown setup blocker:")) {
|
|
2939
|
+
return "it is the first anchored setup failure in this unknown bucket";
|
|
2940
|
+
}
|
|
2941
|
+
if (args.bucket.reason.startsWith("unknown failure family:")) {
|
|
2942
|
+
return "it is the first anchored failing test in this unknown bucket";
|
|
2943
|
+
}
|
|
2234
2944
|
if (args.bucket.type === "contract_snapshot_drift") {
|
|
2235
2945
|
if (args.bucketLabel === "route drift") {
|
|
2236
2946
|
return "it maps to the visible route drift bucket";
|
|
@@ -2243,6 +2953,9 @@ function buildReadTargetWhy(args) {
|
|
|
2243
2953
|
}
|
|
2244
2954
|
return "it maps to the visible stale snapshot expectation";
|
|
2245
2955
|
}
|
|
2956
|
+
if (args.bucket.type === "snapshot_mismatch") {
|
|
2957
|
+
return "it maps to the visible snapshot mismatch bucket";
|
|
2958
|
+
}
|
|
2246
2959
|
if (args.bucket.type === "import_dependency_failure") {
|
|
2247
2960
|
return "it is the first visible failing module in this missing dependency bucket";
|
|
2248
2961
|
}
|
|
@@ -2254,11 +2967,54 @@ function buildReadTargetWhy(args) {
|
|
|
2254
2967
|
}
|
|
2255
2968
|
return `it maps to the visible ${args.bucketLabel} bucket`;
|
|
2256
2969
|
}
|
|
2970
|
+
function buildExtendedBucketSearchHint(bucket, anchor) {
|
|
2971
|
+
const extended = findExtendedBucketSpec(bucket.reason);
|
|
2972
|
+
if (!extended) {
|
|
2973
|
+
return null;
|
|
2974
|
+
}
|
|
2975
|
+
const detail = extractReasonDetail(bucket.reason, extended.prefix);
|
|
2976
|
+
if (!detail) {
|
|
2977
|
+
return anchor.label.split("::")[1]?.trim() ?? anchor.label ?? null;
|
|
2978
|
+
}
|
|
2979
|
+
if (extended.type === "timeout_failure") {
|
|
2980
|
+
const duration = detail.match(/>\s*([0-9]+(?:\.[0-9]+)?s?)/i)?.[1];
|
|
2981
|
+
return duration ?? anchor.label.split("::")[1]?.trim() ?? detail;
|
|
2982
|
+
}
|
|
2983
|
+
if (extended.type === "db_migration_failure") {
|
|
2984
|
+
const relation = detail.match(/\b(?:relation|table)\s+([A-Za-z0-9_.-]+)/i)?.[1];
|
|
2985
|
+
return relation ?? detail;
|
|
2986
|
+
}
|
|
2987
|
+
if (extended.type === "network_failure") {
|
|
2988
|
+
const url = detail.match(/\bhttps?:\/\/[^\s)'"`]+/i)?.[0];
|
|
2989
|
+
const host = detail.match(/\b(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}\b/)?.[0];
|
|
2990
|
+
return url ?? host ?? detail;
|
|
2991
|
+
}
|
|
2992
|
+
if (extended.type === "xdist_worker_crash") {
|
|
2993
|
+
return detail.match(/\bgw\d+\b/)?.[0] ?? detail;
|
|
2994
|
+
}
|
|
2995
|
+
if (extended.type === "fixture_teardown_failure") {
|
|
2996
|
+
return detail.replace(/^of\s+/i, "") || anchor.label;
|
|
2997
|
+
}
|
|
2998
|
+
if (extended.type === "file_not_found_failure") {
|
|
2999
|
+
const path8 = detail.match(/['"]([^'"]+)['"]/)?.[1];
|
|
3000
|
+
return path8 ?? detail;
|
|
3001
|
+
}
|
|
3002
|
+
if (extended.type === "permission_denied_failure") {
|
|
3003
|
+
const path8 = detail.match(/['"]([^'"]+)['"]/)?.[1];
|
|
3004
|
+
const port = detail.match(/\bport\s+(\d+)\b/i)?.[1];
|
|
3005
|
+
return path8 ?? (port ? `port ${port}` : detail);
|
|
3006
|
+
}
|
|
3007
|
+
return detail;
|
|
3008
|
+
}
|
|
2257
3009
|
function buildReadTargetSearchHint(bucket, anchor) {
|
|
2258
3010
|
const envVar = bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
|
|
2259
3011
|
if (envVar) {
|
|
2260
3012
|
return envVar;
|
|
2261
3013
|
}
|
|
3014
|
+
const extendedHint = buildExtendedBucketSearchHint(bucket, anchor);
|
|
3015
|
+
if (extendedHint) {
|
|
3016
|
+
return extendedHint;
|
|
3017
|
+
}
|
|
2262
3018
|
if (bucket.type === "contract_snapshot_drift") {
|
|
2263
3019
|
return bucket.entities.find((value) => value.startsWith("/api/")) ?? bucket.entities[0] ?? null;
|
|
2264
3020
|
}
|
|
@@ -2280,6 +3036,9 @@ function buildReadTargetSearchHint(bucket, anchor) {
|
|
|
2280
3036
|
if (assertionText) {
|
|
2281
3037
|
return assertionText;
|
|
2282
3038
|
}
|
|
3039
|
+
if (bucket.reason.startsWith("unknown ")) {
|
|
3040
|
+
return anchor.reason;
|
|
3041
|
+
}
|
|
2283
3042
|
const fallbackLabel = anchor.label.split("::")[1]?.trim();
|
|
2284
3043
|
return fallbackLabel || null;
|
|
2285
3044
|
}
|
|
@@ -2339,6 +3098,12 @@ function buildConcreteNextNote(args) {
|
|
|
2339
3098
|
if (args.nextBestAction.code === "read_source_for_bucket") {
|
|
2340
3099
|
return lead;
|
|
2341
3100
|
}
|
|
3101
|
+
if (args.nextBestAction.code === "insufficient_signal") {
|
|
3102
|
+
if (args.nextBestAction.note.startsWith("Provider follow-up failed")) {
|
|
3103
|
+
return args.nextBestAction.note;
|
|
3104
|
+
}
|
|
3105
|
+
return `${lead} Then take one deeper sift pass before raw traceback.`;
|
|
3106
|
+
}
|
|
2342
3107
|
return args.nextBestAction.note;
|
|
2343
3108
|
}
|
|
2344
3109
|
function extractMiniDiff(input, bucket) {
|
|
@@ -2363,6 +3128,156 @@ function extractMiniDiff(input, bucket) {
|
|
|
2363
3128
|
...changedTaskMappings > 0 ? { changed_task_mappings: changedTaskMappings } : {}
|
|
2364
3129
|
};
|
|
2365
3130
|
}
|
|
3131
|
+
function inferSupplementCoverageKind(args) {
|
|
3132
|
+
const extended = findExtendedBucketSpec(args.rootCause);
|
|
3133
|
+
if (extended?.defaultCoverage === "error" || extended?.defaultCoverage === "failed") {
|
|
3134
|
+
return extended.defaultCoverage;
|
|
3135
|
+
}
|
|
3136
|
+
const normalized = `${args.label} ${args.rootCause}`.toLowerCase();
|
|
3137
|
+
if (/env|setup|fixture|import|dependency|service|db|database|auth bypass|collection|connection refused/.test(
|
|
3138
|
+
normalized
|
|
3139
|
+
)) {
|
|
3140
|
+
return "error";
|
|
3141
|
+
}
|
|
3142
|
+
if (/snapshot|contract|drift|assertion|expected|actual|golden/.test(normalized)) {
|
|
3143
|
+
return "failed";
|
|
3144
|
+
}
|
|
3145
|
+
if (args.remainingErrors > 0 && args.remainingFailed === 0) {
|
|
3146
|
+
return "error";
|
|
3147
|
+
}
|
|
3148
|
+
return "failed";
|
|
3149
|
+
}
|
|
3150
|
+
function buildProviderSupplementBuckets(args) {
|
|
3151
|
+
let remainingErrors = args.remainingErrors;
|
|
3152
|
+
let remainingFailed = args.remainingFailed;
|
|
3153
|
+
return args.supplements.flatMap((supplement) => {
|
|
3154
|
+
const coverageKind = inferSupplementCoverageKind({
|
|
3155
|
+
label: supplement.label,
|
|
3156
|
+
rootCause: supplement.root_cause,
|
|
3157
|
+
remainingErrors,
|
|
3158
|
+
remainingFailed
|
|
3159
|
+
});
|
|
3160
|
+
const budget = coverageKind === "error" ? remainingErrors : remainingFailed;
|
|
3161
|
+
const count = Math.max(0, Math.min(supplement.count, budget));
|
|
3162
|
+
if (count === 0) {
|
|
3163
|
+
return [];
|
|
3164
|
+
}
|
|
3165
|
+
if (coverageKind === "error") {
|
|
3166
|
+
remainingErrors -= count;
|
|
3167
|
+
} else {
|
|
3168
|
+
remainingFailed -= count;
|
|
3169
|
+
}
|
|
3170
|
+
const representativeLabel = supplement.anchor.file ?? `${supplement.label} supplement`;
|
|
3171
|
+
const representativeItem = {
|
|
3172
|
+
label: representativeLabel,
|
|
3173
|
+
reason: supplement.root_cause,
|
|
3174
|
+
group: supplement.label,
|
|
3175
|
+
file: supplement.anchor.file,
|
|
3176
|
+
line: supplement.anchor.line,
|
|
3177
|
+
anchor_kind: supplement.anchor.file && supplement.anchor.line !== null ? "traceback" : supplement.anchor.file ? "test_label" : supplement.anchor.search_hint ? "entity" : "none",
|
|
3178
|
+
anchor_confidence: Math.max(0.4, Math.min(supplement.confidence, 0.82))
|
|
3179
|
+
};
|
|
3180
|
+
return [
|
|
3181
|
+
{
|
|
3182
|
+
type: classifyGenericBucketType(supplement.root_cause),
|
|
3183
|
+
headline: `${supplement.label}: ${formatCount(count, "visible failure")} share ${supplement.root_cause}.`,
|
|
3184
|
+
summaryLines: [
|
|
3185
|
+
`${supplement.label}: ${formatCount(count, "visible failure")} share ${supplement.root_cause}.`
|
|
3186
|
+
],
|
|
3187
|
+
reason: supplement.root_cause,
|
|
3188
|
+
count,
|
|
3189
|
+
confidence: Math.max(0.4, Math.min(supplement.confidence, 0.82)),
|
|
3190
|
+
representativeItems: [representativeItem],
|
|
3191
|
+
entities: supplement.anchor.search_hint ? [supplement.anchor.search_hint] : [],
|
|
3192
|
+
hint: supplement.fix_hint ?? void 0,
|
|
3193
|
+
overflowCount: Math.max(count - 1, 0),
|
|
3194
|
+
overflowLabel: "failing tests/modules",
|
|
3195
|
+
labelOverride: supplement.label,
|
|
3196
|
+
coverage: buildCoverageCounts({
|
|
3197
|
+
count,
|
|
3198
|
+
coverageKind
|
|
3199
|
+
}),
|
|
3200
|
+
source: "provider"
|
|
3201
|
+
}
|
|
3202
|
+
];
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
3205
|
+
function pickUnknownAnchor(args) {
|
|
3206
|
+
const fromStatusItems = args.kind === "error" ? args.analysis.visibleErrorItems[0] : null;
|
|
3207
|
+
if (fromStatusItems) {
|
|
3208
|
+
return {
|
|
3209
|
+
label: fromStatusItems.label,
|
|
3210
|
+
reason: fromStatusItems.reason,
|
|
3211
|
+
group: fromStatusItems.group,
|
|
3212
|
+
file: fromStatusItems.file,
|
|
3213
|
+
line: fromStatusItems.line,
|
|
3214
|
+
anchor_kind: fromStatusItems.anchor_kind,
|
|
3215
|
+
anchor_confidence: fromStatusItems.anchor_confidence
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
3218
|
+
const label = args.kind === "error" ? args.analysis.visibleErrorLabels[0] : args.analysis.visibleFailedLabels[0];
|
|
3219
|
+
if (label) {
|
|
3220
|
+
const normalizedLabel = normalizeTestId(label);
|
|
3221
|
+
const fileMatch = normalizedLabel.match(/^([A-Za-z0-9_./-]+\.[A-Za-z0-9]+)\b/);
|
|
3222
|
+
const file = fileMatch?.[1] ?? normalizedLabel.split("::")[0] ?? null;
|
|
3223
|
+
return {
|
|
3224
|
+
label,
|
|
3225
|
+
reason: args.kind === "error" ? "setup failures share a repeated but unclassified pattern" : "failing tests share a repeated but unclassified pattern",
|
|
3226
|
+
group: args.kind === "error" ? "unknown setup blocker" : "unknown failure family",
|
|
3227
|
+
file: file && file !== label ? file : null,
|
|
3228
|
+
line: null,
|
|
3229
|
+
anchor_kind: file && file !== label ? "test_label" : "none",
|
|
3230
|
+
anchor_confidence: file && file !== label ? 0.6 : 0
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
return null;
|
|
3234
|
+
}
|
|
3235
|
+
function buildUnknownBucket(args) {
|
|
3236
|
+
if (args.count <= 0) {
|
|
3237
|
+
return null;
|
|
3238
|
+
}
|
|
3239
|
+
const anchor = pickUnknownAnchor(args);
|
|
3240
|
+
const isError = args.kind === "error";
|
|
3241
|
+
const label = isError ? "unknown setup blocker" : "unknown failure family";
|
|
3242
|
+
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";
|
|
3243
|
+
return {
|
|
3244
|
+
type: "unknown_failure",
|
|
3245
|
+
headline: `${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`,
|
|
3246
|
+
summaryLines: [
|
|
3247
|
+
`${label}: ${formatCount(args.count, "visible failure")} share a repeated but unclassified pattern.`
|
|
3248
|
+
],
|
|
3249
|
+
reason,
|
|
3250
|
+
count: args.count,
|
|
3251
|
+
confidence: 0.45,
|
|
3252
|
+
representativeItems: anchor ? [anchor] : [],
|
|
3253
|
+
entities: [],
|
|
3254
|
+
hint: isError ? "Take one deeper sift pass or inspect the first anchored setup failure." : "Take one deeper sift pass or inspect the first anchored failing test.",
|
|
3255
|
+
overflowCount: Math.max(args.count - (anchor ? 1 : 0), 0),
|
|
3256
|
+
overflowLabel: "failing tests/modules",
|
|
3257
|
+
labelOverride: label,
|
|
3258
|
+
coverage: buildCoverageCounts({
|
|
3259
|
+
count: args.count,
|
|
3260
|
+
coverageKind: isError ? "error" : "failed"
|
|
3261
|
+
}),
|
|
3262
|
+
source: "unknown"
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
function buildCoverageResiduals(args) {
|
|
3266
|
+
const covered = args.buckets.reduce(
|
|
3267
|
+
(totals, bucket) => ({
|
|
3268
|
+
error: totals.error + bucket.coverage.error,
|
|
3269
|
+
failed: totals.failed + bucket.coverage.failed
|
|
3270
|
+
}),
|
|
3271
|
+
{
|
|
3272
|
+
error: 0,
|
|
3273
|
+
failed: 0
|
|
3274
|
+
}
|
|
3275
|
+
);
|
|
3276
|
+
return {
|
|
3277
|
+
remainingErrors: Math.max(args.analysis.errors - Math.min(args.analysis.errors, covered.error), 0),
|
|
3278
|
+
remainingFailed: Math.max(args.analysis.failed - Math.min(args.analysis.failed, covered.failed), 0)
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
2366
3281
|
function buildOutcomeLines(analysis) {
|
|
2367
3282
|
if (analysis.noTestsCollected) {
|
|
2368
3283
|
return ["- Tests did not run.", "- Collected 0 items."];
|
|
@@ -2461,6 +3376,10 @@ function buildStandardFixText(args) {
|
|
|
2461
3376
|
if (args.bucket.hint) {
|
|
2462
3377
|
return args.bucket.hint;
|
|
2463
3378
|
}
|
|
3379
|
+
const extended = findExtendedBucketSpec(args.bucket.reason);
|
|
3380
|
+
if (extended) {
|
|
3381
|
+
return extended.fix;
|
|
3382
|
+
}
|
|
2464
3383
|
const envVar = args.bucket.reason.match(/^missing test env:\s+([A-Z][A-Z0-9_]{2,})$/)?.[1];
|
|
2465
3384
|
if (envVar) {
|
|
2466
3385
|
return `Set ${envVar} before rerunning the affected tests.`;
|
|
@@ -2481,9 +3400,18 @@ function buildStandardFixText(args) {
|
|
|
2481
3400
|
if (args.bucket.reason.startsWith("auth bypass absent:")) {
|
|
2482
3401
|
return "Restore the test auth bypass setup and rerun the full suite at standard.";
|
|
2483
3402
|
}
|
|
3403
|
+
if (args.bucket.reason.startsWith("unknown setup blocker:")) {
|
|
3404
|
+
return "Take one deeper sift pass or inspect the first anchored setup failure before rerunning.";
|
|
3405
|
+
}
|
|
3406
|
+
if (args.bucket.reason.startsWith("unknown failure family:")) {
|
|
3407
|
+
return "Take one deeper sift pass or inspect the first anchored failing test before rerunning.";
|
|
3408
|
+
}
|
|
2484
3409
|
if (args.bucket.type === "contract_snapshot_drift") {
|
|
2485
3410
|
return "Review the visible drift and regenerate the contract snapshots if the changes are intentional.";
|
|
2486
3411
|
}
|
|
3412
|
+
if (args.bucket.type === "snapshot_mismatch") {
|
|
3413
|
+
return "Update the snapshots if these output changes are intentional, then rerun the full suite at standard.";
|
|
3414
|
+
}
|
|
2487
3415
|
if (args.bucket.type === "assertion_failure") {
|
|
2488
3416
|
return "Inspect the failing assertion and rerun the full suite at standard.";
|
|
2489
3417
|
}
|
|
@@ -2577,7 +3505,35 @@ function renderVerbose(args) {
|
|
|
2577
3505
|
return lines.join("\n");
|
|
2578
3506
|
}
|
|
2579
3507
|
function buildTestStatusDiagnoseContract(args) {
|
|
2580
|
-
const
|
|
3508
|
+
const heuristicBuckets = mergeBuckets(args.analysis);
|
|
3509
|
+
const preUnknownSimpleCollectionFailure = args.analysis.collectionErrorCount !== void 0 && args.analysis.collectionItems.length === 0 && heuristicBuckets.length === 0 && (args.providerBucketSupplements?.length ?? 0) === 0;
|
|
3510
|
+
const heuristicResiduals = buildCoverageResiduals({
|
|
3511
|
+
analysis: args.analysis,
|
|
3512
|
+
buckets: heuristicBuckets
|
|
3513
|
+
});
|
|
3514
|
+
const providerSupplementBuckets = buildProviderSupplementBuckets({
|
|
3515
|
+
supplements: args.providerBucketSupplements ?? [],
|
|
3516
|
+
remainingErrors: heuristicResiduals.remainingErrors,
|
|
3517
|
+
remainingFailed: heuristicResiduals.remainingFailed
|
|
3518
|
+
});
|
|
3519
|
+
const combinedBuckets = mergeBuckets(args.analysis, providerSupplementBuckets);
|
|
3520
|
+
const residuals = buildCoverageResiduals({
|
|
3521
|
+
analysis: args.analysis,
|
|
3522
|
+
buckets: combinedBuckets
|
|
3523
|
+
});
|
|
3524
|
+
const unknownBuckets = preUnknownSimpleCollectionFailure ? [] : [
|
|
3525
|
+
buildUnknownBucket({
|
|
3526
|
+
analysis: args.analysis,
|
|
3527
|
+
kind: "error",
|
|
3528
|
+
count: residuals.remainingErrors
|
|
3529
|
+
}),
|
|
3530
|
+
buildUnknownBucket({
|
|
3531
|
+
analysis: args.analysis,
|
|
3532
|
+
kind: "failed",
|
|
3533
|
+
count: residuals.remainingFailed
|
|
3534
|
+
})
|
|
3535
|
+
].filter((bucket) => Boolean(bucket));
|
|
3536
|
+
const buckets = prioritizeBuckets([...combinedBuckets, ...unknownBuckets]).slice(0, 3);
|
|
2581
3537
|
const simpleCollectionFailure = args.analysis.collectionErrorCount !== void 0 && args.analysis.collectionItems.length === 0 && buckets.length === 0;
|
|
2582
3538
|
const dominantBucket = buckets.map((bucket, index) => ({
|
|
2583
3539
|
bucket,
|
|
@@ -2588,8 +3544,10 @@ function buildTestStatusDiagnoseContract(args) {
|
|
|
2588
3544
|
}
|
|
2589
3545
|
return right.bucket.confidence - left.bucket.confidence;
|
|
2590
3546
|
})[0] ?? null;
|
|
2591
|
-
const
|
|
2592
|
-
const
|
|
3547
|
+
const hasUnknownBucket = buckets.some((bucket) => isUnknownBucket(bucket));
|
|
3548
|
+
const hasConcreteCoverage = args.analysis.failed === 0 && args.analysis.errors === 0 ? true : residuals.remainingErrors === 0 && residuals.remainingFailed === 0;
|
|
3549
|
+
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;
|
|
3550
|
+
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);
|
|
2593
3551
|
const dominantBlockerBucketIndex = dominantBucket && isDominantBlockerType(dominantBucket.bucket.type) ? dominantBucket.index + 1 : null;
|
|
2594
3552
|
const readTargets = buildReadTargets({
|
|
2595
3553
|
buckets,
|
|
@@ -2624,6 +3582,12 @@ function buildTestStatusDiagnoseContract(args) {
|
|
|
2624
3582
|
bucket_index: null,
|
|
2625
3583
|
note: "Inspect the collection traceback or setup code next; the run failed before tests executed."
|
|
2626
3584
|
};
|
|
3585
|
+
} else if (hasUnknownBucket) {
|
|
3586
|
+
nextBestAction = {
|
|
3587
|
+
code: "insufficient_signal",
|
|
3588
|
+
bucket_index: dominantBucket ? dominantBucket.index + 1 : null,
|
|
3589
|
+
note: "Take one deeper sift pass or inspect the first anchored failure before falling back to raw traceback."
|
|
3590
|
+
};
|
|
2627
3591
|
} else if (!diagnosisComplete) {
|
|
2628
3592
|
nextBestAction = {
|
|
2629
3593
|
code: rawNeeded ? "read_raw_for_exact_traceback" : "insufficient_signal",
|
|
@@ -2661,11 +3625,15 @@ function buildTestStatusDiagnoseContract(args) {
|
|
|
2661
3625
|
read_targets: readTargets,
|
|
2662
3626
|
next_best_action: nextBestAction
|
|
2663
3627
|
};
|
|
3628
|
+
const effectiveDiagnosisComplete = Boolean(args.contractOverrides?.diagnosis_complete ?? diagnosisComplete) && !hasUnknownBucket;
|
|
3629
|
+
const requestedDecision = args.contractOverrides?.decision;
|
|
3630
|
+
const effectiveDecision = hasUnknownBucket && requestedDecision && (requestedDecision === "stop" || requestedDecision === "read_source") ? "zoom" : requestedDecision;
|
|
2664
3631
|
const effectiveNextBestAction = args.contractOverrides?.next_best_action ?? baseContract.next_best_action;
|
|
2665
3632
|
const mergedContractWithoutDecision = {
|
|
2666
3633
|
...baseContract,
|
|
2667
3634
|
...args.contractOverrides,
|
|
2668
|
-
|
|
3635
|
+
diagnosis_complete: effectiveDiagnosisComplete,
|
|
3636
|
+
status: effectiveDiagnosisComplete ? "ok" : "insufficient",
|
|
2669
3637
|
next_best_action: {
|
|
2670
3638
|
...effectiveNextBestAction,
|
|
2671
3639
|
note: buildConcreteNextNote({
|
|
@@ -2679,7 +3647,7 @@ function buildTestStatusDiagnoseContract(args) {
|
|
|
2679
3647
|
};
|
|
2680
3648
|
const contract = testStatusDiagnoseContractSchema.parse({
|
|
2681
3649
|
...mergedContractWithoutDecision,
|
|
2682
|
-
decision:
|
|
3650
|
+
decision: effectiveDecision ?? deriveDecision(mergedContractWithoutDecision)
|
|
2683
3651
|
});
|
|
2684
3652
|
return {
|
|
2685
3653
|
contract,
|
|
@@ -2916,9 +3884,12 @@ function resolvePromptPolicy(args) {
|
|
|
2916
3884
|
"Return only valid JSON.",
|
|
2917
3885
|
`Use this exact contract: ${args.outputContract ?? TEST_STATUS_DIAGNOSE_JSON_CONTRACT}.`,
|
|
2918
3886
|
"Treat the heuristic context as extraction guidance, but do not invent hidden failures.",
|
|
2919
|
-
"Use the heuristic extract as the bucket truth unless the visible command output clearly disproves it.",
|
|
3887
|
+
"Use the heuristic extract as the base bucket truth unless the visible command output clearly disproves it.",
|
|
3888
|
+
"If some visible failure or error families remain unexplained, add at most 2 bucket_supplements for the residual families only.",
|
|
3889
|
+
"Do not rewrite or delete heuristic buckets; only supplement missing residual coverage.",
|
|
3890
|
+
"Keep bucket_supplement counts within the unexplained residual failures or errors.",
|
|
2920
3891
|
"Identify the dominant blocker, remaining visible failure buckets, the decision, and the next best action.",
|
|
2921
|
-
"Set diagnosis_complete to true only when the visible output is already sufficient to stop and act.",
|
|
3892
|
+
"Set diagnosis_complete to true only when the visible output is already sufficient to stop and act and no unknown residual family remains.",
|
|
2922
3893
|
"Set raw_needed to true only when exact traceback lines are still required.",
|
|
2923
3894
|
"Set provider_confidence to a number between 0 and 1, or null only when confidence cannot be estimated."
|
|
2924
3895
|
] : [
|
|
@@ -3099,6 +4070,63 @@ function getCount(input, label) {
|
|
|
3099
4070
|
const lastMatch = matches.at(-1);
|
|
3100
4071
|
return lastMatch ? Number(lastMatch[1]) : 0;
|
|
3101
4072
|
}
|
|
4073
|
+
function detectTestRunner(input) {
|
|
4074
|
+
if (/^\s*Test Files?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /^\s*Tests?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /^\s*Snapshots?\s+(?:\d+\s+failed\s*\|\s*)?\d+\s+passed/m.test(input) || /⎯{2,}\s+Failed Tests?\s+\d+\s+⎯{2,}/.test(input)) {
|
|
4075
|
+
return "vitest";
|
|
4076
|
+
}
|
|
4077
|
+
if (/^\s*Test Suites:\s+\d+\s+failed,\s+\d+\s+passed(?:,\s+\d+\s+total)?/m.test(input) || /^\s*Tests:\s+\d+\s+failed,\s+\d+\s+passed(?:,\s+\d+\s+total)?/m.test(input)) {
|
|
4078
|
+
return "jest";
|
|
4079
|
+
}
|
|
4080
|
+
if (/\bpytest\b/i.test(input) || /^\s*=+.*\b\d+\s+failed\b.*=+\s*$/m.test(input) || /\bcollected\s+\d+\s+items\b/i.test(input)) {
|
|
4081
|
+
return "pytest";
|
|
4082
|
+
}
|
|
4083
|
+
return "unknown";
|
|
4084
|
+
}
|
|
4085
|
+
function extractVitestLineCount(input, label, metric) {
|
|
4086
|
+
const matcher = new RegExp(`^\\s*${label}\\s+(.+)$`, "gmi");
|
|
4087
|
+
const lines = [...input.matchAll(matcher)];
|
|
4088
|
+
const line = lines.at(-1)?.[1];
|
|
4089
|
+
if (!line) {
|
|
4090
|
+
return null;
|
|
4091
|
+
}
|
|
4092
|
+
const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
|
|
4093
|
+
return metricMatch ? Number(metricMatch[1]) : null;
|
|
4094
|
+
}
|
|
4095
|
+
function extractJestLineCount(input, label, metric) {
|
|
4096
|
+
const matcher = new RegExp(`^\\s*${label}:\\s+(.+)$`, "gmi");
|
|
4097
|
+
const lines = [...input.matchAll(matcher)];
|
|
4098
|
+
const line = lines.at(-1)?.[1];
|
|
4099
|
+
if (!line) {
|
|
4100
|
+
return null;
|
|
4101
|
+
}
|
|
4102
|
+
const metricMatch = line.match(new RegExp(`(\\d+)\\s+${metric}`, "i"));
|
|
4103
|
+
return metricMatch ? Number(metricMatch[1]) : null;
|
|
4104
|
+
}
|
|
4105
|
+
function extractTestStatusCounts(input, runner) {
|
|
4106
|
+
if (runner === "vitest") {
|
|
4107
|
+
return {
|
|
4108
|
+
passed: extractVitestLineCount(input, "Tests?", "passed") ?? getCount(input, "passed"),
|
|
4109
|
+
failed: extractVitestLineCount(input, "Tests?", "failed") ?? getCount(input, "failed"),
|
|
4110
|
+
errors: extractVitestLineCount(input, "Errors?", "error") ?? extractVitestLineCount(input, "Errors?", "errors") ?? Math.max(getCount(input, "errors"), getCount(input, "error")),
|
|
4111
|
+
skipped: extractVitestLineCount(input, "Tests?", "skipped") ?? getCount(input, "skipped"),
|
|
4112
|
+
snapshotFailures: extractVitestLineCount(input, "Snapshots?", "failed") ?? void 0
|
|
4113
|
+
};
|
|
4114
|
+
}
|
|
4115
|
+
if (runner === "jest") {
|
|
4116
|
+
return {
|
|
4117
|
+
passed: extractJestLineCount(input, "Tests", "passed") ?? getCount(input, "passed"),
|
|
4118
|
+
failed: extractJestLineCount(input, "Tests", "failed") ?? getCount(input, "failed"),
|
|
4119
|
+
errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
|
|
4120
|
+
skipped: extractJestLineCount(input, "Tests", "skipped") ?? getCount(input, "skipped")
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
return {
|
|
4124
|
+
passed: getCount(input, "passed"),
|
|
4125
|
+
failed: getCount(input, "failed"),
|
|
4126
|
+
errors: Math.max(getCount(input, "errors"), getCount(input, "error")),
|
|
4127
|
+
skipped: getCount(input, "skipped")
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
3102
4130
|
function formatCount2(count, singular, plural = `${singular}s`) {
|
|
3103
4131
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
3104
4132
|
}
|
|
@@ -3131,7 +4159,8 @@ function normalizeAnchorFile(value) {
|
|
|
3131
4159
|
return value.replace(/\\/g, "/").trim();
|
|
3132
4160
|
}
|
|
3133
4161
|
function inferFileFromLabel(label) {
|
|
3134
|
-
const
|
|
4162
|
+
const cleaned = cleanFailureLabel(label);
|
|
4163
|
+
const candidate = cleaned.split("::")[0]?.split(" > ")[0]?.trim();
|
|
3135
4164
|
if (!candidate) {
|
|
3136
4165
|
return null;
|
|
3137
4166
|
}
|
|
@@ -3186,6 +4215,15 @@ function parseObservedAnchor(line) {
|
|
|
3186
4215
|
anchor_confidence: 0.92
|
|
3187
4216
|
};
|
|
3188
4217
|
}
|
|
4218
|
+
const vitestTraceback = normalized.match(/^\s*❯\s+([^:\s][^:]*\.[A-Za-z0-9]+):(\d+)(?::\d+)?/);
|
|
4219
|
+
if (vitestTraceback) {
|
|
4220
|
+
return {
|
|
4221
|
+
file: normalizeAnchorFile(vitestTraceback[1]),
|
|
4222
|
+
line: Number(vitestTraceback[2]),
|
|
4223
|
+
anchor_kind: "traceback",
|
|
4224
|
+
anchor_confidence: 1
|
|
4225
|
+
};
|
|
4226
|
+
}
|
|
3189
4227
|
return null;
|
|
3190
4228
|
}
|
|
3191
4229
|
function resolveAnchorForLabel(args) {
|
|
@@ -3202,96 +4240,362 @@ function isLowValueInternalReason(normalized) {
|
|
|
3202
4240
|
) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
|
|
3203
4241
|
}
|
|
3204
4242
|
function scoreFailureReason(reason) {
|
|
4243
|
+
if (reason.startsWith("configuration:")) {
|
|
4244
|
+
return 6;
|
|
4245
|
+
}
|
|
3205
4246
|
if (reason.startsWith("missing test env:")) {
|
|
3206
4247
|
return 6;
|
|
3207
4248
|
}
|
|
3208
|
-
if (reason.startsWith("missing module:")) {
|
|
3209
|
-
return 5;
|
|
4249
|
+
if (reason.startsWith("missing module:")) {
|
|
4250
|
+
return 5;
|
|
4251
|
+
}
|
|
4252
|
+
if (reason.startsWith("snapshot mismatch:")) {
|
|
4253
|
+
return 4;
|
|
4254
|
+
}
|
|
4255
|
+
if (reason.startsWith("assertion failed:")) {
|
|
4256
|
+
return 4;
|
|
4257
|
+
}
|
|
4258
|
+
if (reason.startsWith("timeout:") || reason.startsWith("async loop:") || reason.startsWith("django db access:") || reason.startsWith("db migration:")) {
|
|
4259
|
+
return 3;
|
|
4260
|
+
}
|
|
4261
|
+
if (reason.startsWith("permission:") || reason.startsWith("xdist worker crash:") || reason.startsWith("network:") || reason.startsWith("segfault:") || reason.startsWith("memory:") || reason.startsWith("type error:") || reason.startsWith("serialization:") || reason.startsWith("file not found:") || reason.startsWith("deprecation as error:") || reason.startsWith("xfail strict:") || reason.startsWith("resource leak:") || reason.startsWith("flaky:") || reason.startsWith("fixture teardown:")) {
|
|
4262
|
+
return 2;
|
|
4263
|
+
}
|
|
4264
|
+
if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
|
|
4265
|
+
return 3;
|
|
4266
|
+
}
|
|
4267
|
+
if (reason === "import error during collection") {
|
|
4268
|
+
return 2;
|
|
4269
|
+
}
|
|
4270
|
+
return 1;
|
|
4271
|
+
}
|
|
4272
|
+
function buildClassifiedReason(prefix, detail) {
|
|
4273
|
+
return `${prefix}: ${detail}`.slice(0, 120);
|
|
4274
|
+
}
|
|
4275
|
+
function buildExcerptDetail(value, fallback) {
|
|
4276
|
+
const trimmed = value.trim().replace(/\s+/g, " ");
|
|
4277
|
+
return trimmed.length > 0 ? trimmed : fallback;
|
|
4278
|
+
}
|
|
4279
|
+
function sharedBlockerThreshold(reason) {
|
|
4280
|
+
return reason.startsWith("configuration:") ? 1 : 3;
|
|
4281
|
+
}
|
|
4282
|
+
function extractEnvBlockerName(normalized) {
|
|
4283
|
+
const directMatch = normalized.match(
|
|
4284
|
+
/\bDB-isolated tests require\s+([A-Z][A-Z0-9_]{2,})\b/
|
|
4285
|
+
);
|
|
4286
|
+
if (directMatch) {
|
|
4287
|
+
return directMatch[1];
|
|
4288
|
+
}
|
|
4289
|
+
const fallbackMatch = normalized.match(
|
|
4290
|
+
/\b([A-Z][A-Z0-9_]{2,})\b(?=[^.\n]*DB-isolated tests)/
|
|
4291
|
+
);
|
|
4292
|
+
if (fallbackMatch) {
|
|
4293
|
+
return fallbackMatch[1];
|
|
4294
|
+
}
|
|
4295
|
+
const leadingEnvMatch = normalized.match(
|
|
4296
|
+
/\b([A-Z][A-Z0-9_]{2,})\b(?=[^.\n]{0,80}\b(?:is\s+)?(?:missing|unset|not set|not configured|required)\b)/
|
|
4297
|
+
);
|
|
4298
|
+
if (leadingEnvMatch) {
|
|
4299
|
+
return leadingEnvMatch[1];
|
|
4300
|
+
}
|
|
4301
|
+
const trailingEnvMatch = normalized.match(
|
|
4302
|
+
/\b(?:missing|unset|not set|not configured|required)\b[^.\n]{0,80}\b([A-Z][A-Z0-9_]{2,})\b/
|
|
4303
|
+
);
|
|
4304
|
+
if (trailingEnvMatch) {
|
|
4305
|
+
return trailingEnvMatch[1];
|
|
4306
|
+
}
|
|
4307
|
+
const validationEnvMatch = normalized.match(
|
|
4308
|
+
/\bValidationError\b[^.\n]{0,120}\b([A-Z][A-Z0-9_]{2,})\b/
|
|
4309
|
+
);
|
|
4310
|
+
return validationEnvMatch?.[1] ?? null;
|
|
4311
|
+
}
|
|
4312
|
+
function classifyFailureReason(line, options) {
|
|
4313
|
+
const normalized = line.trim().replace(/^[A-Z]\s+/, "");
|
|
4314
|
+
if (normalized.length === 0) {
|
|
4315
|
+
return null;
|
|
4316
|
+
}
|
|
4317
|
+
if (isLowValueInternalReason(normalized)) {
|
|
4318
|
+
return null;
|
|
4319
|
+
}
|
|
4320
|
+
if (/^([A-Za-z0-9_./-]+\.[A-Za-z0-9]+):\d+(?::\d+)?:\s+in\b/.test(normalized) || /^([^:\s][^:]*\.[A-Za-z0-9]+):\d+(?::\d+)?:\s+in\b/.test(normalized) || /^File\s+"[^"]+",\s+line\s+\d+/.test(normalized)) {
|
|
4321
|
+
return null;
|
|
4322
|
+
}
|
|
4323
|
+
const envBlocker = extractEnvBlockerName(normalized);
|
|
4324
|
+
if (envBlocker) {
|
|
4325
|
+
return {
|
|
4326
|
+
reason: `missing test env: ${envBlocker}`,
|
|
4327
|
+
group: "DB-backed tests are blocked by missing test environment configuration"
|
|
4328
|
+
};
|
|
4329
|
+
}
|
|
4330
|
+
const missingEnv = normalized.match(
|
|
4331
|
+
/\b(?:environment variable|env(?:ironment)? var(?:iable)?|missing required env(?:ironment)? variable)\s+([A-Z][A-Z0-9_]{2,})\b/
|
|
4332
|
+
);
|
|
4333
|
+
if (missingEnv) {
|
|
4334
|
+
return {
|
|
4335
|
+
reason: `missing test env: ${missingEnv[1]}`,
|
|
4336
|
+
group: "tests are blocked by missing environment configuration"
|
|
4337
|
+
};
|
|
4338
|
+
}
|
|
4339
|
+
const keyErrorEnv = normalized.match(/KeyError:\s*['"]([A-Z][A-Z0-9_]{2,})['"]/);
|
|
4340
|
+
if (keyErrorEnv) {
|
|
4341
|
+
return {
|
|
4342
|
+
reason: `missing test env: ${keyErrorEnv[1]}`,
|
|
4343
|
+
group: "tests are blocked by missing environment configuration"
|
|
4344
|
+
};
|
|
4345
|
+
}
|
|
4346
|
+
const fixtureGuard = normalized.match(
|
|
4347
|
+
/(?:FixtureLookupError|fixture guard|requires fixture)\b[^A-Za-z0-9_'-]*([a-z_][a-z0-9_]*)?/i
|
|
4348
|
+
);
|
|
4349
|
+
if (fixtureGuard) {
|
|
4350
|
+
return {
|
|
4351
|
+
reason: `fixture guard: ${fixtureGuard[1] ?? "required fixture unavailable"}`.trim(),
|
|
4352
|
+
group: "fixture guards or setup gates"
|
|
4353
|
+
};
|
|
4354
|
+
}
|
|
4355
|
+
if (/(ECONNREFUSED|ConnectionRefusedError|connection refused|could not connect to server)/i.test(
|
|
4356
|
+
normalized
|
|
4357
|
+
) && /(postgres|database|db|5432)/i.test(normalized)) {
|
|
4358
|
+
return {
|
|
4359
|
+
reason: "db refused: database connection was refused",
|
|
4360
|
+
group: "database connectivity failures"
|
|
4361
|
+
};
|
|
4362
|
+
}
|
|
4363
|
+
if (/(ECONNREFUSED|ConnectionRefusedError|connection refused)/i.test(normalized)) {
|
|
4364
|
+
return {
|
|
4365
|
+
reason: "service unavailable: dependency connection was refused",
|
|
4366
|
+
group: "service availability failures"
|
|
4367
|
+
};
|
|
4368
|
+
}
|
|
4369
|
+
if (/(503\b|service unavailable|temporarily unavailable)/i.test(normalized)) {
|
|
4370
|
+
return {
|
|
4371
|
+
reason: "service unavailable: dependency service is unavailable",
|
|
4372
|
+
group: "service availability failures"
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
if (/(auth bypass|test auth|bypass token)/i.test(normalized) && /(missing|absent|not configured|not set|unavailable)/i.test(normalized)) {
|
|
4376
|
+
return {
|
|
4377
|
+
reason: "auth bypass absent: test auth bypass is missing",
|
|
4378
|
+
group: "authentication test setup failures"
|
|
4379
|
+
};
|
|
4380
|
+
}
|
|
4381
|
+
const snapshotMismatch = normalized.match(
|
|
4382
|
+
/((?:Error:\s*)?Snapshot\b.+\bmismatched\b[^$]*|Snapshot comparison failed[^$]*)/i
|
|
4383
|
+
);
|
|
4384
|
+
if (snapshotMismatch) {
|
|
4385
|
+
return {
|
|
4386
|
+
reason: buildClassifiedReason(
|
|
4387
|
+
"snapshot mismatch",
|
|
4388
|
+
buildExcerptDetail(snapshotMismatch[1] ?? normalized, "snapshot output changed")
|
|
4389
|
+
),
|
|
4390
|
+
group: "snapshot mismatches"
|
|
4391
|
+
};
|
|
4392
|
+
}
|
|
4393
|
+
const timeoutFailure = normalized.match(
|
|
4394
|
+
/(Failed:\s*Timeout\s*>[^,;]+|asyncio\.exceptions\.TimeoutError:\s*.+|TimeoutError:\s*.+|(?:Test|Hook)\s+timed out in\s+\d+(?:\.\d+)?m?s[^$]*|(?:\[vitest-(?:worker|pool)\]:\s*)?Timeout[^$]*)$/i
|
|
4395
|
+
);
|
|
4396
|
+
if (timeoutFailure) {
|
|
4397
|
+
return {
|
|
4398
|
+
reason: buildClassifiedReason(
|
|
4399
|
+
"timeout",
|
|
4400
|
+
buildExcerptDetail(timeoutFailure[1] ?? normalized, "test exceeded timeout threshold")
|
|
4401
|
+
),
|
|
4402
|
+
group: "timeout failures"
|
|
4403
|
+
};
|
|
4404
|
+
}
|
|
4405
|
+
const asyncLoopFailure = normalized.match(
|
|
4406
|
+
/(Event loop is closed|no current event loop|coroutine .* was never awaited|coroutine was never awaited)/i
|
|
4407
|
+
);
|
|
4408
|
+
if (asyncLoopFailure) {
|
|
4409
|
+
return {
|
|
4410
|
+
reason: buildClassifiedReason(
|
|
4411
|
+
"async loop",
|
|
4412
|
+
buildExcerptDetail(asyncLoopFailure[1] ?? normalized, "async event loop failure")
|
|
4413
|
+
),
|
|
4414
|
+
group: "async event loop failures"
|
|
4415
|
+
};
|
|
4416
|
+
}
|
|
4417
|
+
const permissionFailure = normalized.match(
|
|
4418
|
+
/(PermissionError:\s*\[Errno 13\][^$]*|Address already in use)/i
|
|
4419
|
+
);
|
|
4420
|
+
if (permissionFailure) {
|
|
4421
|
+
return {
|
|
4422
|
+
reason: buildClassifiedReason(
|
|
4423
|
+
"permission",
|
|
4424
|
+
buildExcerptDetail(permissionFailure[1] ?? normalized, "permission denied or resource locked")
|
|
4425
|
+
),
|
|
4426
|
+
group: "permission or locked resource failures"
|
|
4427
|
+
};
|
|
4428
|
+
}
|
|
4429
|
+
const xdistWorkerCrash = normalized.match(
|
|
4430
|
+
/(worker ['"][^'"]+['"] crashed|node down:\s*[^,;]+|WorkerLost[^,;]*|Worker exited unexpectedly[^,;]*|worker exited unexpectedly[^,;]*)/i
|
|
4431
|
+
);
|
|
4432
|
+
if (xdistWorkerCrash) {
|
|
4433
|
+
return {
|
|
4434
|
+
reason: buildClassifiedReason(
|
|
4435
|
+
"xdist worker crash",
|
|
4436
|
+
buildExcerptDetail(xdistWorkerCrash[1] ?? normalized, "pytest-xdist worker crashed")
|
|
4437
|
+
),
|
|
4438
|
+
group: "xdist worker crashes"
|
|
4439
|
+
};
|
|
4440
|
+
}
|
|
4441
|
+
if (/Worker terminated due to reaching memory limit/i.test(normalized)) {
|
|
4442
|
+
return {
|
|
4443
|
+
reason: "memory: Worker terminated due to reaching memory limit",
|
|
4444
|
+
group: "memory exhaustion failures"
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
if (/Database access not allowed, use the ["']django_db["'] mark/i.test(normalized)) {
|
|
4448
|
+
return {
|
|
4449
|
+
reason: 'django db access: Database access not allowed, use the "django_db" mark',
|
|
4450
|
+
group: "django database marker failures"
|
|
4451
|
+
};
|
|
4452
|
+
}
|
|
4453
|
+
const networkFailure = normalized.match(
|
|
4454
|
+
/(Max retries exceeded[^,;]*|gaierror[^,;]*|SSLCertVerificationError[^,;]*|Network is unreachable)/i
|
|
4455
|
+
);
|
|
4456
|
+
if (networkFailure) {
|
|
4457
|
+
return {
|
|
4458
|
+
reason: buildClassifiedReason(
|
|
4459
|
+
"network",
|
|
4460
|
+
buildExcerptDetail(networkFailure[1] ?? normalized, "network dependency failure")
|
|
4461
|
+
),
|
|
4462
|
+
group: "network dependency failures"
|
|
4463
|
+
};
|
|
3210
4464
|
}
|
|
3211
|
-
|
|
3212
|
-
|
|
4465
|
+
const relationMigration = normalized.match(/relation ["'`]([^"'`]+)["'`] does not exist/i);
|
|
4466
|
+
if (relationMigration) {
|
|
4467
|
+
return {
|
|
4468
|
+
reason: buildClassifiedReason("db migration", `relation ${relationMigration[1]} does not exist`),
|
|
4469
|
+
group: "database migration or schema failures"
|
|
4470
|
+
};
|
|
3213
4471
|
}
|
|
3214
|
-
|
|
3215
|
-
|
|
4472
|
+
const noSuchTable = normalized.match(/no such table(?::)?\s*([A-Za-z0-9_.-]+)/i);
|
|
4473
|
+
if (noSuchTable) {
|
|
4474
|
+
return {
|
|
4475
|
+
reason: buildClassifiedReason("db migration", `no such table ${noSuchTable[1]}`),
|
|
4476
|
+
group: "database migration or schema failures"
|
|
4477
|
+
};
|
|
3216
4478
|
}
|
|
3217
|
-
if (
|
|
3218
|
-
return
|
|
4479
|
+
if (/InconsistentMigrationHistory/i.test(normalized)) {
|
|
4480
|
+
return {
|
|
4481
|
+
reason: "db migration: InconsistentMigrationHistory",
|
|
4482
|
+
group: "database migration or schema failures"
|
|
4483
|
+
};
|
|
3219
4484
|
}
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
4485
|
+
if (/(Segmentation fault|SIGSEGV|\bexit 139\b)/i.test(normalized)) {
|
|
4486
|
+
return {
|
|
4487
|
+
reason: buildClassifiedReason(
|
|
4488
|
+
"segfault",
|
|
4489
|
+
buildExcerptDetail(normalized, "subprocess crashed with SIGSEGV")
|
|
4490
|
+
),
|
|
4491
|
+
group: "subprocess crash failures"
|
|
4492
|
+
};
|
|
3228
4493
|
}
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
return null;
|
|
4494
|
+
if (/(MemoryError\b|\bexit 137\b|OOMKilled|OutOfMemory)/i.test(normalized)) {
|
|
4495
|
+
return {
|
|
4496
|
+
reason: buildClassifiedReason(
|
|
4497
|
+
"memory",
|
|
4498
|
+
buildExcerptDetail(normalized, "process exhausted available memory")
|
|
4499
|
+
),
|
|
4500
|
+
group: "memory exhaustion failures"
|
|
4501
|
+
};
|
|
3238
4502
|
}
|
|
3239
|
-
|
|
3240
|
-
|
|
4503
|
+
const typeErrorFailure = normalized.match(/TypeError:\s*(.+)$/i);
|
|
4504
|
+
if (typeErrorFailure) {
|
|
4505
|
+
return {
|
|
4506
|
+
reason: buildClassifiedReason(
|
|
4507
|
+
"type error",
|
|
4508
|
+
buildExcerptDetail(typeErrorFailure[1] ?? normalized, "TypeError")
|
|
4509
|
+
),
|
|
4510
|
+
group: "type errors"
|
|
4511
|
+
};
|
|
3241
4512
|
}
|
|
3242
|
-
|
|
3243
|
-
|
|
4513
|
+
const serializationFailure = normalized.match(
|
|
4514
|
+
/\b(UnicodeDecodeError|JSONDecodeError|PicklingError):\s*(.+)$/i
|
|
4515
|
+
);
|
|
4516
|
+
if (serializationFailure) {
|
|
4517
|
+
return {
|
|
4518
|
+
reason: buildClassifiedReason(
|
|
4519
|
+
"serialization",
|
|
4520
|
+
`${serializationFailure[1]}: ${buildExcerptDetail(serializationFailure[2] ?? "", serializationFailure[1] ?? "serialization failure")}`
|
|
4521
|
+
),
|
|
4522
|
+
group: "serialization and encoding failures"
|
|
4523
|
+
};
|
|
3244
4524
|
}
|
|
3245
|
-
const
|
|
3246
|
-
if (
|
|
4525
|
+
const fileNotFoundFailure = normalized.match(/FileNotFoundError:\s*(.+)$/i);
|
|
4526
|
+
if (fileNotFoundFailure) {
|
|
3247
4527
|
return {
|
|
3248
|
-
reason:
|
|
3249
|
-
|
|
4528
|
+
reason: buildClassifiedReason(
|
|
4529
|
+
"file not found",
|
|
4530
|
+
buildExcerptDetail(fileNotFoundFailure[1] ?? normalized, "missing file during test execution")
|
|
4531
|
+
),
|
|
4532
|
+
group: "missing file failures"
|
|
3250
4533
|
};
|
|
3251
4534
|
}
|
|
3252
|
-
const
|
|
3253
|
-
/\b(
|
|
4535
|
+
const deprecationFailure = normalized.match(
|
|
4536
|
+
/\b(DeprecationWarning|FutureWarning|PytestRemovedIn9Warning):\s*(.+)$/i
|
|
3254
4537
|
);
|
|
3255
|
-
if (
|
|
4538
|
+
if (deprecationFailure) {
|
|
3256
4539
|
return {
|
|
3257
|
-
reason:
|
|
3258
|
-
|
|
4540
|
+
reason: buildClassifiedReason(
|
|
4541
|
+
"deprecation as error",
|
|
4542
|
+
`${deprecationFailure[1]}: ${buildExcerptDetail(deprecationFailure[2] ?? "", deprecationFailure[1] ?? "warning treated as error")}`
|
|
4543
|
+
),
|
|
4544
|
+
group: "warnings treated as errors"
|
|
3259
4545
|
};
|
|
3260
4546
|
}
|
|
3261
|
-
const
|
|
3262
|
-
if (
|
|
4547
|
+
const strictXfail = normalized.match(/XPASS\(strict\)\s*:?\s*(.+)?$/i);
|
|
4548
|
+
if (strictXfail) {
|
|
3263
4549
|
return {
|
|
3264
|
-
reason:
|
|
3265
|
-
|
|
4550
|
+
reason: buildClassifiedReason(
|
|
4551
|
+
"xfail strict",
|
|
4552
|
+
buildExcerptDetail(strictXfail[1] ?? normalized, "strict xfail unexpectedly passed")
|
|
4553
|
+
),
|
|
4554
|
+
group: "strict xfail expectation failures"
|
|
3266
4555
|
};
|
|
3267
4556
|
}
|
|
3268
|
-
const
|
|
3269
|
-
/(
|
|
4557
|
+
const resourceLeak = normalized.match(
|
|
4558
|
+
/(PytestUnraisableExceptionWarning[^,;]*|ResourceWarning:\s*unclosed[^,;]*)/i
|
|
3270
4559
|
);
|
|
3271
|
-
if (
|
|
4560
|
+
if (resourceLeak) {
|
|
3272
4561
|
return {
|
|
3273
|
-
reason:
|
|
3274
|
-
|
|
4562
|
+
reason: buildClassifiedReason(
|
|
4563
|
+
"resource leak",
|
|
4564
|
+
buildExcerptDetail(resourceLeak[1] ?? normalized, "resource leak warning promoted to failure")
|
|
4565
|
+
),
|
|
4566
|
+
group: "resource leak warnings"
|
|
3275
4567
|
};
|
|
3276
4568
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
) && /(postgres|database|db|5432)/i.test(normalized)) {
|
|
4569
|
+
const flakyFailure = normalized.match(/\b(RERUN|[0-9]+\s+rerun|Flaky test passed)\b[^$]*/i);
|
|
4570
|
+
if (flakyFailure) {
|
|
3280
4571
|
return {
|
|
3281
|
-
reason:
|
|
3282
|
-
|
|
4572
|
+
reason: buildClassifiedReason(
|
|
4573
|
+
"flaky",
|
|
4574
|
+
buildExcerptDetail(flakyFailure[0] ?? normalized, "test required reruns before passing")
|
|
4575
|
+
),
|
|
4576
|
+
group: "flaky test detections"
|
|
3283
4577
|
};
|
|
3284
4578
|
}
|
|
3285
|
-
|
|
4579
|
+
const teardownFailure = normalized.match(/ERROR at teardown of\s+(.+)$/i);
|
|
4580
|
+
if (teardownFailure) {
|
|
3286
4581
|
return {
|
|
3287
|
-
reason:
|
|
3288
|
-
|
|
4582
|
+
reason: buildClassifiedReason(
|
|
4583
|
+
"fixture teardown",
|
|
4584
|
+
buildExcerptDetail(teardownFailure[1] ?? normalized, "fixture teardown failed")
|
|
4585
|
+
),
|
|
4586
|
+
group: "fixture teardown failures"
|
|
3289
4587
|
};
|
|
3290
4588
|
}
|
|
3291
|
-
|
|
4589
|
+
const configurationFailure = normalized.match(
|
|
4590
|
+
/(INTERNALERROR>.+|ConftestImportFailure[^,;]*|UsageError:\s*.+|ERROR:\s*usage:\s*.+|pytest:\s*error:\s*.+|Cannot use import statement outside a module[^$]*|Named export.+not found.+CommonJS[^$]*|failed to load config from.+|localStorage is not available[^$]*|No test suite found in file.+|No test found in suite.+)$/i
|
|
4591
|
+
);
|
|
4592
|
+
if (configurationFailure) {
|
|
3292
4593
|
return {
|
|
3293
|
-
reason:
|
|
3294
|
-
|
|
4594
|
+
reason: buildClassifiedReason(
|
|
4595
|
+
"configuration",
|
|
4596
|
+
buildExcerptDetail(configurationFailure[1] ?? normalized, "test configuration error")
|
|
4597
|
+
),
|
|
4598
|
+
group: "test configuration failures"
|
|
3295
4599
|
};
|
|
3296
4600
|
}
|
|
3297
4601
|
const pythonMissingModule = normalized.match(
|
|
@@ -3310,6 +4614,20 @@ function classifyFailureReason(line, options) {
|
|
|
3310
4614
|
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
3311
4615
|
};
|
|
3312
4616
|
}
|
|
4617
|
+
const importResolutionFailure = normalized.match(/Failed to resolve import ['"]([^'"]+)['"]/i);
|
|
4618
|
+
if (importResolutionFailure) {
|
|
4619
|
+
return {
|
|
4620
|
+
reason: `missing module: ${importResolutionFailure[1]}`,
|
|
4621
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
4622
|
+
};
|
|
4623
|
+
}
|
|
4624
|
+
const esmModuleFailure = normalized.match(/ERR_MODULE_NOT_FOUND[^'"`]*['"`]([^'"`]+)['"`]/i) ?? normalized.match(/Cannot find package ['"`]([^'"`]+)['"`]/i);
|
|
4625
|
+
if (esmModuleFailure) {
|
|
4626
|
+
return {
|
|
4627
|
+
reason: `missing module: ${esmModuleFailure[1]}`,
|
|
4628
|
+
group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
|
|
4629
|
+
};
|
|
4630
|
+
}
|
|
3313
4631
|
const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
|
|
3314
4632
|
if (assertionFailure) {
|
|
3315
4633
|
return {
|
|
@@ -3317,6 +4635,16 @@ function classifyFailureReason(line, options) {
|
|
|
3317
4635
|
group: "assertion failures"
|
|
3318
4636
|
};
|
|
3319
4637
|
}
|
|
4638
|
+
const vitestUnhandled = normalized.match(/Vitest caught\s+\d+\s+unhandled errors?/i);
|
|
4639
|
+
if (vitestUnhandled) {
|
|
4640
|
+
return {
|
|
4641
|
+
reason: `RuntimeError: ${buildExcerptDetail(vitestUnhandled[0] ?? normalized, "Vitest caught unhandled errors")}`.slice(
|
|
4642
|
+
0,
|
|
4643
|
+
120
|
|
4644
|
+
),
|
|
4645
|
+
group: "runtime failures"
|
|
4646
|
+
};
|
|
4647
|
+
}
|
|
3320
4648
|
const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
|
|
3321
4649
|
if (genericError) {
|
|
3322
4650
|
const errorType = genericError[1];
|
|
@@ -3361,6 +4689,125 @@ function chooseStrongestFailureItems(items) {
|
|
|
3361
4689
|
}
|
|
3362
4690
|
return order.map((label) => strongest.get(label));
|
|
3363
4691
|
}
|
|
4692
|
+
function extractJsTestFile(value) {
|
|
4693
|
+
const match = value.match(/([A-Za-z0-9_./-]+\.(?:test|spec)\.[cm]?[jt]sx?)/i);
|
|
4694
|
+
return match ? normalizeAnchorFile(match[1]) : null;
|
|
4695
|
+
}
|
|
4696
|
+
function normalizeJsFailureLabel(label) {
|
|
4697
|
+
return cleanFailureLabel(label).replace(/^[❯×]\s*/, "").replace(/\s+\[[^\]]+\]\s*$/, "").replace(/\s+/g, " ").trim();
|
|
4698
|
+
}
|
|
4699
|
+
function classifyFailureLines(args) {
|
|
4700
|
+
let observedAnchor = null;
|
|
4701
|
+
let strongest = null;
|
|
4702
|
+
for (const line of args.lines) {
|
|
4703
|
+
observedAnchor = parseObservedAnchor(line) ?? observedAnchor;
|
|
4704
|
+
const classification = classifyFailureReason(line, {
|
|
4705
|
+
duringCollection: args.duringCollection
|
|
4706
|
+
});
|
|
4707
|
+
if (!classification) {
|
|
4708
|
+
continue;
|
|
4709
|
+
}
|
|
4710
|
+
const score = scoreFailureReason(classification.reason);
|
|
4711
|
+
if (!strongest || score > strongest.score) {
|
|
4712
|
+
strongest = {
|
|
4713
|
+
classification,
|
|
4714
|
+
score,
|
|
4715
|
+
observedAnchor: parseObservedAnchor(line) ?? observedAnchor
|
|
4716
|
+
};
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
if (!strongest) {
|
|
4720
|
+
return null;
|
|
4721
|
+
}
|
|
4722
|
+
return {
|
|
4723
|
+
classification: strongest.classification,
|
|
4724
|
+
observedAnchor: strongest.observedAnchor ?? observedAnchor
|
|
4725
|
+
};
|
|
4726
|
+
}
|
|
4727
|
+
function collectJsFailureBlocks(input) {
|
|
4728
|
+
const blocks = [];
|
|
4729
|
+
let current = null;
|
|
4730
|
+
let section = null;
|
|
4731
|
+
let currentFile = null;
|
|
4732
|
+
const flushCurrent = () => {
|
|
4733
|
+
if (!current) {
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4736
|
+
blocks.push(current);
|
|
4737
|
+
current = null;
|
|
4738
|
+
};
|
|
4739
|
+
for (const rawLine of input.split("\n")) {
|
|
4740
|
+
const line = rawLine.trimEnd();
|
|
4741
|
+
const trimmed = line.trim();
|
|
4742
|
+
if (/⎯{2,}\s+Failed Tests?\s+\d+\s+⎯{2,}/.test(line)) {
|
|
4743
|
+
flushCurrent();
|
|
4744
|
+
section = "failed_tests";
|
|
4745
|
+
continue;
|
|
4746
|
+
}
|
|
4747
|
+
if (/⎯{2,}\s+Failed Suites?\s+\d+\s+⎯{2,}/.test(line)) {
|
|
4748
|
+
flushCurrent();
|
|
4749
|
+
section = "failed_suites";
|
|
4750
|
+
continue;
|
|
4751
|
+
}
|
|
4752
|
+
if (section && /^⎯{2,}.+⎯{2,}\s*$/.test(line)) {
|
|
4753
|
+
flushCurrent();
|
|
4754
|
+
section = null;
|
|
4755
|
+
continue;
|
|
4756
|
+
}
|
|
4757
|
+
const progress = line.match(
|
|
4758
|
+
/^(.+?\.(?:test|spec)\.[cm]?[jt]sx?(?:\s+>.+?)?)\s+(FAILED|ERROR)\s+\[[^\]]+\]\s*$/
|
|
4759
|
+
);
|
|
4760
|
+
if (progress) {
|
|
4761
|
+
flushCurrent();
|
|
4762
|
+
const label = normalizeJsFailureLabel(progress[1]);
|
|
4763
|
+
current = {
|
|
4764
|
+
label,
|
|
4765
|
+
status: progress[2] === "ERROR" ? "error" : "failed",
|
|
4766
|
+
detailLines: []
|
|
4767
|
+
};
|
|
4768
|
+
currentFile = extractJsTestFile(label);
|
|
4769
|
+
continue;
|
|
4770
|
+
}
|
|
4771
|
+
const failHeader = line.match(/^\s*FAIL\s+(.+)$/);
|
|
4772
|
+
if (failHeader) {
|
|
4773
|
+
const label = normalizeJsFailureLabel(failHeader[1]);
|
|
4774
|
+
if (extractJsTestFile(label)) {
|
|
4775
|
+
flushCurrent();
|
|
4776
|
+
current = {
|
|
4777
|
+
label,
|
|
4778
|
+
status: section === "failed_suites" || !label.includes(" > ") ? "error" : "failed",
|
|
4779
|
+
detailLines: []
|
|
4780
|
+
};
|
|
4781
|
+
currentFile = extractJsTestFile(label);
|
|
4782
|
+
continue;
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
const failedTest = line.match(/^\s*×\s+(.+)$/);
|
|
4786
|
+
if (failedTest && (section === "failed_tests" || extractJsTestFile(failedTest[1]))) {
|
|
4787
|
+
flushCurrent();
|
|
4788
|
+
const candidate = normalizeJsFailureLabel(failedTest[1]);
|
|
4789
|
+
const file = extractJsTestFile(candidate) ?? currentFile;
|
|
4790
|
+
const label = file && !extractJsTestFile(candidate) ? `${file} > ${candidate}` : candidate;
|
|
4791
|
+
current = {
|
|
4792
|
+
label,
|
|
4793
|
+
status: "failed",
|
|
4794
|
+
detailLines: []
|
|
4795
|
+
};
|
|
4796
|
+
currentFile = extractJsTestFile(label) ?? currentFile;
|
|
4797
|
+
continue;
|
|
4798
|
+
}
|
|
4799
|
+
if (/^\s*(?:Tests?|Snapshots?|Test Files?|Test Suites?)\b/.test(line)) {
|
|
4800
|
+
flushCurrent();
|
|
4801
|
+
section = null;
|
|
4802
|
+
continue;
|
|
4803
|
+
}
|
|
4804
|
+
if (current && trimmed.length > 0) {
|
|
4805
|
+
current.detailLines.push(line);
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
flushCurrent();
|
|
4809
|
+
return blocks;
|
|
4810
|
+
}
|
|
3364
4811
|
function collectCollectionFailureItems(input) {
|
|
3365
4812
|
const items = [];
|
|
3366
4813
|
const lines = input.split("\n");
|
|
@@ -3368,6 +4815,24 @@ function collectCollectionFailureItems(input) {
|
|
|
3368
4815
|
let pendingGenericReason = null;
|
|
3369
4816
|
let currentAnchor = null;
|
|
3370
4817
|
for (const line of lines) {
|
|
4818
|
+
const standaloneCollectionLabel = line.match(/No test suite found in file\s+(.+)$/i)?.[1] ?? line.match(/No test found in suite\s+(.+)$/i)?.[1] ?? line.match(/failed to load config from\s+(.+)$/i)?.[1];
|
|
4819
|
+
if (standaloneCollectionLabel) {
|
|
4820
|
+
const classification2 = classifyFailureReason(line, {
|
|
4821
|
+
duringCollection: true
|
|
4822
|
+
});
|
|
4823
|
+
if (classification2) {
|
|
4824
|
+
pushFocusedFailureItem(items, {
|
|
4825
|
+
label: cleanFailureLabel(standaloneCollectionLabel),
|
|
4826
|
+
reason: classification2.reason,
|
|
4827
|
+
group: classification2.group,
|
|
4828
|
+
...resolveAnchorForLabel({
|
|
4829
|
+
label: cleanFailureLabel(standaloneCollectionLabel),
|
|
4830
|
+
observedAnchor: parseObservedAnchor(line)
|
|
4831
|
+
})
|
|
4832
|
+
});
|
|
4833
|
+
}
|
|
4834
|
+
continue;
|
|
4835
|
+
}
|
|
3371
4836
|
const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
|
|
3372
4837
|
if (collecting) {
|
|
3373
4838
|
if (currentLabel && pendingGenericReason) {
|
|
@@ -3456,6 +4921,24 @@ function collectInlineFailureItems(input) {
|
|
|
3456
4921
|
})
|
|
3457
4922
|
});
|
|
3458
4923
|
}
|
|
4924
|
+
for (const block of collectJsFailureBlocks(input)) {
|
|
4925
|
+
const resolved = classifyFailureLines({
|
|
4926
|
+
lines: block.detailLines,
|
|
4927
|
+
duringCollection: block.status === "error"
|
|
4928
|
+
});
|
|
4929
|
+
if (!resolved) {
|
|
4930
|
+
continue;
|
|
4931
|
+
}
|
|
4932
|
+
pushFocusedFailureItem(items, {
|
|
4933
|
+
label: block.label,
|
|
4934
|
+
reason: resolved.classification.reason,
|
|
4935
|
+
group: resolved.classification.group,
|
|
4936
|
+
...resolveAnchorForLabel({
|
|
4937
|
+
label: block.label,
|
|
4938
|
+
observedAnchor: resolved.observedAnchor
|
|
4939
|
+
})
|
|
4940
|
+
});
|
|
4941
|
+
}
|
|
3459
4942
|
return items;
|
|
3460
4943
|
}
|
|
3461
4944
|
function collectInlineFailureItemsWithStatus(input) {
|
|
@@ -3490,16 +4973,42 @@ function collectInlineFailureItemsWithStatus(input) {
|
|
|
3490
4973
|
})
|
|
3491
4974
|
});
|
|
3492
4975
|
}
|
|
4976
|
+
for (const block of collectJsFailureBlocks(input)) {
|
|
4977
|
+
const resolved = classifyFailureLines({
|
|
4978
|
+
lines: block.detailLines,
|
|
4979
|
+
duringCollection: block.status === "error"
|
|
4980
|
+
});
|
|
4981
|
+
if (!resolved) {
|
|
4982
|
+
continue;
|
|
4983
|
+
}
|
|
4984
|
+
items.push({
|
|
4985
|
+
label: block.label,
|
|
4986
|
+
reason: resolved.classification.reason,
|
|
4987
|
+
group: resolved.classification.group,
|
|
4988
|
+
status: block.status,
|
|
4989
|
+
...resolveAnchorForLabel({
|
|
4990
|
+
label: block.label,
|
|
4991
|
+
observedAnchor: resolved.observedAnchor
|
|
4992
|
+
})
|
|
4993
|
+
});
|
|
4994
|
+
}
|
|
3493
4995
|
return items;
|
|
3494
4996
|
}
|
|
3495
4997
|
function collectStandaloneErrorClassifications(input) {
|
|
3496
4998
|
const classifications = [];
|
|
3497
4999
|
for (const line of input.split("\n")) {
|
|
5000
|
+
const trimmed = line.trim();
|
|
5001
|
+
if (!trimmed) {
|
|
5002
|
+
continue;
|
|
5003
|
+
}
|
|
3498
5004
|
const standalone = line.match(/^\s*E\s+(.+)$/);
|
|
3499
|
-
|
|
5005
|
+
const candidate = standalone?.[1] ?? (/^(INTERNALERROR>|ConftestImportFailure\b|UsageError:|ERROR:\s*usage:|pytest:\s*error:)/i.test(
|
|
5006
|
+
trimmed
|
|
5007
|
+
) ? trimmed : null);
|
|
5008
|
+
if (!candidate) {
|
|
3500
5009
|
continue;
|
|
3501
5010
|
}
|
|
3502
|
-
const classification = classifyFailureReason(
|
|
5011
|
+
const classification = classifyFailureReason(candidate, {
|
|
3503
5012
|
duringCollection: false
|
|
3504
5013
|
});
|
|
3505
5014
|
if (!classification || classification.reason === "import error during collection") {
|
|
@@ -3615,6 +5124,9 @@ function collectFailureLabels(input) {
|
|
|
3615
5124
|
pushLabel(summary[2], summary[1] === "FAILED" ? "failed" : "error");
|
|
3616
5125
|
}
|
|
3617
5126
|
}
|
|
5127
|
+
for (const block of collectJsFailureBlocks(input)) {
|
|
5128
|
+
pushLabel(block.label, block.status);
|
|
5129
|
+
}
|
|
3618
5130
|
return labels;
|
|
3619
5131
|
}
|
|
3620
5132
|
function classifyBucketTypeFromReason(reason) {
|
|
@@ -3624,6 +5136,60 @@ function classifyBucketTypeFromReason(reason) {
|
|
|
3624
5136
|
if (reason.startsWith("fixture guard:")) {
|
|
3625
5137
|
return "fixture_guard_failure";
|
|
3626
5138
|
}
|
|
5139
|
+
if (reason.startsWith("timeout:")) {
|
|
5140
|
+
return "timeout_failure";
|
|
5141
|
+
}
|
|
5142
|
+
if (reason.startsWith("permission:")) {
|
|
5143
|
+
return "permission_denied_failure";
|
|
5144
|
+
}
|
|
5145
|
+
if (reason.startsWith("async loop:")) {
|
|
5146
|
+
return "async_event_loop_failure";
|
|
5147
|
+
}
|
|
5148
|
+
if (reason.startsWith("fixture teardown:")) {
|
|
5149
|
+
return "fixture_teardown_failure";
|
|
5150
|
+
}
|
|
5151
|
+
if (reason.startsWith("db migration:")) {
|
|
5152
|
+
return "db_migration_failure";
|
|
5153
|
+
}
|
|
5154
|
+
if (reason.startsWith("configuration:")) {
|
|
5155
|
+
return "configuration_error";
|
|
5156
|
+
}
|
|
5157
|
+
if (reason.startsWith("xdist worker crash:")) {
|
|
5158
|
+
return "xdist_worker_crash";
|
|
5159
|
+
}
|
|
5160
|
+
if (reason.startsWith("type error:")) {
|
|
5161
|
+
return "type_error_failure";
|
|
5162
|
+
}
|
|
5163
|
+
if (reason.startsWith("resource leak:")) {
|
|
5164
|
+
return "resource_leak_warning";
|
|
5165
|
+
}
|
|
5166
|
+
if (reason.startsWith("django db access:")) {
|
|
5167
|
+
return "django_db_access_denied";
|
|
5168
|
+
}
|
|
5169
|
+
if (reason.startsWith("network:")) {
|
|
5170
|
+
return "network_failure";
|
|
5171
|
+
}
|
|
5172
|
+
if (reason.startsWith("segfault:")) {
|
|
5173
|
+
return "subprocess_crash_segfault";
|
|
5174
|
+
}
|
|
5175
|
+
if (reason.startsWith("flaky:")) {
|
|
5176
|
+
return "flaky_test_detected";
|
|
5177
|
+
}
|
|
5178
|
+
if (reason.startsWith("serialization:")) {
|
|
5179
|
+
return "serialization_encoding_failure";
|
|
5180
|
+
}
|
|
5181
|
+
if (reason.startsWith("file not found:")) {
|
|
5182
|
+
return "file_not_found_failure";
|
|
5183
|
+
}
|
|
5184
|
+
if (reason.startsWith("memory:")) {
|
|
5185
|
+
return "memory_error";
|
|
5186
|
+
}
|
|
5187
|
+
if (reason.startsWith("deprecation as error:")) {
|
|
5188
|
+
return "deprecation_warning_as_error";
|
|
5189
|
+
}
|
|
5190
|
+
if (reason.startsWith("xfail strict:")) {
|
|
5191
|
+
return "xfail_strict_unexpected_pass";
|
|
5192
|
+
}
|
|
3627
5193
|
if (reason.startsWith("service unavailable:")) {
|
|
3628
5194
|
return "service_unavailable";
|
|
3629
5195
|
}
|
|
@@ -3633,6 +5199,9 @@ function classifyBucketTypeFromReason(reason) {
|
|
|
3633
5199
|
if (reason.startsWith("auth bypass absent:")) {
|
|
3634
5200
|
return "auth_bypass_absent";
|
|
3635
5201
|
}
|
|
5202
|
+
if (reason.startsWith("snapshot mismatch:")) {
|
|
5203
|
+
return "snapshot_mismatch";
|
|
5204
|
+
}
|
|
3636
5205
|
if (reason.startsWith("missing module:")) {
|
|
3637
5206
|
return "import_dependency_failure";
|
|
3638
5207
|
}
|
|
@@ -3645,9 +5214,6 @@ function classifyBucketTypeFromReason(reason) {
|
|
|
3645
5214
|
return "unknown_failure";
|
|
3646
5215
|
}
|
|
3647
5216
|
function synthesizeSharedBlockerBucket(args) {
|
|
3648
|
-
if (args.errors === 0) {
|
|
3649
|
-
return null;
|
|
3650
|
-
}
|
|
3651
5217
|
const visibleReasonGroups = /* @__PURE__ */ new Map();
|
|
3652
5218
|
for (const item of args.visibleErrorItems) {
|
|
3653
5219
|
const entry = visibleReasonGroups.get(item.reason);
|
|
@@ -3662,7 +5228,7 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
3662
5228
|
items: [item]
|
|
3663
5229
|
});
|
|
3664
5230
|
}
|
|
3665
|
-
const top = [...visibleReasonGroups.entries()].filter(([, entry]) => entry.count >=
|
|
5231
|
+
const top = [...visibleReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
|
|
3666
5232
|
const standaloneReasonGroups = /* @__PURE__ */ new Map();
|
|
3667
5233
|
for (const classification of collectStandaloneErrorClassifications(args.input)) {
|
|
3668
5234
|
const entry = standaloneReasonGroups.get(classification.reason);
|
|
@@ -3675,7 +5241,7 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
3675
5241
|
group: classification.group
|
|
3676
5242
|
});
|
|
3677
5243
|
}
|
|
3678
|
-
const standaloneTop = [...standaloneReasonGroups.entries()].filter(([, entry]) => entry.count >=
|
|
5244
|
+
const standaloneTop = [...standaloneReasonGroups.entries()].filter(([reason, entry]) => entry.count >= sharedBlockerThreshold(reason)).sort((left, right) => right[1].count - left[1].count)[0];
|
|
3679
5245
|
const visibleTopReason = top?.[0];
|
|
3680
5246
|
const visibleTopStats = top?.[1];
|
|
3681
5247
|
const standaloneTopReason = standaloneTop?.[0];
|
|
@@ -3714,6 +5280,12 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
3714
5280
|
let hint;
|
|
3715
5281
|
if (envVar) {
|
|
3716
5282
|
hint = `Set ${envVar} (or pass --pgtest-dsn) before rerunning DB-isolated tests.`;
|
|
5283
|
+
} else if (effectiveReason.startsWith("configuration:")) {
|
|
5284
|
+
hint = "Fix the pytest configuration or conftest import error before rerunning the suite.";
|
|
5285
|
+
} else if (effectiveReason.startsWith("xdist worker crash:")) {
|
|
5286
|
+
hint = "Check shared state, worker startup, or resource contention between xdist workers before rerunning.";
|
|
5287
|
+
} else if (effectiveReason.startsWith("network:")) {
|
|
5288
|
+
hint = "Restore DNS, TLS, or outbound network access for the affected dependency before rerunning.";
|
|
3717
5289
|
} else if (effectiveReason.startsWith("fixture guard:")) {
|
|
3718
5290
|
hint = "Unblock the required fixture or setup guard before rerunning the affected tests.";
|
|
3719
5291
|
} else if (effectiveReason.startsWith("db refused:")) {
|
|
@@ -3728,6 +5300,12 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
3728
5300
|
let headline;
|
|
3729
5301
|
if (envVar) {
|
|
3730
5302
|
headline = `Shared blocker: ${atLeastPrefix}${countText} errors require ${envVar} for DB-isolated tests.`;
|
|
5303
|
+
} else if (effectiveReason.startsWith("configuration:")) {
|
|
5304
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} visible failure${countText === 1 ? "" : "s"} are caused by a pytest configuration error.`;
|
|
5305
|
+
} else if (effectiveReason.startsWith("xdist worker crash:")) {
|
|
5306
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by xdist worker crashes.`;
|
|
5307
|
+
} else if (effectiveReason.startsWith("network:")) {
|
|
5308
|
+
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are caused by a network dependency failure.`;
|
|
3731
5309
|
} else if (effectiveReason.startsWith("fixture guard:")) {
|
|
3732
5310
|
headline = `Shared blocker: ${atLeastPrefix}${countText} errors are gated by the same fixture/setup guard.`;
|
|
3733
5311
|
} else if (effectiveReason.startsWith("db refused:")) {
|
|
@@ -3758,22 +5336,28 @@ function synthesizeSharedBlockerBucket(args) {
|
|
|
3758
5336
|
};
|
|
3759
5337
|
}
|
|
3760
5338
|
function synthesizeImportDependencyBucket(args) {
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
const
|
|
3765
|
-
|
|
5339
|
+
const visibleImportItems = args.visibleErrorItems.filter(
|
|
5340
|
+
(item) => item.reason.startsWith("missing module:")
|
|
5341
|
+
);
|
|
5342
|
+
const inlineImportItems = chooseStrongestFailureItems(
|
|
5343
|
+
args.inlineItems.filter((item) => item.reason.startsWith("missing module:"))
|
|
5344
|
+
);
|
|
5345
|
+
const importItems = visibleImportItems.length > 0 ? visibleImportItems : inlineImportItems.map((item) => ({
|
|
5346
|
+
...item,
|
|
5347
|
+
status: "failed"
|
|
5348
|
+
}));
|
|
5349
|
+
if (importItems.length === 0) {
|
|
3766
5350
|
return null;
|
|
3767
5351
|
}
|
|
3768
5352
|
const allVisibleErrorsAreImportRelated = args.visibleErrorItems.length > 0 && args.visibleErrorItems.every((item) => item.reason.startsWith("missing module:"));
|
|
3769
|
-
const countClaimed = allVisibleErrorsAreImportRelated && importItems.length >=
|
|
5353
|
+
const countClaimed = allVisibleErrorsAreImportRelated && importItems.length >= 2 && args.errors >= importItems.length ? args.errors : void 0;
|
|
3770
5354
|
const modules = Array.from(
|
|
3771
5355
|
new Set(
|
|
3772
5356
|
importItems.map((item) => item.reason.replace("missing module:", "").trim()).filter(Boolean)
|
|
3773
5357
|
)
|
|
3774
5358
|
).slice(0, 6);
|
|
3775
5359
|
const headlineCount = countClaimed ?? importItems.length;
|
|
3776
|
-
const headline = countClaimed ? `Import/dependency blocker: ${headlineCount} errors are caused by missing dependencies during test collection.` : `Import/dependency blocker: at least ${headlineCount} visible
|
|
5360
|
+
const headline = countClaimed ? `Import/dependency blocker: ${headlineCount} errors are caused by missing dependencies during test collection.` : `Import/dependency blocker: at least ${headlineCount} visible failure${headlineCount === 1 ? "" : "s"} are caused by missing dependencies during test collection.`;
|
|
3777
5361
|
const summaryLines = [headline];
|
|
3778
5362
|
if (modules.length > 0) {
|
|
3779
5363
|
summaryLines.push(`Missing modules include ${modules.join(", ")}.`);
|
|
@@ -3783,7 +5367,7 @@ function synthesizeImportDependencyBucket(args) {
|
|
|
3783
5367
|
headline,
|
|
3784
5368
|
countVisible: importItems.length,
|
|
3785
5369
|
countClaimed,
|
|
3786
|
-
reason: "missing dependencies during test collection",
|
|
5370
|
+
reason: modules.length === 1 ? `missing module: ${modules[0]}` : "missing dependencies during test collection",
|
|
3787
5371
|
representativeItems: importItems.slice(0, 4).map((item) => ({
|
|
3788
5372
|
label: item.label,
|
|
3789
5373
|
reason: item.reason,
|
|
@@ -3802,7 +5386,7 @@ function synthesizeImportDependencyBucket(args) {
|
|
|
3802
5386
|
};
|
|
3803
5387
|
}
|
|
3804
5388
|
function isContractDriftLabel(label) {
|
|
3805
|
-
return /(freeze|
|
|
5389
|
+
return /(freeze|contract|manifest|openapi|golden)/i.test(label);
|
|
3806
5390
|
}
|
|
3807
5391
|
function looksLikeTaskKey(value) {
|
|
3808
5392
|
return /^[a-z]+(?:_[a-z0-9]+)+$/i.test(value) && !value.startsWith("/api/");
|
|
@@ -3864,7 +5448,7 @@ function extractContractDriftEntities(input) {
|
|
|
3864
5448
|
}
|
|
3865
5449
|
function buildContractRepresentativeReason(args) {
|
|
3866
5450
|
if (/openapi/i.test(args.label) && args.entities.apiPaths.length > 0) {
|
|
3867
|
-
const nextPath = args.entities.apiPaths.find((
|
|
5451
|
+
const nextPath = args.entities.apiPaths.find((path8) => !args.usedPaths.has(path8)) ?? args.entities.apiPaths[0];
|
|
3868
5452
|
args.usedPaths.add(nextPath);
|
|
3869
5453
|
return `added path: ${nextPath}`;
|
|
3870
5454
|
}
|
|
@@ -3933,13 +5517,67 @@ function synthesizeContractDriftBucket(args) {
|
|
|
3933
5517
|
overflowLabel: "changed entities"
|
|
3934
5518
|
};
|
|
3935
5519
|
}
|
|
5520
|
+
function synthesizeSnapshotMismatchBucket(args) {
|
|
5521
|
+
const snapshotItems = chooseStrongestFailureItems(
|
|
5522
|
+
args.inlineItems.filter((item) => item.reason.startsWith("snapshot mismatch:"))
|
|
5523
|
+
);
|
|
5524
|
+
if (snapshotItems.length === 0) {
|
|
5525
|
+
return null;
|
|
5526
|
+
}
|
|
5527
|
+
const countClaimed = args.snapshotFailures && args.snapshotFailures >= snapshotItems.length ? args.snapshotFailures : void 0;
|
|
5528
|
+
const countText = countClaimed ?? snapshotItems.length;
|
|
5529
|
+
const summaryLines = [
|
|
5530
|
+
`Snapshot mismatches: ${formatCount2(countText, "snapshot expectation")} ${countText === 1 ? "is" : "are"} out of date with current output.`
|
|
5531
|
+
];
|
|
5532
|
+
return {
|
|
5533
|
+
type: "snapshot_mismatch",
|
|
5534
|
+
headline: summaryLines[0],
|
|
5535
|
+
countVisible: snapshotItems.length,
|
|
5536
|
+
countClaimed,
|
|
5537
|
+
reason: "snapshot mismatch: snapshot expectations differ from current output",
|
|
5538
|
+
representativeItems: snapshotItems.slice(0, 4),
|
|
5539
|
+
entities: snapshotItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
|
|
5540
|
+
hint: "Update the snapshots if these output changes are intentional.",
|
|
5541
|
+
confidence: countClaimed ? 0.92 : 0.8,
|
|
5542
|
+
summaryLines,
|
|
5543
|
+
overflowCount: Math.max((countClaimed ?? snapshotItems.length) - Math.min(snapshotItems.length, 4), 0),
|
|
5544
|
+
overflowLabel: "snapshot failures"
|
|
5545
|
+
};
|
|
5546
|
+
}
|
|
5547
|
+
function synthesizeTimeoutBucket(args) {
|
|
5548
|
+
const timeoutItems = chooseStrongestFailureItems(
|
|
5549
|
+
args.inlineItems.filter((item) => item.reason.startsWith("timeout:"))
|
|
5550
|
+
);
|
|
5551
|
+
if (timeoutItems.length === 0) {
|
|
5552
|
+
return null;
|
|
5553
|
+
}
|
|
5554
|
+
const summaryLines = [
|
|
5555
|
+
`Timeout failures: ${formatCount2(timeoutItems.length, "test")} exceeded the configured timeout threshold.`
|
|
5556
|
+
];
|
|
5557
|
+
return {
|
|
5558
|
+
type: "timeout_failure",
|
|
5559
|
+
headline: summaryLines[0],
|
|
5560
|
+
countVisible: timeoutItems.length,
|
|
5561
|
+
countClaimed: timeoutItems.length,
|
|
5562
|
+
reason: timeoutItems.length === 1 ? timeoutItems[0].reason : "timeout: tests exceeded the configured timeout threshold",
|
|
5563
|
+
representativeItems: timeoutItems.slice(0, 4),
|
|
5564
|
+
entities: timeoutItems.map((item) => item.label.split(" > ").slice(1).join(" > ").trim() || item.label).slice(0, 6),
|
|
5565
|
+
hint: "Check for deadlocks, slow setup, or increase the timeout threshold before rerunning.",
|
|
5566
|
+
confidence: 0.84,
|
|
5567
|
+
summaryLines,
|
|
5568
|
+
overflowCount: Math.max(timeoutItems.length - Math.min(timeoutItems.length, 4), 0),
|
|
5569
|
+
overflowLabel: "timeout failures"
|
|
5570
|
+
};
|
|
5571
|
+
}
|
|
3936
5572
|
function analyzeTestStatus(input) {
|
|
3937
|
-
const
|
|
3938
|
-
const
|
|
3939
|
-
const
|
|
3940
|
-
const
|
|
5573
|
+
const runner = detectTestRunner(input);
|
|
5574
|
+
const counts = extractTestStatusCounts(input, runner);
|
|
5575
|
+
const passed = counts.passed;
|
|
5576
|
+
const failed = counts.failed;
|
|
5577
|
+
const errors = counts.errors;
|
|
5578
|
+
const skipped = counts.skipped;
|
|
3941
5579
|
const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
|
|
3942
|
-
const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
|
|
5580
|
+
const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input) || /No test suite found in file/i.test(input) || /No test found in suite/i.test(input);
|
|
3943
5581
|
const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
|
|
3944
5582
|
const collectionItems = chooseStrongestFailureItems(collectCollectionFailureItems(input));
|
|
3945
5583
|
const inlineItems = chooseStrongestFailureItems(collectInlineFailureItems(input));
|
|
@@ -3966,7 +5604,8 @@ function analyzeTestStatus(input) {
|
|
|
3966
5604
|
if (!sharedBlocker) {
|
|
3967
5605
|
const importDependencyBucket = synthesizeImportDependencyBucket({
|
|
3968
5606
|
errors,
|
|
3969
|
-
visibleErrorItems
|
|
5607
|
+
visibleErrorItems,
|
|
5608
|
+
inlineItems
|
|
3970
5609
|
});
|
|
3971
5610
|
if (importDependencyBucket) {
|
|
3972
5611
|
buckets.push(importDependencyBucket);
|
|
@@ -3979,11 +5618,26 @@ function analyzeTestStatus(input) {
|
|
|
3979
5618
|
if (contractDrift) {
|
|
3980
5619
|
buckets.push(contractDrift);
|
|
3981
5620
|
}
|
|
5621
|
+
const snapshotMismatch = synthesizeSnapshotMismatchBucket({
|
|
5622
|
+
inlineItems,
|
|
5623
|
+
snapshotFailures: counts.snapshotFailures
|
|
5624
|
+
});
|
|
5625
|
+
if (snapshotMismatch) {
|
|
5626
|
+
buckets.push(snapshotMismatch);
|
|
5627
|
+
}
|
|
5628
|
+
const timeoutBucket = synthesizeTimeoutBucket({
|
|
5629
|
+
inlineItems
|
|
5630
|
+
});
|
|
5631
|
+
if (timeoutBucket) {
|
|
5632
|
+
buckets.push(timeoutBucket);
|
|
5633
|
+
}
|
|
3982
5634
|
return {
|
|
5635
|
+
runner,
|
|
3983
5636
|
passed,
|
|
3984
5637
|
failed,
|
|
3985
5638
|
errors,
|
|
3986
5639
|
skipped,
|
|
5640
|
+
snapshotFailures: counts.snapshotFailures,
|
|
3987
5641
|
noTestsCollected,
|
|
3988
5642
|
interrupted,
|
|
3989
5643
|
collectionErrorCount: collectionErrors ? Number(collectionErrors[1]) : void 0,
|
|
@@ -4495,6 +6149,7 @@ function buildGenericRawSlice(args) {
|
|
|
4495
6149
|
|
|
4496
6150
|
// src/core/run.ts
|
|
4497
6151
|
var RETRY_DELAY_MS = 300;
|
|
6152
|
+
var PROVIDER_PENDING_NOTICE_DELAY_MS = 150;
|
|
4498
6153
|
function estimateTokenCount(text) {
|
|
4499
6154
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
4500
6155
|
}
|
|
@@ -4515,6 +6170,8 @@ function logVerboseTestStatusTelemetry(args) {
|
|
|
4515
6170
|
`${pc2.dim("sift")} diagnosis_complete_at_layer=${getDiagnosisCompleteAtLayer(args.contract)}`,
|
|
4516
6171
|
`${pc2.dim("sift")} heuristic_short_circuit=${!args.contract.provider_used && args.contract.diagnosis_complete && !args.contract.raw_needed && !args.contract.provider_failed}`,
|
|
4517
6172
|
`${pc2.dim("sift")} raw_input_chars=${args.request.stdin.length}`,
|
|
6173
|
+
`${pc2.dim("sift")} heuristic_input_chars=${args.heuristicInputChars}`,
|
|
6174
|
+
`${pc2.dim("sift")} heuristic_input_truncated=${args.heuristicInputTruncated}`,
|
|
4518
6175
|
`${pc2.dim("sift")} prepared_input_chars=${args.prepared.meta.finalLength}`,
|
|
4519
6176
|
`${pc2.dim("sift")} raw_slice_chars=${args.rawSliceChars ?? 0}`,
|
|
4520
6177
|
`${pc2.dim("sift")} provider_input_chars=${args.providerInputChars ?? 0}`,
|
|
@@ -4556,6 +6213,7 @@ function buildDryRunOutput(args) {
|
|
|
4556
6213
|
responseMode: args.responseMode,
|
|
4557
6214
|
policy: args.request.policyName ?? null,
|
|
4558
6215
|
heuristicOutput: args.heuristicOutput ?? null,
|
|
6216
|
+
heuristicInput: args.heuristicInput,
|
|
4559
6217
|
input: {
|
|
4560
6218
|
originalLength: args.prepared.meta.originalLength,
|
|
4561
6219
|
finalLength: args.prepared.meta.finalLength,
|
|
@@ -4572,6 +6230,25 @@ function buildDryRunOutput(args) {
|
|
|
4572
6230
|
async function delay(ms) {
|
|
4573
6231
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
4574
6232
|
}
|
|
6233
|
+
function startProviderPendingNotice() {
|
|
6234
|
+
if (!process.stderr.isTTY) {
|
|
6235
|
+
return () => {
|
|
6236
|
+
};
|
|
6237
|
+
}
|
|
6238
|
+
const message = "sift waiting for provider...";
|
|
6239
|
+
let shown = false;
|
|
6240
|
+
const timer = setTimeout(() => {
|
|
6241
|
+
shown = true;
|
|
6242
|
+
process.stderr.write(`${message}\r`);
|
|
6243
|
+
}, PROVIDER_PENDING_NOTICE_DELAY_MS);
|
|
6244
|
+
return () => {
|
|
6245
|
+
clearTimeout(timer);
|
|
6246
|
+
if (!shown) {
|
|
6247
|
+
return;
|
|
6248
|
+
}
|
|
6249
|
+
process.stderr.write(`\r${" ".repeat(message.length)}\r`);
|
|
6250
|
+
};
|
|
6251
|
+
}
|
|
4575
6252
|
function withInsufficientHint(args) {
|
|
4576
6253
|
if (!isInsufficientSignalOutput(args.output)) {
|
|
4577
6254
|
return args.output;
|
|
@@ -4592,22 +6269,27 @@ async function generateWithRetry(args) {
|
|
|
4592
6269
|
responseMode: args.responseMode,
|
|
4593
6270
|
jsonResponseFormat: args.request.config.provider.jsonResponseFormat
|
|
4594
6271
|
});
|
|
6272
|
+
const stopPendingNotice = startProviderPendingNotice();
|
|
4595
6273
|
try {
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
6274
|
+
try {
|
|
6275
|
+
return await generate();
|
|
6276
|
+
} catch (error) {
|
|
6277
|
+
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
6278
|
+
if (!isRetriableReason(reason)) {
|
|
6279
|
+
throw error;
|
|
6280
|
+
}
|
|
6281
|
+
if (args.request.config.runtime.verbose) {
|
|
6282
|
+
process.stderr.write(
|
|
6283
|
+
`${pc2.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
|
|
4605
6284
|
`
|
|
4606
|
-
|
|
6285
|
+
);
|
|
6286
|
+
}
|
|
6287
|
+
await delay(RETRY_DELAY_MS);
|
|
4607
6288
|
}
|
|
4608
|
-
await
|
|
6289
|
+
return await generate();
|
|
6290
|
+
} finally {
|
|
6291
|
+
stopPendingNotice();
|
|
4609
6292
|
}
|
|
4610
|
-
return generate();
|
|
4611
6293
|
}
|
|
4612
6294
|
function hasRecognizableTestStatusSignal(input) {
|
|
4613
6295
|
const analysis = analyzeTestStatus(input);
|
|
@@ -4662,11 +6344,22 @@ function buildTestStatusProviderFailureDecision(args) {
|
|
|
4662
6344
|
}
|
|
4663
6345
|
async function runSift(request) {
|
|
4664
6346
|
const prepared = prepareInput(request.stdin, request.config.input);
|
|
6347
|
+
const heuristicInput = prepared.redacted;
|
|
6348
|
+
const heuristicInputTruncated = false;
|
|
6349
|
+
const heuristicPrepared = {
|
|
6350
|
+
...prepared,
|
|
6351
|
+
truncated: heuristicInput,
|
|
6352
|
+
meta: {
|
|
6353
|
+
...prepared.meta,
|
|
6354
|
+
finalLength: heuristicInput.length,
|
|
6355
|
+
truncatedApplied: heuristicInputTruncated
|
|
6356
|
+
}
|
|
6357
|
+
};
|
|
4665
6358
|
const provider = createProvider(request.config);
|
|
4666
|
-
const hasTestStatusSignal = request.policyName === "test-status" && hasRecognizableTestStatusSignal(
|
|
4667
|
-
const testStatusAnalysis = hasTestStatusSignal ? analyzeTestStatus(
|
|
6359
|
+
const hasTestStatusSignal = request.policyName === "test-status" && hasRecognizableTestStatusSignal(heuristicInput);
|
|
6360
|
+
const testStatusAnalysis = hasTestStatusSignal ? analyzeTestStatus(heuristicInput) : null;
|
|
4668
6361
|
const testStatusDecision = hasTestStatusSignal && testStatusAnalysis ? buildTestStatusDiagnoseContract({
|
|
4669
|
-
input:
|
|
6362
|
+
input: heuristicInput,
|
|
4670
6363
|
analysis: testStatusAnalysis,
|
|
4671
6364
|
resolvedTests: request.testStatusContext?.resolvedTests,
|
|
4672
6365
|
remainingTests: request.testStatusContext?.remainingTests
|
|
@@ -4681,7 +6374,7 @@ async function runSift(request) {
|
|
|
4681
6374
|
`
|
|
4682
6375
|
);
|
|
4683
6376
|
}
|
|
4684
|
-
const heuristicOutput = request.policyName === "test-status" ? testStatusDecision?.contract.diagnosis_complete ? testStatusHeuristicOutput : null : applyHeuristicPolicy(request.policyName,
|
|
6377
|
+
const heuristicOutput = request.policyName === "test-status" ? testStatusDecision?.contract.diagnosis_complete ? testStatusHeuristicOutput : null : applyHeuristicPolicy(request.policyName, heuristicInput, request.detail);
|
|
4685
6378
|
if (heuristicOutput) {
|
|
4686
6379
|
if (request.config.runtime.verbose) {
|
|
4687
6380
|
process.stderr.write(`${pc2.dim("sift")} heuristic=${request.policyName}
|
|
@@ -4691,7 +6384,7 @@ async function runSift(request) {
|
|
|
4691
6384
|
question: request.question,
|
|
4692
6385
|
format: request.format,
|
|
4693
6386
|
goal: request.goal,
|
|
4694
|
-
input:
|
|
6387
|
+
input: heuristicInput,
|
|
4695
6388
|
detail: request.detail,
|
|
4696
6389
|
policyName: request.policyName,
|
|
4697
6390
|
outputContract: request.policyName === "test-status" && request.goal === "diagnose" && request.format === "json" ? request.outputContract ?? TEST_STATUS_DIAGNOSE_JSON_CONTRACT : request.outputContract,
|
|
@@ -4711,6 +6404,11 @@ async function runSift(request) {
|
|
|
4711
6404
|
prompt: heuristicPrompt.prompt,
|
|
4712
6405
|
responseMode: heuristicPrompt.responseMode,
|
|
4713
6406
|
prepared,
|
|
6407
|
+
heuristicInput: {
|
|
6408
|
+
length: heuristicInput.length,
|
|
6409
|
+
truncatedApplied: heuristicInputTruncated,
|
|
6410
|
+
strategy: "full-redacted"
|
|
6411
|
+
},
|
|
4714
6412
|
heuristicOutput,
|
|
4715
6413
|
strategy: "heuristic"
|
|
4716
6414
|
});
|
|
@@ -4724,6 +6422,8 @@ async function runSift(request) {
|
|
|
4724
6422
|
logVerboseTestStatusTelemetry({
|
|
4725
6423
|
request,
|
|
4726
6424
|
prepared,
|
|
6425
|
+
heuristicInputChars: heuristicInput.length,
|
|
6426
|
+
heuristicInputTruncated,
|
|
4727
6427
|
contract: testStatusDecision.contract,
|
|
4728
6428
|
finalOutput
|
|
4729
6429
|
});
|
|
@@ -4775,6 +6475,11 @@ async function runSift(request) {
|
|
|
4775
6475
|
prompt: prompt.prompt,
|
|
4776
6476
|
responseMode: prompt.responseMode,
|
|
4777
6477
|
prepared: providerPrepared2,
|
|
6478
|
+
heuristicInput: {
|
|
6479
|
+
length: heuristicInput.length,
|
|
6480
|
+
truncatedApplied: heuristicInputTruncated,
|
|
6481
|
+
strategy: "full-redacted"
|
|
6482
|
+
},
|
|
4778
6483
|
heuristicOutput: testStatusHeuristicOutput,
|
|
4779
6484
|
strategy: "hybrid"
|
|
4780
6485
|
});
|
|
@@ -4788,10 +6493,11 @@ async function runSift(request) {
|
|
|
4788
6493
|
});
|
|
4789
6494
|
const supplement = parseTestStatusProviderSupplement(result.text);
|
|
4790
6495
|
const mergedDecision = buildTestStatusDiagnoseContract({
|
|
4791
|
-
input:
|
|
6496
|
+
input: heuristicInput,
|
|
4792
6497
|
analysis: testStatusAnalysis,
|
|
4793
6498
|
resolvedTests: request.testStatusContext?.resolvedTests,
|
|
4794
6499
|
remainingTests: request.testStatusContext?.remainingTests,
|
|
6500
|
+
providerBucketSupplements: supplement.bucket_supplements,
|
|
4795
6501
|
contractOverrides: {
|
|
4796
6502
|
diagnosis_complete: supplement.diagnosis_complete,
|
|
4797
6503
|
raw_needed: supplement.raw_needed,
|
|
@@ -4813,6 +6519,8 @@ async function runSift(request) {
|
|
|
4813
6519
|
logVerboseTestStatusTelemetry({
|
|
4814
6520
|
request,
|
|
4815
6521
|
prepared,
|
|
6522
|
+
heuristicInputChars: heuristicInput.length,
|
|
6523
|
+
heuristicInputTruncated,
|
|
4816
6524
|
contract: mergedDecision.contract,
|
|
4817
6525
|
finalOutput,
|
|
4818
6526
|
rawSliceChars: rawSlice.text.length,
|
|
@@ -4825,7 +6533,7 @@ async function runSift(request) {
|
|
|
4825
6533
|
const failureDecision = buildTestStatusProviderFailureDecision({
|
|
4826
6534
|
request,
|
|
4827
6535
|
baseDecision: testStatusDecision,
|
|
4828
|
-
input:
|
|
6536
|
+
input: heuristicInput,
|
|
4829
6537
|
analysis: testStatusAnalysis,
|
|
4830
6538
|
reason,
|
|
4831
6539
|
rawSliceUsed: rawSlice.used,
|
|
@@ -4846,6 +6554,8 @@ async function runSift(request) {
|
|
|
4846
6554
|
logVerboseTestStatusTelemetry({
|
|
4847
6555
|
request,
|
|
4848
6556
|
prepared,
|
|
6557
|
+
heuristicInputChars: heuristicInput.length,
|
|
6558
|
+
heuristicInputTruncated,
|
|
4849
6559
|
contract: failureDecision.contract,
|
|
4850
6560
|
finalOutput,
|
|
4851
6561
|
rawSliceChars: rawSlice.text.length,
|
|
@@ -4884,6 +6594,11 @@ async function runSift(request) {
|
|
|
4884
6594
|
prompt: providerPrompt.prompt,
|
|
4885
6595
|
responseMode: providerPrompt.responseMode,
|
|
4886
6596
|
prepared: providerPrepared,
|
|
6597
|
+
heuristicInput: {
|
|
6598
|
+
length: heuristicInput.length,
|
|
6599
|
+
truncatedApplied: heuristicInputTruncated,
|
|
6600
|
+
strategy: "full-redacted"
|
|
6601
|
+
},
|
|
4887
6602
|
heuristicOutput: testStatusDecision ? testStatusHeuristicOutput : null,
|
|
4888
6603
|
strategy: testStatusDecision ? "hybrid" : "provider"
|
|
4889
6604
|
});
|
|
@@ -4925,16 +6640,35 @@ async function runSift(request) {
|
|
|
4925
6640
|
|
|
4926
6641
|
// src/core/testStatusState.ts
|
|
4927
6642
|
import fs5 from "fs";
|
|
4928
|
-
import
|
|
6643
|
+
import path7 from "path";
|
|
4929
6644
|
import { z as z3 } from "zod";
|
|
4930
6645
|
var detailSchema = z3.enum(["standard", "focused", "verbose"]);
|
|
4931
6646
|
var failureBucketTypeSchema = z3.enum([
|
|
4932
6647
|
"shared_environment_blocker",
|
|
4933
6648
|
"fixture_guard_failure",
|
|
6649
|
+
"timeout_failure",
|
|
6650
|
+
"permission_denied_failure",
|
|
6651
|
+
"async_event_loop_failure",
|
|
6652
|
+
"fixture_teardown_failure",
|
|
6653
|
+
"db_migration_failure",
|
|
6654
|
+
"configuration_error",
|
|
6655
|
+
"xdist_worker_crash",
|
|
6656
|
+
"type_error_failure",
|
|
6657
|
+
"resource_leak_warning",
|
|
6658
|
+
"django_db_access_denied",
|
|
6659
|
+
"network_failure",
|
|
6660
|
+
"subprocess_crash_segfault",
|
|
6661
|
+
"flaky_test_detected",
|
|
6662
|
+
"serialization_encoding_failure",
|
|
6663
|
+
"file_not_found_failure",
|
|
6664
|
+
"memory_error",
|
|
6665
|
+
"deprecation_warning_as_error",
|
|
6666
|
+
"xfail_strict_unexpected_pass",
|
|
4934
6667
|
"service_unavailable",
|
|
4935
6668
|
"db_connection_failure",
|
|
4936
6669
|
"auth_bypass_absent",
|
|
4937
6670
|
"contract_snapshot_drift",
|
|
6671
|
+
"snapshot_mismatch",
|
|
4938
6672
|
"import_dependency_failure",
|
|
4939
6673
|
"collection_failure",
|
|
4940
6674
|
"assertion_failure",
|
|
@@ -5036,7 +6770,7 @@ function buildBucketSignature(bucket) {
|
|
|
5036
6770
|
]);
|
|
5037
6771
|
}
|
|
5038
6772
|
function basenameMatches(value, matcher) {
|
|
5039
|
-
return matcher.test(
|
|
6773
|
+
return matcher.test(path7.basename(value));
|
|
5040
6774
|
}
|
|
5041
6775
|
function isPytestExecutable(value) {
|
|
5042
6776
|
return basenameMatches(value, /^pytest(?:\.exe)?$/i);
|
|
@@ -5230,7 +6964,7 @@ function tryReadCachedTestStatusRun(statePath = getDefaultTestStatusStatePath())
|
|
|
5230
6964
|
}
|
|
5231
6965
|
}
|
|
5232
6966
|
function writeCachedTestStatusRun(state, statePath = getDefaultTestStatusStatePath()) {
|
|
5233
|
-
fs5.mkdirSync(
|
|
6967
|
+
fs5.mkdirSync(path7.dirname(statePath), {
|
|
5234
6968
|
recursive: true
|
|
5235
6969
|
});
|
|
5236
6970
|
fs5.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}
|
|
@@ -6039,6 +7773,7 @@ var defaultCliDeps = {
|
|
|
6039
7773
|
configInit,
|
|
6040
7774
|
configSetup,
|
|
6041
7775
|
configShow,
|
|
7776
|
+
configUse,
|
|
6042
7777
|
configValidate,
|
|
6043
7778
|
runDoctor,
|
|
6044
7779
|
listPresets,
|
|
@@ -6143,9 +7878,12 @@ function shouldKeepPresetPolicy(args) {
|
|
|
6143
7878
|
return args.requestedFormat === void 0 || args.requestedFormat === args.presetFormat;
|
|
6144
7879
|
}
|
|
6145
7880
|
function applySharedOptions(command) {
|
|
6146
|
-
return command.option(
|
|
7881
|
+
return command.option(
|
|
7882
|
+
"--provider <provider>",
|
|
7883
|
+
"Provider: openai | openai-compatible | openrouter"
|
|
7884
|
+
).option("--model <model>", "Model name").option("--base-url <url>", "Provider base URL").option(
|
|
6147
7885
|
"--api-key <key>",
|
|
6148
|
-
"Provider API key (or set OPENAI_API_KEY for provider=openai; use SIFT_PROVIDER_API_KEY or endpoint-native envs for openai-compatible)"
|
|
7886
|
+
"Provider API key (or set OPENAI_API_KEY for provider=openai, OPENROUTER_API_KEY for provider=openrouter; use SIFT_PROVIDER_API_KEY or endpoint-native envs for openai-compatible)"
|
|
6149
7887
|
).option(
|
|
6150
7888
|
"--json-response-format <mode>",
|
|
6151
7889
|
"JSON response format mode: auto | on | off"
|
|
@@ -6690,10 +8428,10 @@ function createCliApp(args = {}) {
|
|
|
6690
8428
|
}
|
|
6691
8429
|
throw new Error(`Unknown agent action: ${action}`);
|
|
6692
8430
|
});
|
|
6693
|
-
cli.command("config <action>", "Config commands: setup | init | show | validate").usage("config <setup|init|show|validate> [options]").example("config setup").example("config setup --global").example("config setup --path ~/.config/sift/config.yaml").example("config init").example("config init --global").example("config show").example("config validate --config ./sift.config.yaml").option("--path <path>", "Target config path for init or setup").option(
|
|
8431
|
+
cli.command("config <action> [provider]", "Config commands: setup | init | show | validate | use").usage("config <setup|init|show|validate|use> [provider] [options]").example("config setup").example("config setup --global").example("config setup --path ~/.config/sift/config.yaml").example("config init").example("config init --global").example("config use openrouter").example("config show").example("config validate --config ./sift.config.yaml").option("--path <path>", "Target config path for init or setup").option(
|
|
6694
8432
|
"--global",
|
|
6695
8433
|
"Use the machine-wide config path (~/.config/sift/config.yaml) for init or setup"
|
|
6696
|
-
).option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action(async (action, options) => {
|
|
8434
|
+
).option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action(async (action, provider, options) => {
|
|
6697
8435
|
if (action === "setup") {
|
|
6698
8436
|
process.exitCode = await deps.configSetup({
|
|
6699
8437
|
targetPath: options.path,
|
|
@@ -6716,6 +8454,13 @@ function createCliApp(args = {}) {
|
|
|
6716
8454
|
deps.configValidate(options.config);
|
|
6717
8455
|
return;
|
|
6718
8456
|
}
|
|
8457
|
+
if (action === "use") {
|
|
8458
|
+
if (!provider) {
|
|
8459
|
+
throw new Error("Missing provider name.");
|
|
8460
|
+
}
|
|
8461
|
+
deps.configUse(provider, options.config, env);
|
|
8462
|
+
return;
|
|
8463
|
+
}
|
|
6719
8464
|
throw new Error(`Unknown config action: ${action}`);
|
|
6720
8465
|
});
|
|
6721
8466
|
cli.command("doctor", "Check which config is active and whether local setup looks complete").usage("doctor [options]").option("--config <path>", "Path to config file").action((options) => {
|