@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
@@ -31,39 +31,39 @@ export type MockResponseMap = Record<string, Partial<Response> | (() => Partial<
31
31
 
32
32
  /** Account data structure for harness initialization */
33
33
  export interface HarnessAccount {
34
- refreshToken?: string;
35
- access?: string;
36
- expires?: number;
37
- email?: string;
38
- enabled?: boolean;
34
+ refreshToken?: string;
35
+ access?: string;
36
+ expires?: number;
37
+ email?: string;
38
+ enabled?: boolean;
39
39
  }
40
40
 
41
41
  /** Options for creating a fetch harness */
42
42
  export interface HarnessOptions {
43
- /** Account overrides to initialize the harness with (default: single test account) */
44
- accounts?: HarnessAccount[];
45
- /** Plugin configuration overrides (default: test-friendly defaults) */
46
- config?: Partial<AnthropicAuthConfig>;
47
- /** Mock responses for specific URLs (default: empty) */
48
- mockResponses?: MockResponseMap;
49
- /** Initial account index to set as active (default: 0) */
50
- initialAccount?: number;
43
+ /** Account overrides to initialize the harness with (default: single test account) */
44
+ accounts?: HarnessAccount[];
45
+ /** Plugin configuration overrides (default: test-friendly defaults) */
46
+ config?: Partial<AnthropicAuthConfig>;
47
+ /** Mock responses for specific URLs (default: empty) */
48
+ mockResponses?: MockResponseMap;
49
+ /** Initial account index to set as active (default: 0) */
50
+ initialAccount?: number;
51
51
  }
52
52
 
53
53
  /** The fetch harness instance returned by createFetchHarness */
54
54
  export interface FetchHarness {
55
- /** The fetch interceptor function returned by plugin.auth.loader */
56
- fetch: (input: string | Request | URL, init?: RequestInit) => Promise<Response>;
57
- /** The mocked global fetch (vi.fn()) — use for assertions */
58
- mockFetch: Mock;
59
- /** Cleanup function to restore global state */
60
- tearDown: () => void;
61
- /** Helper to wait for an assertion to pass (polling) */
62
- waitFor: (assertion: () => void, timeoutMs?: number) => Promise<void>;
63
- /** Get the headers from a specific mock fetch call */
64
- getFetchHeaders: (callIndex: number) => Headers | undefined;
65
- /** Get the URL from a specific mock fetch call */
66
- getFetchUrl: (callIndex: number) => string | Request | undefined;
55
+ /** The fetch interceptor function returned by plugin.auth.loader */
56
+ fetch: (input: string | Request | URL, init?: RequestInit) => Promise<Response>;
57
+ /** The mocked global fetch (vi.fn()) — use for assertions */
58
+ mockFetch: Mock;
59
+ /** Cleanup function to restore global state */
60
+ tearDown: () => void;
61
+ /** Helper to wait for an assertion to pass (polling) */
62
+ waitFor: (assertion: () => void, timeoutMs?: number) => Promise<void>;
63
+ /** Get the headers from a specific mock fetch call */
64
+ getFetchHeaders: (callIndex: number) => Headers | undefined;
65
+ /** Get the URL from a specific mock fetch call */
66
+ getFetchUrl: (callIndex: number) => string | Request | undefined;
67
67
  }
68
68
 
69
69
  // ---------------------------------------------------------------------------
@@ -72,112 +72,112 @@ export interface FetchHarness {
72
72
 
73
73
  /** Default test account factory */
74
74
  function makeStoredAccount(index: number, overrides: HarnessAccount = {}) {
75
- const addedAt = Date.now();
76
- return {
77
- id: `test-${addedAt}-${index}`,
78
- index,
79
- refreshToken: overrides.refreshToken ?? `refresh-${index + 1}`,
80
- access: overrides.access ?? `access-${index + 1}`,
81
- expires: overrides.expires ?? Date.now() + 3600_000,
82
- token_updated_at: addedAt,
83
- addedAt,
84
- lastUsed: 0,
85
- enabled: overrides.enabled ?? true,
86
- rateLimitResetTimes: {},
87
- consecutiveFailures: 0,
88
- lastFailureTime: null,
89
- email: overrides.email,
90
- stats: {
91
- requests: 0,
92
- inputTokens: 0,
93
- outputTokens: 0,
94
- cacheReadTokens: 0,
95
- cacheWriteTokens: 0,
96
- lastReset: addedAt,
97
- },
98
- source: "oauth" as const,
99
- };
75
+ const addedAt = Date.now();
76
+ return {
77
+ id: `test-${addedAt}-${index}`,
78
+ index,
79
+ refreshToken: overrides.refreshToken ?? `refresh-${index + 1}`,
80
+ access: overrides.access ?? `access-${index + 1}`,
81
+ expires: overrides.expires ?? Date.now() + 3600_000,
82
+ token_updated_at: addedAt,
83
+ addedAt,
84
+ lastUsed: 0,
85
+ enabled: overrides.enabled ?? true,
86
+ rateLimitResetTimes: {},
87
+ consecutiveFailures: 0,
88
+ lastFailureTime: null,
89
+ email: overrides.email,
90
+ stats: {
91
+ requests: 0,
92
+ inputTokens: 0,
93
+ outputTokens: 0,
94
+ cacheReadTokens: 0,
95
+ cacheWriteTokens: 0,
96
+ lastReset: addedAt,
97
+ },
98
+ source: "oauth" as const,
99
+ };
100
100
  }
101
101
 
102
102
  /** Default mock client factory */
103
103
  function makeMockClient() {
104
- return {
105
- auth: {
106
- set: vi.fn().mockResolvedValue(undefined),
107
- },
108
- session: {
109
- prompt: vi.fn().mockResolvedValue(undefined),
110
- },
111
- tui: {
112
- showToast: vi.fn().mockResolvedValue(undefined),
113
- },
114
- };
104
+ return {
105
+ auth: {
106
+ set: vi.fn().mockResolvedValue(undefined),
107
+ },
108
+ session: {
109
+ prompt: vi.fn().mockResolvedValue(undefined),
110
+ },
111
+ tui: {
112
+ showToast: vi.fn().mockResolvedValue(undefined),
113
+ },
114
+ };
115
115
  }
116
116
 
117
117
  /** Default mock provider factory */
118
118
  function makeMockProvider() {
119
- return {
120
- models: {
121
- "claude-sonnet": {
122
- id: "claude-sonnet",
123
- cost: { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
124
- limit: { context: 200_000, output: 8192 },
125
- },
126
- "claude-opus-4-6": {
127
- id: "claude-opus-4-6",
128
- cost: { input: 15, output: 75, cache: { read: 1.5, write: 18.75 } },
129
- limit: { context: 200_000, output: 32_000 },
130
- },
131
- },
132
- };
119
+ return {
120
+ models: {
121
+ "claude-sonnet": {
122
+ id: "claude-sonnet",
123
+ cost: { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
124
+ limit: { context: 200_000, output: 8192 },
125
+ },
126
+ "claude-opus-4-6": {
127
+ id: "claude-opus-4-6",
128
+ cost: { input: 15, output: 75, cache: { read: 1.5, write: 18.75 } },
129
+ limit: { context: 200_000, output: 32_000 },
130
+ },
131
+ },
132
+ };
133
133
  }
134
134
 
135
135
  /** Default test configuration */
136
136
  const DEFAULT_TEST_CONFIG: AnthropicAuthConfig = {
137
- account_selection_strategy: "sticky",
138
- failure_ttl_seconds: 3600,
139
- debug: false,
140
- signature_emulation: {
141
- enabled: true,
142
- fetch_claude_code_version_on_startup: false,
143
- prompt_compaction: "minimal",
144
- sanitize_system_prompt: false,
145
- },
146
- override_model_limits: {
147
- enabled: false,
148
- context: 1_000_000,
149
- output: 0,
150
- },
151
- custom_betas: [],
152
- health_score: {
153
- initial: 70,
154
- success_reward: 1,
155
- rate_limit_penalty: -10,
156
- failure_penalty: -20,
157
- recovery_rate_per_hour: 2,
158
- min_usable: 50,
159
- max_score: 100,
160
- },
161
- token_bucket: {
162
- max_tokens: 50,
163
- regeneration_rate_per_minute: 6,
164
- initial_tokens: 50,
165
- },
166
- toasts: {
167
- quiet: true,
168
- debounce_seconds: 30,
169
- },
170
- headers: {},
171
- idle_refresh: {
172
- enabled: false,
173
- window_minutes: 60,
174
- min_interval_minutes: 30,
175
- },
176
- cc_credential_reuse: {
177
- enabled: false,
178
- auto_detect: false,
179
- prefer_over_oauth: false,
180
- },
137
+ account_selection_strategy: "sticky",
138
+ failure_ttl_seconds: 3600,
139
+ debug: false,
140
+ signature_emulation: {
141
+ enabled: true,
142
+ fetch_claude_code_version_on_startup: false,
143
+ prompt_compaction: "minimal",
144
+ sanitize_system_prompt: false,
145
+ },
146
+ override_model_limits: {
147
+ enabled: false,
148
+ context: 1_000_000,
149
+ output: 0,
150
+ },
151
+ custom_betas: [],
152
+ health_score: {
153
+ initial: 70,
154
+ success_reward: 1,
155
+ rate_limit_penalty: -10,
156
+ failure_penalty: -20,
157
+ recovery_rate_per_hour: 2,
158
+ min_usable: 50,
159
+ max_score: 100,
160
+ },
161
+ token_bucket: {
162
+ max_tokens: 50,
163
+ regeneration_rate_per_minute: 6,
164
+ initial_tokens: 50,
165
+ },
166
+ toasts: {
167
+ quiet: true,
168
+ debounce_seconds: 30,
169
+ },
170
+ headers: {},
171
+ idle_refresh: {
172
+ enabled: false,
173
+ window_minutes: 60,
174
+ min_interval_minutes: 30,
175
+ },
176
+ cc_credential_reuse: {
177
+ enabled: false,
178
+ auto_detect: false,
179
+ prefer_over_oauth: false,
180
+ },
181
181
  };
182
182
 
183
183
  // ---------------------------------------------------------------------------
@@ -185,9 +185,9 @@ const DEFAULT_TEST_CONFIG: AnthropicAuthConfig = {
185
185
  // ---------------------------------------------------------------------------
186
186
 
187
187
  let mockAccountsData: {
188
- version: number;
189
- accounts: ReturnType<typeof makeStoredAccount>[];
190
- activeIndex: number;
188
+ version: number;
189
+ accounts: ReturnType<typeof makeStoredAccount>[];
190
+ activeIndex: number;
191
191
  } | null = null;
192
192
 
193
193
  /**
@@ -195,14 +195,14 @@ let mockAccountsData: {
195
195
  * Call this before createFetchHarness to pre-seed accounts.
196
196
  */
197
197
  export function setMockAccounts(accounts: ReturnType<typeof makeStoredAccount>[], activeIndex = 0): void {
198
- mockAccountsData = { version: 1, accounts, activeIndex };
198
+ mockAccountsData = { version: 1, accounts, activeIndex };
199
199
  }
200
200
 
201
201
  /**
202
202
  * Clear the mock accounts data.
203
203
  */
204
204
  export function clearMockAccounts(): void {
205
- mockAccountsData = null;
205
+ mockAccountsData = null;
206
206
  }
207
207
 
208
208
  // ---------------------------------------------------------------------------
@@ -223,180 +223,180 @@ export function clearMockAccounts(): void {
223
223
  * @returns A FetchHarness instance ready for testing
224
224
  */
225
225
  export async function createFetchHarness(opts: HarnessOptions = {}): Promise<FetchHarness> {
226
- const { accounts = [{}], config = {}, mockResponses = {}, initialAccount = 0 } = opts;
227
-
228
- // Merge config with defaults
229
- const mergedConfig: AnthropicAuthConfig = {
230
- ...DEFAULT_TEST_CONFIG,
231
- ...config,
232
- signature_emulation: {
233
- ...DEFAULT_TEST_CONFIG.signature_emulation,
234
- ...config.signature_emulation,
235
- },
236
- health_score: { ...DEFAULT_TEST_CONFIG.health_score, ...config.health_score },
237
- token_bucket: { ...DEFAULT_TEST_CONFIG.token_bucket, ...config.token_bucket },
238
- toasts: { ...DEFAULT_TEST_CONFIG.toasts, ...config.toasts },
239
- override_model_limits: {
240
- ...DEFAULT_TEST_CONFIG.override_model_limits,
241
- ...config.override_model_limits,
242
- },
243
- idle_refresh: { ...DEFAULT_TEST_CONFIG.idle_refresh, ...config.idle_refresh },
244
- cc_credential_reuse: { ...DEFAULT_TEST_CONFIG.cc_credential_reuse, ...config.cc_credential_reuse },
245
- };
246
-
247
- // Create mock client
248
- const client = makeMockClient();
249
-
250
- // Store original fetch and create mock
251
- const originalFetch = globalThis.fetch;
252
- const mockFetch = vi.fn();
253
-
254
- // Set up mock response handler
255
- mockFetch.mockImplementation((input: string | Request | URL) => {
256
- let url: string;
257
- if (typeof input === "string") {
258
- url = input;
259
- } else if (input instanceof URL) {
260
- url = input.toString();
261
- } else {
262
- url = input.url;
263
- }
264
-
265
- // Check for matching mock response
266
- for (const [pattern, response] of Object.entries(mockResponses)) {
267
- if (url && url.includes(pattern)) {
268
- const responseObj = typeof response === "function" ? response() : response;
269
- if (responseObj instanceof Response) {
270
- return Promise.resolve(responseObj);
226
+ const { accounts = [{}], config = {}, mockResponses = {}, initialAccount = 0 } = opts;
227
+
228
+ // Merge config with defaults
229
+ const mergedConfig: AnthropicAuthConfig = {
230
+ ...DEFAULT_TEST_CONFIG,
231
+ ...config,
232
+ signature_emulation: {
233
+ ...DEFAULT_TEST_CONFIG.signature_emulation,
234
+ ...config.signature_emulation,
235
+ },
236
+ health_score: { ...DEFAULT_TEST_CONFIG.health_score, ...config.health_score },
237
+ token_bucket: { ...DEFAULT_TEST_CONFIG.token_bucket, ...config.token_bucket },
238
+ toasts: { ...DEFAULT_TEST_CONFIG.toasts, ...config.toasts },
239
+ override_model_limits: {
240
+ ...DEFAULT_TEST_CONFIG.override_model_limits,
241
+ ...config.override_model_limits,
242
+ },
243
+ idle_refresh: { ...DEFAULT_TEST_CONFIG.idle_refresh, ...config.idle_refresh },
244
+ cc_credential_reuse: { ...DEFAULT_TEST_CONFIG.cc_credential_reuse, ...config.cc_credential_reuse },
245
+ };
246
+
247
+ // Create mock client
248
+ const client = makeMockClient();
249
+
250
+ // Store original fetch and create mock
251
+ const originalFetch = globalThis.fetch;
252
+ const mockFetch = vi.fn();
253
+
254
+ // Set up mock response handler
255
+ mockFetch.mockImplementation((input: string | Request | URL) => {
256
+ let url: string;
257
+ if (typeof input === "string") {
258
+ url = input;
259
+ } else if (input instanceof URL) {
260
+ url = input.toString();
261
+ } else {
262
+ url = input.url;
271
263
  }
272
264
 
273
- if (typeof responseObj.json === "function") {
274
- return responseObj.json().then(
275
- (jsonBody) =>
276
- new Response(JSON.stringify(jsonBody), {
277
- status: responseObj.status ?? 200,
278
- statusText: responseObj.statusText ?? "OK",
279
- headers:
280
- responseObj.headers instanceof Headers
281
- ? responseObj.headers
282
- : new Headers(responseObj.headers ?? { "content-type": "application/json" }),
283
- }),
284
- );
265
+ // Check for matching mock response
266
+ for (const [pattern, response] of Object.entries(mockResponses)) {
267
+ if (url && url.includes(pattern)) {
268
+ const responseObj = typeof response === "function" ? response() : response;
269
+ if (responseObj instanceof Response) {
270
+ return Promise.resolve(responseObj);
271
+ }
272
+
273
+ if (typeof responseObj.json === "function") {
274
+ return responseObj.json().then(
275
+ (jsonBody) =>
276
+ new Response(JSON.stringify(jsonBody), {
277
+ status: responseObj.status ?? 200,
278
+ statusText: responseObj.statusText ?? "OK",
279
+ headers:
280
+ responseObj.headers instanceof Headers
281
+ ? responseObj.headers
282
+ : new Headers(responseObj.headers ?? { "content-type": "application/json" }),
283
+ }),
284
+ );
285
+ }
286
+
287
+ if (typeof responseObj.text === "function") {
288
+ return responseObj.text().then(
289
+ (textBody) =>
290
+ new Response(textBody, {
291
+ status: responseObj.status ?? 200,
292
+ statusText: responseObj.statusText ?? "OK",
293
+ headers:
294
+ responseObj.headers instanceof Headers
295
+ ? responseObj.headers
296
+ : new Headers(responseObj.headers ?? undefined),
297
+ }),
298
+ );
299
+ }
300
+
301
+ return Promise.resolve(
302
+ new Response(undefined, {
303
+ status: responseObj.status ?? 200,
304
+ statusText: responseObj.statusText ?? "OK",
305
+ headers:
306
+ responseObj.headers instanceof Headers
307
+ ? responseObj.headers
308
+ : new Headers(responseObj.headers ?? undefined),
309
+ }),
310
+ );
311
+ }
285
312
  }
286
313
 
287
- if (typeof responseObj.text === "function") {
288
- return responseObj.text().then(
289
- (textBody) =>
290
- new Response(textBody, {
291
- status: responseObj.status ?? 200,
292
- statusText: responseObj.statusText ?? "OK",
293
- headers:
294
- responseObj.headers instanceof Headers
295
- ? responseObj.headers
296
- : new Headers(responseObj.headers ?? undefined),
297
- }),
298
- );
314
+ // Default: return empty successful response
315
+ return Promise.resolve(new Response("{}", { status: 200, headers: { "content-type": "application/json" } }));
316
+ });
317
+
318
+ // Install mock
319
+ globalThis.fetch = mockFetch as unknown as typeof globalThis.fetch;
320
+
321
+ // Build accounts data
322
+ const managedAccounts = accounts.map((overrides, index) => makeStoredAccount(index, overrides));
323
+
324
+ // Pre-seed mock accounts
325
+ mockAccountsData = { version: 1, accounts: managedAccounts, activeIndex: initialAccount };
326
+
327
+ // Dynamically import modules to get mocked versions
328
+ const { AnthropicAuthPlugin } = await import("../../index.js");
329
+ const { loadConfig, loadConfigFresh } = await import("../../config.js");
330
+ const { loadAccounts } = await import("../../storage.js");
331
+
332
+ // Override config mock to return our merged config
333
+ vi.mocked(loadConfig).mockReturnValue(mergedConfig);
334
+ vi.mocked(loadConfigFresh).mockReturnValue(mergedConfig);
335
+ vi.mocked(loadAccounts).mockResolvedValue(mockAccountsData);
336
+
337
+ // Initialize plugin
338
+ const plugin = await AnthropicAuthPlugin({ client });
339
+
340
+ // Create mock auth getter
341
+ const getAuth = vi.fn().mockResolvedValue({
342
+ type: "oauth",
343
+ refresh: managedAccounts[initialAccount]?.refreshToken ?? "refresh-1",
344
+ access: managedAccounts[initialAccount]?.access ?? "access-1",
345
+ expires: Date.now() + 3600_000,
346
+ });
347
+
348
+ // Initialize the plugin's auth loader
349
+ const provider = makeMockProvider();
350
+ const result = await plugin.auth.loader(getAuth, provider);
351
+
352
+ // Helper: wait for assertion
353
+ async function waitFor(assertion: () => void, timeoutMs = 500): Promise<void> {
354
+ const started = Date.now();
355
+ while (true) {
356
+ try {
357
+ assertion();
358
+ return;
359
+ } catch (err) {
360
+ if (Date.now() - started >= timeoutMs) throw err;
361
+ await new Promise((resolve) => setTimeout(resolve, 10));
362
+ }
299
363
  }
364
+ }
300
365
 
301
- return Promise.resolve(
302
- new Response(undefined, {
303
- status: responseObj.status ?? 200,
304
- statusText: responseObj.statusText ?? "OK",
305
- headers:
306
- responseObj.headers instanceof Headers
307
- ? responseObj.headers
308
- : new Headers(responseObj.headers ?? undefined),
309
- }),
310
- );
311
- }
366
+ // Helper: get fetch headers
367
+ function getFetchHeaders(callIndex: number): Headers | undefined {
368
+ const [input, init] = mockFetch.mock.calls[callIndex] ?? [];
369
+ if (init?.headers) {
370
+ return init.headers instanceof Headers ? init.headers : new Headers(init.headers);
371
+ }
372
+ if (input instanceof Request) {
373
+ return input.headers;
374
+ }
375
+ return undefined;
312
376
  }
313
377
 
314
- // Default: return empty successful response
315
- return Promise.resolve(new Response("{}", { status: 200, headers: { "content-type": "application/json" } }));
316
- });
317
-
318
- // Install mock
319
- globalThis.fetch = mockFetch as unknown as typeof globalThis.fetch;
320
-
321
- // Build accounts data
322
- const managedAccounts = accounts.map((overrides, index) => makeStoredAccount(index, overrides));
323
-
324
- // Pre-seed mock accounts
325
- mockAccountsData = { version: 1, accounts: managedAccounts, activeIndex: initialAccount };
326
-
327
- // Dynamically import modules to get mocked versions
328
- const { AnthropicAuthPlugin } = await import("../../index.js");
329
- const { loadConfig, loadConfigFresh } = await import("../../config.js");
330
- const { loadAccounts } = await import("../../storage.js");
331
-
332
- // Override config mock to return our merged config
333
- vi.mocked(loadConfig).mockReturnValue(mergedConfig);
334
- vi.mocked(loadConfigFresh).mockReturnValue(mergedConfig);
335
- vi.mocked(loadAccounts).mockResolvedValue(mockAccountsData);
336
-
337
- // Initialize plugin
338
- const plugin = await AnthropicAuthPlugin({ client });
339
-
340
- // Create mock auth getter
341
- const getAuth = vi.fn().mockResolvedValue({
342
- type: "oauth",
343
- refresh: managedAccounts[initialAccount]?.refreshToken ?? "refresh-1",
344
- access: managedAccounts[initialAccount]?.access ?? "access-1",
345
- expires: Date.now() + 3600_000,
346
- });
347
-
348
- // Initialize the plugin's auth loader
349
- const provider = makeMockProvider();
350
- const result = await plugin.auth.loader(getAuth, provider);
351
-
352
- // Helper: wait for assertion
353
- async function waitFor(assertion: () => void, timeoutMs = 500): Promise<void> {
354
- const started = Date.now();
355
- while (true) {
356
- try {
357
- assertion();
358
- return;
359
- } catch (err) {
360
- if (Date.now() - started >= timeoutMs) throw err;
361
- await new Promise((resolve) => setTimeout(resolve, 10));
362
- }
378
+ // Helper: get fetch URL
379
+ function getFetchUrl(callIndex: number): string | Request | undefined {
380
+ const [input] = mockFetch.mock.calls[callIndex] ?? [];
381
+ return input;
363
382
  }
364
- }
365
383
 
366
- // Helper: get fetch headers
367
- function getFetchHeaders(callIndex: number): Headers | undefined {
368
- const [input, init] = mockFetch.mock.calls[callIndex] ?? [];
369
- if (init?.headers) {
370
- return init.headers instanceof Headers ? init.headers : new Headers(init.headers);
384
+ // Cleanup function
385
+ function tearDown(): void {
386
+ globalThis.fetch = originalFetch;
387
+ clearMockAccounts();
371
388
  }
372
- if (input instanceof Request) {
373
- return input.headers;
389
+
390
+ if (!result.fetch) {
391
+ throw new Error("Plugin did not return a fetch function");
374
392
  }
375
- return undefined;
376
- }
377
-
378
- // Helper: get fetch URL
379
- function getFetchUrl(callIndex: number): string | Request | undefined {
380
- const [input] = mockFetch.mock.calls[callIndex] ?? [];
381
- return input;
382
- }
383
-
384
- // Cleanup function
385
- function tearDown(): void {
386
- globalThis.fetch = originalFetch;
387
- clearMockAccounts();
388
- }
389
-
390
- if (!result.fetch) {
391
- throw new Error("Plugin did not return a fetch function");
392
- }
393
-
394
- return {
395
- fetch: result.fetch,
396
- mockFetch,
397
- tearDown,
398
- waitFor,
399
- getFetchHeaders,
400
- getFetchUrl,
401
- };
393
+
394
+ return {
395
+ fetch: result.fetch,
396
+ mockFetch,
397
+ tearDown,
398
+ waitFor,
399
+ getFetchHeaders,
400
+ getFetchUrl,
401
+ };
402
402
  }