@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.
Files changed (107) hide show
  1. package/README.md +88 -88
  2. package/dist/opencode-anthropic-auth-cli.mjs +804 -507
  3. package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
  4. package/package.json +67 -59
  5. package/src/__tests__/billing-edge-cases.test.ts +59 -59
  6. package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
  7. package/src/__tests__/cc-comparison.test.ts +87 -87
  8. package/src/__tests__/cc-credentials.test.ts +254 -250
  9. package/src/__tests__/cch-drift-checker.test.ts +51 -51
  10. package/src/__tests__/cch-native-style.test.ts +56 -56
  11. package/src/__tests__/debug-gating.test.ts +42 -42
  12. package/src/__tests__/decomposition-smoke.test.ts +68 -68
  13. package/src/__tests__/fingerprint-regression.test.ts +575 -566
  14. package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
  15. package/src/__tests__/helpers/conversation-history.ts +119 -119
  16. package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
  17. package/src/__tests__/helpers/deferred.ts +69 -69
  18. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
  19. package/src/__tests__/helpers/in-memory-storage.ts +88 -88
  20. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
  21. package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
  22. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
  23. package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
  24. package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
  25. package/src/__tests__/helpers/sse.ts +209 -209
  26. package/src/__tests__/index.parallel.test.ts +605 -595
  27. package/src/__tests__/sanitization-regex.test.ts +112 -112
  28. package/src/__tests__/state-bounds.test.ts +90 -90
  29. package/src/account-identity.test.ts +197 -192
  30. package/src/account-identity.ts +69 -67
  31. package/src/account-state.test.ts +86 -86
  32. package/src/account-state.ts +25 -25
  33. package/src/accounts/matching.test.ts +335 -0
  34. package/src/accounts/matching.ts +167 -0
  35. package/src/accounts/persistence.test.ts +345 -0
  36. package/src/accounts/persistence.ts +432 -0
  37. package/src/accounts/repair.test.ts +276 -0
  38. package/src/accounts/repair.ts +407 -0
  39. package/src/accounts.dedup.test.ts +621 -621
  40. package/src/accounts.test.ts +933 -929
  41. package/src/accounts.ts +633 -989
  42. package/src/backoff.test.ts +345 -345
  43. package/src/backoff.ts +219 -219
  44. package/src/betas.ts +124 -124
  45. package/src/bun-fetch.test.ts +345 -342
  46. package/src/bun-fetch.ts +424 -424
  47. package/src/bun-proxy.test.ts +25 -25
  48. package/src/bun-proxy.ts +209 -209
  49. package/src/cc-credentials.ts +111 -111
  50. package/src/circuit-breaker.test.ts +184 -184
  51. package/src/circuit-breaker.ts +169 -169
  52. package/src/cli/commands/auth.ts +963 -0
  53. package/src/cli/commands/config.ts +547 -0
  54. package/src/cli/formatting.test.ts +406 -0
  55. package/src/cli/formatting.ts +219 -0
  56. package/src/cli.ts +255 -2022
  57. package/src/commands/handlers/betas.ts +100 -0
  58. package/src/commands/handlers/config.ts +99 -0
  59. package/src/commands/handlers/files.ts +375 -0
  60. package/src/commands/oauth-flow.ts +181 -166
  61. package/src/commands/prompts.ts +61 -61
  62. package/src/commands/router.test.ts +421 -0
  63. package/src/commands/router.ts +143 -635
  64. package/src/config.test.ts +482 -482
  65. package/src/config.ts +412 -404
  66. package/src/constants.ts +48 -48
  67. package/src/drift/cch-constants.ts +95 -95
  68. package/src/env.ts +111 -105
  69. package/src/headers/billing.ts +33 -33
  70. package/src/headers/builder.ts +130 -130
  71. package/src/headers/cch.ts +75 -75
  72. package/src/headers/stainless.ts +25 -25
  73. package/src/headers/user-agent.ts +23 -23
  74. package/src/index.ts +436 -828
  75. package/src/models.ts +27 -27
  76. package/src/oauth.test.ts +102 -102
  77. package/src/oauth.ts +178 -178
  78. package/src/parent-pid-watcher.test.ts +148 -148
  79. package/src/parent-pid-watcher.ts +69 -69
  80. package/src/plugin-helpers.ts +82 -82
  81. package/src/refresh-helpers.ts +145 -139
  82. package/src/refresh-lock.test.ts +94 -94
  83. package/src/refresh-lock.ts +93 -93
  84. package/src/request/body.history.test.ts +579 -571
  85. package/src/request/body.ts +255 -255
  86. package/src/request/metadata.ts +65 -65
  87. package/src/request/retry.test.ts +156 -156
  88. package/src/request/retry.ts +67 -67
  89. package/src/request/url.ts +21 -21
  90. package/src/request-orchestration-helpers.ts +648 -0
  91. package/src/response/index.ts +5 -5
  92. package/src/response/mcp.ts +58 -58
  93. package/src/response/streaming.test.ts +313 -311
  94. package/src/response/streaming.ts +412 -410
  95. package/src/rotation.test.ts +304 -301
  96. package/src/rotation.ts +205 -205
  97. package/src/storage.test.ts +547 -547
  98. package/src/storage.ts +315 -291
  99. package/src/system-prompt/builder.ts +38 -38
  100. package/src/system-prompt/index.ts +5 -5
  101. package/src/system-prompt/normalize.ts +60 -60
  102. package/src/system-prompt/sanitize.ts +30 -30
  103. package/src/thinking.ts +21 -20
  104. package/src/token-refresh.test.ts +265 -265
  105. package/src/token-refresh.ts +219 -214
  106. package/src/types.ts +30 -30
  107. package/dist/bun-proxy.mjs +0 -291
@@ -6,463 +6,469 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { createDeferred, nextTick } from "./helpers/deferred.js";
7
7
  import { createMockBunProxy } from "./helpers/mock-bun-proxy.js";
8
8
  import {
9
- contentBlockDeltaEvent,
10
- contentBlockStartEvent,
11
- contentBlockStopEvent,
12
- encodeSSEStream,
13
- makeSSEResponse,
14
- messageStartEvent,
15
- messageStopEvent,
9
+ contentBlockDeltaEvent,
10
+ contentBlockStartEvent,
11
+ contentBlockStopEvent,
12
+ encodeSSEStream,
13
+ makeSSEResponse,
14
+ messageStartEvent,
15
+ messageStopEvent,
16
16
  } from "./helpers/sse.js";
17
17
 
18
18
  interface ParentWatcher {
19
- start(): void;
20
- stop(): void;
19
+ start(): void;
20
+ stop(): void;
21
21
  }
22
22
 
23
23
  type ParentWatcherFactory = (options: {
24
- parentPid: number;
25
- onParentExit: (exitCode?: number) => void;
26
- pollIntervalMs?: number;
27
- exitCode?: number;
24
+ parentPid: number;
25
+ onParentExit: (exitCode?: number) => void;
26
+ pollIntervalMs?: number;
27
+ exitCode?: number;
28
28
  }) => ParentWatcher;
29
29
 
30
30
  interface BunProxyRequestHandlerOptions {
31
- fetchImpl: typeof fetch;
32
- allowHosts?: string[];
33
- requestTimeoutMs?: number;
31
+ fetchImpl: typeof fetch;
32
+ allowHosts?: string[];
33
+ requestTimeoutMs?: number;
34
34
  }
35
35
 
36
36
  interface BunProxyProcessRuntimeOptions {
37
- argv?: string[];
38
- fetchImpl?: typeof fetch;
39
- exit?: (code?: number) => void;
40
- parentWatcherFactory?: ParentWatcherFactory;
37
+ argv?: string[];
38
+ fetchImpl?: typeof fetch;
39
+ exit?: (code?: number) => void;
40
+ parentWatcherFactory?: ParentWatcherFactory;
41
41
  }
42
42
 
43
43
  interface BunProxyModuleContract {
44
- createProxyRequestHandler(options: BunProxyRequestHandlerOptions): (request: Request) => Promise<Response>;
45
- createProxyProcessRuntime?(options: BunProxyProcessRuntimeOptions): ParentWatcher;
44
+ createProxyRequestHandler(options: BunProxyRequestHandlerOptions): (request: Request) => Promise<Response>;
45
+ createProxyProcessRuntime?(options: BunProxyProcessRuntimeOptions): ParentWatcher;
46
46
  }
47
47
 
48
48
  const bunProxySourcePath = fileURLToPath(new URL("../bun-proxy.ts", import.meta.url));
49
49
  const bunProxySource = readFileSync(bunProxySourcePath, "utf-8");
50
50
 
51
51
  async function loadBunProxyModule(): Promise<BunProxyModuleContract> {
52
- const modulePath = "../bun-proxy.js";
53
- return (await import(modulePath)) as BunProxyModuleContract;
52
+ const modulePath = "../bun-proxy.js";
53
+ return (await import(modulePath)) as BunProxyModuleContract;
54
54
  }
55
55
 
56
56
  async function createProxyRequestHandler(
57
- fetchImpl: typeof fetch,
58
- overrides: Partial<BunProxyRequestHandlerOptions> = {},
57
+ fetchImpl: typeof fetch,
58
+ overrides: Partial<BunProxyRequestHandlerOptions> = {},
59
59
  ): Promise<(request: Request) => Promise<Response>> {
60
- const { createProxyRequestHandler } = await loadBunProxyModule();
61
-
62
- return createProxyRequestHandler({
63
- fetchImpl,
64
- allowHosts: ["api.anthropic.com", "platform.claude.com"],
65
- requestTimeoutMs: 50,
66
- ...overrides,
67
- });
60
+ const { createProxyRequestHandler } = await loadBunProxyModule();
61
+
62
+ return createProxyRequestHandler({
63
+ fetchImpl,
64
+ allowHosts: ["api.anthropic.com", "platform.claude.com"],
65
+ requestTimeoutMs: 50,
66
+ ...overrides,
67
+ });
68
68
  }
69
69
 
70
70
  function makeProxyRequest(
71
- requestId: number,
72
- overrides: {
73
- body?: string;
74
- headers?: HeadersInit;
75
- method?: string;
76
- signal?: AbortSignal;
77
- targetUrl?: string;
78
- } = {},
71
+ requestId: number,
72
+ overrides: {
73
+ body?: string;
74
+ headers?: HeadersInit;
75
+ method?: string;
76
+ signal?: AbortSignal;
77
+ targetUrl?: string;
78
+ } = {},
79
79
  ): Request {
80
- const headers = new Headers(overrides.headers);
81
- headers.set("x-proxy-url", overrides.targetUrl ?? "https://api.anthropic.com/v1/messages");
82
-
83
- if (!headers.has("content-type")) {
84
- headers.set("content-type", "application/json");
85
- }
86
-
87
- return new Request("http://127.0.0.1/proxy", {
88
- method: overrides.method ?? "POST",
89
- headers,
90
- body: overrides.body ?? JSON.stringify({ requestId }),
91
- signal: overrides.signal,
92
- });
80
+ const headers = new Headers(overrides.headers);
81
+ headers.set("x-proxy-url", overrides.targetUrl ?? "https://api.anthropic.com/v1/messages");
82
+
83
+ if (!headers.has("content-type")) {
84
+ headers.set("content-type", "application/json");
85
+ }
86
+
87
+ return new Request("http://127.0.0.1/proxy", {
88
+ method: overrides.method ?? "POST",
89
+ headers,
90
+ body: overrides.body ?? JSON.stringify({ requestId }),
91
+ signal: overrides.signal,
92
+ });
93
93
  }
94
94
 
95
95
  function makeSSETranscript(requestId: number): string {
96
- return encodeSSEStream([
97
- messageStartEvent({
98
- message: {
99
- id: `msg_${requestId}`,
100
- type: "message",
101
- role: "assistant",
102
- content: [],
103
- model: "claude-3-opus-20240229",
104
- stop_reason: null,
105
- stop_sequence: null,
106
- usage: {
107
- input_tokens: 10,
108
- output_tokens: 0,
109
- },
110
- },
111
- }),
112
- contentBlockStartEvent(0, {
113
- content_block: {
114
- type: "text",
115
- text: "",
116
- },
117
- }),
118
- contentBlockDeltaEvent(0, `stream-${requestId}-a`),
119
- contentBlockDeltaEvent(0, `stream-${requestId}-b`),
120
- contentBlockStopEvent(0),
121
- messageStopEvent(),
122
- ]);
96
+ return encodeSSEStream([
97
+ messageStartEvent({
98
+ message: {
99
+ id: `msg_${requestId}`,
100
+ type: "message",
101
+ role: "assistant",
102
+ content: [],
103
+ model: "claude-3-opus-20240229",
104
+ stop_reason: null,
105
+ stop_sequence: null,
106
+ usage: {
107
+ input_tokens: 10,
108
+ output_tokens: 0,
109
+ },
110
+ },
111
+ }),
112
+ contentBlockStartEvent(0, {
113
+ content_block: {
114
+ type: "text",
115
+ text: "",
116
+ },
117
+ }),
118
+ contentBlockDeltaEvent(0, `stream-${requestId}-a`),
119
+ contentBlockDeltaEvent(0, `stream-${requestId}-b`),
120
+ contentBlockStopEvent(0),
121
+ messageStopEvent(),
122
+ ]);
123
123
  }
124
124
 
125
125
  async function flushMicrotasks(turns = 8): Promise<void> {
126
- for (let index = 0; index < turns; index += 1) {
127
- await nextTick();
128
- }
126
+ for (let index = 0; index < turns; index += 1) {
127
+ await nextTick();
128
+ }
129
129
  }
130
130
 
131
131
  beforeEach(() => {
132
- vi.resetModules();
132
+ vi.resetModules();
133
133
  });
134
134
 
135
135
  afterEach(() => {
136
- vi.restoreAllMocks();
137
- vi.doUnmock("../parent-pid-watcher.js");
136
+ vi.restoreAllMocks();
137
+ vi.doUnmock("../parent-pid-watcher.js");
138
138
  });
139
139
 
140
140
  describe("bun-proxy parallel request contract (RED)", () => {
141
- it("single proxy handles 10 concurrent fetches with distinct bodies and responses", async () => {
142
- const proxy = createMockBunProxy({
143
- forwardToMockFetch: vi.fn(async (_input, init) => new Response(String(init?.body), { status: 200 })),
144
- });
145
- const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
146
-
147
- const responses = await Promise.all(
148
- Array.from({ length: 10 }, (_, requestId) => handler(makeProxyRequest(requestId))),
149
- );
150
- const bodies = await Promise.all(responses.map((response) => response.text()));
141
+ it("single proxy handles 10 concurrent fetches with distinct bodies and responses", async () => {
142
+ const proxy = createMockBunProxy({
143
+ forwardToMockFetch: vi.fn(async (_input, init) => new Response(String(init?.body), { status: 200 })),
144
+ });
145
+ const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
151
146
 
152
- expect(bodies).toEqual(Array.from({ length: 10 }, (_, requestId) => JSON.stringify({ requestId })));
153
- });
147
+ const responses = await Promise.all(
148
+ Array.from({ length: 10 }, (_, requestId) => handler(makeProxyRequest(requestId))),
149
+ );
150
+ const bodies = await Promise.all(responses.map((response) => response.text()));
154
151
 
155
- it("single proxy handles 50 concurrent fetches with distinct bodies", async () => {
156
- const proxy = createMockBunProxy({
157
- forwardToMockFetch: vi.fn(async (_input, init) => new Response(String(init?.body), { status: 200 })),
152
+ expect(bodies).toEqual(Array.from({ length: 10 }, (_, requestId) => JSON.stringify({ requestId })));
158
153
  });
159
- const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
160
154
 
161
- const responses = await Promise.all(
162
- Array.from({ length: 50 }, (_, requestId) => handler(makeProxyRequest(requestId))),
163
- );
164
- const bodies = await Promise.all(responses.map((response) => response.text()));
155
+ it("single proxy handles 50 concurrent fetches with distinct bodies", async () => {
156
+ const proxy = createMockBunProxy({
157
+ forwardToMockFetch: vi.fn(async (_input, init) => new Response(String(init?.body), { status: 200 })),
158
+ });
159
+ const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
165
160
 
166
- expect(bodies).toEqual(Array.from({ length: 50 }, (_, requestId) => JSON.stringify({ requestId })));
167
- });
161
+ const responses = await Promise.all(
162
+ Array.from({ length: 50 }, (_, requestId) => handler(makeProxyRequest(requestId))),
163
+ );
164
+ const bodies = await Promise.all(responses.map((response) => response.text()));
168
165
 
169
- it("starts all concurrent upstream fetches in parallel without serialization", async () => {
170
- const deferredResponses = Array.from({ length: 50 }, () => createDeferred<Response>());
171
- let nextResponse = 0;
172
- const proxy = createMockBunProxy({
173
- forwardToMockFetch: vi.fn(() => deferredResponses[nextResponse++]!.promise),
166
+ expect(bodies).toEqual(Array.from({ length: 50 }, (_, requestId) => JSON.stringify({ requestId })));
174
167
  });
175
- const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
176
168
 
177
- const pendingResponses = Array.from({ length: 50 }, (_, requestId) => handler(makeProxyRequest(requestId)));
169
+ it("starts all concurrent upstream fetches in parallel without serialization", async () => {
170
+ const deferredResponses = Array.from({ length: 50 }, () => createDeferred<Response>());
171
+ let nextResponse = 0;
172
+ const proxy = createMockBunProxy({
173
+ forwardToMockFetch: vi.fn(() => deferredResponses[nextResponse++]!.promise),
174
+ });
175
+ const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
178
176
 
179
- await flushMicrotasks();
177
+ const pendingResponses = Array.from({ length: 50 }, (_, requestId) => handler(makeProxyRequest(requestId)));
180
178
 
181
- expect(proxy.getInFlightCount()).toBe(50);
179
+ await flushMicrotasks();
182
180
 
183
- deferredResponses.forEach((deferred, requestId) => {
184
- deferred.resolve(new Response(`ok-${requestId}`, { status: 200 }));
181
+ expect(proxy.getInFlightCount()).toBe(50);
182
+
183
+ deferredResponses.forEach((deferred, requestId) => {
184
+ deferred.resolve(new Response(`ok-${requestId}`, { status: 200 }));
185
+ });
186
+
187
+ await Promise.all(pendingResponses);
185
188
  });
186
189
 
187
- await Promise.all(pendingResponses);
188
- });
190
+ it("slow request does not head-of-line block siblings", async () => {
191
+ const slowResponse = createDeferred<Response>();
192
+ const proxy = createMockBunProxy({
193
+ forwardToMockFetch: vi.fn(async (_input, init) => {
194
+ const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
195
+ if (requestId === 0) {
196
+ return slowResponse.promise;
197
+ }
198
+
199
+ return new Response(`fast-${requestId}`, { status: 200 });
200
+ }),
201
+ });
202
+ const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
203
+
204
+ const responses = Array.from({ length: 10 }, (_, requestId) => handler(makeProxyRequest(requestId)));
205
+ const fastBodies = await Promise.all(
206
+ responses.slice(1).map(async (responsePromise) => {
207
+ const response = await responsePromise;
208
+ return response.text();
209
+ }),
210
+ );
189
211
 
190
- it("slow request does not head-of-line block siblings", async () => {
191
- const slowResponse = createDeferred<Response>();
192
- const proxy = createMockBunProxy({
193
- forwardToMockFetch: vi.fn(async (_input, init) => {
194
- const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
195
- if (requestId === 0) {
196
- return slowResponse.promise;
197
- }
212
+ expect(fastBodies).toEqual(Array.from({ length: 9 }, (_, index) => `fast-${index + 1}`));
213
+ expect(proxy.getInFlightCount()).toBe(1);
214
+
215
+ slowResponse.resolve(new Response("fast-0", { status: 200 }));
198
216
 
199
- return new Response(`fast-${requestId}`, { status: 200 });
200
- }),
217
+ const slowBody = await (await responses[0]).text();
218
+ expect(slowBody).toBe("fast-0");
201
219
  });
202
- const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
203
-
204
- const responses = Array.from({ length: 10 }, (_, requestId) => handler(makeProxyRequest(requestId)));
205
- const fastBodies = await Promise.all(
206
- responses.slice(1).map(async (responsePromise) => {
207
- const response = await responsePromise;
208
- return response.text();
209
- }),
210
- );
211
-
212
- expect(fastBodies).toEqual(Array.from({ length: 9 }, (_, index) => `fast-${index + 1}`));
213
- expect(proxy.getInFlightCount()).toBe(1);
214
-
215
- slowResponse.resolve(new Response("fast-0", { status: 200 }));
216
-
217
- const slowBody = await (await responses[0]).text();
218
- expect(slowBody).toBe("fast-0");
219
- });
220
-
221
- it("concurrent SSE streams maintain per-stream event ordering", async () => {
222
- const proxy = createMockBunProxy({
223
- forwardToMockFetch: vi.fn(async (_input, init) => {
224
- const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
225
- return makeSSEResponse(makeSSETranscript(requestId));
226
- }),
220
+
221
+ it("concurrent SSE streams maintain per-stream event ordering", async () => {
222
+ const proxy = createMockBunProxy({
223
+ forwardToMockFetch: vi.fn(async (_input, init) => {
224
+ const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
225
+ return makeSSEResponse(makeSSETranscript(requestId));
226
+ }),
227
+ });
228
+ const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
229
+
230
+ const responses = await Promise.all(
231
+ Array.from({ length: 5 }, (_, requestId) => handler(makeProxyRequest(requestId))),
232
+ );
233
+ const transcripts = await Promise.all(responses.map((response) => response.text()));
234
+
235
+ transcripts.forEach((transcript, requestId) => {
236
+ expect(transcript).toContain(`stream-${requestId}-a`);
237
+ expect(transcript).toContain(`stream-${requestId}-b`);
238
+ expect(transcript.indexOf(`stream-${requestId}-a`)).toBeLessThan(
239
+ transcript.indexOf(`stream-${requestId}-b`),
240
+ );
241
+ });
227
242
  });
228
- const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
229
243
 
230
- const responses = await Promise.all(
231
- Array.from({ length: 5 }, (_, requestId) => handler(makeProxyRequest(requestId))),
232
- );
233
- const transcripts = await Promise.all(responses.map((response) => response.text()));
244
+ it("upstream error in 1 request does not affect siblings", async () => {
245
+ const proxy = createMockBunProxy({
246
+ forwardToMockFetch: vi.fn(async (_input, init) => {
247
+ const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
248
+ if (requestId === 3) {
249
+ throw new Error("boom-3");
250
+ }
234
251
 
235
- transcripts.forEach((transcript, requestId) => {
236
- expect(transcript).toContain(`stream-${requestId}-a`);
237
- expect(transcript).toContain(`stream-${requestId}-b`);
238
- expect(transcript.indexOf(`stream-${requestId}-a`)).toBeLessThan(transcript.indexOf(`stream-${requestId}-b`));
252
+ return new Response(`ok-${requestId}`, { status: 200 });
253
+ }),
254
+ });
255
+ const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
256
+
257
+ const outcomes = await Promise.allSettled(
258
+ Array.from({ length: 10 }, async (_, requestId) => {
259
+ const response = await handler(makeProxyRequest(requestId));
260
+ return {
261
+ status: response.status,
262
+ body: await response.text(),
263
+ };
264
+ }),
265
+ );
266
+
267
+ outcomes.forEach((outcome, requestId) => {
268
+ if (requestId === 3) {
269
+ expect(
270
+ outcome.status === "rejected" || (outcome.status === "fulfilled" && outcome.value.status >= 500),
271
+ ).toBe(true);
272
+ return;
273
+ }
274
+
275
+ expect(outcome.status).toBe("fulfilled");
276
+ if (outcome.status === "fulfilled") {
277
+ expect(outcome.value).toEqual({
278
+ status: 200,
279
+ body: `ok-${requestId}`,
280
+ });
281
+ }
282
+ });
239
283
  });
240
- });
241
-
242
- it("upstream error in 1 request does not affect siblings", async () => {
243
- const proxy = createMockBunProxy({
244
- forwardToMockFetch: vi.fn(async (_input, init) => {
245
- const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
246
- if (requestId === 3) {
247
- throw new Error("boom-3");
284
+
285
+ it("upstream timeout of 1 request does not crash the proxy or cascade to later requests", async () => {
286
+ const abortedRequestIds: number[] = [];
287
+ const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
288
+ const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
289
+ const deferred = createDeferred<Response>();
290
+
291
+ init?.signal?.addEventListener(
292
+ "abort",
293
+ () => {
294
+ abortedRequestIds.push(requestId);
295
+ deferred.reject(init.signal?.reason ?? new DOMException("Timed out", "TimeoutError"));
296
+ },
297
+ { once: true },
298
+ );
299
+
300
+ if (requestId !== 0) {
301
+ deferred.resolve(new Response(`ok-${requestId}`, { status: 200 }));
302
+ }
303
+
304
+ return deferred.promise;
305
+ }) as typeof fetch;
306
+ const handler = await createProxyRequestHandler(fetchImpl, { requestTimeoutMs: 25 });
307
+
308
+ const responses = Array.from({ length: 10 }, (_, requestId) => handler(makeProxyRequest(requestId)));
309
+ const fastStatuses = await Promise.all(
310
+ responses.slice(1).map(async (responsePromise) => {
311
+ const response = await responsePromise;
312
+ return response.status;
313
+ }),
314
+ );
315
+
316
+ // Wait for the 25ms proxy timeout to actually fire on request 0. The
317
+ // previous hard 50ms sleep flaked under host load (husky pre-publish,
318
+ // lint-staged overhead, CI workers) because the abort callback would
319
+ // not have run yet when the assertion fired. Poll for the expected
320
+ // state with a generous upper bound instead of racing a fixed delay.
321
+ const deadline = Date.now() + 500;
322
+ while (abortedRequestIds.length < 1 && Date.now() < deadline) {
323
+ await new Promise((resolve) => setTimeout(resolve, 5));
248
324
  }
249
325
 
250
- return new Response(`ok-${requestId}`, { status: 200 });
251
- }),
326
+ expect(fastStatuses).toEqual(Array.from({ length: 9 }, () => 200));
327
+ expect(abortedRequestIds).toEqual([0]);
328
+
329
+ await expect(handler(makeProxyRequest(99))).resolves.toBeInstanceOf(Response);
252
330
  });
253
- const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
254
-
255
- const outcomes = await Promise.allSettled(
256
- Array.from({ length: 10 }, async (_, requestId) => {
257
- const response = await handler(makeProxyRequest(requestId));
258
- return {
259
- status: response.status,
260
- body: await response.text(),
261
- };
262
- }),
263
- );
264
-
265
- outcomes.forEach((outcome, requestId) => {
266
- if (requestId === 3) {
267
- expect(outcome.status === "rejected" || (outcome.status === "fulfilled" && outcome.value.status >= 500)).toBe(
268
- true,
331
+
332
+ it("canceling 1 of 10 requests aborts only that upstream signal and not siblings", async () => {
333
+ const abortedRequestIds: number[] = [];
334
+ const deferredResponses = new Map<number, ReturnType<typeof createDeferred<Response>>>();
335
+ const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
336
+ const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
337
+ const deferred = createDeferred<Response>();
338
+
339
+ deferredResponses.set(requestId, deferred);
340
+ init?.signal?.addEventListener(
341
+ "abort",
342
+ () => {
343
+ abortedRequestIds.push(requestId);
344
+ deferred.reject(init.signal?.reason ?? new DOMException("Aborted", "AbortError"));
345
+ },
346
+ { once: true },
347
+ );
348
+
349
+ return deferred.promise;
350
+ }) as typeof fetch;
351
+ const handler = await createProxyRequestHandler(fetchImpl);
352
+ const controllers = Array.from({ length: 10 }, () => new AbortController());
353
+ const responses = Array.from({ length: 10 }, (_, requestId) =>
354
+ handler(makeProxyRequest(requestId, { signal: controllers[requestId].signal })),
269
355
  );
270
- return;
271
- }
272
-
273
- expect(outcome.status).toBe("fulfilled");
274
- if (outcome.status === "fulfilled") {
275
- expect(outcome.value).toEqual({
276
- status: 200,
277
- body: `ok-${requestId}`,
356
+
357
+ await flushMicrotasks();
358
+ controllers[4].abort(new DOMException("client disconnected", "AbortError"));
359
+
360
+ deferredResponses.forEach((deferred, requestId) => {
361
+ if (requestId !== 4) {
362
+ deferred.resolve(new Response(`ok-${requestId}`, { status: 200 }));
363
+ }
278
364
  });
279
- }
280
- });
281
- });
282
-
283
- it("upstream timeout of 1 request does not crash the proxy or cascade to later requests", async () => {
284
- const abortedRequestIds: number[] = [];
285
- const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
286
- const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
287
- const deferred = createDeferred<Response>();
288
-
289
- init?.signal?.addEventListener(
290
- "abort",
291
- () => {
292
- abortedRequestIds.push(requestId);
293
- deferred.reject(init.signal?.reason ?? new DOMException("Timed out", "TimeoutError"));
294
- },
295
- { once: true },
296
- );
297
-
298
- if (requestId !== 0) {
299
- deferred.resolve(new Response(`ok-${requestId}`, { status: 200 }));
300
- }
301
-
302
- return deferred.promise;
303
- }) as typeof fetch;
304
- const handler = await createProxyRequestHandler(fetchImpl, { requestTimeoutMs: 25 });
305
-
306
- const responses = Array.from({ length: 10 }, (_, requestId) => handler(makeProxyRequest(requestId)));
307
- const fastStatuses = await Promise.all(
308
- responses.slice(1).map(async (responsePromise) => {
309
- const response = await responsePromise;
310
- return response.status;
311
- }),
312
- );
313
-
314
- // Wait for the 25ms proxy timeout to actually fire on request 0. The
315
- // previous hard 50ms sleep flaked under host load (husky pre-publish,
316
- // lint-staged overhead, CI workers) because the abort callback would
317
- // not have run yet when the assertion fired. Poll for the expected
318
- // state with a generous upper bound instead of racing a fixed delay.
319
- const deadline = Date.now() + 500;
320
- while (abortedRequestIds.length < 1 && Date.now() < deadline) {
321
- await new Promise((resolve) => setTimeout(resolve, 5));
322
- }
323
365
 
324
- expect(fastStatuses).toEqual(Array.from({ length: 9 }, () => 200));
325
- expect(abortedRequestIds).toEqual([0]);
326
-
327
- await expect(handler(makeProxyRequest(99))).resolves.toBeInstanceOf(Response);
328
- });
329
-
330
- it("canceling 1 of 10 requests aborts only that upstream signal and not siblings", async () => {
331
- const abortedRequestIds: number[] = [];
332
- const deferredResponses = new Map<number, ReturnType<typeof createDeferred<Response>>>();
333
- const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
334
- const { requestId } = JSON.parse(String(init?.body)) as { requestId: number };
335
- const deferred = createDeferred<Response>();
336
-
337
- deferredResponses.set(requestId, deferred);
338
- init?.signal?.addEventListener(
339
- "abort",
340
- () => {
341
- abortedRequestIds.push(requestId);
342
- deferred.reject(init.signal?.reason ?? new DOMException("Aborted", "AbortError"));
343
- },
344
- { once: true },
345
- );
346
-
347
- return deferred.promise;
348
- }) as typeof fetch;
349
- const handler = await createProxyRequestHandler(fetchImpl);
350
- const controllers = Array.from({ length: 10 }, () => new AbortController());
351
- const responses = Array.from({ length: 10 }, (_, requestId) =>
352
- handler(makeProxyRequest(requestId, { signal: controllers[requestId].signal })),
353
- );
354
-
355
- await flushMicrotasks();
356
- controllers[4].abort(new DOMException("client disconnected", "AbortError"));
357
-
358
- deferredResponses.forEach((deferred, requestId) => {
359
- if (requestId !== 4) {
360
- deferred.resolve(new Response(`ok-${requestId}`, { status: 200 }));
361
- }
362
- });
366
+ const outcomes = await Promise.allSettled(
367
+ responses.map(async (responsePromise) => {
368
+ const response = await responsePromise;
369
+ return response.status;
370
+ }),
371
+ );
363
372
 
364
- const outcomes = await Promise.allSettled(
365
- responses.map(async (responsePromise) => {
366
- const response = await responsePromise;
367
- return response.status;
368
- }),
369
- );
370
-
371
- expect(abortedRequestIds).toEqual([4]);
372
- outcomes.forEach((outcome, requestId) => {
373
- if (requestId === 4) {
374
- expect(outcome.status === "rejected" || (outcome.status === "fulfilled" && outcome.value >= 400)).toBe(true);
375
- return;
376
- }
377
-
378
- expect(outcome).toMatchObject({
379
- status: "fulfilled",
380
- value: 200,
381
- });
373
+ expect(abortedRequestIds).toEqual([4]);
374
+ outcomes.forEach((outcome, requestId) => {
375
+ if (requestId === 4) {
376
+ expect(outcome.status === "rejected" || (outcome.status === "fulfilled" && outcome.value >= 400)).toBe(
377
+ true,
378
+ );
379
+ return;
380
+ }
381
+
382
+ expect(outcome).toMatchObject({
383
+ status: "fulfilled",
384
+ value: 200,
385
+ });
386
+ });
382
387
  });
383
- });
384
-
385
- it("client disconnect aborts the upstream fetch signal", async () => {
386
- let upstreamSignal: AbortSignal | null | undefined;
387
- const upstreamResponse = createDeferred<Response>();
388
- const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
389
- upstreamSignal = init?.signal;
390
- init?.signal?.addEventListener(
391
- "abort",
392
- () => {
393
- upstreamResponse.reject(init.signal?.reason ?? new DOMException("client disconnected", "AbortError"));
394
- },
395
- { once: true },
396
- );
397
-
398
- return upstreamResponse.promise;
399
- }) as typeof fetch;
400
- const handler = await createProxyRequestHandler(fetchImpl);
401
- const controller = new AbortController();
402
- const responsePromise = handler(makeProxyRequest(123, { signal: controller.signal }));
403
-
404
- await flushMicrotasks();
405
-
406
- expect(fetchImpl).toHaveBeenCalledTimes(1);
407
- expect(upstreamSignal).toBeDefined();
408
- expect(upstreamSignal?.aborted).toBe(false);
409
-
410
- controller.abort(new DOMException("client disconnected", "AbortError"));
411
-
412
- await expect(responsePromise).resolves.toMatchObject({ status: 499 });
413
- expect(upstreamSignal?.aborted).toBe(true);
414
- expect(upstreamSignal?.reason).toBeInstanceOf(DOMException);
415
- expect((upstreamSignal?.reason as DOMException | undefined)?.name).toBe("AbortError");
416
- });
417
-
418
- it("releases all in-flight bookkeeping after repeated bursts to keep memory bounded", async () => {
419
- const proxy = createMockBunProxy({
420
- forwardToMockFetch: vi.fn(async (_input, init) => new Response(String(init?.body), { status: 200 })),
388
+
389
+ it("client disconnect aborts the upstream fetch signal", async () => {
390
+ let upstreamSignal: AbortSignal | null | undefined;
391
+ const upstreamResponse = createDeferred<Response>();
392
+ const fetchImpl = vi.fn((_input: string | URL | Request, init?: RequestInit) => {
393
+ upstreamSignal = init?.signal;
394
+ init?.signal?.addEventListener(
395
+ "abort",
396
+ () => {
397
+ upstreamResponse.reject(
398
+ init.signal?.reason ?? new DOMException("client disconnected", "AbortError"),
399
+ );
400
+ },
401
+ { once: true },
402
+ );
403
+
404
+ return upstreamResponse.promise;
405
+ }) as typeof fetch;
406
+ const handler = await createProxyRequestHandler(fetchImpl);
407
+ const controller = new AbortController();
408
+ const responsePromise = handler(makeProxyRequest(123, { signal: controller.signal }));
409
+
410
+ await flushMicrotasks();
411
+
412
+ expect(fetchImpl).toHaveBeenCalledTimes(1);
413
+ expect(upstreamSignal).toBeDefined();
414
+ expect(upstreamSignal?.aborted).toBe(false);
415
+
416
+ controller.abort(new DOMException("client disconnected", "AbortError"));
417
+
418
+ await expect(responsePromise).resolves.toMatchObject({ status: 499 });
419
+ expect(upstreamSignal?.aborted).toBe(true);
420
+ expect(upstreamSignal?.reason).toBeInstanceOf(DOMException);
421
+ expect((upstreamSignal?.reason as DOMException | undefined)?.name).toBe("AbortError");
421
422
  });
422
- const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
423
-
424
- for (let burst = 0; burst < 3; burst += 1) {
425
- const responses = await Promise.all(
426
- Array.from({ length: 50 }, (_, requestId) => handler(makeProxyRequest(burst * 100 + requestId))),
427
- );
428
- await Promise.all(responses.map((response) => response.text()));
429
- expect(proxy.getInFlightCount()).toBe(0);
430
- }
431
- });
432
-
433
- it("ties the upstream fetch to the incoming request signal without pre-fetch body awaits or mutable globals", () => {
434
- expect(bunProxySource).toMatch(/AbortSignal\.any\s*\(\s*\[\s*req\.signal/i);
435
- expect(bunProxySource).not.toMatch(/await\s+req\.arrayBuffer\s*\(/);
436
- expect(bunProxySource).not.toMatch(/^let\s+/m);
437
- });
438
-
439
- it("starts a parent-PID watcher so the subprocess exits when the parent dies", async () => {
440
- const parentWatcher = {
441
- start: vi.fn(),
442
- stop: vi.fn(),
443
- } satisfies ParentWatcher;
444
- const parentWatcherFactory = vi.fn(({ onParentExit }: Parameters<ParentWatcherFactory>[0]) => {
445
- onParentExit(0);
446
- return parentWatcher;
423
+
424
+ it("releases all in-flight bookkeeping after repeated bursts to keep memory bounded", async () => {
425
+ const proxy = createMockBunProxy({
426
+ forwardToMockFetch: vi.fn(async (_input, init) => new Response(String(init?.body), { status: 200 })),
427
+ });
428
+ const handler = await createProxyRequestHandler(proxy.child.forwardFetch as typeof fetch);
429
+
430
+ for (let burst = 0; burst < 3; burst += 1) {
431
+ const responses = await Promise.all(
432
+ Array.from({ length: 50 }, (_, requestId) => handler(makeProxyRequest(burst * 100 + requestId))),
433
+ );
434
+ await Promise.all(responses.map((response) => response.text()));
435
+ expect(proxy.getInFlightCount()).toBe(0);
436
+ }
447
437
  });
448
- const exit = vi.fn();
449
- const proxy = createMockBunProxy();
450
- const { createProxyProcessRuntime } = await loadBunProxyModule();
451
-
452
- const runtime = createProxyProcessRuntime?.({
453
- argv: ["bun", "run", "bun-proxy.ts", "--parent-pid", "4242"],
454
- fetchImpl: proxy.child.forwardFetch as typeof fetch,
455
- exit,
456
- parentWatcherFactory,
438
+
439
+ it("ties the upstream fetch to the incoming request signal without pre-fetch body awaits or mutable globals", () => {
440
+ expect(bunProxySource).toMatch(/AbortSignal\.any\s*\(\s*\[\s*req\.signal/i);
441
+ expect(bunProxySource).not.toMatch(/await\s+req\.arrayBuffer\s*\(/);
442
+ expect(bunProxySource).not.toMatch(/^let\s+/m);
457
443
  });
458
444
 
459
- runtime?.start();
445
+ it("starts a parent-PID watcher so the subprocess exits when the parent dies", async () => {
446
+ const parentWatcher = {
447
+ start: vi.fn(),
448
+ stop: vi.fn(),
449
+ } satisfies ParentWatcher;
450
+ const parentWatcherFactory = vi.fn(({ onParentExit }: Parameters<ParentWatcherFactory>[0]) => {
451
+ onParentExit(0);
452
+ return parentWatcher;
453
+ });
454
+ const exit = vi.fn();
455
+ const proxy = createMockBunProxy();
456
+ const { createProxyProcessRuntime } = await loadBunProxyModule();
457
+
458
+ const runtime = createProxyProcessRuntime?.({
459
+ argv: ["bun", "run", "bun-proxy.ts", "--parent-pid", "4242"],
460
+ fetchImpl: proxy.child.forwardFetch as typeof fetch,
461
+ exit,
462
+ parentWatcherFactory,
463
+ });
464
+
465
+ runtime?.start();
460
466
 
461
- expect(parentWatcherFactory).toHaveBeenCalledWith(
462
- expect.objectContaining({
463
- parentPid: 4242,
464
- }),
465
- );
466
- expect(exit).toHaveBeenCalledWith(0);
467
- });
467
+ expect(parentWatcherFactory).toHaveBeenCalledWith(
468
+ expect.objectContaining({
469
+ parentPid: 4242,
470
+ }),
471
+ );
472
+ expect(exit).toHaveBeenCalledWith(0);
473
+ });
468
474
  });