@emseepea/testing 0.26.14 → 0.26.16
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/package.json +2 -2
- package/semantic/provider.mjs +85 -24
- package/semantic/test.mjs +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emseepea/testing",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.16",
|
|
4
4
|
"description": "MCP integration and semantic testing helpers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"test:llm:built": "emseepea-test eval"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@emseepea/server": "0.
|
|
60
|
+
"@emseepea/server": "0.26.1",
|
|
61
61
|
"@modelcontextprotocol/client": "2.3.1",
|
|
62
62
|
"@modelcontextprotocol/server": "2.3.1",
|
|
63
63
|
"@modelcontextprotocol/core": "2.3.1"
|
package/semantic/provider.mjs
CHANGED
|
@@ -25,15 +25,28 @@ const providerErrorMessages = new Map([
|
|
|
25
25
|
["error_max_turns", "Model command exceeded its turn limit"],
|
|
26
26
|
]);
|
|
27
27
|
const authenticationFailure = /not logged in|failed to authenticate|oauth session expired/i;
|
|
28
|
-
const
|
|
28
|
+
const providerFailureCategories = new Set([
|
|
29
29
|
"authentication",
|
|
30
|
+
"billing",
|
|
30
31
|
"configuration",
|
|
31
32
|
"host-storage",
|
|
32
33
|
"permission-or-sandbox",
|
|
33
34
|
"provider",
|
|
35
|
+
"rate-limit",
|
|
34
36
|
"transport",
|
|
35
37
|
"unknown",
|
|
36
38
|
]);
|
|
39
|
+
const claudeAssistantFailureCategories = new Map([
|
|
40
|
+
["authentication_failed", "authentication"],
|
|
41
|
+
["oauth_org_not_allowed", "authentication"],
|
|
42
|
+
["account_on_hold", "provider"],
|
|
43
|
+
["billing_error", "billing"],
|
|
44
|
+
["rate_limit", "rate-limit"],
|
|
45
|
+
["overloaded", "provider"],
|
|
46
|
+
["invalid_request", "configuration"],
|
|
47
|
+
["model_not_found", "provider"],
|
|
48
|
+
["server_error", "provider"],
|
|
49
|
+
]);
|
|
37
50
|
const stderrLimit = 32 * 1024;
|
|
38
51
|
const forbiddenItemLimit = 64;
|
|
39
52
|
const hasAuthenticationFailure = (events) => events.some((event) => (
|
|
@@ -81,7 +94,7 @@ export function providerSettings(provider) {
|
|
|
81
94
|
: { effort: "low", maxTurns: null, judgeMaxTurns: 4, permissionMode: "dontAsk", userSettings: false };
|
|
82
95
|
}
|
|
83
96
|
|
|
84
|
-
export function parseClaudeEvents(stdout, processExitCode = 0) {
|
|
97
|
+
export function parseClaudeEvents(stdout, processExitCode = 0, failureCategory) {
|
|
85
98
|
let events;
|
|
86
99
|
try {
|
|
87
100
|
events = stdout.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
@@ -93,11 +106,15 @@ export function parseClaudeEvents(stdout, processExitCode = 0) {
|
|
|
93
106
|
const toolUses = events.flatMap(({ message }) => (
|
|
94
107
|
Array.isArray(message?.content) ? message.content.filter(({ type }) => type === "tool_use") : []
|
|
95
108
|
));
|
|
96
|
-
if (hasAuthenticationFailure(events))
|
|
97
|
-
|
|
109
|
+
if (hasAuthenticationFailure(events)) {
|
|
110
|
+
throw providerEvidenceError("Model command is not signed in", "authentication");
|
|
111
|
+
}
|
|
112
|
+
if (processExitCode !== 0) {
|
|
113
|
+
throw providerProcessError(processExitCode, claudeFailureCategory(events, result, failureCategory));
|
|
114
|
+
}
|
|
98
115
|
if (!result) throw new Error("Model command omitted its result event");
|
|
99
116
|
if (result.is_error) {
|
|
100
|
-
throw
|
|
117
|
+
throw claudeResultError(events, result);
|
|
101
118
|
}
|
|
102
119
|
if (typeof answer !== "string") throw new Error("Model command returned a non-text answer");
|
|
103
120
|
if (toolUses.length > 0) {
|
|
@@ -172,10 +189,12 @@ export function parseNativeClaudeEvents(stdout, advertisedTools, requireInit = f
|
|
|
172
189
|
responseSha256: hash(response.content),
|
|
173
190
|
};
|
|
174
191
|
});
|
|
175
|
-
if (hasAuthenticationFailure(events))
|
|
192
|
+
if (hasAuthenticationFailure(events)) {
|
|
193
|
+
throw providerEvidenceError("Model command is not signed in", "authentication");
|
|
194
|
+
}
|
|
176
195
|
if (!result) throw new Error("Model command omitted its result event");
|
|
177
196
|
if (result.is_error) {
|
|
178
|
-
throw
|
|
197
|
+
throw claudeResultError(events, result);
|
|
179
198
|
}
|
|
180
199
|
if (typeof result.result !== "string") throw new Error("Model command returned a non-text answer");
|
|
181
200
|
if ((result.permission_denials?.length ?? 0) > 0) {
|
|
@@ -425,12 +444,12 @@ export async function runModel(provider, prompt, directory, signal) {
|
|
|
425
444
|
if (execution.errorCode === "ABORT_ERR") throw new Error("Model command was cancelled");
|
|
426
445
|
if (execution.errorCode) throw new Error("Model command could not start");
|
|
427
446
|
if (execution.code !== 0 && !execution.stdout) {
|
|
428
|
-
throw
|
|
447
|
+
throw providerProcessError(execution.code, execution.providerFailureCategory);
|
|
429
448
|
}
|
|
430
449
|
return provider.startsWith("codex-")
|
|
431
450
|
? parseCodexEvents(execution.stdout, [], undefined, execution.code,
|
|
432
451
|
providerModel(provider), invocation.providerSecrets, execution.providerFailureCategory)
|
|
433
|
-
: parseClaudeEvents(execution.stdout, execution.code);
|
|
452
|
+
: parseClaudeEvents(execution.stdout, execution.code, execution.providerFailureCategory);
|
|
434
453
|
}
|
|
435
454
|
|
|
436
455
|
export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, processExitCode = 0,
|
|
@@ -452,12 +471,9 @@ export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, proc
|
|
|
452
471
|
const errorText = events.flatMap((event) => [event.message, event.error?.message, event.item?.message])
|
|
453
472
|
.filter((value) => typeof value === "string").join("\n");
|
|
454
473
|
if (authentication.test(errorText)) {
|
|
455
|
-
throw
|
|
474
|
+
throw providerEvidenceError("Model command is not signed in", "authentication");
|
|
456
475
|
}
|
|
457
|
-
|
|
458
|
-
&& (item.status === "failed" || item.result?.isError === true || item.result?.is_error === true));
|
|
459
|
-
if (failedTool) throw new Error("Model command reported a failed MCP tool call");
|
|
460
|
-
if (processExitCode !== 0) throw codexProcessError(processExitCode, providerFailureCategory);
|
|
476
|
+
if (processExitCode !== 0) throw providerProcessError(processExitCode, providerFailureCategory);
|
|
461
477
|
if (events.some(({ type }) => type === "turn.failed" || type === "error")) {
|
|
462
478
|
throw new Error("Model command reported an error");
|
|
463
479
|
}
|
|
@@ -479,6 +495,20 @@ export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, proc
|
|
|
479
495
|
const forbidden = completed.filter((item) => !["agent_message", "reasoning", "mcp_tool_call"].includes(item?.type));
|
|
480
496
|
if (forbidden.length) throw forbiddenItemError(forbidden);
|
|
481
497
|
const toolItems = completed.filter(({ type }) => type === "mcp_tool_call");
|
|
498
|
+
const terminalAnswerIndex = completed.findLastIndex(({ type }) => type === "agent_message");
|
|
499
|
+
const codexApplicationError = (item) => item.status === "failed"
|
|
500
|
+
&& item.server === mcpServerName && advertised.has(item.tool)
|
|
501
|
+
&& item.arguments && typeof item.arguments === "object" && !Array.isArray(item.arguments)
|
|
502
|
+
&& item.result && typeof item.result === "object" && !Array.isArray(item.result)
|
|
503
|
+
&& Object.keys(item.result).sort().join(",") === "content,structured_content"
|
|
504
|
+
&& item.result.structured_content === null
|
|
505
|
+
&& Array.isArray(item.result.content) && item.result.content.length > 0
|
|
506
|
+
&& item.result.content.every((block) => block && block.type === "text"
|
|
507
|
+
&& typeof block.text === "string" && block.text.length > 0)
|
|
508
|
+
&& completed.indexOf(item) < terminalAnswerIndex;
|
|
509
|
+
if (toolItems.some((item) => item.status === "failed" && !codexApplicationError(item))) {
|
|
510
|
+
throw new Error("Model command reported a failed MCP tool call");
|
|
511
|
+
}
|
|
482
512
|
const resourceItems = resourceCatalogue ? toolItems.flatMap((item) => {
|
|
483
513
|
const operation = resourceHelperOperation(item, resourceCatalogue);
|
|
484
514
|
return operation ? [{ item, operation }] : [];
|
|
@@ -495,8 +525,10 @@ export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, proc
|
|
|
495
525
|
|| !item.arguments || typeof item.arguments !== "object" || Array.isArray(item.arguments)) {
|
|
496
526
|
throw forbiddenItemError([item]);
|
|
497
527
|
}
|
|
498
|
-
|
|
499
|
-
|| item.result
|
|
528
|
+
const applicationError = codexApplicationError(item)
|
|
529
|
+
|| item.result?.isError === true || item.result?.is_error === true;
|
|
530
|
+
if (!item.result || typeof item.result !== "object"
|
|
531
|
+
|| (item.status !== "completed" && !(item.status === "failed" && applicationError))) {
|
|
500
532
|
throw new Error("Model command reported a failed MCP tool call");
|
|
501
533
|
}
|
|
502
534
|
return { name: item.tool, arguments: item.arguments };
|
|
@@ -504,7 +536,11 @@ export function parseCodexEvents(stdout, advertisedTools, expectedThreadId, proc
|
|
|
504
536
|
const messages = completed.filter(({ type }) => type === "agent_message");
|
|
505
537
|
const answer = messages.at(-1)?.text;
|
|
506
538
|
if (typeof answer !== "string") throw new Error("Model command returned no answer");
|
|
507
|
-
const toolResults = applicationToolItems.map((
|
|
539
|
+
const toolResults = applicationToolItems.map((item) => ({
|
|
540
|
+
content: item.result.content ?? item.result,
|
|
541
|
+
isError: item.result.isError === true || item.result.is_error === true
|
|
542
|
+
|| codexApplicationError(item),
|
|
543
|
+
}));
|
|
508
544
|
return {
|
|
509
545
|
answer,
|
|
510
546
|
calls,
|
|
@@ -667,7 +703,7 @@ function runProcess(command, args, options) {
|
|
|
667
703
|
if (settled) return;
|
|
668
704
|
settled = true;
|
|
669
705
|
clearTimeout(timer);
|
|
670
|
-
const providerFailureCategory =
|
|
706
|
+
const providerFailureCategory = classifyProviderFailure(stderr.toString("utf8"));
|
|
671
707
|
stderr = Buffer.alloc(0);
|
|
672
708
|
resolve({ ...result, providerFailureCategory });
|
|
673
709
|
};
|
|
@@ -680,7 +716,7 @@ function runProcess(command, args, options) {
|
|
|
680
716
|
});
|
|
681
717
|
}
|
|
682
718
|
|
|
683
|
-
function
|
|
719
|
+
function classifyProviderFailure(stderr) {
|
|
684
720
|
if (/no space left|disk (?:is )?full|storage (?:capacity|quota)|\benospc\b/i.test(stderr)) {
|
|
685
721
|
return "host-storage";
|
|
686
722
|
}
|
|
@@ -696,20 +732,45 @@ function classifyCodexFailure(stderr) {
|
|
|
696
732
|
if (/connection (?:refused|reset)|network|transport|timed? out|\btls\b|certificate|\bdns\b|\beconn/i.test(stderr)) {
|
|
697
733
|
return "transport";
|
|
698
734
|
}
|
|
699
|
-
if (/provider (?:error|failure|unavailable)|model (?:is )?(?:unavailable|overloaded)|rate limit|service unavailable/i.test(stderr)) {
|
|
735
|
+
if (/provider (?:error|failure|unavailable)|model (?:is )?(?:unavailable|overloaded)|rate limit|service unavailable|(?:hit|reached) your limit/i.test(stderr)) {
|
|
700
736
|
return "provider";
|
|
701
737
|
}
|
|
702
738
|
return "unknown";
|
|
703
739
|
}
|
|
704
740
|
|
|
705
|
-
function
|
|
741
|
+
function claudeFailureCategory(events, result, fallback) {
|
|
742
|
+
const end = result ? events.lastIndexOf(result) : events.length;
|
|
743
|
+
const assistantFailure = events.findLast((event, index) => (
|
|
744
|
+
index < end
|
|
745
|
+
&& event.type === "assistant"
|
|
746
|
+
&& (event.parent_tool_use_id ?? null) === null
|
|
747
|
+
&& (event.agent_id ?? null) === null
|
|
748
|
+
&& claudeAssistantFailureCategories.has(event.error)
|
|
749
|
+
));
|
|
750
|
+
if (assistantFailure) return claudeAssistantFailureCategories.get(assistantFailure.error);
|
|
751
|
+
if (!result?.is_error) return fallback;
|
|
752
|
+
const errors = Array.isArray(result.errors) ? result.errors : [];
|
|
753
|
+
for (const detail of [result.result, ...errors]) {
|
|
754
|
+
if (typeof detail !== "string") continue;
|
|
755
|
+
const category = classifyProviderFailure(detail);
|
|
756
|
+
if (category !== "unknown") return category;
|
|
757
|
+
}
|
|
758
|
+
return fallback;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function claudeResultError(events, result) {
|
|
762
|
+
const message = providerErrorMessages.get(result.subtype) ?? "Model command reported an error";
|
|
763
|
+
return providerEvidenceError(message, claudeFailureCategory(events, result));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function providerEvidenceError(message, category) {
|
|
706
767
|
return Object.assign(new Error(message), {
|
|
707
|
-
providerFailureCategory:
|
|
768
|
+
providerFailureCategory: providerFailureCategories.has(category) ? category : "unknown",
|
|
708
769
|
});
|
|
709
770
|
}
|
|
710
771
|
|
|
711
|
-
function
|
|
712
|
-
return
|
|
772
|
+
function providerProcessError(code, category) {
|
|
773
|
+
return providerEvidenceError(`Model command exited ${code}`, category);
|
|
713
774
|
}
|
|
714
775
|
|
|
715
776
|
function forbiddenItemError(items) {
|
package/semantic/test.mjs
CHANGED
|
@@ -606,10 +606,12 @@ function recordModelFailure(record, error) {
|
|
|
606
606
|
const category = error?.providerFailureCategory;
|
|
607
607
|
if ([
|
|
608
608
|
"authentication",
|
|
609
|
+
"billing",
|
|
609
610
|
"configuration",
|
|
610
611
|
"host-storage",
|
|
611
612
|
"permission-or-sandbox",
|
|
612
613
|
"provider",
|
|
614
|
+
"rate-limit",
|
|
613
615
|
"transport",
|
|
614
616
|
"unknown",
|
|
615
617
|
].includes(category)) record.providerFailureCategory = category;
|