@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
package/src/bun-fetch.test.ts
CHANGED
|
@@ -17,468 +17,471 @@ const originalNodeEnv = process.env.NODE_ENV;
|
|
|
17
17
|
const originalVitest = process.env.VITEST;
|
|
18
18
|
|
|
19
19
|
vi.mock("node:child_process", () => ({
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
execFileSync: (...args: unknown[]) => execFileSyncMock(...args),
|
|
21
|
+
spawn: (...args: unknown[]) => spawnMock(...args),
|
|
22
22
|
}));
|
|
23
23
|
|
|
24
24
|
vi.mock("node:fs", async () => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
const actual = await vi.importActual<typeof FsModule>("node:fs");
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
...actual,
|
|
29
|
+
existsSync: (...args: unknown[]) => existsSyncMock(...args),
|
|
30
|
+
readFileSync: (...args: unknown[]) => readFileSyncMock(...args),
|
|
31
|
+
statSync: (...args: unknown[]) => statSyncMock(...args),
|
|
32
|
+
unlinkSync: (...args: unknown[]) => unlinkSyncMock(...args),
|
|
33
|
+
writeFileSync: (...args: unknown[]) => writeFileSyncMock(...args),
|
|
34
|
+
};
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
type BunFetchModuleType = Awaited<typeof BunFetchModule> & {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
createBunFetch?: (options?: { debug?: boolean; onProxyStatus?: (status: unknown) => void }) => {
|
|
39
|
+
fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
40
|
+
shutdown: () => Promise<void>;
|
|
41
|
+
getStatus: () => unknown;
|
|
42
|
+
};
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
async function readBunFetchSource(): Promise<string> {
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const fs = await vi.importActual<typeof FsModule>("node:fs");
|
|
47
|
+
return fs.readFileSync(new URL("./bun-fetch.ts", import.meta.url), "utf8");
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
async function loadBunFetchModule(): Promise<BunFetchModuleType> {
|
|
51
|
-
|
|
51
|
+
return import("./bun-fetch.js") as Promise<BunFetchModuleType>;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function installMockFetch(implementation?: Parameters<typeof vi.fn>[0]): ReturnType<typeof vi.fn> {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const fetchMock = vi.fn(implementation ?? (async () => new Response("native-fallback", { status: 200 })));
|
|
56
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
57
|
+
return fetchMock;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function getCreateBunFetch(moduleNs: BunFetchModuleType): NonNullable<BunFetchModuleType["createBunFetch"]> {
|
|
61
|
-
|
|
61
|
+
const createBunFetch = moduleNs.createBunFetch;
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
expect(createBunFetch, "T20 must export createBunFetch() for per-instance lifecycle ownership").toBeTypeOf(
|
|
64
|
+
"function",
|
|
65
|
+
);
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
if (typeof createBunFetch !== "function") {
|
|
68
|
+
throw new TypeError("createBunFetch is missing");
|
|
69
|
+
}
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
return createBunFetch;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
beforeEach(async () => {
|
|
75
|
-
|
|
75
|
+
const fs = await vi.importActual<typeof FsModule>("node:fs");
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
vi.resetModules();
|
|
78
|
+
vi.useRealTimers();
|
|
79
|
+
vi.unstubAllGlobals();
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
execFileSyncMock = vi.fn().mockReturnValue(undefined);
|
|
82
|
+
spawnMock = vi.fn();
|
|
83
|
+
existsSyncMock = vi.fn((filePath: unknown) => {
|
|
84
|
+
if (typeof filePath === "string" && /bun-proxy\.(mjs|ts)$/.test(filePath)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return fs.existsSync(filePath as Parameters<typeof fs.existsSync>[0]);
|
|
89
|
+
});
|
|
90
|
+
readFileSyncMock = vi.fn((...args: unknown[]) =>
|
|
91
|
+
fs.readFileSync(
|
|
92
|
+
args[0] as Parameters<typeof fs.readFileSync>[0],
|
|
93
|
+
args[1] as Parameters<typeof fs.readFileSync>[1],
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
statSyncMock = vi.fn((...args: unknown[]) => fs.statSync(args[0] as Parameters<typeof fs.statSync>[0]));
|
|
97
|
+
unlinkSyncMock = vi.fn();
|
|
98
|
+
writeFileSyncMock = vi.fn();
|
|
87
99
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
readFileSyncMock = vi.fn((...args: unknown[]) =>
|
|
91
|
-
fs.readFileSync(args[0] as Parameters<typeof fs.readFileSync>[0], args[1] as Parameters<typeof fs.readFileSync>[1]),
|
|
92
|
-
);
|
|
93
|
-
statSyncMock = vi.fn((...args: unknown[]) => fs.statSync(args[0] as Parameters<typeof fs.statSync>[0]));
|
|
94
|
-
unlinkSyncMock = vi.fn();
|
|
95
|
-
writeFileSyncMock = vi.fn();
|
|
96
|
-
|
|
97
|
-
process.env.NODE_ENV = "development";
|
|
98
|
-
delete process.env.VITEST;
|
|
100
|
+
process.env.NODE_ENV = "development";
|
|
101
|
+
delete process.env.VITEST;
|
|
99
102
|
});
|
|
100
103
|
|
|
101
104
|
afterEach(() => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
vi.useRealTimers();
|
|
106
|
+
vi.unstubAllGlobals();
|
|
107
|
+
vi.restoreAllMocks();
|
|
108
|
+
|
|
109
|
+
if (originalNodeEnv === undefined) {
|
|
110
|
+
delete process.env.NODE_ENV;
|
|
111
|
+
} else {
|
|
112
|
+
process.env.NODE_ENV = originalNodeEnv;
|
|
113
|
+
}
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
if (originalVitest === undefined) {
|
|
116
|
+
delete process.env.VITEST;
|
|
117
|
+
} else {
|
|
118
|
+
process.env.VITEST = originalVitest;
|
|
119
|
+
}
|
|
117
120
|
});
|
|
118
121
|
|
|
119
122
|
describe("bun-fetch source guardrails (RED until T20/T21)", () => {
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
it("removes module-level proxy/process/counter state in favor of instance-owned closures", async () => {
|
|
124
|
+
const source = await readBunFetchSource();
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
expect(source).not.toMatch(/^let (proxyPort|proxyProcess|starting|healthCheckFails|exitHandlerRegistered)\b/m);
|
|
127
|
+
});
|
|
125
128
|
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
it("does not hard-code a fixed port, pid file, or MAX_HEALTH_FAILS singleton constants", async () => {
|
|
130
|
+
const source = await readBunFetchSource();
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
expect(source).not.toMatch(/\b(FIXED_PORT|PID_FILE|MAX_HEALTH_FAILS|healthCheckFails)\b/);
|
|
133
|
+
expect(source).not.toContain("48372");
|
|
134
|
+
});
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
it("spawns Bun with --parent-pid and without passing a fixed port argument", async () => {
|
|
137
|
+
const source = await readBunFetchSource();
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
expect(source).toContain("--parent-pid");
|
|
140
|
+
expect(source).not.toMatch(/String\(FIXED_PORT\)|48372/);
|
|
141
|
+
});
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
it("parses proxy banners with a line-buffered stdout reader", async () => {
|
|
144
|
+
const source = await readBunFetchSource();
|
|
142
145
|
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
expect(source).toMatch(/readline\.createInterface|createInterface\(/);
|
|
147
|
+
});
|
|
145
148
|
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
it("never calls stopBunProxy from fetchViaBun catch blocks", async () => {
|
|
150
|
+
const source = await readBunFetchSource();
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
expect(source).not.toMatch(/catch\s*\([^)]*\)\s*\{[\s\S]*stopBunProxy\(/);
|
|
153
|
+
});
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
it("uses a circuit breaker instead of a shared healthCheckFails counter", async () => {
|
|
156
|
+
const source = await readBunFetchSource();
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
expect(source).toMatch(/createCircuitBreaker|CircuitBreaker/);
|
|
159
|
+
expect(source).not.toContain("healthCheckFails");
|
|
160
|
+
});
|
|
158
161
|
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
it("installs no global process.on handlers or process.exit calls", async () => {
|
|
163
|
+
const source = await readBunFetchSource();
|
|
161
164
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
expect(source).not.toMatch(/process\.on\s*\(/);
|
|
166
|
+
expect(source).not.toMatch(/process\.exit\s*\(/);
|
|
167
|
+
});
|
|
165
168
|
});
|
|
166
169
|
|
|
167
170
|
describe("createBunFetch runtime lifecycle (RED until T20)", () => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
171
|
+
it("exports a createBunFetch factory with fetch/shutdown/getStatus instance API", async () => {
|
|
172
|
+
const proxy = createMockBunProxy();
|
|
173
|
+
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
174
|
+
installMockFetch();
|
|
175
|
+
|
|
176
|
+
const moduleNs = await loadBunFetchModule();
|
|
177
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
178
|
+
const instance = createBunFetch({ debug: false });
|
|
179
|
+
|
|
180
|
+
expect(instance).toMatchObject({
|
|
181
|
+
fetch: expect.any(Function),
|
|
182
|
+
shutdown: expect.any(Function),
|
|
183
|
+
getStatus: expect.any(Function),
|
|
184
|
+
});
|
|
181
185
|
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("creates a new proxy per plugin instance instead of sharing module-level state", async () => {
|
|
185
|
-
const proxyA = createMockBunProxy();
|
|
186
|
-
const proxyB = createMockBunProxy();
|
|
187
|
-
spawnMock.mockImplementationOnce(proxyA.mockSpawn).mockImplementationOnce(proxyB.mockSpawn);
|
|
188
|
-
installMockFetch();
|
|
189
186
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
187
|
+
it("creates a new proxy per plugin instance instead of sharing module-level state", async () => {
|
|
188
|
+
const proxyA = createMockBunProxy();
|
|
189
|
+
const proxyB = createMockBunProxy();
|
|
190
|
+
spawnMock.mockImplementationOnce(proxyA.mockSpawn).mockImplementationOnce(proxyB.mockSpawn);
|
|
191
|
+
installMockFetch();
|
|
194
192
|
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
const moduleNs = await loadBunFetchModule();
|
|
194
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
195
|
+
const instanceA = createBunFetch({ debug: false });
|
|
196
|
+
const instanceB = createBunFetch({ debug: false });
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
instanceB.fetch("https://example.com/b", { method: "POST", body: "b" }),
|
|
201
|
-
]);
|
|
198
|
+
proxyA.simulateStdoutBanner(41001);
|
|
199
|
+
proxyB.simulateStdoutBanner(41002);
|
|
202
200
|
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
await Promise.all([
|
|
202
|
+
instanceA.fetch("https://example.com/a", { method: "POST", body: "a" }),
|
|
203
|
+
instanceB.fetch("https://example.com/b", { method: "POST", body: "b" }),
|
|
204
|
+
]);
|
|
205
205
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
209
|
-
installMockFetch(async () => proxy.child.forwardFetch("https://example.com/reused", { method: "POST" }));
|
|
206
|
+
expect(spawnMock).toHaveBeenCalledTimes(2);
|
|
207
|
+
});
|
|
210
208
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
209
|
+
it("reuses the same proxy for sequential requests from a single instance", async () => {
|
|
210
|
+
const proxy = createMockBunProxy();
|
|
211
|
+
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
212
|
+
installMockFetch(async () => proxy.child.forwardFetch("https://example.com/reused", { method: "POST" }));
|
|
214
213
|
|
|
215
|
-
|
|
214
|
+
const moduleNs = await loadBunFetchModule();
|
|
215
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
216
|
+
const instance = createBunFetch({ debug: false });
|
|
216
217
|
|
|
217
|
-
|
|
218
|
-
await instance.fetch("https://example.com/second", { method: "POST", body: "second" });
|
|
218
|
+
proxy.simulateStdoutBanner(41011);
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
await instance.fetch("https://example.com/first", { method: "POST", body: "first" });
|
|
221
|
+
await instance.fetch("https://example.com/second", { method: "POST", body: "second" });
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
224
|
+
});
|
|
225
225
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
installMockFetch();
|
|
226
|
+
it("parses a split BUN_PROXY_PORT banner line-by-line instead of per-chunk", async () => {
|
|
227
|
+
vi.useFakeTimers();
|
|
229
228
|
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
const proxy = createMockBunProxy();
|
|
230
|
+
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
231
|
+
installMockFetch();
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
proxy.child.stdout.write("PORT=43123\n");
|
|
233
|
+
const moduleNs = await loadBunFetchModule();
|
|
234
|
+
expect(moduleNs.ensureBunProxy).toBeTypeOf("function");
|
|
236
235
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
const startup = moduleNs.ensureBunProxy(false);
|
|
237
|
+
proxy.child.stdout.write("BUN_PROXY_");
|
|
238
|
+
proxy.child.stdout.write("PORT=43123\n");
|
|
240
239
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const proxy = createMockBunProxy({
|
|
244
|
-
forwardToMockFetch: async () => responses.enqueue().promise,
|
|
240
|
+
await vi.advanceTimersByTimeAsync(5001);
|
|
241
|
+
await expect(startup).resolves.toBe(43123);
|
|
245
242
|
});
|
|
246
|
-
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
247
|
-
installMockFetch();
|
|
248
243
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
244
|
+
it("keeps 10 concurrent sibling requests on one shared proxy without interference", async () => {
|
|
245
|
+
const responses = createDeferredQueue<Response>();
|
|
246
|
+
const proxy = createMockBunProxy({
|
|
247
|
+
forwardToMockFetch: async () => responses.enqueue().promise,
|
|
248
|
+
});
|
|
249
|
+
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
250
|
+
installMockFetch();
|
|
252
251
|
|
|
253
|
-
|
|
252
|
+
const moduleNs = await loadBunFetchModule();
|
|
253
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
254
|
+
const instance = createBunFetch({ debug: false });
|
|
254
255
|
|
|
255
|
-
|
|
256
|
-
instance.fetch(`https://example.com/${index}`, {
|
|
257
|
-
method: "POST",
|
|
258
|
-
body: `body-${index}`,
|
|
259
|
-
}),
|
|
260
|
-
);
|
|
256
|
+
proxy.simulateStdoutBanner(41021);
|
|
261
257
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
258
|
+
const requests = Array.from({ length: 10 }, (_, index) =>
|
|
259
|
+
instance.fetch(`https://example.com/${index}`, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
body: `body-${index}`,
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
267
264
|
|
|
268
|
-
|
|
269
|
-
});
|
|
265
|
+
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
270
266
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const proxy = createMockBunProxy({
|
|
274
|
-
forwardToMockFetch: async (input) => {
|
|
275
|
-
if (String(input).includes("/fail")) {
|
|
276
|
-
throw new Error("upstream exploded");
|
|
267
|
+
for (let index = 0; index < 10; index += 1) {
|
|
268
|
+
responses.resolveNext(new Response(`ok-${index}`, { status: 200 }));
|
|
277
269
|
}
|
|
278
270
|
|
|
279
|
-
|
|
280
|
-
},
|
|
271
|
+
await expect(Promise.all(requests)).resolves.toHaveLength(10);
|
|
281
272
|
});
|
|
282
|
-
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
283
|
-
installMockFetch();
|
|
284
273
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
274
|
+
it("does not kill sibling streams or the proxy when one concurrent request fails", async () => {
|
|
275
|
+
const slowSuccess = createDeferred<Response>();
|
|
276
|
+
const proxy = createMockBunProxy({
|
|
277
|
+
forwardToMockFetch: async (input) => {
|
|
278
|
+
if (String(input).includes("/fail")) {
|
|
279
|
+
throw new Error("upstream exploded");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return slowSuccess.promise;
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
286
|
+
installMockFetch();
|
|
287
|
+
|
|
288
|
+
const moduleNs = await loadBunFetchModule();
|
|
289
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
290
|
+
const instance = createBunFetch({ debug: false });
|
|
291
|
+
|
|
292
|
+
proxy.simulateStdoutBanner(41031);
|
|
293
|
+
|
|
294
|
+
const goodRequest = instance.fetch("https://example.com/stream-ok", {
|
|
295
|
+
method: "POST",
|
|
296
|
+
body: "ok",
|
|
297
|
+
});
|
|
298
|
+
const badRequest = instance.fetch("https://example.com/fail", {
|
|
299
|
+
method: "POST",
|
|
300
|
+
body: "fail",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
slowSuccess.resolve(new Response("still-open", { status: 200 }));
|
|
304
|
+
|
|
305
|
+
await expect(goodRequest).resolves.toBeInstanceOf(Response);
|
|
306
|
+
await expect(badRequest).rejects.toThrow("upstream exploded");
|
|
307
|
+
expect(proxy.child.killSignals).toEqual([]);
|
|
308
|
+
});
|
|
288
309
|
|
|
289
|
-
|
|
310
|
+
it("falls back gracefully when Bun spawn fails without calling process.exit", async () => {
|
|
311
|
+
const nativeFetch = installMockFetch(async () => new Response("native", { status: 200 }));
|
|
312
|
+
spawnMock.mockImplementation(() => {
|
|
313
|
+
throw new Error("spawn failed");
|
|
314
|
+
});
|
|
290
315
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
});
|
|
295
|
-
const badRequest = instance.fetch("https://example.com/fail", {
|
|
296
|
-
method: "POST",
|
|
297
|
-
body: "fail",
|
|
298
|
-
});
|
|
316
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: string | number | null) => {
|
|
317
|
+
throw new Error(`unexpected process.exit(${code ?? ""})`);
|
|
318
|
+
}) as typeof process.exit);
|
|
299
319
|
|
|
300
|
-
|
|
320
|
+
const moduleNs = await loadBunFetchModule();
|
|
321
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
322
|
+
const onProxyStatus = vi.fn();
|
|
323
|
+
const instance = createBunFetch({ debug: false, onProxyStatus });
|
|
301
324
|
|
|
302
|
-
|
|
303
|
-
await expect(badRequest).rejects.toThrow("upstream exploded");
|
|
304
|
-
expect(proxy.child.killSignals).toEqual([]);
|
|
305
|
-
});
|
|
325
|
+
const response = await instance.fetch("https://example.com/native", { method: "POST", body: "native" });
|
|
306
326
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
327
|
+
expect(await response.text()).toBe("native");
|
|
328
|
+
expect(nativeFetch).toHaveBeenCalledTimes(1);
|
|
329
|
+
expect(onProxyStatus).toHaveBeenCalled();
|
|
330
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
311
331
|
});
|
|
312
332
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
333
|
+
it("keeps an old instance alive for in-flight work when a new hot-reload instance is created", async () => {
|
|
334
|
+
const firstResponse = createDeferred<Response>();
|
|
335
|
+
const proxyA = createMockBunProxy({ forwardToMockFetch: async () => firstResponse.promise });
|
|
336
|
+
const proxyB = createMockBunProxy({
|
|
337
|
+
forwardToMockFetch: async () => new Response("fresh-instance", { status: 200 }),
|
|
338
|
+
});
|
|
339
|
+
spawnMock.mockImplementationOnce(proxyA.mockSpawn).mockImplementationOnce(proxyB.mockSpawn);
|
|
340
|
+
installMockFetch();
|
|
341
|
+
|
|
342
|
+
const moduleNs = await loadBunFetchModule();
|
|
343
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
344
|
+
const oldInstance = createBunFetch({ debug: false });
|
|
345
|
+
const oldRequest = oldInstance.fetch("https://example.com/old", { method: "POST", body: "old" });
|
|
346
|
+
|
|
347
|
+
proxyA.simulateStdoutBanner(41041);
|
|
316
348
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const onProxyStatus = vi.fn();
|
|
320
|
-
const instance = createBunFetch({ debug: false, onProxyStatus });
|
|
349
|
+
const newInstance = createBunFetch({ debug: false });
|
|
350
|
+
proxyB.simulateStdoutBanner(41042);
|
|
321
351
|
|
|
322
|
-
|
|
352
|
+
const newRequest = newInstance.fetch("https://example.com/new", { method: "POST", body: "new" });
|
|
323
353
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
expect(onProxyStatus).toHaveBeenCalled();
|
|
327
|
-
expect(exitSpy).not.toHaveBeenCalled();
|
|
328
|
-
});
|
|
354
|
+
expect(proxyA.getInFlightCount()).toBe(1);
|
|
355
|
+
firstResponse.resolve(new Response("old-instance-still-streaming", { status: 200 }));
|
|
329
356
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const proxyA = createMockBunProxy({ forwardToMockFetch: async () => firstResponse.promise });
|
|
333
|
-
const proxyB = createMockBunProxy({
|
|
334
|
-
forwardToMockFetch: async () => new Response("fresh-instance", { status: 200 }),
|
|
357
|
+
await expect(oldRequest).resolves.toBeInstanceOf(Response);
|
|
358
|
+
await expect(newRequest).resolves.toBeInstanceOf(Response);
|
|
335
359
|
});
|
|
336
|
-
spawnMock.mockImplementationOnce(proxyA.mockSpawn).mockImplementationOnce(proxyB.mockSpawn);
|
|
337
|
-
installMockFetch();
|
|
338
360
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
361
|
+
it("cleans up the current child on shutdown without clearing state for an older child exit", async () => {
|
|
362
|
+
const proxyA = createMockBunProxy();
|
|
363
|
+
const proxyB = createMockBunProxy();
|
|
364
|
+
spawnMock.mockImplementationOnce(proxyA.mockSpawn).mockImplementationOnce(proxyB.mockSpawn);
|
|
365
|
+
installMockFetch(async () => new Response("ok", { status: 200 }));
|
|
343
366
|
|
|
344
|
-
|
|
367
|
+
const moduleNs = await loadBunFetchModule();
|
|
368
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
369
|
+
const instanceA = createBunFetch({ debug: false });
|
|
370
|
+
const instanceB = createBunFetch({ debug: false });
|
|
345
371
|
|
|
346
|
-
|
|
347
|
-
|
|
372
|
+
proxyA.simulateStdoutBanner(41051);
|
|
373
|
+
proxyB.simulateStdoutBanner(41052);
|
|
348
374
|
|
|
349
|
-
|
|
375
|
+
await instanceA.fetch("https://example.com/a", { method: "POST", body: "a" });
|
|
376
|
+
await instanceB.fetch("https://example.com/b", { method: "POST", body: "b" });
|
|
350
377
|
|
|
351
|
-
|
|
352
|
-
|
|
378
|
+
proxyA.simulateExit(0, null);
|
|
379
|
+
await instanceB.shutdown();
|
|
353
380
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
381
|
+
expect(proxyB.child.killSignals).toContain("SIGTERM");
|
|
382
|
+
expect(proxyA.child.killSignals).toEqual([]);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
357
385
|
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
386
|
+
describe("createBunFetch debug request dumping", () => {
|
|
387
|
+
const UNIQUE_REQUEST_PATTERN =
|
|
388
|
+
/^\/tmp\/opencode-request-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[0-9a-f]{8}\.json$/;
|
|
389
|
+
const UNIQUE_HEADERS_PATTERN =
|
|
390
|
+
/^\/tmp\/opencode-headers-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[0-9a-f]{8}\.json$/;
|
|
391
|
+
const LATEST_REQUEST_PATH = "/tmp/opencode-last-request.json";
|
|
392
|
+
const LATEST_HEADERS_PATH = "/tmp/opencode-last-headers.json";
|
|
393
|
+
|
|
394
|
+
function writtenPaths(): string[] {
|
|
395
|
+
return writeFileSyncMock.mock.calls.map((call) => String(call[0]));
|
|
396
|
+
}
|
|
363
397
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
398
|
+
it("writes a uniquely-named request file AND a latest-alias file when debug=true", async () => {
|
|
399
|
+
const proxy = createMockBunProxy();
|
|
400
|
+
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
401
|
+
installMockFetch();
|
|
368
402
|
|
|
369
|
-
|
|
370
|
-
|
|
403
|
+
const moduleNs = await loadBunFetchModule();
|
|
404
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
405
|
+
const instance = createBunFetch({ debug: true });
|
|
371
406
|
|
|
372
|
-
|
|
373
|
-
await instanceB.fetch("https://example.com/b", { method: "POST", body: "b" });
|
|
407
|
+
proxy.simulateStdoutBanner(42001);
|
|
374
408
|
|
|
375
|
-
|
|
376
|
-
|
|
409
|
+
await instance.fetch("https://api.anthropic.com/v1/messages?beta=true", {
|
|
410
|
+
method: "POST",
|
|
411
|
+
body: JSON.stringify({ hello: "world" }),
|
|
412
|
+
});
|
|
377
413
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
});
|
|
414
|
+
const paths = writtenPaths();
|
|
415
|
+
const uniqueRequest = paths.find((path) => UNIQUE_REQUEST_PATTERN.test(path));
|
|
416
|
+
const uniqueHeaders = paths.find((path) => UNIQUE_HEADERS_PATTERN.test(path));
|
|
382
417
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
/^\/tmp\/opencode-headers-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[0-9a-f]{8}\.json$/;
|
|
388
|
-
const LATEST_REQUEST_PATH = "/tmp/opencode-last-request.json";
|
|
389
|
-
const LATEST_HEADERS_PATH = "/tmp/opencode-last-headers.json";
|
|
390
|
-
|
|
391
|
-
function writtenPaths(): string[] {
|
|
392
|
-
return writeFileSyncMock.mock.calls.map((call) => String(call[0]));
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
it("writes a uniquely-named request file AND a latest-alias file when debug=true", async () => {
|
|
396
|
-
const proxy = createMockBunProxy();
|
|
397
|
-
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
398
|
-
installMockFetch();
|
|
399
|
-
|
|
400
|
-
const moduleNs = await loadBunFetchModule();
|
|
401
|
-
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
402
|
-
const instance = createBunFetch({ debug: true });
|
|
403
|
-
|
|
404
|
-
proxy.simulateStdoutBanner(42001);
|
|
405
|
-
|
|
406
|
-
await instance.fetch("https://api.anthropic.com/v1/messages?beta=true", {
|
|
407
|
-
method: "POST",
|
|
408
|
-
body: JSON.stringify({ hello: "world" }),
|
|
418
|
+
expect(uniqueRequest, "expected a uniquely-named request dump").toBeDefined();
|
|
419
|
+
expect(uniqueHeaders, "expected a uniquely-named headers dump").toBeDefined();
|
|
420
|
+
expect(paths).toContain(LATEST_REQUEST_PATH);
|
|
421
|
+
expect(paths).toContain(LATEST_HEADERS_PATH);
|
|
409
422
|
});
|
|
410
423
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
424
|
+
it("produces a different unique path for each sequential debug request", async () => {
|
|
425
|
+
const proxy = createMockBunProxy();
|
|
426
|
+
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
427
|
+
installMockFetch();
|
|
414
428
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
expect(paths).toContain(LATEST_HEADERS_PATH);
|
|
419
|
-
});
|
|
429
|
+
const moduleNs = await loadBunFetchModule();
|
|
430
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
431
|
+
const instance = createBunFetch({ debug: true });
|
|
420
432
|
|
|
421
|
-
|
|
422
|
-
const proxy = createMockBunProxy();
|
|
423
|
-
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
424
|
-
installMockFetch();
|
|
433
|
+
proxy.simulateStdoutBanner(42002);
|
|
425
434
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
435
|
+
await instance.fetch("https://api.anthropic.com/v1/messages?beta=true", {
|
|
436
|
+
method: "POST",
|
|
437
|
+
body: JSON.stringify({ n: 1 }),
|
|
438
|
+
});
|
|
439
|
+
await instance.fetch("https://api.anthropic.com/v1/messages?beta=true", {
|
|
440
|
+
method: "POST",
|
|
441
|
+
body: JSON.stringify({ n: 2 }),
|
|
442
|
+
});
|
|
429
443
|
|
|
430
|
-
|
|
444
|
+
const uniquePaths = writtenPaths().filter((path) => UNIQUE_REQUEST_PATTERN.test(path));
|
|
431
445
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
body: JSON.stringify({ n: 1 }),
|
|
435
|
-
});
|
|
436
|
-
await instance.fetch("https://api.anthropic.com/v1/messages?beta=true", {
|
|
437
|
-
method: "POST",
|
|
438
|
-
body: JSON.stringify({ n: 2 }),
|
|
446
|
+
expect(uniquePaths).toHaveLength(2);
|
|
447
|
+
expect(uniquePaths[0]).not.toBe(uniquePaths[1]);
|
|
439
448
|
});
|
|
440
449
|
|
|
441
|
-
|
|
450
|
+
it("does not dump any artifact for count_tokens requests even when debug=true", async () => {
|
|
451
|
+
const proxy = createMockBunProxy();
|
|
452
|
+
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
453
|
+
installMockFetch();
|
|
442
454
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
455
|
+
const moduleNs = await loadBunFetchModule();
|
|
456
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
457
|
+
const instance = createBunFetch({ debug: true });
|
|
446
458
|
|
|
447
|
-
|
|
448
|
-
const proxy = createMockBunProxy();
|
|
449
|
-
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
450
|
-
installMockFetch();
|
|
459
|
+
proxy.simulateStdoutBanner(42003);
|
|
451
460
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
461
|
+
await instance.fetch("https://api.anthropic.com/v1/messages/count_tokens", {
|
|
462
|
+
method: "POST",
|
|
463
|
+
body: JSON.stringify({ hello: "world" }),
|
|
464
|
+
});
|
|
455
465
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
await instance.fetch("https://api.anthropic.com/v1/messages/count_tokens", {
|
|
459
|
-
method: "POST",
|
|
460
|
-
body: JSON.stringify({ hello: "world" }),
|
|
466
|
+
expect(writeFileSyncMock).not.toHaveBeenCalled();
|
|
461
467
|
});
|
|
462
468
|
|
|
463
|
-
|
|
464
|
-
|
|
469
|
+
it("does not dump any artifact when debug=false", async () => {
|
|
470
|
+
const proxy = createMockBunProxy();
|
|
471
|
+
spawnMock.mockImplementation(proxy.mockSpawn);
|
|
472
|
+
installMockFetch();
|
|
465
473
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
installMockFetch();
|
|
474
|
+
const moduleNs = await loadBunFetchModule();
|
|
475
|
+
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
476
|
+
const instance = createBunFetch({ debug: false });
|
|
470
477
|
|
|
471
|
-
|
|
472
|
-
const createBunFetch = getCreateBunFetch(moduleNs);
|
|
473
|
-
const instance = createBunFetch({ debug: false });
|
|
478
|
+
proxy.simulateStdoutBanner(42004);
|
|
474
479
|
|
|
475
|
-
|
|
480
|
+
await instance.fetch("https://api.anthropic.com/v1/messages?beta=true", {
|
|
481
|
+
method: "POST",
|
|
482
|
+
body: JSON.stringify({ hello: "world" }),
|
|
483
|
+
});
|
|
476
484
|
|
|
477
|
-
|
|
478
|
-
method: "POST",
|
|
479
|
-
body: JSON.stringify({ hello: "world" }),
|
|
485
|
+
expect(writeFileSyncMock).not.toHaveBeenCalled();
|
|
480
486
|
});
|
|
481
|
-
|
|
482
|
-
expect(writeFileSyncMock).not.toHaveBeenCalled();
|
|
483
|
-
});
|
|
484
487
|
});
|