opencode-anthropic-fix 0.2.1 → 0.3.0

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 (39) hide show
  1. package/README.md +24 -0
  2. package/index.mjs +155 -149
  3. package/lib/config.mjs +47 -25
  4. package/lib/mimicry/adapter-input.mjs +671 -0
  5. package/lib/mimicry/context-hint-threshold.mjs +127 -0
  6. package/lib/mimicry/headers.mjs +71 -64
  7. package/lib/mimicry/request-body.mjs +23 -22
  8. package/lib/mimicry/system-prompt.mjs +54 -23
  9. package/lib/mimicry/wire-compat.mjs +305 -0
  10. package/package.json +3 -3
  11. package/lib/account-state.test.mjs +0 -109
  12. package/lib/accounts.test.mjs +0 -912
  13. package/lib/adaptive-thinking-models.test.mjs +0 -30
  14. package/lib/backoff.test.mjs +0 -411
  15. package/lib/cc-credentials.test.mjs +0 -391
  16. package/lib/config.test.mjs +0 -764
  17. package/lib/context-hint-persist.mjs +0 -68
  18. package/lib/diagnose.test.mjs +0 -122
  19. package/lib/env.test.mjs +0 -25
  20. package/lib/haiku-call.test.mjs +0 -72
  21. package/lib/message-transform.test.mjs +0 -173
  22. package/lib/mimicry/cache.test.mjs +0 -112
  23. package/lib/mimicry/headers.test.mjs +0 -50
  24. package/lib/mimicry/models.test.mjs +0 -132
  25. package/lib/mimicry/request-body.test.mjs +0 -34
  26. package/lib/mimicry/request-helpers.test.mjs +0 -155
  27. package/lib/mimicry/response-stream.test.mjs +0 -127
  28. package/lib/mimicry/system-prompt.test.mjs +0 -73
  29. package/lib/oauth.test.mjs +0 -343
  30. package/lib/redact.test.mjs +0 -154
  31. package/lib/refresh-lock.test.mjs +0 -131
  32. package/lib/request-headers.test.mjs +0 -164
  33. package/lib/retry/overload-loop.test.mjs +0 -107
  34. package/lib/rotation.test.mjs +0 -392
  35. package/lib/session-metrics.test.mjs +0 -67
  36. package/lib/storage.test.mjs +0 -696
  37. package/lib/token-economy/microcompact.test.mjs +0 -28
  38. package/lib/token-economy/transforms.test.mjs +0 -192
  39. package/lib/tuning.test.mjs +0 -89
@@ -1,696 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import {
3
- deduplicateByRefreshToken,
4
- ensureGitignore,
5
- getStoragePath,
6
- loadAccounts,
7
- saveAccounts,
8
- clearAccounts,
9
- createDefaultStats,
10
- } from "./storage.mjs";
11
- import { promises as fs } from "node:fs";
12
- import { existsSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
13
- import { randomBytes } from "node:crypto";
14
-
15
- const itWindows = process.platform === "win32" ? it : it.skip;
16
-
17
- // Mock fs modules
18
- vi.mock("node:fs", () => ({
19
- existsSync: vi.fn(),
20
- readFileSync: vi.fn(),
21
- appendFileSync: vi.fn(),
22
- writeFileSync: vi.fn(),
23
- promises: {
24
- readFile: vi.fn(),
25
- writeFile: vi.fn(),
26
- rename: vi.fn(),
27
- chmod: vi.fn(),
28
- unlink: vi.fn(),
29
- mkdir: vi.fn(),
30
- },
31
- }));
32
-
33
- vi.mock("node:crypto", async (importOriginal) => {
34
- const actual = await importOriginal();
35
- return {
36
- randomBytes: vi.fn(() => ({
37
- toString: () => "abcdef123456",
38
- })),
39
- createHash: actual.createHash,
40
- };
41
- });
42
-
43
- // ---------------------------------------------------------------------------
44
- // deduplicateByRefreshToken
45
- // ---------------------------------------------------------------------------
46
-
47
- describe("deduplicateByRefreshToken", () => {
48
- it("returns empty array for empty input", () => {
49
- expect(deduplicateByRefreshToken([])).toEqual([]);
50
- });
51
-
52
- it("returns single account unchanged", () => {
53
- const accounts = [
54
- {
55
- refreshToken: "token1",
56
- addedAt: 1000,
57
- lastUsed: 2000,
58
- enabled: true,
59
- rateLimitResetTimes: {},
60
- consecutiveFailures: 0,
61
- lastFailureTime: null,
62
- },
63
- ];
64
- const result = deduplicateByRefreshToken(accounts);
65
- expect(result).toHaveLength(1);
66
- expect(result[0].refreshToken).toBe("token1");
67
- });
68
-
69
- it("keeps most recently used when duplicates exist", () => {
70
- const accounts = [
71
- {
72
- refreshToken: "token1",
73
- addedAt: 1000,
74
- lastUsed: 1000,
75
- enabled: true,
76
- rateLimitResetTimes: {},
77
- consecutiveFailures: 0,
78
- lastFailureTime: null,
79
- },
80
- {
81
- refreshToken: "token1",
82
- addedAt: 2000,
83
- lastUsed: 5000,
84
- enabled: true,
85
- rateLimitResetTimes: {},
86
- consecutiveFailures: 0,
87
- lastFailureTime: null,
88
- },
89
- ];
90
- const result = deduplicateByRefreshToken(accounts);
91
- expect(result).toHaveLength(1);
92
- expect(result[0].lastUsed).toBe(5000);
93
- });
94
-
95
- it("keeps different tokens as separate accounts", () => {
96
- const accounts = [
97
- {
98
- refreshToken: "token1",
99
- addedAt: 1000,
100
- lastUsed: 1000,
101
- enabled: true,
102
- rateLimitResetTimes: {},
103
- consecutiveFailures: 0,
104
- lastFailureTime: null,
105
- },
106
- {
107
- refreshToken: "token2",
108
- addedAt: 2000,
109
- lastUsed: 2000,
110
- enabled: true,
111
- rateLimitResetTimes: {},
112
- consecutiveFailures: 0,
113
- lastFailureTime: null,
114
- },
115
- ];
116
- const result = deduplicateByRefreshToken(accounts);
117
- expect(result).toHaveLength(2);
118
- });
119
-
120
- it("skips accounts without refreshToken", () => {
121
- const accounts = [
122
- {
123
- refreshToken: "",
124
- addedAt: 1000,
125
- lastUsed: 1000,
126
- enabled: true,
127
- rateLimitResetTimes: {},
128
- consecutiveFailures: 0,
129
- lastFailureTime: null,
130
- },
131
- ];
132
- const result = deduplicateByRefreshToken(accounts);
133
- expect(result).toHaveLength(0);
134
- });
135
- });
136
-
137
- // ---------------------------------------------------------------------------
138
- // ensureGitignore
139
- // ---------------------------------------------------------------------------
140
-
141
- describe("ensureGitignore", () => {
142
- beforeEach(() => {
143
- vi.resetAllMocks();
144
- });
145
-
146
- it("creates new .gitignore when none exists", () => {
147
- existsSync.mockReturnValue(false);
148
- ensureGitignore("/config/dir");
149
- expect(writeFileSync).toHaveBeenCalledWith(
150
- expect.stringMatching(/[\\/]config[\\/]dir[\\/]\.gitignore$/),
151
- expect.stringContaining("anthropic-accounts.json"),
152
- "utf-8",
153
- );
154
- });
155
-
156
- it("appends missing entries to existing .gitignore", () => {
157
- existsSync.mockReturnValue(true);
158
- readFileSync.mockReturnValue("some-other-file\n");
159
- ensureGitignore("/config/dir");
160
- expect(appendFileSync).toHaveBeenCalledWith(
161
- expect.stringMatching(/[\\/]config[\\/]dir[\\/]\.gitignore$/),
162
- expect.stringContaining("anthropic-accounts.json"),
163
- "utf-8",
164
- );
165
- });
166
-
167
- it("does nothing when all entries already present", () => {
168
- existsSync.mockReturnValue(true);
169
- readFileSync.mockReturnValue(".gitignore\nanthropic-accounts.json\nanthropic-accounts.json.*.tmp\n");
170
- ensureGitignore("/config/dir");
171
- expect(appendFileSync).not.toHaveBeenCalled();
172
- expect(writeFileSync).not.toHaveBeenCalled();
173
- });
174
-
175
- it("handles errors gracefully", () => {
176
- existsSync.mockImplementation(() => {
177
- throw new Error("permission denied");
178
- });
179
- // Should not throw
180
- expect(() => ensureGitignore("/config/dir")).not.toThrow();
181
- });
182
- });
183
-
184
- // ---------------------------------------------------------------------------
185
- // getStoragePath
186
- // ---------------------------------------------------------------------------
187
-
188
- describe("getStoragePath", () => {
189
- it("returns path ending with anthropic-accounts.json", () => {
190
- const path = getStoragePath();
191
- expect(path.endsWith("anthropic-accounts.json")).toBe(true);
192
- });
193
- });
194
-
195
- // ---------------------------------------------------------------------------
196
- // loadAccounts
197
- // ---------------------------------------------------------------------------
198
-
199
- describe("loadAccounts", () => {
200
- beforeEach(() => {
201
- vi.resetAllMocks();
202
- });
203
-
204
- it("returns null when file does not exist", async () => {
205
- fs.readFile.mockRejectedValue(Object.assign(new Error("ENOENT"), { code: "ENOENT" }));
206
- const result = await loadAccounts();
207
- expect(result).toBeNull();
208
- });
209
-
210
- it("returns null for invalid JSON", async () => {
211
- fs.readFile.mockResolvedValue("not json");
212
- const result = await loadAccounts();
213
- expect(result).toBeNull();
214
- });
215
-
216
- it("returns best-effort data for newer version (forward compat)", async () => {
217
- // QA fix L-warnSpy: mock console.warn to prevent test output contamination
218
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
219
- fs.readFile.mockResolvedValue(JSON.stringify({ version: 99, accounts: [] }));
220
- const result = await loadAccounts();
221
- // H12 fix: no longer returns null; attempts best-effort load with a warning
222
- expect(result).not.toBeNull();
223
- expect(result.accounts).toEqual([]);
224
- expect(result.version).toBe(1); // normalized to current version
225
- expect(warnSpy).toHaveBeenCalledOnce();
226
- warnSpy.mockRestore();
227
- });
228
-
229
- it("returns null when accounts is not an array", async () => {
230
- fs.readFile.mockResolvedValue(JSON.stringify({ version: 1, accounts: "not-array" }));
231
- const result = await loadAccounts();
232
- expect(result).toBeNull();
233
- });
234
-
235
- it("loads valid accounts", async () => {
236
- fs.readFile.mockResolvedValue(
237
- JSON.stringify({
238
- version: 1,
239
- accounts: [
240
- {
241
- refreshToken: "token1",
242
- access: "access1",
243
- expires: 9999999999,
244
- addedAt: 1000,
245
- lastUsed: 2000,
246
- enabled: true,
247
- rateLimitResetTimes: {},
248
- consecutiveFailures: 0,
249
- lastFailureTime: null,
250
- },
251
- ],
252
- activeIndex: 0,
253
- }),
254
- );
255
- const result = await loadAccounts();
256
- expect(result).not.toBeNull();
257
- expect(result.accounts).toHaveLength(1);
258
- expect(result.accounts[0].refreshToken).toBe("token1");
259
- expect(result.accounts[0].access).toBe("access1");
260
- expect(result.accounts[0].expires).toBe(9999999999);
261
- expect(result.activeIndex).toBe(0);
262
- });
263
-
264
- it("filters out invalid accounts (missing refreshToken)", async () => {
265
- fs.readFile.mockResolvedValue(
266
- JSON.stringify({
267
- version: 1,
268
- accounts: [{ refreshToken: "valid", addedAt: 1000 }, { email: "no-token" }, null],
269
- activeIndex: 0,
270
- }),
271
- );
272
- const result = await loadAccounts();
273
- expect(result.accounts).toHaveLength(1);
274
- expect(result.accounts[0].refreshToken).toBe("valid");
275
- });
276
-
277
- it("clamps activeIndex to valid range", async () => {
278
- fs.readFile.mockResolvedValue(
279
- JSON.stringify({
280
- version: 1,
281
- accounts: [{ refreshToken: "token1" }],
282
- activeIndex: 99,
283
- }),
284
- );
285
- const result = await loadAccounts();
286
- expect(result.activeIndex).toBe(0);
287
- });
288
-
289
- it("deduplicates accounts by refresh token", async () => {
290
- fs.readFile.mockResolvedValue(
291
- JSON.stringify({
292
- version: 1,
293
- accounts: [
294
- { refreshToken: "token1", lastUsed: 1000 },
295
- { refreshToken: "token1", lastUsed: 5000 },
296
- ],
297
- activeIndex: 0,
298
- }),
299
- );
300
- const result = await loadAccounts();
301
- expect(result.accounts).toHaveLength(1);
302
- expect(result.accounts[0].lastUsed).toBe(5000);
303
- });
304
-
305
- it("applies defaults for missing fields", async () => {
306
- fs.readFile.mockResolvedValue(
307
- JSON.stringify({
308
- version: 1,
309
- accounts: [{ refreshToken: "token1" }],
310
- activeIndex: 0,
311
- }),
312
- );
313
- const result = await loadAccounts();
314
- const acc = result.accounts[0];
315
- expect(acc.enabled).toBe(true);
316
- expect(acc.consecutiveFailures).toBe(0);
317
- expect(acc.lastFailureTime).toBeNull();
318
- expect(acc.rateLimitResetTimes).toEqual({});
319
- expect(acc.lastUsed).toBe(0);
320
- });
321
- });
322
-
323
- // ---------------------------------------------------------------------------
324
- // saveAccounts
325
- // ---------------------------------------------------------------------------
326
-
327
- describe("saveAccounts", () => {
328
- beforeEach(() => {
329
- vi.resetAllMocks();
330
- existsSync.mockReturnValue(true);
331
- readFileSync.mockReturnValue(".gitignore\nanthropic-accounts.json\nanthropic-accounts.json.*.tmp\n");
332
- fs.mkdir.mockResolvedValue(undefined);
333
- fs.writeFile.mockResolvedValue(undefined);
334
- fs.rename.mockResolvedValue(undefined);
335
- fs.chmod.mockResolvedValue(undefined);
336
- });
337
-
338
- it("writes atomically via temp file + rename", async () => {
339
- const storage = {
340
- version: 1,
341
- accounts: [{ refreshToken: "token1" }],
342
- activeIndex: 0,
343
- };
344
- await saveAccounts(storage);
345
-
346
- expect(fs.writeFile).toHaveBeenCalledWith(expect.stringContaining(".tmp"), expect.any(String), {
347
- encoding: "utf-8",
348
- mode: 0o600,
349
- });
350
- expect(fs.rename).toHaveBeenCalled();
351
- });
352
-
353
- it("creates config directory if needed", async () => {
354
- const storage = { version: 1, accounts: [], activeIndex: 0 };
355
- await saveAccounts(storage);
356
- expect(fs.mkdir).toHaveBeenCalledWith(expect.any(String), {
357
- recursive: true,
358
- });
359
- });
360
-
361
- it("cleans up temp file on write error", async () => {
362
- fs.writeFile.mockRejectedValue(new Error("disk full"));
363
- fs.unlink.mockResolvedValue(undefined);
364
-
365
- const storage = { version: 1, accounts: [], activeIndex: 0 };
366
- await expect(saveAccounts(storage)).rejects.toThrow("disk full");
367
- expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining(".tmp"));
368
- });
369
-
370
- it("keeps persisted JSON consistent when concurrent saves commit out of order", async () => {
371
- randomBytes
372
- .mockReturnValueOnce({ toString: () => "writer-one" })
373
- .mockReturnValueOnce({ toString: () => "writer-two" });
374
-
375
- const files = new Map();
376
- let markFirstWriteStarted;
377
- const firstWriteStarted = new Promise((resolve) => {
378
- markFirstWriteStarted = resolve;
379
- });
380
- let releaseFirstWrite;
381
- const firstWriteBlocked = new Promise((resolve) => {
382
- releaseFirstWrite = resolve;
383
- });
384
-
385
- fs.writeFile.mockImplementation(async (path, content) => {
386
- if (path.includes("writer-one")) {
387
- markFirstWriteStarted();
388
- await firstWriteBlocked;
389
- }
390
- files.set(path, content);
391
- });
392
- fs.rename.mockImplementation(async (tempPath, storagePath) => {
393
- const content = files.get(tempPath);
394
- if (content === undefined) throw new Error("temp file missing");
395
- files.set(storagePath, content);
396
- files.delete(tempPath);
397
- });
398
-
399
- const firstStorage = {
400
- version: 1,
401
- accounts: [{ refreshToken: "first-writer" }],
402
- activeIndex: 0,
403
- };
404
- const secondStorage = {
405
- version: 1,
406
- accounts: [{ refreshToken: "second-writer" }],
407
- activeIndex: 0,
408
- };
409
- const emptyDisk = { version: 1, accounts: [], activeIndex: 0 };
410
-
411
- const firstSave = saveAccounts(firstStorage, emptyDisk);
412
- await firstWriteStarted;
413
- await saveAccounts(secondStorage, emptyDisk);
414
- releaseFirstWrite();
415
- await firstSave;
416
-
417
- const storagePath = fs.rename.mock.calls.at(-1)[1];
418
- const persisted = JSON.parse(files.get(storagePath));
419
- expect(persisted).toEqual(firstStorage);
420
- expect([...files.keys()].filter((path) => path.endsWith(".tmp"))).toEqual([]);
421
- });
422
-
423
- itWindows("propagates a Windows ACL-denied write and removes the temp file", async () => {
424
- const accessDenied = Object.assign(new Error("access denied"), { code: "EACCES" });
425
- fs.writeFile.mockRejectedValue(accessDenied);
426
- fs.unlink.mockResolvedValue(undefined);
427
-
428
- const storage = { version: 1, accounts: [], activeIndex: 0 };
429
- await expect(saveAccounts(storage, storage)).rejects.toBe(accessDenied);
430
-
431
- const tempPath = fs.writeFile.mock.calls[0][0];
432
- expect(fs.rename).not.toHaveBeenCalled();
433
- expect(fs.unlink).toHaveBeenCalledWith(tempPath);
434
- });
435
-
436
- it("cleans up the temp file when atomic rename fails", async () => {
437
- const renameError = Object.assign(new Error("rename failed"), { code: "EPERM" });
438
- fs.rename.mockRejectedValue(renameError);
439
- fs.unlink.mockResolvedValue(undefined);
440
-
441
- const storage = { version: 1, accounts: [], activeIndex: 0 };
442
- await expect(saveAccounts(storage, storage)).rejects.toBe(renameError);
443
-
444
- const tempPath = fs.writeFile.mock.calls[0][0];
445
- expect(fs.rename).toHaveBeenCalledWith(tempPath, expect.stringMatching(/anthropic-accounts\.json$/));
446
- expect(fs.unlink).toHaveBeenCalledWith(tempPath);
447
- });
448
-
449
- it("merges auth fields from fresher disk state", async () => {
450
- fs.readFile.mockResolvedValue(
451
- JSON.stringify({
452
- version: 1,
453
- accounts: [
454
- {
455
- id: "a1",
456
- refreshToken: "disk-refresh",
457
- access: "disk-access",
458
- expires: 999999,
459
- token_updated_at: 2000,
460
- addedAt: 1000,
461
- lastUsed: 0,
462
- enabled: true,
463
- rateLimitResetTimes: {},
464
- consecutiveFailures: 0,
465
- lastFailureTime: null,
466
- stats: createDefaultStats(1000),
467
- },
468
- ],
469
- activeIndex: 0,
470
- }),
471
- );
472
-
473
- const storage = {
474
- version: 1,
475
- accounts: [
476
- {
477
- id: "a1",
478
- refreshToken: "mem-refresh",
479
- access: "mem-access",
480
- expires: 123,
481
- token_updated_at: 1000,
482
- addedAt: 1000,
483
- lastUsed: 0,
484
- enabled: true,
485
- rateLimitResetTimes: {},
486
- consecutiveFailures: 0,
487
- lastFailureTime: null,
488
- stats: createDefaultStats(1000),
489
- },
490
- ],
491
- activeIndex: 0,
492
- };
493
-
494
- await saveAccounts(storage);
495
-
496
- const written = JSON.parse(fs.writeFile.mock.calls[0][1]);
497
- expect(written.accounts[0].refreshToken).toBe("disk-refresh");
498
- expect(written.accounts[0].access).toBe("disk-access");
499
- expect(written.accounts[0].expires).toBe(999999);
500
- expect(written.accounts[0].token_updated_at).toBe(2000);
501
- });
502
-
503
- it("matches id-less disk accounts by addedAt during freshness merge", async () => {
504
- fs.readFile.mockResolvedValue(
505
- JSON.stringify({
506
- version: 1,
507
- accounts: [
508
- {
509
- refreshToken: "disk-refresh-rotated",
510
- access: "disk-access",
511
- expires: 888888,
512
- token_updated_at: 3000,
513
- addedAt: 1111,
514
- lastUsed: 0,
515
- enabled: true,
516
- rateLimitResetTimes: {},
517
- consecutiveFailures: 0,
518
- lastFailureTime: null,
519
- stats: createDefaultStats(1111),
520
- },
521
- ],
522
- activeIndex: 0,
523
- }),
524
- );
525
-
526
- const storage = {
527
- version: 1,
528
- accounts: [
529
- {
530
- id: "legacy-a1",
531
- refreshToken: "old-refresh",
532
- access: "old-access",
533
- expires: 111,
534
- token_updated_at: 1000,
535
- addedAt: 1111,
536
- lastUsed: 0,
537
- enabled: true,
538
- rateLimitResetTimes: {},
539
- consecutiveFailures: 0,
540
- lastFailureTime: null,
541
- stats: createDefaultStats(1111),
542
- },
543
- ],
544
- activeIndex: 0,
545
- };
546
-
547
- await saveAccounts(storage);
548
-
549
- const written = JSON.parse(fs.writeFile.mock.calls[0][1]);
550
- expect(written.accounts[0].refreshToken).toBe("disk-refresh-rotated");
551
- expect(written.accounts[0].token_updated_at).toBe(3000);
552
- });
553
-
554
- it("does not resurrect accounts removed by caller", async () => {
555
- fs.readFile.mockResolvedValue(
556
- JSON.stringify({
557
- version: 1,
558
- accounts: [
559
- {
560
- id: "a1",
561
- refreshToken: "disk-refresh",
562
- token_updated_at: 2000,
563
- addedAt: 1000,
564
- lastUsed: 0,
565
- enabled: true,
566
- rateLimitResetTimes: {},
567
- consecutiveFailures: 0,
568
- lastFailureTime: null,
569
- stats: createDefaultStats(1000),
570
- },
571
- ],
572
- activeIndex: 0,
573
- }),
574
- );
575
-
576
- await saveAccounts({ version: 1, accounts: [], activeIndex: 0 });
577
-
578
- const written = JSON.parse(fs.writeFile.mock.calls[0][1]);
579
- expect(written.accounts).toEqual([]);
580
- });
581
- });
582
-
583
- // ---------------------------------------------------------------------------
584
- // clearAccounts
585
- // ---------------------------------------------------------------------------
586
-
587
- describe("clearAccounts", () => {
588
- beforeEach(() => {
589
- vi.resetAllMocks();
590
- });
591
-
592
- it("deletes the storage file", async () => {
593
- fs.unlink.mockResolvedValue(undefined);
594
- await clearAccounts();
595
- expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining("anthropic-accounts.json"));
596
- });
597
-
598
- it("ignores ENOENT errors", async () => {
599
- fs.unlink.mockRejectedValue(Object.assign(new Error("ENOENT"), { code: "ENOENT" }));
600
- await expect(clearAccounts()).resolves.toBeUndefined();
601
- });
602
-
603
- it("rethrows non-ENOENT errors", async () => {
604
- fs.unlink.mockRejectedValue(Object.assign(new Error("permission denied"), { code: "EACCES" }));
605
- await expect(clearAccounts()).rejects.toThrow("permission denied");
606
- });
607
- });
608
-
609
- // ---------------------------------------------------------------------------
610
- // Stats validation
611
- // ---------------------------------------------------------------------------
612
-
613
- describe("createDefaultStats", () => {
614
- it("creates zeroed stats with given timestamp", () => {
615
- const stats = createDefaultStats(1000);
616
- expect(stats).toEqual({
617
- requests: 0,
618
- inputTokens: 0,
619
- outputTokens: 0,
620
- cacheReadTokens: 0,
621
- cacheWriteTokens: 0,
622
- lastReset: 1000,
623
- });
624
- });
625
-
626
- it("uses Date.now() when no timestamp given", () => {
627
- const before = Date.now();
628
- const stats = createDefaultStats();
629
- expect(stats.lastReset).toBeGreaterThanOrEqual(before);
630
- expect(stats.lastReset).toBeLessThanOrEqual(Date.now());
631
- });
632
- });
633
-
634
- describe("loadAccounts with stats", () => {
635
- it("loads accounts with stats fields preserved", async () => {
636
- const stored = {
637
- version: 1,
638
- accounts: [
639
- {
640
- refreshToken: "tok1",
641
- stats: {
642
- requests: 42,
643
- inputTokens: 10000,
644
- outputTokens: 5000,
645
- cacheReadTokens: 2000,
646
- cacheWriteTokens: 100,
647
- lastReset: 1700000000000,
648
- },
649
- },
650
- ],
651
- activeIndex: 0,
652
- };
653
- fs.readFile.mockResolvedValue(JSON.stringify(stored));
654
-
655
- const result = await loadAccounts();
656
- expect(result.accounts[0].stats.requests).toBe(42);
657
- expect(result.accounts[0].stats.inputTokens).toBe(10000);
658
- expect(result.accounts[0].stats.outputTokens).toBe(5000);
659
- expect(result.accounts[0].stats.cacheReadTokens).toBe(2000);
660
- expect(result.accounts[0].stats.cacheWriteTokens).toBe(100);
661
- expect(result.accounts[0].stats.lastReset).toBe(1700000000000);
662
- });
663
-
664
- it("provides default stats when missing from stored data", async () => {
665
- const stored = {
666
- version: 1,
667
- accounts: [{ refreshToken: "tok1" }],
668
- activeIndex: 0,
669
- };
670
- fs.readFile.mockResolvedValue(JSON.stringify(stored));
671
-
672
- const result = await loadAccounts();
673
- expect(result.accounts[0].stats.requests).toBe(0);
674
- expect(result.accounts[0].stats.inputTokens).toBe(0);
675
- expect(result.accounts[0].stats.outputTokens).toBe(0);
676
- });
677
-
678
- it("clamps negative stats values to 0", async () => {
679
- const stored = {
680
- version: 1,
681
- accounts: [
682
- {
683
- refreshToken: "tok1",
684
- stats: { requests: -5, inputTokens: -100, outputTokens: NaN },
685
- },
686
- ],
687
- activeIndex: 0,
688
- };
689
- fs.readFile.mockResolvedValue(JSON.stringify(stored));
690
-
691
- const result = await loadAccounts();
692
- expect(result.accounts[0].stats.requests).toBe(0);
693
- expect(result.accounts[0].stats.inputTokens).toBe(0);
694
- expect(result.accounts[0].stats.outputTokens).toBe(0);
695
- });
696
- });