@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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 +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
|
@@ -3,149 +3,149 @@ import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vite
|
|
|
3
3
|
import { createDeferred, createDeferredQueue, nextTick } from "./helpers/deferred.js";
|
|
4
4
|
import { clearMockAccounts, createFetchHarness } from "./helpers/plugin-fetch-harness.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
contentBlockStartEvent,
|
|
7
|
+
contentBlockStopEvent,
|
|
8
|
+
encodeSSEStream,
|
|
9
|
+
makeSSEResponse,
|
|
10
|
+
messageDeltaEvent,
|
|
11
|
+
messageStartEvent,
|
|
12
|
+
messageStopEvent,
|
|
13
13
|
} from "./helpers/sse.js";
|
|
14
14
|
|
|
15
15
|
vi.mock("node:readline/promises", () => ({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
createInterface: vi.fn(() => ({
|
|
17
|
+
question: vi.fn().mockResolvedValue("a"),
|
|
18
|
+
close: vi.fn(),
|
|
19
|
+
})),
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
22
|
vi.mock("../storage.js", () => ({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
createDefaultStats: (now?: number) => ({
|
|
24
|
+
requests: 0,
|
|
25
|
+
inputTokens: 0,
|
|
26
|
+
outputTokens: 0,
|
|
27
|
+
cacheReadTokens: 0,
|
|
28
|
+
cacheWriteTokens: 0,
|
|
29
|
+
lastReset: now ?? Date.now(),
|
|
30
|
+
}),
|
|
31
|
+
loadAccounts: vi.fn().mockResolvedValue(null),
|
|
32
|
+
saveAccounts: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
clearAccounts: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
getStoragePath: vi.fn(() => "/tmp/test-accounts.json"),
|
|
35
35
|
}));
|
|
36
36
|
|
|
37
37
|
vi.mock("../config.js", () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
38
|
+
const DEFAULT_CONFIG = {
|
|
39
|
+
account_selection_strategy: "sticky",
|
|
40
|
+
failure_ttl_seconds: 3600,
|
|
41
|
+
debug: false,
|
|
42
|
+
signature_emulation: {
|
|
43
|
+
enabled: true,
|
|
44
|
+
fetch_claude_code_version_on_startup: false,
|
|
45
|
+
prompt_compaction: "minimal",
|
|
46
|
+
},
|
|
47
|
+
override_model_limits: {
|
|
48
|
+
enabled: false,
|
|
49
|
+
context: 1_000_000,
|
|
50
|
+
output: 0,
|
|
51
|
+
},
|
|
52
|
+
custom_betas: [],
|
|
53
|
+
health_score: {
|
|
54
|
+
initial: 70,
|
|
55
|
+
success_reward: 1,
|
|
56
|
+
rate_limit_penalty: -10,
|
|
57
|
+
failure_penalty: -20,
|
|
58
|
+
recovery_rate_per_hour: 2,
|
|
59
|
+
min_usable: 50,
|
|
60
|
+
max_score: 100,
|
|
61
|
+
},
|
|
62
|
+
token_bucket: {
|
|
63
|
+
max_tokens: 50,
|
|
64
|
+
regeneration_rate_per_minute: 6,
|
|
65
|
+
initial_tokens: 50,
|
|
66
|
+
},
|
|
67
|
+
toasts: {
|
|
68
|
+
quiet: true,
|
|
69
|
+
debounce_seconds: 30,
|
|
70
|
+
},
|
|
71
|
+
headers: {},
|
|
72
|
+
idle_refresh: {
|
|
73
|
+
enabled: false,
|
|
74
|
+
window_minutes: 60,
|
|
75
|
+
min_interval_minutes: 30,
|
|
76
|
+
},
|
|
77
|
+
cc_credential_reuse: {
|
|
78
|
+
enabled: false,
|
|
79
|
+
auto_detect: false,
|
|
80
|
+
prefer_over_oauth: false,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const createBaseConfig = () => ({
|
|
85
|
+
...DEFAULT_CONFIG,
|
|
86
|
+
signature_emulation: { ...DEFAULT_CONFIG.signature_emulation },
|
|
87
|
+
override_model_limits: { ...DEFAULT_CONFIG.override_model_limits },
|
|
88
|
+
custom_betas: [...DEFAULT_CONFIG.custom_betas],
|
|
89
|
+
health_score: { ...DEFAULT_CONFIG.health_score },
|
|
90
|
+
token_bucket: { ...DEFAULT_CONFIG.token_bucket },
|
|
91
|
+
toasts: { ...DEFAULT_CONFIG.toasts },
|
|
92
|
+
headers: { ...DEFAULT_CONFIG.headers },
|
|
93
|
+
idle_refresh: { ...DEFAULT_CONFIG.idle_refresh },
|
|
94
|
+
cc_credential_reuse: { ...DEFAULT_CONFIG.cc_credential_reuse },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
CLIENT_ID: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
99
|
+
DEFAULT_CONFIG,
|
|
100
|
+
VALID_STRATEGIES: ["sticky", "round-robin", "hybrid"],
|
|
101
|
+
loadConfig: vi.fn(() => createBaseConfig()),
|
|
102
|
+
loadConfigFresh: vi.fn(() => createBaseConfig()),
|
|
103
|
+
saveConfig: vi.fn(),
|
|
104
|
+
getConfigDir: vi.fn(() => "/tmp/test-config"),
|
|
105
|
+
};
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
vi.mock("../cc-credentials.js", () => ({
|
|
109
|
-
|
|
109
|
+
readCCCredentials: vi.fn(() => []),
|
|
110
110
|
}));
|
|
111
111
|
|
|
112
112
|
vi.mock("../refresh-lock.js", () => ({
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
acquireRefreshLock: vi.fn().mockResolvedValue({
|
|
114
|
+
acquired: true,
|
|
115
|
+
lockPath: "/tmp/opencode-test.lock",
|
|
116
|
+
owner: null,
|
|
117
|
+
lockInode: null,
|
|
118
|
+
}),
|
|
119
|
+
releaseRefreshLock: vi.fn().mockResolvedValue(undefined),
|
|
120
120
|
}));
|
|
121
121
|
|
|
122
122
|
vi.mock("../oauth.js", () => ({
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
authorize: vi.fn(),
|
|
124
|
+
exchange: vi.fn(),
|
|
125
|
+
refreshToken: vi.fn(),
|
|
126
126
|
}));
|
|
127
127
|
|
|
128
128
|
vi.mock("@clack/prompts", () => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
129
|
+
const noop = vi.fn();
|
|
130
|
+
return {
|
|
131
|
+
text: vi.fn().mockResolvedValue(""),
|
|
132
|
+
confirm: vi.fn().mockResolvedValue(false),
|
|
133
|
+
select: vi.fn().mockResolvedValue("cancel"),
|
|
134
|
+
spinner: vi.fn(() => ({ start: noop, stop: noop, message: noop })),
|
|
135
|
+
intro: noop,
|
|
136
|
+
outro: noop,
|
|
137
|
+
isCancel: vi.fn().mockReturnValue(false),
|
|
138
|
+
log: {
|
|
139
|
+
info: noop,
|
|
140
|
+
success: noop,
|
|
141
|
+
warn: noop,
|
|
142
|
+
error: noop,
|
|
143
|
+
message: noop,
|
|
144
|
+
step: noop,
|
|
145
|
+
},
|
|
146
|
+
note: noop,
|
|
147
|
+
cancel: noop,
|
|
148
|
+
};
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
import { refreshToken } from "../oauth.js";
|
|
@@ -153,559 +153,569 @@ import { refreshToken } from "../oauth.js";
|
|
|
153
153
|
const mockRefreshToken = refreshToken as Mock;
|
|
154
154
|
|
|
155
155
|
function jsonResponse(payload: unknown, init: ResponseInit = {}): Response {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
return new Response(JSON.stringify(payload), {
|
|
157
|
+
status: init.status ?? 200,
|
|
158
|
+
headers: {
|
|
159
|
+
"content-type": "application/json",
|
|
160
|
+
...(init.headers as Record<string, string> | undefined),
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
function rateLimitResponse(message = "rate limit exceeded"): Response {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
166
|
+
return jsonResponse(
|
|
167
|
+
{
|
|
168
|
+
error: {
|
|
169
|
+
type: "rate_limit_error",
|
|
170
|
+
message,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{ status: 429 },
|
|
174
|
+
);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
function makeRequestBody(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
options: {
|
|
179
|
+
toolName?: string;
|
|
180
|
+
historicalToolName?: string;
|
|
181
|
+
text?: string;
|
|
182
|
+
} = {},
|
|
183
183
|
): string {
|
|
184
|
-
|
|
184
|
+
const messages: Array<Record<string, unknown>> = [];
|
|
185
|
+
|
|
186
|
+
if (options.historicalToolName) {
|
|
187
|
+
messages.push({
|
|
188
|
+
role: "assistant",
|
|
189
|
+
content: [
|
|
190
|
+
{
|
|
191
|
+
type: "tool_use",
|
|
192
|
+
id: `tool_${options.historicalToolName}`,
|
|
193
|
+
name: options.historicalToolName,
|
|
194
|
+
input: { from: options.text ?? "history" },
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
185
199
|
|
|
186
|
-
if (options.historicalToolName) {
|
|
187
200
|
messages.push({
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
201
|
+
role: "user",
|
|
202
|
+
content: options.text ?? "parallel test",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return JSON.stringify({
|
|
206
|
+
model: "claude-sonnet",
|
|
207
|
+
max_tokens: 128,
|
|
208
|
+
messages,
|
|
209
|
+
...(options.toolName
|
|
210
|
+
? {
|
|
211
|
+
tools: [
|
|
212
|
+
{
|
|
213
|
+
name: options.toolName,
|
|
214
|
+
description: "Parallel test tool",
|
|
215
|
+
input_schema: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: {
|
|
218
|
+
id: { type: "number" },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
}
|
|
224
|
+
: {}),
|
|
197
225
|
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
messages.push({
|
|
201
|
-
role: "user",
|
|
202
|
-
content: options.text ?? "parallel test",
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
return JSON.stringify({
|
|
206
|
-
model: "claude-sonnet",
|
|
207
|
-
max_tokens: 128,
|
|
208
|
-
messages,
|
|
209
|
-
...(options.toolName
|
|
210
|
-
? {
|
|
211
|
-
tools: [
|
|
212
|
-
{
|
|
213
|
-
name: options.toolName,
|
|
214
|
-
description: "Parallel test tool",
|
|
215
|
-
input_schema: {
|
|
216
|
-
type: "object",
|
|
217
|
-
properties: {
|
|
218
|
-
id: { type: "number" },
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
],
|
|
223
|
-
}
|
|
224
|
-
: {}),
|
|
225
|
-
});
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
function parseSentBody(call: unknown[]): Record<string, any> {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
229
|
+
const init = call[1] as RequestInit | undefined;
|
|
230
|
+
if (typeof init?.body !== "string") {
|
|
231
|
+
throw new Error(`Expected string body, received ${typeof init?.body}`);
|
|
232
|
+
}
|
|
233
|
+
return JSON.parse(init.body);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
function callHeaders(call: unknown[]): Headers {
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
const init = call[1] as RequestInit | undefined;
|
|
238
|
+
return new Headers(init?.headers as HeadersInit | undefined);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
function callUrl(call: unknown[]): string {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
242
|
+
const input = call[0];
|
|
243
|
+
if (typeof input === "string") return input;
|
|
244
|
+
if (input instanceof URL) return input.toString();
|
|
245
|
+
if (input instanceof Request) return input.url;
|
|
246
|
+
return String(input);
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
function makeToolUseSseResponse(prefixedName: string): Response {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
250
|
+
return makeSSEResponse(
|
|
251
|
+
encodeSSEStream([
|
|
252
|
+
messageStartEvent(),
|
|
253
|
+
contentBlockStartEvent(0, {
|
|
254
|
+
content_block: {
|
|
255
|
+
type: "tool_use",
|
|
256
|
+
id: "tool_stream_1",
|
|
257
|
+
name: prefixedName,
|
|
258
|
+
input: { ok: true },
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
contentBlockStopEvent(0),
|
|
262
|
+
messageDeltaEvent({
|
|
263
|
+
delta: { stop_reason: "tool_use", stop_sequence: null },
|
|
264
|
+
usage: { output_tokens: 1 },
|
|
265
|
+
}),
|
|
266
|
+
messageStopEvent(),
|
|
267
|
+
]),
|
|
268
|
+
);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
describe("index.parallel RED", () => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
afterEach(() => {
|
|
283
|
-
clearMockAccounts();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("intercepts 50 concurrent requests without double-prefixing historical tool definitions", async () => {
|
|
287
|
-
const harness = await createFetchHarness();
|
|
288
|
-
const queue = createDeferredQueue<Response>();
|
|
289
|
-
|
|
290
|
-
harness.mockFetch.mockImplementation(() => queue.enqueue().promise);
|
|
291
|
-
|
|
292
|
-
const requests = Array.from({ length: 50 }, (_, index) =>
|
|
293
|
-
harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
294
|
-
method: "POST",
|
|
295
|
-
headers: { "content-type": "application/json" },
|
|
296
|
-
body: makeRequestBody({
|
|
297
|
-
toolName: `mcp_parallel_tool_${index}`,
|
|
298
|
-
text: `fan-out-${index}`,
|
|
299
|
-
}),
|
|
300
|
-
}),
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
await harness.waitFor(() => expect(queue.pending).toBe(50), 1000);
|
|
304
|
-
|
|
305
|
-
for (let index = 0; index < 50; index += 1) {
|
|
306
|
-
queue.resolveNext(jsonResponse({ id: `msg_${index}`, content: [{ type: "text", text: `ok-${index}` }] }));
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const responses = await Promise.all(requests);
|
|
310
|
-
await Promise.all(responses.map((response) => response.json()));
|
|
311
|
-
|
|
312
|
-
const sentBodies = harness.mockFetch.mock.calls.map((call) => parseSentBody(call));
|
|
313
|
-
sentBodies.forEach((body, index) => {
|
|
314
|
-
expect(body.tools[0].name).toBe(`mcp_mcp_parallel_tool_${index}`);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
harness.tearDown();
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it("keeps per-request tool definition state isolated under concurrent mixed prefix load", async () => {
|
|
321
|
-
const harness = await createFetchHarness();
|
|
322
|
-
const toolNames = [
|
|
323
|
-
"read_file",
|
|
324
|
-
"mcp_existing_read",
|
|
325
|
-
"write_file",
|
|
326
|
-
"mcp_existing_write",
|
|
327
|
-
"list_files",
|
|
328
|
-
"mcp_existing_list",
|
|
329
|
-
];
|
|
330
|
-
|
|
331
|
-
harness.mockFetch.mockImplementation(() => jsonResponse({ id: "msg_mixed", content: [] }));
|
|
332
|
-
|
|
333
|
-
await Promise.all(
|
|
334
|
-
toolNames.map((toolName) =>
|
|
335
|
-
harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
336
|
-
method: "POST",
|
|
337
|
-
headers: { "content-type": "application/json" },
|
|
338
|
-
body: makeRequestBody({ toolName, text: toolName }),
|
|
339
|
-
}),
|
|
340
|
-
),
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
const transformedNames = harness.mockFetch.mock.calls.map((call) => parseSentBody(call).tools[0].name);
|
|
344
|
-
expect(transformedNames).toEqual([
|
|
345
|
-
"mcp_read_file",
|
|
346
|
-
"mcp_mcp_existing_read",
|
|
347
|
-
"mcp_write_file",
|
|
348
|
-
"mcp_mcp_existing_write",
|
|
349
|
-
"mcp_list_files",
|
|
350
|
-
"mcp_mcp_existing_list",
|
|
351
|
-
]);
|
|
352
|
-
|
|
353
|
-
harness.tearDown();
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it("keeps historical tool_use blocks isolated under concurrent request fan-out", async () => {
|
|
357
|
-
const harness = await createFetchHarness();
|
|
358
|
-
|
|
359
|
-
harness.mockFetch.mockImplementation(() => jsonResponse({ id: "msg_history", content: [] }));
|
|
360
|
-
|
|
361
|
-
await Promise.all(
|
|
362
|
-
Array.from({ length: 12 }, (_, index) =>
|
|
363
|
-
harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
364
|
-
method: "POST",
|
|
365
|
-
headers: { "content-type": "application/json" },
|
|
366
|
-
body: makeRequestBody({
|
|
367
|
-
historicalToolName: `mcp_history_tool_${index}`,
|
|
368
|
-
text: `history-${index}`,
|
|
369
|
-
}),
|
|
370
|
-
}),
|
|
371
|
-
),
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
const transformedNames = harness.mockFetch.mock.calls.map(
|
|
375
|
-
(call) => parseSentBody(call).messages[0].content[0].name,
|
|
376
|
-
);
|
|
377
|
-
transformedNames.forEach((name, index) => {
|
|
378
|
-
expect(name).toBe(`mcp_history_tool_${index}`);
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
clearMockAccounts();
|
|
274
|
+
vi.clearAllMocks();
|
|
275
|
+
mockRefreshToken.mockResolvedValue({
|
|
276
|
+
access_token: "access-default-refresh",
|
|
277
|
+
expires_in: 3600,
|
|
278
|
+
refresh_token: "refresh-default-refresh",
|
|
279
|
+
});
|
|
379
280
|
});
|
|
380
281
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
it("clones Request input bodies before service-wide retries", async () => {
|
|
385
|
-
const harness = await createFetchHarness();
|
|
386
|
-
|
|
387
|
-
harness.mockFetch
|
|
388
|
-
.mockResolvedValueOnce(new Response("temporary outage", { status: 503 }))
|
|
389
|
-
.mockResolvedValueOnce(jsonResponse({ id: "msg_retry", content: [] }));
|
|
390
|
-
|
|
391
|
-
const request = new Request("https://api.anthropic.com/v1/messages", {
|
|
392
|
-
method: "POST",
|
|
393
|
-
headers: { "content-type": "application/json" },
|
|
394
|
-
body: makeRequestBody({ toolName: "mcp_retry_body" }),
|
|
282
|
+
afterEach(() => {
|
|
283
|
+
clearMockAccounts();
|
|
395
284
|
});
|
|
396
285
|
|
|
397
|
-
|
|
398
|
-
|
|
286
|
+
it("intercepts 50 concurrent requests without double-prefixing historical tool definitions", async () => {
|
|
287
|
+
const harness = await createFetchHarness();
|
|
288
|
+
const queue = createDeferredQueue<Response>();
|
|
399
289
|
|
|
400
|
-
|
|
401
|
-
const secondInit = harness.mockFetch.mock.calls[1]?.[1] as RequestInit | undefined;
|
|
290
|
+
harness.mockFetch.mockImplementation(() => queue.enqueue().promise);
|
|
402
291
|
|
|
403
|
-
|
|
404
|
-
|
|
292
|
+
const requests = Array.from({ length: 50 }, (_, index) =>
|
|
293
|
+
harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: { "content-type": "application/json" },
|
|
296
|
+
body: makeRequestBody({
|
|
297
|
+
toolName: `mcp_parallel_tool_${index}`,
|
|
298
|
+
text: `fan-out-${index}`,
|
|
299
|
+
}),
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
405
302
|
|
|
406
|
-
|
|
407
|
-
});
|
|
303
|
+
await harness.waitFor(() => expect(queue.pending).toBe(50), 1000);
|
|
408
304
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
305
|
+
for (let index = 0; index < 50; index += 1) {
|
|
306
|
+
queue.resolveNext(jsonResponse({ id: `msg_${index}`, content: [{ type: "text", text: `ok-${index}` }] }));
|
|
307
|
+
}
|
|
412
308
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const attempt = attempts.get(url) ?? 0;
|
|
416
|
-
attempts.set(url, attempt + 1);
|
|
309
|
+
const responses = await Promise.all(requests);
|
|
310
|
+
await Promise.all(responses.map((response) => response.json()));
|
|
417
311
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
312
|
+
const sentBodies = harness.mockFetch.mock.calls.map((call) => parseSentBody(call));
|
|
313
|
+
sentBodies.forEach((body, index) => {
|
|
314
|
+
expect(body.tools[0].name).toBe(`mcp_mcp_parallel_tool_${index}`);
|
|
315
|
+
});
|
|
421
316
|
|
|
422
|
-
|
|
317
|
+
harness.tearDown();
|
|
423
318
|
});
|
|
424
319
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
320
|
+
it("keeps per-request tool definition state isolated under concurrent mixed prefix load", async () => {
|
|
321
|
+
const harness = await createFetchHarness();
|
|
322
|
+
const toolNames = [
|
|
323
|
+
"read_file",
|
|
324
|
+
"mcp_existing_read",
|
|
325
|
+
"write_file",
|
|
326
|
+
"mcp_existing_write",
|
|
327
|
+
"list_files",
|
|
328
|
+
"mcp_existing_list",
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
harness.mockFetch.mockImplementation(() => jsonResponse({ id: "msg_mixed", content: [] }));
|
|
332
|
+
|
|
333
|
+
await Promise.all(
|
|
334
|
+
toolNames.map((toolName) =>
|
|
335
|
+
harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: { "content-type": "application/json" },
|
|
338
|
+
body: makeRequestBody({ toolName, text: toolName }),
|
|
339
|
+
}),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const transformedNames = harness.mockFetch.mock.calls.map((call) => parseSentBody(call).tools[0].name);
|
|
344
|
+
expect(transformedNames).toEqual([
|
|
345
|
+
"mcp_read_file",
|
|
346
|
+
"mcp_mcp_existing_read",
|
|
347
|
+
"mcp_write_file",
|
|
348
|
+
"mcp_mcp_existing_write",
|
|
349
|
+
"mcp_list_files",
|
|
350
|
+
"mcp_mcp_existing_list",
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
harness.tearDown();
|
|
456
354
|
});
|
|
457
355
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
356
|
+
it("keeps historical tool_use blocks isolated under concurrent request fan-out", async () => {
|
|
357
|
+
const harness = await createFetchHarness();
|
|
358
|
+
|
|
359
|
+
harness.mockFetch.mockImplementation(() => jsonResponse({ id: "msg_history", content: [] }));
|
|
360
|
+
|
|
361
|
+
await Promise.all(
|
|
362
|
+
Array.from({ length: 12 }, (_, index) =>
|
|
363
|
+
harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
364
|
+
method: "POST",
|
|
365
|
+
headers: { "content-type": "application/json" },
|
|
366
|
+
body: makeRequestBody({
|
|
367
|
+
historicalToolName: `mcp_history_tool_${index}`,
|
|
368
|
+
text: `history-${index}`,
|
|
369
|
+
}),
|
|
370
|
+
}),
|
|
371
|
+
),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const transformedNames = harness.mockFetch.mock.calls.map(
|
|
375
|
+
(call) => parseSentBody(call).messages[0].content[0].name,
|
|
376
|
+
);
|
|
377
|
+
transformedNames.forEach((name, index) => {
|
|
378
|
+
expect(name).toBe(`mcp_history_tool_${index}`);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
harness.tearDown();
|
|
465
382
|
});
|
|
466
383
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
470
|
-
method: "POST",
|
|
471
|
-
headers: { "content-type": "application/json" },
|
|
472
|
-
body: makeRequestBody({ toolName: `mcp_rotation_tool_${index}` }),
|
|
473
|
-
}),
|
|
474
|
-
),
|
|
475
|
-
);
|
|
476
|
-
await Promise.all(responses.map((response) => response.json()));
|
|
384
|
+
it("clones Request input bodies before service-wide retries", async () => {
|
|
385
|
+
const harness = await createFetchHarness();
|
|
477
386
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
387
|
+
harness.mockFetch
|
|
388
|
+
.mockResolvedValueOnce(new Response("temporary outage", { status: 503 }))
|
|
389
|
+
.mockResolvedValueOnce(jsonResponse({ id: "msg_retry", content: [] }));
|
|
481
390
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
391
|
+
const request = new Request("https://api.anthropic.com/v1/messages", {
|
|
392
|
+
method: "POST",
|
|
393
|
+
headers: { "content-type": "application/json" },
|
|
394
|
+
body: makeRequestBody({ toolName: "mcp_retry_body" }),
|
|
395
|
+
});
|
|
486
396
|
|
|
487
|
-
|
|
488
|
-
|
|
397
|
+
const response = await harness.fetch(request);
|
|
398
|
+
await response.json();
|
|
489
399
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
accounts: [
|
|
493
|
-
{
|
|
494
|
-
email: "refresh@example.com",
|
|
495
|
-
access: "",
|
|
496
|
-
refreshToken: "refresh-stale",
|
|
497
|
-
expires: Date.now() - 1_000,
|
|
498
|
-
},
|
|
499
|
-
],
|
|
500
|
-
});
|
|
501
|
-
const refreshDeferred = createDeferred<{
|
|
502
|
-
access_token: string;
|
|
503
|
-
expires_in: number;
|
|
504
|
-
refresh_token: string;
|
|
505
|
-
}>();
|
|
506
|
-
|
|
507
|
-
mockRefreshToken.mockReturnValue(refreshDeferred.promise);
|
|
508
|
-
harness.mockFetch.mockImplementation(() => jsonResponse({ id: "msg_refresh", content: [] }));
|
|
509
|
-
|
|
510
|
-
const requests = Array.from({ length: 10 }, (_, index) =>
|
|
511
|
-
harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
512
|
-
method: "POST",
|
|
513
|
-
headers: { "content-type": "application/json" },
|
|
514
|
-
body: makeRequestBody({ toolName: `mcp_refresh_tool_${index}` }),
|
|
515
|
-
}),
|
|
516
|
-
);
|
|
400
|
+
const firstInit = harness.mockFetch.mock.calls[0]?.[1] as RequestInit | undefined;
|
|
401
|
+
const secondInit = harness.mockFetch.mock.calls[1]?.[1] as RequestInit | undefined;
|
|
517
402
|
|
|
518
|
-
|
|
403
|
+
expect(typeof firstInit?.body).toBe("string");
|
|
404
|
+
expect(firstInit?.body).toBe(secondInit?.body);
|
|
519
405
|
|
|
520
|
-
|
|
521
|
-
access_token: "access-fresh",
|
|
522
|
-
expires_in: 3600,
|
|
523
|
-
refresh_token: "refresh-fresh",
|
|
406
|
+
harness.tearDown();
|
|
524
407
|
});
|
|
525
408
|
|
|
526
|
-
|
|
527
|
-
|
|
409
|
+
it("preserves each concurrent Request body independently across retry fan-out", async () => {
|
|
410
|
+
const harness = await createFetchHarness();
|
|
411
|
+
const attempts = new Map<string, number>();
|
|
412
|
+
|
|
413
|
+
harness.mockFetch.mockImplementation((input) => {
|
|
414
|
+
const url = typeof input === "string" ? input : input instanceof Request ? input.url : input.toString();
|
|
415
|
+
const attempt = attempts.get(url) ?? 0;
|
|
416
|
+
attempts.set(url, attempt + 1);
|
|
417
|
+
|
|
418
|
+
if (attempt === 0) {
|
|
419
|
+
return Promise.resolve(new Response("try again", { status: 503 }));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return Promise.resolve(jsonResponse({ id: `ok:${url}`, content: [] }));
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const requests = Array.from({ length: 10 }, (_, index) =>
|
|
426
|
+
harness.fetch(
|
|
427
|
+
new Request(`https://api.anthropic.com/v1/messages?retry=${index}`, {
|
|
428
|
+
method: "POST",
|
|
429
|
+
headers: { "content-type": "application/json" },
|
|
430
|
+
body: makeRequestBody({ toolName: `mcp_retry_parallel_${index}` }),
|
|
431
|
+
}),
|
|
432
|
+
),
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const responses = await Promise.all(requests);
|
|
436
|
+
await Promise.all(responses.map((response) => response.json()));
|
|
437
|
+
|
|
438
|
+
for (let index = 0; index < 10; index += 1) {
|
|
439
|
+
const callsForUrl = harness.mockFetch.mock.calls.filter((call) => callUrl(call).includes(`retry=${index}`));
|
|
440
|
+
expect(callsForUrl).toHaveLength(2);
|
|
441
|
+
callsForUrl.forEach((call) => {
|
|
442
|
+
const init = call[1] as RequestInit | undefined;
|
|
443
|
+
expect(typeof init?.body).toBe("string");
|
|
444
|
+
});
|
|
445
|
+
}
|
|
528
446
|
|
|
529
|
-
|
|
530
|
-
expect(callHeaders(call).get("authorization")).toBe("Bearer access-fresh");
|
|
531
|
-
expect(parseSentBody(call).tools[0].name).toBe(`mcp_mcp_refresh_tool_${index}`);
|
|
447
|
+
harness.tearDown();
|
|
532
448
|
});
|
|
533
449
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
450
|
+
it("rotates accounts under concurrent 429 load without tool-prefix drift", async () => {
|
|
451
|
+
const harness = await createFetchHarness({
|
|
452
|
+
accounts: [
|
|
453
|
+
{
|
|
454
|
+
email: "first@example.com",
|
|
455
|
+
access: "access-1",
|
|
456
|
+
refreshToken: "refresh-1",
|
|
457
|
+
expires: Date.now() + 60_000,
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
email: "second@example.com",
|
|
461
|
+
access: "access-2",
|
|
462
|
+
refreshToken: "refresh-2",
|
|
463
|
+
expires: Date.now() + 60_000,
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
harness.mockFetch.mockImplementation((_input, init) => {
|
|
469
|
+
const headers = new Headers(init?.headers as HeadersInit | undefined);
|
|
470
|
+
const auth = headers.get("authorization");
|
|
471
|
+
if (auth === "Bearer access-1") {
|
|
472
|
+
return Promise.resolve(rateLimitResponse());
|
|
473
|
+
}
|
|
474
|
+
return Promise.resolve(jsonResponse({ id: "msg_rotated", content: [] }));
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const responses = await Promise.all(
|
|
478
|
+
Array.from({ length: 12 }, (_, index) =>
|
|
479
|
+
harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
480
|
+
method: "POST",
|
|
481
|
+
headers: { "content-type": "application/json" },
|
|
482
|
+
body: makeRequestBody({ toolName: `mcp_rotation_tool_${index}` }),
|
|
483
|
+
}),
|
|
484
|
+
),
|
|
485
|
+
);
|
|
486
|
+
await Promise.all(responses.map((response) => response.json()));
|
|
487
|
+
|
|
488
|
+
const successfulRotations = harness.mockFetch.mock.calls.filter(
|
|
489
|
+
(call) => callHeaders(call).get("authorization") !== "Bearer access-1",
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
expect(successfulRotations).toHaveLength(12);
|
|
493
|
+
successfulRotations.forEach((call) => {
|
|
494
|
+
expect(parseSentBody(call).tools[0].name).not.toMatch(/^mcp_mcp_mcp_/);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
harness.tearDown();
|
|
550
498
|
});
|
|
551
499
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
500
|
+
it("shares one refresh across concurrent requests without cross-request contamination", async () => {
|
|
501
|
+
const harness = await createFetchHarness({
|
|
502
|
+
accounts: [
|
|
503
|
+
{
|
|
504
|
+
email: "refresh@example.com",
|
|
505
|
+
access: "",
|
|
506
|
+
refreshToken: "refresh-stale",
|
|
507
|
+
expires: Date.now() - 1_000,
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
});
|
|
511
|
+
const refreshDeferred = createDeferred<{
|
|
512
|
+
access_token: string;
|
|
513
|
+
expires_in: number;
|
|
514
|
+
refresh_token: string;
|
|
515
|
+
}>();
|
|
516
|
+
|
|
517
|
+
mockRefreshToken.mockReturnValue(refreshDeferred.promise);
|
|
518
|
+
harness.mockFetch.mockImplementation(() => jsonResponse({ id: "msg_refresh", content: [] }));
|
|
519
|
+
|
|
520
|
+
const requests = Array.from({ length: 10 }, (_, index) =>
|
|
521
|
+
harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
522
|
+
method: "POST",
|
|
523
|
+
headers: { "content-type": "application/json" },
|
|
524
|
+
body: makeRequestBody({ toolName: `mcp_refresh_tool_${index}` }),
|
|
525
|
+
}),
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
await harness.waitFor(() => expect(mockRefreshToken).toHaveBeenCalledTimes(1), 1000);
|
|
529
|
+
|
|
530
|
+
refreshDeferred.resolve({
|
|
531
|
+
access_token: "access-fresh",
|
|
532
|
+
expires_in: 3600,
|
|
533
|
+
refresh_token: "refresh-fresh",
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const responses = await Promise.all(requests);
|
|
537
|
+
await Promise.all(responses.map((response) => response.json()));
|
|
538
|
+
|
|
539
|
+
harness.mockFetch.mock.calls.forEach((call, index) => {
|
|
540
|
+
expect(callHeaders(call).get("authorization")).toBe("Bearer access-fresh");
|
|
541
|
+
expect(parseSentBody(call).tools[0].name).toBe(`mcp_mcp_refresh_tool_${index}`);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
harness.tearDown();
|
|
583
545
|
});
|
|
584
546
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
547
|
+
it("keeps SSE and JSON tool-name rewriting consistent when both run concurrently", async () => {
|
|
548
|
+
const harness = await createFetchHarness();
|
|
549
|
+
|
|
550
|
+
harness.mockFetch.mockImplementation((input) => {
|
|
551
|
+
const url = typeof input === "string" ? input : input instanceof Request ? input.url : input.toString();
|
|
552
|
+
if (url.includes("stream")) {
|
|
553
|
+
return Promise.resolve(makeToolUseSseResponse("mcp_parallel_stream"));
|
|
554
|
+
}
|
|
555
|
+
return Promise.resolve(
|
|
556
|
+
jsonResponse({
|
|
557
|
+
content: [{ type: "tool_use", name: "mcp_parallel_json", input: { ok: true } }],
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const [streamResponse, jsonResponseValue] = await Promise.all([
|
|
563
|
+
harness.fetch("https://api.anthropic.com/v1/messages/stream", {
|
|
564
|
+
method: "POST",
|
|
565
|
+
headers: { "content-type": "application/json" },
|
|
566
|
+
body: makeRequestBody({ toolName: "mcp_parallel_stream" }),
|
|
567
|
+
}),
|
|
568
|
+
harness.fetch("https://api.anthropic.com/v1/messages/json", {
|
|
569
|
+
method: "POST",
|
|
570
|
+
headers: { "content-type": "application/json" },
|
|
571
|
+
body: makeRequestBody({ toolName: "mcp_parallel_json" }),
|
|
572
|
+
}),
|
|
573
|
+
]);
|
|
574
|
+
|
|
575
|
+
const [streamText, jsonPayload] = await Promise.all([streamResponse.text(), jsonResponseValue.json()]);
|
|
576
|
+
|
|
577
|
+
expect(streamText).toContain('"name":"parallel_stream"');
|
|
578
|
+
expect(streamText).not.toContain('"name":"mcp_parallel_stream"');
|
|
579
|
+
expect(jsonPayload.content[0].name).toBe("parallel_json");
|
|
580
|
+
|
|
581
|
+
harness.tearDown();
|
|
582
|
+
});
|
|
608
583
|
|
|
609
|
-
|
|
610
|
-
|
|
584
|
+
it("does not leak one request error into sibling concurrent requests", async () => {
|
|
585
|
+
const harness = await createFetchHarness();
|
|
586
|
+
|
|
587
|
+
harness.mockFetch.mockImplementation((input) => {
|
|
588
|
+
const url = typeof input === "string" ? input : input instanceof Request ? input.url : input.toString();
|
|
589
|
+
if (url.includes("explode")) {
|
|
590
|
+
return Promise.reject(new Error("socket reset"));
|
|
591
|
+
}
|
|
592
|
+
return Promise.resolve(jsonResponse({ id: "msg_ok", content: [] }));
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const [failed, succeeded] = await Promise.allSettled([
|
|
596
|
+
harness.fetch("https://api.anthropic.com/v1/messages/explode", {
|
|
597
|
+
method: "POST",
|
|
598
|
+
headers: { "content-type": "application/json" },
|
|
599
|
+
body: makeRequestBody({ toolName: "mcp_fail_tool" }),
|
|
600
|
+
}),
|
|
601
|
+
harness.fetch("https://api.anthropic.com/v1/messages/ok", {
|
|
602
|
+
method: "POST",
|
|
603
|
+
headers: { "content-type": "application/json" },
|
|
604
|
+
body: makeRequestBody({ toolName: "mcp_ok_tool" }),
|
|
605
|
+
}),
|
|
606
|
+
]);
|
|
607
|
+
|
|
608
|
+
expect(failed.status).toBe("rejected");
|
|
609
|
+
expect(succeeded.status).toBe("fulfilled");
|
|
610
|
+
|
|
611
|
+
if (succeeded.status === "fulfilled") {
|
|
612
|
+
await succeeded.value.json();
|
|
613
|
+
}
|
|
611
614
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
+
const successCall = harness.mockFetch.mock.calls.find((call) => callUrl(call).includes("/ok"));
|
|
616
|
+
expect(successCall).toBeDefined();
|
|
617
|
+
expect(parseSentBody(successCall!).tools[0].name).toBe("mcp_mcp_ok_tool");
|
|
615
618
|
|
|
616
|
-
|
|
619
|
+
harness.tearDown();
|
|
620
|
+
});
|
|
617
621
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
headers: { "content-type": "application/json" },
|
|
622
|
-
body: sharedBody,
|
|
623
|
-
}),
|
|
624
|
-
);
|
|
622
|
+
it("does not mutate shared request payloads across concurrent batches", async () => {
|
|
623
|
+
const harness = await createFetchHarness();
|
|
624
|
+
const sharedBody = makeRequestBody({ toolName: "mcp_shared_tool", text: "shared" });
|
|
625
625
|
|
|
626
|
-
|
|
626
|
+
harness.mockFetch.mockImplementation(() => jsonResponse({ id: "msg_shared", content: [] }));
|
|
627
627
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
628
|
+
const firstWave = Array.from({ length: 20 }, () =>
|
|
629
|
+
harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
630
|
+
method: "POST",
|
|
631
|
+
headers: { "content-type": "application/json" },
|
|
632
|
+
body: sharedBody,
|
|
633
|
+
}),
|
|
634
|
+
);
|
|
634
635
|
|
|
635
|
-
|
|
636
|
+
await Promise.all(firstWave);
|
|
636
637
|
|
|
637
|
-
|
|
638
|
-
|
|
638
|
+
const followUp = await harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
639
|
+
method: "POST",
|
|
640
|
+
headers: { "content-type": "application/json" },
|
|
641
|
+
body: sharedBody,
|
|
642
|
+
});
|
|
643
|
+
await followUp.json();
|
|
639
644
|
|
|
640
|
-
|
|
641
|
-
});
|
|
645
|
+
const lastSharedCall = harness.mockFetch.mock.calls[harness.mockFetch.mock.calls.length - 1];
|
|
642
646
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
let firstRetryAttempt = true;
|
|
647
|
+
expect(JSON.parse(sharedBody).tools[0].name).toBe("mcp_shared_tool");
|
|
648
|
+
expect(parseSentBody(lastSharedCall).tools[0].name).toBe("mcp_mcp_shared_tool");
|
|
646
649
|
|
|
647
|
-
|
|
648
|
-
const url = typeof input === "string" ? input : input instanceof Request ? input.url : input.toString();
|
|
649
|
-
if (url.includes("retry-once") && firstRetryAttempt) {
|
|
650
|
-
firstRetryAttempt = false;
|
|
651
|
-
return Promise.resolve(new Response("temporary outage", { status: 503 }));
|
|
652
|
-
}
|
|
653
|
-
return Promise.resolve(jsonResponse({ id: `msg:${url}`, content: [] }));
|
|
650
|
+
harness.tearDown();
|
|
654
651
|
});
|
|
655
652
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
653
|
+
it("cleans up retry bookkeeping after each request", async () => {
|
|
654
|
+
const harness = await createFetchHarness();
|
|
655
|
+
let firstRetryAttempt = true;
|
|
656
|
+
|
|
657
|
+
harness.mockFetch.mockImplementation((input) => {
|
|
658
|
+
const url = typeof input === "string" ? input : input instanceof Request ? input.url : input.toString();
|
|
659
|
+
if (url.includes("retry-once") && firstRetryAttempt) {
|
|
660
|
+
firstRetryAttempt = false;
|
|
661
|
+
return Promise.resolve(new Response("temporary outage", { status: 503 }));
|
|
662
|
+
}
|
|
663
|
+
return Promise.resolve(jsonResponse({ id: `msg:${url}`, content: [] }));
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
const retried = await harness.fetch("https://api.anthropic.com/v1/messages/retry-once", {
|
|
667
|
+
method: "POST",
|
|
668
|
+
headers: { "content-type": "application/json" },
|
|
669
|
+
body: makeRequestBody({ toolName: "mcp_retry_cleanup" }),
|
|
670
|
+
});
|
|
671
|
+
await retried.json();
|
|
672
|
+
|
|
673
|
+
const clean = await harness.fetch("https://api.anthropic.com/v1/messages/clean", {
|
|
674
|
+
method: "POST",
|
|
675
|
+
headers: { "content-type": "application/json" },
|
|
676
|
+
body: makeRequestBody({ toolName: "mcp_clean_followup" }),
|
|
677
|
+
});
|
|
678
|
+
await clean.json();
|
|
679
|
+
|
|
680
|
+
const cleanCall = harness.mockFetch.mock.calls.find((call) => callUrl(call).includes("/clean"));
|
|
681
|
+
expect(cleanCall).toBeDefined();
|
|
682
|
+
expect(callHeaders(cleanCall!).get("x-stainless-retry-count")).toBe("0");
|
|
683
|
+
expect(parseSentBody(cleanCall!).tools[0].name).toBe("mcp_mcp_clean_followup");
|
|
684
|
+
|
|
685
|
+
harness.tearDown();
|
|
660
686
|
});
|
|
661
|
-
await retried.json();
|
|
662
687
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
body: makeRequestBody({ toolName: "mcp_clean_followup" }),
|
|
667
|
-
});
|
|
668
|
-
await clean.json();
|
|
688
|
+
it("leaves no dangling deferred work after concurrent request cleanup", async () => {
|
|
689
|
+
const harness = await createFetchHarness();
|
|
690
|
+
const gate = createDeferred<Response>();
|
|
669
691
|
|
|
670
|
-
|
|
671
|
-
expect(cleanCall).toBeDefined();
|
|
672
|
-
expect(callHeaders(cleanCall!).get("x-stainless-retry-count")).toBe("0");
|
|
673
|
-
expect(parseSentBody(cleanCall!).tools[0].name).toBe("mcp_mcp_clean_followup");
|
|
692
|
+
harness.mockFetch.mockImplementation(() => gate.promise);
|
|
674
693
|
|
|
675
|
-
|
|
676
|
-
|
|
694
|
+
const pendingRequest = harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
695
|
+
method: "POST",
|
|
696
|
+
headers: { "content-type": "application/json" },
|
|
697
|
+
body: makeRequestBody({ toolName: "mcp_cleanup_gate" }),
|
|
698
|
+
});
|
|
677
699
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
const gate = createDeferred<Response>();
|
|
700
|
+
await nextTick();
|
|
701
|
+
gate.resolve(jsonResponse({ id: "msg_cleanup_gate", content: [] }));
|
|
681
702
|
|
|
682
|
-
|
|
703
|
+
const response = await pendingRequest;
|
|
704
|
+
await response.json();
|
|
683
705
|
|
|
684
|
-
|
|
685
|
-
method: "POST",
|
|
686
|
-
headers: { "content-type": "application/json" },
|
|
687
|
-
body: makeRequestBody({ toolName: "mcp_cleanup_gate" }),
|
|
688
|
-
});
|
|
706
|
+
harness.mockFetch.mockImplementation(() => jsonResponse({ id: "msg_cleanup_followup", content: [] }));
|
|
689
707
|
|
|
690
|
-
|
|
691
|
-
|
|
708
|
+
const followUp = await harness.fetch("https://api.anthropic.com/v1/messages", {
|
|
709
|
+
method: "POST",
|
|
710
|
+
headers: { "content-type": "application/json" },
|
|
711
|
+
body: makeRequestBody({ toolName: "mcp_cleanup_gate_followup" }),
|
|
712
|
+
});
|
|
713
|
+
await followUp.json();
|
|
692
714
|
|
|
693
|
-
|
|
694
|
-
await response.json();
|
|
715
|
+
const lastCleanupCall = harness.mockFetch.mock.calls[harness.mockFetch.mock.calls.length - 1];
|
|
695
716
|
|
|
696
|
-
|
|
717
|
+
expect(parseSentBody(lastCleanupCall).tools[0].name).toBe("mcp_mcp_cleanup_gate_followup");
|
|
697
718
|
|
|
698
|
-
|
|
699
|
-
method: "POST",
|
|
700
|
-
headers: { "content-type": "application/json" },
|
|
701
|
-
body: makeRequestBody({ toolName: "mcp_cleanup_gate_followup" }),
|
|
719
|
+
harness.tearDown();
|
|
702
720
|
});
|
|
703
|
-
await followUp.json();
|
|
704
|
-
|
|
705
|
-
const lastCleanupCall = harness.mockFetch.mock.calls[harness.mockFetch.mock.calls.length - 1];
|
|
706
|
-
|
|
707
|
-
expect(parseSentBody(lastCleanupCall).tools[0].name).toBe("mcp_mcp_cleanup_gate_followup");
|
|
708
|
-
|
|
709
|
-
harness.tearDown();
|
|
710
|
-
});
|
|
711
721
|
});
|