@vacbo/opencode-anthropic-fix 0.0.44 → 0.1.1

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 (61) hide show
  1. package/README.md +19 -0
  2. package/dist/bun-proxy.mjs +282 -55
  3. package/dist/opencode-anthropic-auth-cli.mjs +194 -55
  4. package/dist/opencode-anthropic-auth-plugin.js +1816 -594
  5. package/package.json +1 -1
  6. package/src/__tests__/billing-edge-cases.test.ts +84 -0
  7. package/src/__tests__/bun-proxy.parallel.test.ts +460 -0
  8. package/src/__tests__/debug-gating.test.ts +76 -0
  9. package/src/__tests__/decomposition-smoke.test.ts +92 -0
  10. package/src/__tests__/fingerprint-regression.test.ts +1 -1
  11. package/src/__tests__/helpers/conversation-history.smoke.test.ts +338 -0
  12. package/src/__tests__/helpers/conversation-history.ts +376 -0
  13. package/src/__tests__/helpers/deferred.smoke.test.ts +161 -0
  14. package/src/__tests__/helpers/deferred.ts +122 -0
  15. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +166 -0
  16. package/src/__tests__/helpers/in-memory-storage.ts +152 -0
  17. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +92 -0
  18. package/src/__tests__/helpers/mock-bun-proxy.ts +229 -0
  19. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +337 -0
  20. package/src/__tests__/helpers/plugin-fetch-harness.ts +401 -0
  21. package/src/__tests__/helpers/sse.smoke.test.ts +243 -0
  22. package/src/__tests__/helpers/sse.ts +288 -0
  23. package/src/__tests__/index.parallel.test.ts +711 -0
  24. package/src/__tests__/sanitization-regex.test.ts +65 -0
  25. package/src/__tests__/state-bounds.test.ts +110 -0
  26. package/src/account-identity.test.ts +213 -0
  27. package/src/account-identity.ts +108 -0
  28. package/src/accounts.dedup.test.ts +696 -0
  29. package/src/accounts.test.ts +2 -1
  30. package/src/accounts.ts +485 -191
  31. package/src/bun-fetch.test.ts +379 -0
  32. package/src/bun-fetch.ts +447 -174
  33. package/src/bun-proxy.ts +289 -57
  34. package/src/circuit-breaker.test.ts +274 -0
  35. package/src/circuit-breaker.ts +235 -0
  36. package/src/cli.test.ts +1 -0
  37. package/src/cli.ts +37 -18
  38. package/src/commands/router.ts +25 -5
  39. package/src/env.ts +1 -0
  40. package/src/headers/billing.ts +31 -13
  41. package/src/index.ts +224 -247
  42. package/src/oauth.ts +7 -1
  43. package/src/parent-pid-watcher.test.ts +219 -0
  44. package/src/parent-pid-watcher.ts +99 -0
  45. package/src/plugin-helpers.ts +112 -0
  46. package/src/refresh-helpers.ts +169 -0
  47. package/src/refresh-lock.test.ts +36 -9
  48. package/src/refresh-lock.ts +2 -2
  49. package/src/request/body.history.test.ts +398 -0
  50. package/src/request/body.ts +200 -13
  51. package/src/request/metadata.ts +6 -2
  52. package/src/response/index.ts +1 -1
  53. package/src/response/mcp.ts +60 -31
  54. package/src/response/streaming.test.ts +382 -0
  55. package/src/response/streaming.ts +403 -76
  56. package/src/storage.test.ts +127 -104
  57. package/src/storage.ts +152 -62
  58. package/src/system-prompt/builder.ts +33 -3
  59. package/src/system-prompt/sanitize.ts +12 -2
  60. package/src/token-refresh.test.ts +84 -1
  61. package/src/token-refresh.ts +14 -8
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
1
  import { appendFileSync, existsSync, promises as fs, readFileSync, writeFileSync } from "node:fs";
3
2
  import { beforeEach, describe, expect, it, vi, type Mock } from "vitest";
4
3
  import {
@@ -10,6 +9,7 @@ import {
10
9
  loadAccounts,
11
10
  saveAccounts,
12
11
  } from "./storage.js";
12
+ import type { AccountMetadata, AccountStorage } from "./storage.js";
13
13
 
14
14
  // Mock fs modules
15
15
  vi.mock("node:fs", () => ({
@@ -35,6 +35,41 @@ vi.mock("node:crypto", () => ({
35
35
 
36
36
  const mockExistsSync = existsSync as Mock;
37
37
  const mockReadFileSync = readFileSync as Mock;
38
+ const mockFsReadFile = fs.readFile as Mock;
39
+ const mockFsWriteFile = fs.writeFile as Mock;
40
+ const mockFsRename = fs.rename as Mock;
41
+ const mockFsMkdir = fs.mkdir as Mock;
42
+ const mockFsChmod = fs.chmod as Mock;
43
+ const mockFsUnlink = fs.unlink as Mock;
44
+
45
+ function makeAccount(overrides: Partial<AccountMetadata> & Pick<AccountMetadata, "refreshToken">): AccountMetadata {
46
+ const addedAt = overrides.addedAt ?? 1000;
47
+
48
+ return {
49
+ id: overrides.id ?? `${addedAt}:${overrides.refreshToken.slice(0, 12)}`,
50
+ refreshToken: overrides.refreshToken,
51
+ token_updated_at: overrides.token_updated_at ?? addedAt,
52
+ addedAt,
53
+ lastUsed: overrides.lastUsed ?? 0,
54
+ enabled: overrides.enabled ?? true,
55
+ rateLimitResetTimes: overrides.rateLimitResetTimes ?? {},
56
+ consecutiveFailures: overrides.consecutiveFailures ?? 0,
57
+ lastFailureTime: overrides.lastFailureTime ?? null,
58
+ stats: overrides.stats ?? createDefaultStats(addedAt),
59
+ email: overrides.email,
60
+ identity: overrides.identity,
61
+ label: overrides.label,
62
+ access: overrides.access,
63
+ expires: overrides.expires,
64
+ lastSwitchReason: overrides.lastSwitchReason,
65
+ source: overrides.source,
66
+ };
67
+ }
68
+
69
+ function expectLoaded(result: AccountStorage | null): AccountStorage {
70
+ expect(result).not.toBeNull();
71
+ return result as AccountStorage;
72
+ }
38
73
 
39
74
  // ---------------------------------------------------------------------------
40
75
  // deduplicateByRefreshToken
@@ -46,17 +81,7 @@ describe("deduplicateByRefreshToken", () => {
46
81
  });
47
82
 
48
83
  it("returns single account unchanged", () => {
49
- const accounts = [
50
- {
51
- refreshToken: "token1",
52
- addedAt: 1000,
53
- lastUsed: 2000,
54
- enabled: true,
55
- rateLimitResetTimes: {},
56
- consecutiveFailures: 0,
57
- lastFailureTime: null,
58
- },
59
- ];
84
+ const accounts = [makeAccount({ refreshToken: "token1", addedAt: 1000, lastUsed: 2000 })];
60
85
  const result = deduplicateByRefreshToken(accounts);
61
86
  expect(result).toHaveLength(1);
62
87
  expect(result[0].refreshToken).toBe("token1");
@@ -64,24 +89,8 @@ describe("deduplicateByRefreshToken", () => {
64
89
 
65
90
  it("keeps most recently used when duplicates exist", () => {
66
91
  const accounts = [
67
- {
68
- refreshToken: "token1",
69
- addedAt: 1000,
70
- lastUsed: 1000,
71
- enabled: true,
72
- rateLimitResetTimes: {},
73
- consecutiveFailures: 0,
74
- lastFailureTime: null,
75
- },
76
- {
77
- refreshToken: "token1",
78
- addedAt: 2000,
79
- lastUsed: 5000,
80
- enabled: true,
81
- rateLimitResetTimes: {},
82
- consecutiveFailures: 0,
83
- lastFailureTime: null,
84
- },
92
+ makeAccount({ refreshToken: "token1", addedAt: 1000, lastUsed: 1000 }),
93
+ makeAccount({ refreshToken: "token1", addedAt: 2000, lastUsed: 5000 }),
85
94
  ];
86
95
  const result = deduplicateByRefreshToken(accounts);
87
96
  expect(result).toHaveLength(1);
@@ -90,41 +99,15 @@ describe("deduplicateByRefreshToken", () => {
90
99
 
91
100
  it("keeps different tokens as separate accounts", () => {
92
101
  const accounts = [
93
- {
94
- refreshToken: "token1",
95
- addedAt: 1000,
96
- lastUsed: 1000,
97
- enabled: true,
98
- rateLimitResetTimes: {},
99
- consecutiveFailures: 0,
100
- lastFailureTime: null,
101
- },
102
- {
103
- refreshToken: "token2",
104
- addedAt: 2000,
105
- lastUsed: 2000,
106
- enabled: true,
107
- rateLimitResetTimes: {},
108
- consecutiveFailures: 0,
109
- lastFailureTime: null,
110
- },
102
+ makeAccount({ refreshToken: "token1", addedAt: 1000, lastUsed: 1000 }),
103
+ makeAccount({ refreshToken: "token2", addedAt: 2000, lastUsed: 2000 }),
111
104
  ];
112
105
  const result = deduplicateByRefreshToken(accounts);
113
106
  expect(result).toHaveLength(2);
114
107
  });
115
108
 
116
109
  it("skips accounts without refreshToken", () => {
117
- const accounts = [
118
- {
119
- refreshToken: "",
120
- addedAt: 1000,
121
- lastUsed: 1000,
122
- enabled: true,
123
- rateLimitResetTimes: {},
124
- consecutiveFailures: 0,
125
- lastFailureTime: null,
126
- },
127
- ];
110
+ const accounts = [makeAccount({ refreshToken: "", addedAt: 1000, lastUsed: 1000 })];
128
111
  const result = deduplicateByRefreshToken(accounts);
129
112
  expect(result).toHaveLength(0);
130
113
  });
@@ -198,31 +181,47 @@ describe("loadAccounts", () => {
198
181
  });
199
182
 
200
183
  it("returns null when file does not exist", async () => {
201
- fs.readFile.mockRejectedValue(Object.assign(new Error("ENOENT"), { code: "ENOENT" }));
184
+ mockFsReadFile.mockRejectedValue(Object.assign(new Error("ENOENT"), { code: "ENOENT" }));
202
185
  const result = await loadAccounts();
203
186
  expect(result).toBeNull();
204
187
  });
205
188
 
206
189
  it("returns null for invalid JSON", async () => {
207
- fs.readFile.mockResolvedValue("not json");
190
+ mockFsReadFile.mockResolvedValue("not json");
208
191
  const result = await loadAccounts();
209
192
  expect(result).toBeNull();
210
193
  });
211
194
 
212
- it("returns null for wrong version", async () => {
213
- fs.readFile.mockResolvedValue(JSON.stringify({ version: 99, accounts: [] }));
195
+ it("warns and returns best-effort data for unknown version", async () => {
196
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
197
+ mockFsReadFile.mockResolvedValue(
198
+ JSON.stringify({
199
+ version: 99,
200
+ accounts: [{ refreshToken: "token1", enabled: false }],
201
+ activeIndex: 0,
202
+ }),
203
+ );
204
+
214
205
  const result = await loadAccounts();
215
- expect(result).toBeNull();
206
+
207
+ expect(warn).toHaveBeenCalledWith("Storage version mismatch: 99 vs 1. Attempting best-effort migration.");
208
+ expect(result).not.toBeNull();
209
+ expect(result?.version).toBe(1);
210
+ expect(result?.accounts).toHaveLength(1);
211
+ expect(result?.accounts[0]?.refreshToken).toBe("token1");
212
+ expect(result?.accounts[0]?.enabled).toBe(false);
213
+
214
+ warn.mockRestore();
216
215
  });
217
216
 
218
217
  it("returns null when accounts is not an array", async () => {
219
- fs.readFile.mockResolvedValue(JSON.stringify({ version: 1, accounts: "not-array" }));
218
+ mockFsReadFile.mockResolvedValue(JSON.stringify({ version: 1, accounts: "not-array" }));
220
219
  const result = await loadAccounts();
221
220
  expect(result).toBeNull();
222
221
  });
223
222
 
224
223
  it("loads valid accounts", async () => {
225
- fs.readFile.mockResolvedValue(
224
+ mockFsReadFile.mockResolvedValue(
226
225
  JSON.stringify({
227
226
  version: 1,
228
227
  accounts: [
@@ -241,8 +240,7 @@ describe("loadAccounts", () => {
241
240
  activeIndex: 0,
242
241
  }),
243
242
  );
244
- const result = await loadAccounts();
245
- expect(result).not.toBeNull();
243
+ const result = expectLoaded(await loadAccounts());
246
244
  expect(result.accounts).toHaveLength(1);
247
245
  expect(result.accounts[0].refreshToken).toBe("token1");
248
246
  expect(result.accounts[0].access).toBe("access1");
@@ -250,33 +248,58 @@ describe("loadAccounts", () => {
250
248
  expect(result.activeIndex).toBe(0);
251
249
  });
252
250
 
251
+ it("preserves stored source values and leaves missing source undefined", async () => {
252
+ mockFsReadFile.mockResolvedValue(
253
+ JSON.stringify({
254
+ version: 1,
255
+ accounts: [
256
+ {
257
+ refreshToken: "token1",
258
+ source: "cc-file",
259
+ label: "Imported Claude Code",
260
+ },
261
+ {
262
+ refreshToken: "token2",
263
+ },
264
+ ],
265
+ activeIndex: 0,
266
+ }),
267
+ );
268
+
269
+ const result = expectLoaded(await loadAccounts());
270
+
271
+ expect(result.accounts[0]?.source).toBe("cc-file");
272
+ expect(result.accounts[0]?.label).toBe("Imported Claude Code");
273
+ expect(result.accounts[1]?.source).toBeUndefined();
274
+ });
275
+
253
276
  it("filters out invalid accounts (missing refreshToken)", async () => {
254
- fs.readFile.mockResolvedValue(
277
+ mockFsReadFile.mockResolvedValue(
255
278
  JSON.stringify({
256
279
  version: 1,
257
280
  accounts: [{ refreshToken: "valid", addedAt: 1000 }, { email: "no-token" }, null],
258
281
  activeIndex: 0,
259
282
  }),
260
283
  );
261
- const result = await loadAccounts();
284
+ const result = expectLoaded(await loadAccounts());
262
285
  expect(result.accounts).toHaveLength(1);
263
286
  expect(result.accounts[0].refreshToken).toBe("valid");
264
287
  });
265
288
 
266
289
  it("clamps activeIndex to valid range", async () => {
267
- fs.readFile.mockResolvedValue(
290
+ mockFsReadFile.mockResolvedValue(
268
291
  JSON.stringify({
269
292
  version: 1,
270
293
  accounts: [{ refreshToken: "token1" }],
271
294
  activeIndex: 99,
272
295
  }),
273
296
  );
274
- const result = await loadAccounts();
297
+ const result = expectLoaded(await loadAccounts());
275
298
  expect(result.activeIndex).toBe(0);
276
299
  });
277
300
 
278
301
  it("deduplicates accounts by refresh token", async () => {
279
- fs.readFile.mockResolvedValue(
302
+ mockFsReadFile.mockResolvedValue(
280
303
  JSON.stringify({
281
304
  version: 1,
282
305
  accounts: [
@@ -286,21 +309,21 @@ describe("loadAccounts", () => {
286
309
  activeIndex: 0,
287
310
  }),
288
311
  );
289
- const result = await loadAccounts();
312
+ const result = expectLoaded(await loadAccounts());
290
313
  expect(result.accounts).toHaveLength(1);
291
314
  expect(result.accounts[0].lastUsed).toBe(5000);
292
315
  });
293
316
 
294
317
  it("applies defaults for missing fields", async () => {
295
- fs.readFile.mockResolvedValue(
318
+ mockFsReadFile.mockResolvedValue(
296
319
  JSON.stringify({
297
320
  version: 1,
298
321
  accounts: [{ refreshToken: "token1" }],
299
322
  activeIndex: 0,
300
323
  }),
301
324
  );
302
- const result = await loadAccounts();
303
- const acc = result.accounts[0];
325
+ const result = expectLoaded(await loadAccounts());
326
+ const acc = result.accounts[0]!;
304
327
  expect(acc.enabled).toBe(true);
305
328
  expect(acc.consecutiveFailures).toBe(0);
306
329
  expect(acc.lastFailureTime).toBeNull();
@@ -318,46 +341,46 @@ describe("saveAccounts", () => {
318
341
  vi.resetAllMocks();
319
342
  mockExistsSync.mockReturnValue(true);
320
343
  mockReadFileSync.mockReturnValue(".gitignore\nanthropic-accounts.json\nanthropic-accounts.json.*.tmp\n");
321
- fs.mkdir.mockResolvedValue(undefined);
322
- fs.writeFile.mockResolvedValue(undefined);
323
- fs.rename.mockResolvedValue(undefined);
324
- fs.chmod.mockResolvedValue(undefined);
344
+ mockFsMkdir.mockResolvedValue(undefined);
345
+ mockFsWriteFile.mockResolvedValue(undefined);
346
+ mockFsRename.mockResolvedValue(undefined);
347
+ mockFsChmod.mockResolvedValue(undefined);
325
348
  });
326
349
 
327
350
  it("writes atomically via temp file + rename", async () => {
328
351
  const storage = {
329
352
  version: 1,
330
- accounts: [{ refreshToken: "token1" }],
353
+ accounts: [makeAccount({ refreshToken: "token1" })],
331
354
  activeIndex: 0,
332
355
  };
333
356
  await saveAccounts(storage);
334
357
 
335
- expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining(".tmp"), expect.any(String), {
358
+ expect(mockFsWriteFile).toHaveBeenCalledWith(expect.stringContaining(".tmp"), expect.any(String), {
336
359
  encoding: "utf-8",
337
360
  mode: 0o600,
338
361
  });
339
- expect(fs.rename).toHaveBeenCalled();
362
+ expect(mockFsRename).toHaveBeenCalled();
340
363
  });
341
364
 
342
365
  it("creates config directory if needed", async () => {
343
366
  const storage = { version: 1, accounts: [], activeIndex: 0 };
344
367
  await saveAccounts(storage);
345
- expect(fs.mkdir).toHaveBeenCalledWith(expect.any(String), {
368
+ expect(mockFsMkdir).toHaveBeenCalledWith(expect.any(String), {
346
369
  recursive: true,
347
370
  });
348
371
  });
349
372
 
350
373
  it("cleans up temp file on write error", async () => {
351
- fs.writeFile.mockRejectedValue(new Error("disk full"));
352
- fs.unlink.mockResolvedValue(undefined);
374
+ mockFsWriteFile.mockRejectedValue(new Error("disk full"));
375
+ mockFsUnlink.mockResolvedValue(undefined);
353
376
 
354
377
  const storage = { version: 1, accounts: [], activeIndex: 0 };
355
378
  await expect(saveAccounts(storage)).rejects.toThrow("disk full");
356
- expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining(".tmp"));
379
+ expect(mockFsUnlink).toHaveBeenCalledWith(expect.stringContaining(".tmp"));
357
380
  });
358
381
 
359
382
  it("merges auth fields from fresher disk state", async () => {
360
- fs.readFile.mockResolvedValue(
383
+ mockFsReadFile.mockResolvedValue(
361
384
  JSON.stringify({
362
385
  version: 1,
363
386
  accounts: [
@@ -403,7 +426,7 @@ describe("saveAccounts", () => {
403
426
 
404
427
  await saveAccounts(storage);
405
428
 
406
- const written = JSON.parse(fs.writeFile.mock.calls[0][1]);
429
+ const written = JSON.parse(mockFsWriteFile.mock.calls[0][1]);
407
430
  expect(written.accounts[0].refreshToken).toBe("disk-refresh");
408
431
  expect(written.accounts[0].access).toBe("disk-access");
409
432
  expect(written.accounts[0].expires).toBe(999999);
@@ -411,7 +434,7 @@ describe("saveAccounts", () => {
411
434
  });
412
435
 
413
436
  it("matches id-less disk accounts by addedAt during freshness merge", async () => {
414
- fs.readFile.mockResolvedValue(
437
+ mockFsReadFile.mockResolvedValue(
415
438
  JSON.stringify({
416
439
  version: 1,
417
440
  accounts: [
@@ -456,13 +479,13 @@ describe("saveAccounts", () => {
456
479
 
457
480
  await saveAccounts(storage);
458
481
 
459
- const written = JSON.parse(fs.writeFile.mock.calls[0][1]);
482
+ const written = JSON.parse(mockFsWriteFile.mock.calls[0][1]);
460
483
  expect(written.accounts[0].refreshToken).toBe("disk-refresh-rotated");
461
484
  expect(written.accounts[0].token_updated_at).toBe(3000);
462
485
  });
463
486
 
464
487
  it("does not resurrect accounts removed by caller", async () => {
465
- fs.readFile.mockResolvedValue(
488
+ mockFsReadFile.mockResolvedValue(
466
489
  JSON.stringify({
467
490
  version: 1,
468
491
  accounts: [
@@ -485,7 +508,7 @@ describe("saveAccounts", () => {
485
508
 
486
509
  await saveAccounts({ version: 1, accounts: [], activeIndex: 0 });
487
510
 
488
- const written = JSON.parse(fs.writeFile.mock.calls[0][1]);
511
+ const written = JSON.parse(mockFsWriteFile.mock.calls[0][1]);
489
512
  expect(written.accounts).toEqual([]);
490
513
  });
491
514
  });
@@ -500,18 +523,18 @@ describe("clearAccounts", () => {
500
523
  });
501
524
 
502
525
  it("deletes the storage file", async () => {
503
- fs.unlink.mockResolvedValue(undefined);
526
+ mockFsUnlink.mockResolvedValue(undefined);
504
527
  await clearAccounts();
505
- expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining("anthropic-accounts.json"));
528
+ expect(mockFsUnlink).toHaveBeenCalledWith(expect.stringContaining("anthropic-accounts.json"));
506
529
  });
507
530
 
508
531
  it("ignores ENOENT errors", async () => {
509
- fs.unlink.mockRejectedValue(Object.assign(new Error("ENOENT"), { code: "ENOENT" }));
532
+ mockFsUnlink.mockRejectedValue(Object.assign(new Error("ENOENT"), { code: "ENOENT" }));
510
533
  await expect(clearAccounts()).resolves.toBeUndefined();
511
534
  });
512
535
 
513
536
  it("rethrows non-ENOENT errors", async () => {
514
- fs.unlink.mockRejectedValue(Object.assign(new Error("permission denied"), { code: "EACCES" }));
537
+ mockFsUnlink.mockRejectedValue(Object.assign(new Error("permission denied"), { code: "EACCES" }));
515
538
  await expect(clearAccounts()).rejects.toThrow("permission denied");
516
539
  });
517
540
  });
@@ -560,9 +583,9 @@ describe("loadAccounts with stats", () => {
560
583
  ],
561
584
  activeIndex: 0,
562
585
  };
563
- fs.readFile.mockResolvedValue(JSON.stringify(stored));
586
+ mockFsReadFile.mockResolvedValue(JSON.stringify(stored));
564
587
 
565
- const result = await loadAccounts();
588
+ const result = expectLoaded(await loadAccounts());
566
589
  expect(result.accounts[0].stats.requests).toBe(42);
567
590
  expect(result.accounts[0].stats.inputTokens).toBe(10000);
568
591
  expect(result.accounts[0].stats.outputTokens).toBe(5000);
@@ -577,9 +600,9 @@ describe("loadAccounts with stats", () => {
577
600
  accounts: [{ refreshToken: "tok1" }],
578
601
  activeIndex: 0,
579
602
  };
580
- fs.readFile.mockResolvedValue(JSON.stringify(stored));
603
+ mockFsReadFile.mockResolvedValue(JSON.stringify(stored));
581
604
 
582
- const result = await loadAccounts();
605
+ const result = expectLoaded(await loadAccounts());
583
606
  expect(result.accounts[0].stats.requests).toBe(0);
584
607
  expect(result.accounts[0].stats.inputTokens).toBe(0);
585
608
  expect(result.accounts[0].stats.outputTokens).toBe(0);
@@ -596,9 +619,9 @@ describe("loadAccounts with stats", () => {
596
619
  ],
597
620
  activeIndex: 0,
598
621
  };
599
- fs.readFile.mockResolvedValue(JSON.stringify(stored));
622
+ mockFsReadFile.mockResolvedValue(JSON.stringify(stored));
600
623
 
601
- const result = await loadAccounts();
624
+ const result = expectLoaded(await loadAccounts());
602
625
  expect(result.accounts[0].stats.requests).toBe(0);
603
626
  expect(result.accounts[0].stats.inputTokens).toBe(0);
604
627
  expect(result.accounts[0].stats.outputTokens).toBe(0);