opencode-anthropic-fix 0.0.14

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.
@@ -0,0 +1,456 @@
1
+ # Testing Patterns
2
+
3
+ **Analysis Date:** 2026-02-07
4
+
5
+ ## Test Framework
6
+
7
+ **Runner:**
8
+
9
+ - Vitest 4.x
10
+ - No config file — uses Vitest defaults (auto-discovers `*.test.mjs` files)
11
+ - Global test APIs **not** enabled — all test functions must be explicitly imported
12
+
13
+ **Assertion Library:**
14
+
15
+ - Vitest built-in `expect` (Jest-compatible API)
16
+
17
+ **Run Commands:**
18
+
19
+ ```bash
20
+ npm test # Run all tests (vitest run — single pass, CI mode)
21
+ npm run test:watch # Watch mode (vitest — re-runs on file change)
22
+ ```
23
+
24
+ **Pre-push hook:** Husky runs `npm test` before every push.
25
+
26
+ ## Test File Organization
27
+
28
+ **Location:**
29
+
30
+ - Co-located with source files (test file next to the module it tests)
31
+
32
+ **Naming:**
33
+
34
+ - `{module-name}.test.mjs` — same name as source file with `.test` suffix
35
+
36
+ **Structure:**
37
+
38
+ ```
39
+ project-root/
40
+ ├── index.mjs # Plugin entry point
41
+ ├── index.test.mjs # Integration tests (49 tests)
42
+ ├── cli.mjs # CLI tool
43
+ ├── cli.test.mjs # CLI command tests (87 tests)
44
+ └── lib/
45
+ ├── accounts.mjs
46
+ ├── accounts.test.mjs # AccountManager tests (67 tests)
47
+ ├── backoff.mjs
48
+ ├── backoff.test.mjs # Rate limit classification tests (42 tests)
49
+ ├── config.mjs
50
+ ├── config.test.mjs # Config loading/validation tests (25 tests)
51
+ ├── rotation.mjs
52
+ ├── rotation.test.mjs # Health score, token bucket, selection tests (38 tests)
53
+ ├── storage.mjs
54
+ └── storage.test.mjs # Storage I/O and validation tests (27 tests)
55
+ ```
56
+
57
+ **Total:** 7 test files, 335 tests, all passing.
58
+
59
+ ## Test Structure
60
+
61
+ **Imports — always explicit:**
62
+
63
+ ```javascript
64
+ import { describe, it, expect, vi, beforeEach } from "vitest";
65
+ ```
66
+
67
+ **Suite organization — `describe` blocks by feature/method, not by file:**
68
+
69
+ ```javascript
70
+ // ---------------------------------------------------------------------------
71
+ // AccountManager.load
72
+ // ---------------------------------------------------------------------------
73
+
74
+ describe("AccountManager.load", () => {
75
+ beforeEach(() => {
76
+ vi.resetAllMocks();
77
+ vi.useFakeTimers();
78
+ vi.setSystemTime(new Date("2026-01-15T12:00:00Z"));
79
+ });
80
+
81
+ it("creates empty manager when no stored accounts and no fallback", async () => {
82
+ loadAccounts.mockResolvedValue(null);
83
+ const manager = await AccountManager.load(DEFAULT_CONFIG, null);
84
+ expect(manager.getAccountCount()).toBe(0);
85
+ });
86
+ });
87
+ ```
88
+
89
+ **Key patterns:**
90
+
91
+ - Section separator comments (`// ---`) between major `describe` groups
92
+ - `beforeEach` with `vi.resetAllMocks()` at the start of every `describe` block
93
+ - `vi.useFakeTimers()` and `vi.setSystemTime()` for time-sensitive tests
94
+ - Flat `it()` calls with descriptive names — no deep nesting beyond one `describe` level
95
+ - JSDoc comments on test helper functions
96
+
97
+ ## Mocking
98
+
99
+ ### Module Mocks (`vi.mock`)
100
+
101
+ **Pattern: Partial mock with `importOriginal`**
102
+ Keep pure helper functions real, mock only I/O:
103
+
104
+ ```javascript
105
+ vi.mock("./storage.mjs", async (importOriginal) => {
106
+ const actual = await importOriginal();
107
+ return {
108
+ ...actual, // Keep pure helpers
109
+ loadAccounts: vi.fn(), // Mock I/O functions
110
+ saveAccounts: vi.fn().mockResolvedValue(undefined),
111
+ };
112
+ });
113
+ ```
114
+
115
+ Used in: `lib/accounts.test.mjs`, `index.test.mjs`
116
+
117
+ **Pattern: Full mock for system modules**
118
+ Replace entire modules when all exports need control:
119
+
120
+ ```javascript
121
+ vi.mock("node:fs", () => ({
122
+ existsSync: vi.fn(),
123
+ readFileSync: vi.fn(),
124
+ appendFileSync: vi.fn(),
125
+ writeFileSync: vi.fn(),
126
+ promises: {
127
+ readFile: vi.fn(),
128
+ writeFile: vi.fn(),
129
+ rename: vi.fn(),
130
+ chmod: vi.fn(),
131
+ unlink: vi.fn(),
132
+ mkdir: vi.fn(),
133
+ },
134
+ }));
135
+ ```
136
+
137
+ Used in: `lib/storage.test.mjs`
138
+
139
+ **Pattern: Mock with inline return value**
140
+ For simple stubs:
141
+
142
+ ```javascript
143
+ vi.mock("node:crypto", () => ({
144
+ randomBytes: vi.fn(() => ({
145
+ toString: () => "abcdef123456",
146
+ })),
147
+ }));
148
+ ```
149
+
150
+ **Pattern: Mock third-party packages**
151
+
152
+ ```javascript
153
+ vi.mock("@openauthjs/openauth/pkce", () => ({
154
+ generatePKCE: vi.fn(async () => ({
155
+ challenge: "test-challenge",
156
+ verifier: "test-verifier",
157
+ })),
158
+ }));
159
+ ```
160
+
161
+ ### Import Order Rule
162
+
163
+ **Mocks must be declared BEFORE importing the module under test.** Vitest hoists `vi.mock()` calls, but the mock setup still must appear before the import in the file for clarity:
164
+
165
+ ```javascript
166
+ // 1. Set up mocks
167
+ vi.mock("./lib/storage.mjs", async (importOriginal) => { ... });
168
+ vi.mock("./lib/config.mjs", async (importOriginal) => { ... });
169
+
170
+ // 2. Global mocks
171
+ const mockFetch = vi.fn();
172
+ vi.stubGlobal("fetch", mockFetch);
173
+
174
+ // 3. Import module under test
175
+ import { AnthropicAuthPlugin } from "./index.mjs";
176
+ import { saveAccounts, loadAccounts } from "./lib/storage.mjs";
177
+ ```
178
+
179
+ ### Global Mocks (`vi.stubGlobal`)
180
+
181
+ **Pattern: Mock `fetch` globally**
182
+
183
+ ```javascript
184
+ const mockFetch = vi.fn();
185
+ vi.stubGlobal("fetch", mockFetch);
186
+ ```
187
+
188
+ Used in: `index.test.mjs`, `cli.test.mjs`
189
+
190
+ ### Timer Mocks
191
+
192
+ **Pattern: Fake timers for time-dependent logic**
193
+
194
+ ```javascript
195
+ beforeEach(() => {
196
+ vi.useFakeTimers();
197
+ vi.setSystemTime(new Date("2026-01-15T12:00:00Z"));
198
+ });
199
+ ```
200
+
201
+ Used in: `lib/accounts.test.mjs` for debounced save, token expiry, etc.
202
+
203
+ **What to mock:**
204
+
205
+ - File system I/O (`node:fs`)
206
+ - Network requests (`fetch`)
207
+ - System time (`vi.useFakeTimers`)
208
+ - External packages (`@openauthjs/openauth/pkce`)
209
+ - Interactive I/O (`node:readline/promises`)
210
+ - Crypto (`node:crypto` for deterministic tests)
211
+
212
+ **What NOT to mock:**
213
+
214
+ - Pure functions (validation, deduplication, formatting, clamping)
215
+ - Class constructors and their in-memory logic
216
+ - Config defaults and constants
217
+
218
+ ## Factory Helpers
219
+
220
+ **Test data factories are defined per test file, not shared.** Each test file defines its own factory functions at the top, after mocks.
221
+
222
+ **`makeStoredAccount(overrides)` — Build a single account object:**
223
+
224
+ ```javascript
225
+ function makeStoredAccount(overrides = {}) {
226
+ return {
227
+ refreshToken: "token1",
228
+ addedAt: 1000,
229
+ lastUsed: 0,
230
+ enabled: true,
231
+ rateLimitResetTimes: {},
232
+ consecutiveFailures: 0,
233
+ lastFailureTime: null,
234
+ ...overrides,
235
+ };
236
+ }
237
+ ```
238
+
239
+ Used in: `lib/accounts.test.mjs`, `index.test.mjs`
240
+
241
+ **`makeAccountsData(overrides, extra)` — Build full storage payload:**
242
+
243
+ ```javascript
244
+ function makeAccountsData(overrides = [{}], extra = {}) {
245
+ return {
246
+ version: 1,
247
+ accounts: overrides.map((o, i) =>
248
+ makeStoredAccount({ refreshToken: `token${i + 1}`, addedAt: (i + 1) * 1000, ...o }),
249
+ ),
250
+ activeIndex: 0,
251
+ ...extra,
252
+ };
253
+ }
254
+ ```
255
+
256
+ Used in: `lib/accounts.test.mjs`
257
+
258
+ **`makeClient()` / `makeProvider()` — Build OpenCode plugin client/provider stubs:**
259
+
260
+ ```javascript
261
+ function makeClient() {
262
+ return {
263
+ auth: { set: vi.fn().mockResolvedValue(undefined) },
264
+ tui: { showToast: vi.fn().mockResolvedValue(undefined) },
265
+ };
266
+ }
267
+
268
+ function makeProvider() {
269
+ return {
270
+ models: {
271
+ "claude-sonnet": {
272
+ cost: { input: 3, output: 15, cache: { read: 0.3, write: 3.75 } },
273
+ },
274
+ },
275
+ };
276
+ }
277
+ ```
278
+
279
+ Used in: `index.test.mjs`
280
+
281
+ **`makeStorage(overrides)` / `captureOutput()` — CLI test helpers:**
282
+
283
+ ```javascript
284
+ /** Capture console.log and console.error output */
285
+ function captureOutput() {
286
+ const logs = [];
287
+ const errors = [];
288
+ const origLog = console.log;
289
+ const origError = console.error;
290
+
291
+ console.log = (...args) => logs.push(args.join(" "));
292
+ console.error = (...args) => errors.push(args.join(" "));
293
+
294
+ return {
295
+ logs,
296
+ errors,
297
+ text: () => logs.join("\n").replace(/\x1b\[[0-9;]*m/g, ""),
298
+ errorText: () => errors.join("\n").replace(/\x1b\[[0-9;]*m/g, ""),
299
+ restore: () => {
300
+ console.log = origLog;
301
+ console.error = origError;
302
+ },
303
+ };
304
+ }
305
+ ```
306
+
307
+ Used in: `cli.test.mjs`
308
+
309
+ ## Fixture Patterns
310
+
311
+ **No shared fixture files.** All test data is created inline via factory functions (above).
312
+
313
+ **Large data setup — use factory composition:**
314
+
315
+ ```javascript
316
+ loadAccounts.mockResolvedValue(makeAccountsData([{ lastUsed: 2000 }, { lastUsed: 4000 }], { activeIndex: 1 }));
317
+ ```
318
+
319
+ **Mock response setup for fetch:**
320
+
321
+ ```javascript
322
+ mockFetch.mockResolvedValueOnce(
323
+ new Response(JSON.stringify({ error: { type: "overloaded_error", message: "Server is overloaded" } }), {
324
+ status: 529,
325
+ }),
326
+ );
327
+ ```
328
+
329
+ ## Coverage
330
+
331
+ **Requirements:** None enforced. No coverage configuration or thresholds.
332
+
333
+ **No coverage script in package.json.** To run coverage manually:
334
+
335
+ ```bash
336
+ npx vitest run --coverage
337
+ ```
338
+
339
+ ## Test Types
340
+
341
+ **Unit Tests** (`lib/*.test.mjs`):
342
+
343
+ - Test individual modules in isolation
344
+ - Mock I/O dependencies, test pure logic directly
345
+ - Each class/function gets its own `describe` block
346
+ - Files: `lib/accounts.test.mjs`, `lib/backoff.test.mjs`, `lib/config.test.mjs`, `lib/rotation.test.mjs`, `lib/storage.test.mjs`
347
+
348
+ **Integration Tests** (`index.test.mjs`):
349
+
350
+ - Test the plugin lifecycle end-to-end: auth → callback → fetch interceptor
351
+ - Mock external boundaries (fetch, storage, config) but exercise real plugin wiring
352
+ - Tests: OAuth flow, token refresh, rate-limit retry loop, SSE stream rewriting, account rotation
353
+ - File header documents the integration scope:
354
+ ```
355
+ These test the wiring in index.mjs — the ordering of authorize → callback → loader,
356
+ the accountManager initialization, and the fetch interceptor retry loop.
357
+ ```
358
+
359
+ **CLI Tests** (`cli.test.mjs`):
360
+
361
+ - Test each CLI command function independently
362
+ - Mock storage/config I/O, capture console output
363
+ - Verify exit codes, output formatting, and state mutations
364
+
365
+ **E2E Tests:**
366
+
367
+ - Not used. No browser/E2E test framework configured.
368
+
369
+ ## Common Patterns
370
+
371
+ **Async Testing:**
372
+
373
+ ```javascript
374
+ it("loads stored accounts from disk", async () => {
375
+ loadAccounts.mockResolvedValue(makeAccountsData([{ lastUsed: 2000 }]));
376
+ const manager = await AccountManager.load(DEFAULT_CONFIG, null);
377
+ expect(manager.getAccountCount()).toBe(1);
378
+ });
379
+ ```
380
+
381
+ **Error Testing:**
382
+
383
+ ```javascript
384
+ it("returns null on malformed JSON", async () => {
385
+ fs.promises.readFile.mockResolvedValue("not json");
386
+ const result = await loadAccounts();
387
+ expect(result).toBeNull();
388
+ });
389
+ ```
390
+
391
+ **Parameterized Tests (`it.each`):**
392
+
393
+ ```javascript
394
+ it.each([
395
+ { status: 529, errorType: "overloaded_error", errorMsg: "Server is overloaded" },
396
+ { status: 503, errorType: "service_unavailable", errorMsg: "temporarily unavailable" },
397
+ { status: 500, errorType: "internal_error", errorMsg: "internal server error" },
398
+ ])("returns $status directly without switching accounts", async ({ status, errorType, errorMsg }) => {
399
+ // ...test body using destructured params
400
+ });
401
+ ```
402
+
403
+ **Mock assertion pattern:**
404
+
405
+ ```javascript
406
+ expect(saveAccounts).toHaveBeenCalledTimes(1);
407
+ expect(saveAccounts).toHaveBeenCalledWith(
408
+ expect.objectContaining({
409
+ accounts: expect.arrayContaining([expect.objectContaining({ refreshToken: "token1" })]),
410
+ }),
411
+ );
412
+ ```
413
+
414
+ **Console output testing (CLI):**
415
+
416
+ ```javascript
417
+ const out = captureOutput();
418
+ try {
419
+ const exitCode = await cmdList(storage);
420
+ expect(exitCode).toBe(0);
421
+ expect(out.text()).toContain("alice@example.com");
422
+ } finally {
423
+ out.restore();
424
+ }
425
+ ```
426
+
427
+ **Response mock chain (fetch interceptor):**
428
+
429
+ ```javascript
430
+ // Token refresh response
431
+ mockFetch.mockResolvedValueOnce(new Response(JSON.stringify({ access_token: "new-access", expires_in: 3600 })));
432
+ // Actual API response
433
+ mockFetch.mockResolvedValueOnce(new Response(JSON.stringify({ content: [{ text: "Hello" }] }), { status: 200 }));
434
+ ```
435
+
436
+ ## Writing New Tests
437
+
438
+ **When adding a new module `lib/foo.mjs`:**
439
+
440
+ 1. Create `lib/foo.test.mjs` next to the source file
441
+ 2. Import test functions: `import { describe, it, expect, vi, beforeEach } from "vitest"`
442
+ 3. Mock I/O dependencies with `vi.mock()` + `importOriginal` pattern
443
+ 4. Define factory helpers at top of file for test data
444
+ 5. Group tests with `describe` blocks per function/class
445
+ 6. Add `beforeEach(() => { vi.resetAllMocks(); })` in each `describe`
446
+ 7. Test pure logic directly, mock only I/O boundaries
447
+
448
+ **When adding tests to `index.test.mjs`:**
449
+
450
+ 1. Use existing `makeClient()`, `makeProvider()`, `makeStoredAccount()` helpers
451
+ 2. Set up mock responses with `mockFetch.mockResolvedValueOnce()`
452
+ 3. Follow the existing `setupFetchFn()` pattern for fetch interceptor tests
453
+
454
+ ---
455
+
456
+ _Testing analysis: 2026-02-07_
@@ -0,0 +1,3 @@
1
+ dist/
2
+ node_modules/
3
+ .mitm/
package/.prettierrc ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "printWidth": 120,
3
+ "tabWidth": 2,
4
+ "useTabs": false,
5
+ "semi": true,
6
+ "singleQuote": false,
7
+ "trailingComma": "all",
8
+ "bracketSpacing": true,
9
+ "arrowParens": "always"
10
+ }