@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
@@ -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
- execFileSync: (...args: unknown[]) => execFileSyncMock(...args),
21
- spawn: (...args: unknown[]) => spawnMock(...args),
20
+ execFileSync: (...args: unknown[]) => execFileSyncMock(...args),
21
+ spawn: (...args: unknown[]) => spawnMock(...args),
22
22
  }));
23
23
 
24
24
  vi.mock("node:fs", async () => {
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
- };
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
- 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
- };
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
- const fs = await vi.importActual<typeof FsModule>("node:fs");
47
- return fs.readFileSync(new URL("./bun-fetch.ts", import.meta.url), "utf8");
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
- return import("./bun-fetch.js") as Promise<BunFetchModuleType>;
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
- const fetchMock = vi.fn(implementation ?? (async () => new Response("native-fallback", { status: 200 })));
56
- vi.stubGlobal("fetch", fetchMock);
57
- return fetchMock;
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
- const createBunFetch = moduleNs.createBunFetch;
61
+ const createBunFetch = moduleNs.createBunFetch;
62
62
 
63
- expect(createBunFetch, "T20 must export createBunFetch() for per-instance lifecycle ownership").toBeTypeOf(
64
- "function",
65
- );
63
+ expect(createBunFetch, "T20 must export createBunFetch() for per-instance lifecycle ownership").toBeTypeOf(
64
+ "function",
65
+ );
66
66
 
67
- if (typeof createBunFetch !== "function") {
68
- throw new TypeError("createBunFetch is missing");
69
- }
67
+ if (typeof createBunFetch !== "function") {
68
+ throw new TypeError("createBunFetch is missing");
69
+ }
70
70
 
71
- return createBunFetch;
71
+ return createBunFetch;
72
72
  }
73
73
 
74
74
  beforeEach(async () => {
75
- const fs = await vi.importActual<typeof FsModule>("node:fs");
75
+ const fs = await vi.importActual<typeof FsModule>("node:fs");
76
76
 
77
- vi.resetModules();
78
- vi.useRealTimers();
79
- vi.unstubAllGlobals();
77
+ vi.resetModules();
78
+ vi.useRealTimers();
79
+ vi.unstubAllGlobals();
80
80
 
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
- }
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
- return fs.existsSync(filePath as Parameters<typeof fs.existsSync>[0]);
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
- vi.useRealTimers();
103
- vi.unstubAllGlobals();
104
- vi.restoreAllMocks();
105
-
106
- if (originalNodeEnv === undefined) {
107
- delete process.env.NODE_ENV;
108
- } else {
109
- process.env.NODE_ENV = originalNodeEnv;
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
- if (originalVitest === undefined) {
113
- delete process.env.VITEST;
114
- } else {
115
- process.env.VITEST = originalVitest;
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
- it("removes module-level proxy/process/counter state in favor of instance-owned closures", async () => {
121
- const source = await readBunFetchSource();
123
+ it("removes module-level proxy/process/counter state in favor of instance-owned closures", async () => {
124
+ const source = await readBunFetchSource();
122
125
 
123
- expect(source).not.toMatch(/^let (proxyPort|proxyProcess|starting|healthCheckFails|exitHandlerRegistered)\b/m);
124
- });
126
+ expect(source).not.toMatch(/^let (proxyPort|proxyProcess|starting|healthCheckFails|exitHandlerRegistered)\b/m);
127
+ });
125
128
 
126
- it("does not hard-code a fixed port, pid file, or MAX_HEALTH_FAILS singleton constants", async () => {
127
- const source = await readBunFetchSource();
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
- expect(source).not.toMatch(/\b(FIXED_PORT|PID_FILE|MAX_HEALTH_FAILS|healthCheckFails)\b/);
130
- expect(source).not.toContain("48372");
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
- it("spawns Bun with --parent-pid and without passing a fixed port argument", async () => {
134
- const source = await readBunFetchSource();
136
+ it("spawns Bun with --parent-pid and without passing a fixed port argument", async () => {
137
+ const source = await readBunFetchSource();
135
138
 
136
- expect(source).toContain("--parent-pid");
137
- expect(source).not.toMatch(/String\(FIXED_PORT\)|48372/);
138
- });
139
+ expect(source).toContain("--parent-pid");
140
+ expect(source).not.toMatch(/String\(FIXED_PORT\)|48372/);
141
+ });
139
142
 
140
- it("parses proxy banners with a line-buffered stdout reader", async () => {
141
- const source = await readBunFetchSource();
143
+ it("parses proxy banners with a line-buffered stdout reader", async () => {
144
+ const source = await readBunFetchSource();
142
145
 
143
- expect(source).toMatch(/readline\.createInterface|createInterface\(/);
144
- });
146
+ expect(source).toMatch(/readline\.createInterface|createInterface\(/);
147
+ });
145
148
 
146
- it("never calls stopBunProxy from fetchViaBun catch blocks", async () => {
147
- const source = await readBunFetchSource();
149
+ it("never calls stopBunProxy from fetchViaBun catch blocks", async () => {
150
+ const source = await readBunFetchSource();
148
151
 
149
- expect(source).not.toMatch(/catch\s*\([^)]*\)\s*\{[\s\S]*stopBunProxy\(/);
150
- });
152
+ expect(source).not.toMatch(/catch\s*\([^)]*\)\s*\{[\s\S]*stopBunProxy\(/);
153
+ });
151
154
 
152
- it("uses a circuit breaker instead of a shared healthCheckFails counter", async () => {
153
- const source = await readBunFetchSource();
155
+ it("uses a circuit breaker instead of a shared healthCheckFails counter", async () => {
156
+ const source = await readBunFetchSource();
154
157
 
155
- expect(source).toMatch(/createCircuitBreaker|CircuitBreaker/);
156
- expect(source).not.toContain("healthCheckFails");
157
- });
158
+ expect(source).toMatch(/createCircuitBreaker|CircuitBreaker/);
159
+ expect(source).not.toContain("healthCheckFails");
160
+ });
158
161
 
159
- it("installs no global process.on handlers or process.exit calls", async () => {
160
- const source = await readBunFetchSource();
162
+ it("installs no global process.on handlers or process.exit calls", async () => {
163
+ const source = await readBunFetchSource();
161
164
 
162
- expect(source).not.toMatch(/process\.on\s*\(/);
163
- expect(source).not.toMatch(/process\.exit\s*\(/);
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
- it("exports a createBunFetch factory with fetch/shutdown/getStatus instance API", async () => {
169
- const proxy = createMockBunProxy();
170
- spawnMock.mockImplementation(proxy.mockSpawn);
171
- installMockFetch();
172
-
173
- const moduleNs = await loadBunFetchModule();
174
- const createBunFetch = getCreateBunFetch(moduleNs);
175
- const instance = createBunFetch({ debug: false });
176
-
177
- expect(instance).toMatchObject({
178
- fetch: expect.any(Function),
179
- shutdown: expect.any(Function),
180
- getStatus: expect.any(Function),
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
- const moduleNs = await loadBunFetchModule();
191
- const createBunFetch = getCreateBunFetch(moduleNs);
192
- const instanceA = createBunFetch({ debug: false });
193
- const instanceB = createBunFetch({ debug: false });
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
- proxyA.simulateStdoutBanner(41001);
196
- proxyB.simulateStdoutBanner(41002);
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
- await Promise.all([
199
- instanceA.fetch("https://example.com/a", { method: "POST", body: "a" }),
200
- instanceB.fetch("https://example.com/b", { method: "POST", body: "b" }),
201
- ]);
198
+ proxyA.simulateStdoutBanner(41001);
199
+ proxyB.simulateStdoutBanner(41002);
202
200
 
203
- expect(spawnMock).toHaveBeenCalledTimes(2);
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
- it("reuses the same proxy for sequential requests from a single instance", async () => {
207
- const proxy = createMockBunProxy();
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
- const moduleNs = await loadBunFetchModule();
212
- const createBunFetch = getCreateBunFetch(moduleNs);
213
- const instance = createBunFetch({ debug: false });
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
- proxy.simulateStdoutBanner(41011);
214
+ const moduleNs = await loadBunFetchModule();
215
+ const createBunFetch = getCreateBunFetch(moduleNs);
216
+ const instance = createBunFetch({ debug: false });
216
217
 
217
- await instance.fetch("https://example.com/first", { method: "POST", body: "first" });
218
- await instance.fetch("https://example.com/second", { method: "POST", body: "second" });
218
+ proxy.simulateStdoutBanner(41011);
219
219
 
220
- expect(spawnMock).toHaveBeenCalledTimes(1);
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
- it("parses a split BUN_PROXY_PORT banner line-by-line instead of per-chunk", async () => {
224
- vi.useFakeTimers();
223
+ expect(spawnMock).toHaveBeenCalledTimes(1);
224
+ });
225
225
 
226
- const proxy = createMockBunProxy();
227
- spawnMock.mockImplementation(proxy.mockSpawn);
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
- const moduleNs = await loadBunFetchModule();
231
- expect(moduleNs.ensureBunProxy).toBeTypeOf("function");
229
+ const proxy = createMockBunProxy();
230
+ spawnMock.mockImplementation(proxy.mockSpawn);
231
+ installMockFetch();
232
232
 
233
- const startup = moduleNs.ensureBunProxy(false);
234
- proxy.child.stdout.write("BUN_PROXY_");
235
- proxy.child.stdout.write("PORT=43123\n");
233
+ const moduleNs = await loadBunFetchModule();
234
+ expect(moduleNs.ensureBunProxy).toBeTypeOf("function");
236
235
 
237
- await vi.advanceTimersByTimeAsync(5001);
238
- await expect(startup).resolves.toBe(43123);
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
- it("keeps 10 concurrent sibling requests on one shared proxy without interference", async () => {
242
- const responses = createDeferredQueue<Response>();
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
- const moduleNs = await loadBunFetchModule();
250
- const createBunFetch = getCreateBunFetch(moduleNs);
251
- const instance = createBunFetch({ debug: false });
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
- proxy.simulateStdoutBanner(41021);
252
+ const moduleNs = await loadBunFetchModule();
253
+ const createBunFetch = getCreateBunFetch(moduleNs);
254
+ const instance = createBunFetch({ debug: false });
254
255
 
255
- const requests = Array.from({ length: 10 }, (_, index) =>
256
- instance.fetch(`https://example.com/${index}`, {
257
- method: "POST",
258
- body: `body-${index}`,
259
- }),
260
- );
256
+ proxy.simulateStdoutBanner(41021);
261
257
 
262
- expect(spawnMock).toHaveBeenCalledTimes(1);
263
-
264
- for (let index = 0; index < 10; index += 1) {
265
- responses.resolveNext(new Response(`ok-${index}`, { status: 200 }));
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
- await expect(Promise.all(requests)).resolves.toHaveLength(10);
269
- });
265
+ expect(spawnMock).toHaveBeenCalledTimes(1);
270
266
 
271
- it("does not kill sibling streams or the proxy when one concurrent request fails", async () => {
272
- const slowSuccess = createDeferred<Response>();
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
- return slowSuccess.promise;
280
- },
271
+ await expect(Promise.all(requests)).resolves.toHaveLength(10);
281
272
  });
282
- spawnMock.mockImplementation(proxy.mockSpawn);
283
- installMockFetch();
284
273
 
285
- const moduleNs = await loadBunFetchModule();
286
- const createBunFetch = getCreateBunFetch(moduleNs);
287
- const instance = createBunFetch({ debug: false });
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
- proxy.simulateStdoutBanner(41031);
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
- const goodRequest = instance.fetch("https://example.com/stream-ok", {
292
- method: "POST",
293
- body: "ok",
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
- slowSuccess.resolve(new Response("still-open", { status: 200 }));
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
- await expect(goodRequest).resolves.toBeInstanceOf(Response);
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
- it("falls back gracefully when Bun spawn fails without calling process.exit", async () => {
308
- const nativeFetch = installMockFetch(async () => new Response("native", { status: 200 }));
309
- spawnMock.mockImplementation(() => {
310
- throw new Error("spawn failed");
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
- const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: string | number | null) => {
314
- throw new Error(`unexpected process.exit(${code ?? ""})`);
315
- }) as typeof process.exit);
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
- const moduleNs = await loadBunFetchModule();
318
- const createBunFetch = getCreateBunFetch(moduleNs);
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
- const response = await instance.fetch("https://example.com/native", { method: "POST", body: "native" });
352
+ const newRequest = newInstance.fetch("https://example.com/new", { method: "POST", body: "new" });
323
353
 
324
- expect(await response.text()).toBe("native");
325
- expect(nativeFetch).toHaveBeenCalledTimes(1);
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
- it("keeps an old instance alive for in-flight work when a new hot-reload instance is created", async () => {
331
- const firstResponse = createDeferred<Response>();
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
- const moduleNs = await loadBunFetchModule();
340
- const createBunFetch = getCreateBunFetch(moduleNs);
341
- const oldInstance = createBunFetch({ debug: false });
342
- const oldRequest = oldInstance.fetch("https://example.com/old", { method: "POST", body: "old" });
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
- proxyA.simulateStdoutBanner(41041);
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
- const newInstance = createBunFetch({ debug: false });
347
- proxyB.simulateStdoutBanner(41042);
372
+ proxyA.simulateStdoutBanner(41051);
373
+ proxyB.simulateStdoutBanner(41052);
348
374
 
349
- const newRequest = newInstance.fetch("https://example.com/new", { method: "POST", body: "new" });
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
- expect(proxyA.getInFlightCount()).toBe(1);
352
- firstResponse.resolve(new Response("old-instance-still-streaming", { status: 200 }));
378
+ proxyA.simulateExit(0, null);
379
+ await instanceB.shutdown();
353
380
 
354
- await expect(oldRequest).resolves.toBeInstanceOf(Response);
355
- await expect(newRequest).resolves.toBeInstanceOf(Response);
356
- });
381
+ expect(proxyB.child.killSignals).toContain("SIGTERM");
382
+ expect(proxyA.child.killSignals).toEqual([]);
383
+ });
384
+ });
357
385
 
358
- it("cleans up the current child on shutdown without clearing state for an older child exit", async () => {
359
- const proxyA = createMockBunProxy();
360
- const proxyB = createMockBunProxy();
361
- spawnMock.mockImplementationOnce(proxyA.mockSpawn).mockImplementationOnce(proxyB.mockSpawn);
362
- installMockFetch(async () => new Response("ok", { status: 200 }));
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
- const moduleNs = await loadBunFetchModule();
365
- const createBunFetch = getCreateBunFetch(moduleNs);
366
- const instanceA = createBunFetch({ debug: false });
367
- const instanceB = createBunFetch({ debug: false });
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
- proxyA.simulateStdoutBanner(41051);
370
- proxyB.simulateStdoutBanner(41052);
403
+ const moduleNs = await loadBunFetchModule();
404
+ const createBunFetch = getCreateBunFetch(moduleNs);
405
+ const instance = createBunFetch({ debug: true });
371
406
 
372
- await instanceA.fetch("https://example.com/a", { method: "POST", body: "a" });
373
- await instanceB.fetch("https://example.com/b", { method: "POST", body: "b" });
407
+ proxy.simulateStdoutBanner(42001);
374
408
 
375
- proxyA.simulateExit(0, null);
376
- await instanceB.shutdown();
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
- expect(proxyB.child.killSignals).toContain("SIGTERM");
379
- expect(proxyA.child.killSignals).toEqual([]);
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
- describe("createBunFetch debug request dumping", () => {
384
- const UNIQUE_REQUEST_PATTERN =
385
- /^\/tmp\/opencode-request-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[0-9a-f]{8}\.json$/;
386
- const UNIQUE_HEADERS_PATTERN =
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
- const paths = writtenPaths();
412
- const uniqueRequest = paths.find((path) => UNIQUE_REQUEST_PATTERN.test(path));
413
- const uniqueHeaders = paths.find((path) => UNIQUE_HEADERS_PATTERN.test(path));
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
- expect(uniqueRequest, "expected a uniquely-named request dump").toBeDefined();
416
- expect(uniqueHeaders, "expected a uniquely-named headers dump").toBeDefined();
417
- expect(paths).toContain(LATEST_REQUEST_PATH);
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
- it("produces a different unique path for each sequential debug request", async () => {
422
- const proxy = createMockBunProxy();
423
- spawnMock.mockImplementation(proxy.mockSpawn);
424
- installMockFetch();
433
+ proxy.simulateStdoutBanner(42002);
425
434
 
426
- const moduleNs = await loadBunFetchModule();
427
- const createBunFetch = getCreateBunFetch(moduleNs);
428
- const instance = createBunFetch({ debug: true });
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
- proxy.simulateStdoutBanner(42002);
444
+ const uniquePaths = writtenPaths().filter((path) => UNIQUE_REQUEST_PATTERN.test(path));
431
445
 
432
- await instance.fetch("https://api.anthropic.com/v1/messages?beta=true", {
433
- method: "POST",
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
- const uniquePaths = writtenPaths().filter((path) => UNIQUE_REQUEST_PATTERN.test(path));
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
- expect(uniquePaths).toHaveLength(2);
444
- expect(uniquePaths[0]).not.toBe(uniquePaths[1]);
445
- });
455
+ const moduleNs = await loadBunFetchModule();
456
+ const createBunFetch = getCreateBunFetch(moduleNs);
457
+ const instance = createBunFetch({ debug: true });
446
458
 
447
- it("does not dump any artifact for count_tokens requests even when debug=true", async () => {
448
- const proxy = createMockBunProxy();
449
- spawnMock.mockImplementation(proxy.mockSpawn);
450
- installMockFetch();
459
+ proxy.simulateStdoutBanner(42003);
451
460
 
452
- const moduleNs = await loadBunFetchModule();
453
- const createBunFetch = getCreateBunFetch(moduleNs);
454
- const instance = createBunFetch({ debug: true });
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
- proxy.simulateStdoutBanner(42003);
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
- expect(writeFileSyncMock).not.toHaveBeenCalled();
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
- it("does not dump any artifact when debug=false", async () => {
467
- const proxy = createMockBunProxy();
468
- spawnMock.mockImplementation(proxy.mockSpawn);
469
- installMockFetch();
474
+ const moduleNs = await loadBunFetchModule();
475
+ const createBunFetch = getCreateBunFetch(moduleNs);
476
+ const instance = createBunFetch({ debug: false });
470
477
 
471
- const moduleNs = await loadBunFetchModule();
472
- const createBunFetch = getCreateBunFetch(moduleNs);
473
- const instance = createBunFetch({ debug: false });
478
+ proxy.simulateStdoutBanner(42004);
474
479
 
475
- proxy.simulateStdoutBanner(42004);
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
- await instance.fetch("https://api.anthropic.com/v1/messages?beta=true", {
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
  });