@emseepea/testing 0.26.15 → 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.15",
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.26.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"
@@ -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,9 +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
- if (processExitCode !== 0) throw codexProcessError(processExitCode, providerFailureCategory);
476
+ if (processExitCode !== 0) throw providerProcessError(processExitCode, providerFailureCategory);
458
477
  if (events.some(({ type }) => type === "turn.failed" || type === "error")) {
459
478
  throw new Error("Model command reported an error");
460
479
  }
@@ -684,7 +703,7 @@ function runProcess(command, args, options) {
684
703
  if (settled) return;
685
704
  settled = true;
686
705
  clearTimeout(timer);
687
- const providerFailureCategory = classifyCodexFailure(stderr.toString("utf8"));
706
+ const providerFailureCategory = classifyProviderFailure(stderr.toString("utf8"));
688
707
  stderr = Buffer.alloc(0);
689
708
  resolve({ ...result, providerFailureCategory });
690
709
  };
@@ -697,7 +716,7 @@ function runProcess(command, args, options) {
697
716
  });
698
717
  }
699
718
 
700
- function classifyCodexFailure(stderr) {
719
+ function classifyProviderFailure(stderr) {
701
720
  if (/no space left|disk (?:is )?full|storage (?:capacity|quota)|\benospc\b/i.test(stderr)) {
702
721
  return "host-storage";
703
722
  }
@@ -713,20 +732,45 @@ function classifyCodexFailure(stderr) {
713
732
  if (/connection (?:refused|reset)|network|transport|timed? out|\btls\b|certificate|\bdns\b|\beconn/i.test(stderr)) {
714
733
  return "transport";
715
734
  }
716
- 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)) {
717
736
  return "provider";
718
737
  }
719
738
  return "unknown";
720
739
  }
721
740
 
722
- 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) {
723
767
  return Object.assign(new Error(message), {
724
- providerFailureCategory: codexFailureCategories.has(category) ? category : "unknown",
768
+ providerFailureCategory: providerFailureCategories.has(category) ? category : "unknown",
725
769
  });
726
770
  }
727
771
 
728
- function codexProcessError(code, category) {
729
- return codexEvidenceError(`Model command exited ${code}`, category);
772
+ function providerProcessError(code, category) {
773
+ return providerEvidenceError(`Model command exited ${code}`, category);
730
774
  }
731
775
 
732
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;