@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emseepea/testing",
3
- "version": "0.26.14",
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.25.10",
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"
@@ -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 codexFailureCategories = new Set([
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)) throw new Error("Model command is not signed in");
97
- if (processExitCode !== 0) throw new Error(`Model command exited ${processExitCode}`);
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 new Error(providerErrorMessages.get(result.subtype) ?? "Model command reported an error");
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)) throw new Error("Model command is not signed in");
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 new Error(providerErrorMessages.get(result.subtype) ?? "Model command reported an error");
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 codexProcessError(execution.code, execution.providerFailureCategory);
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 codexEvidenceError("Model command is not signed in", "authentication");
474
+ throw providerEvidenceError("Model command is not signed in", "authentication");
456
475
  }
457
- const failedTool = events.some(({ item }) => item?.type === "mcp_tool_call"
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
- if (!item.result || typeof item.result !== "object" || item.status === "failed"
499
- || item.result.isError === true || item.result.is_error === true) {
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(({ result }) => ({ content: result.content ?? result, isError: false }));
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 = classifyCodexFailure(stderr.toString("utf8"));
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 classifyCodexFailure(stderr) {
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 codexEvidenceError(message, category) {
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: codexFailureCategories.has(category) ? category : "unknown",
768
+ providerFailureCategory: providerFailureCategories.has(category) ? category : "unknown",
708
769
  });
709
770
  }
710
771
 
711
- function codexProcessError(code, category) {
712
- return codexEvidenceError(`Model command exited ${code}`, category);
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;