@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.
- package/README.md +19 -0
- package/dist/bun-proxy.mjs +282 -55
- package/dist/opencode-anthropic-auth-cli.mjs +194 -55
- package/dist/opencode-anthropic-auth-plugin.js +1816 -594
- package/package.json +1 -1
- package/src/__tests__/billing-edge-cases.test.ts +84 -0
- package/src/__tests__/bun-proxy.parallel.test.ts +460 -0
- package/src/__tests__/debug-gating.test.ts +76 -0
- package/src/__tests__/decomposition-smoke.test.ts +92 -0
- package/src/__tests__/fingerprint-regression.test.ts +1 -1
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +338 -0
- package/src/__tests__/helpers/conversation-history.ts +376 -0
- package/src/__tests__/helpers/deferred.smoke.test.ts +161 -0
- package/src/__tests__/helpers/deferred.ts +122 -0
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +166 -0
- package/src/__tests__/helpers/in-memory-storage.ts +152 -0
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +92 -0
- package/src/__tests__/helpers/mock-bun-proxy.ts +229 -0
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +337 -0
- package/src/__tests__/helpers/plugin-fetch-harness.ts +401 -0
- package/src/__tests__/helpers/sse.smoke.test.ts +243 -0
- package/src/__tests__/helpers/sse.ts +288 -0
- package/src/__tests__/index.parallel.test.ts +711 -0
- package/src/__tests__/sanitization-regex.test.ts +65 -0
- package/src/__tests__/state-bounds.test.ts +110 -0
- package/src/account-identity.test.ts +213 -0
- package/src/account-identity.ts +108 -0
- package/src/accounts.dedup.test.ts +696 -0
- package/src/accounts.test.ts +2 -1
- package/src/accounts.ts +485 -191
- package/src/bun-fetch.test.ts +379 -0
- package/src/bun-fetch.ts +447 -174
- package/src/bun-proxy.ts +289 -57
- package/src/circuit-breaker.test.ts +274 -0
- package/src/circuit-breaker.ts +235 -0
- package/src/cli.test.ts +1 -0
- package/src/cli.ts +37 -18
- package/src/commands/router.ts +25 -5
- package/src/env.ts +1 -0
- package/src/headers/billing.ts +31 -13
- package/src/index.ts +224 -247
- package/src/oauth.ts +7 -1
- package/src/parent-pid-watcher.test.ts +219 -0
- package/src/parent-pid-watcher.ts +99 -0
- package/src/plugin-helpers.ts +112 -0
- package/src/refresh-helpers.ts +169 -0
- package/src/refresh-lock.test.ts +36 -9
- package/src/refresh-lock.ts +2 -2
- package/src/request/body.history.test.ts +398 -0
- package/src/request/body.ts +200 -13
- package/src/request/metadata.ts +6 -2
- package/src/response/index.ts +1 -1
- package/src/response/mcp.ts +60 -31
- package/src/response/streaming.test.ts +382 -0
- package/src/response/streaming.ts +403 -76
- package/src/storage.test.ts +127 -104
- package/src/storage.ts +152 -62
- package/src/system-prompt/builder.ts +33 -3
- package/src/system-prompt/sanitize.ts +12 -2
- package/src/token-refresh.test.ts +84 -1
- package/src/token-refresh.ts +14 -8
package/src/storage.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
+
mockFsReadFile.mockResolvedValue("not json");
|
|
208
191
|
const result = await loadAccounts();
|
|
209
192
|
expect(result).toBeNull();
|
|
210
193
|
});
|
|
211
194
|
|
|
212
|
-
it("returns
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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(
|
|
358
|
+
expect(mockFsWriteFile).toHaveBeenCalledWith(expect.stringContaining(".tmp"), expect.any(String), {
|
|
336
359
|
encoding: "utf-8",
|
|
337
360
|
mode: 0o600,
|
|
338
361
|
});
|
|
339
|
-
expect(
|
|
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(
|
|
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
|
-
|
|
352
|
-
|
|
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(
|
|
379
|
+
expect(mockFsUnlink).toHaveBeenCalledWith(expect.stringContaining(".tmp"));
|
|
357
380
|
});
|
|
358
381
|
|
|
359
382
|
it("merges auth fields from fresher disk state", async () => {
|
|
360
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
526
|
+
mockFsUnlink.mockResolvedValue(undefined);
|
|
504
527
|
await clearAccounts();
|
|
505
|
-
expect(
|
|
528
|
+
expect(mockFsUnlink).toHaveBeenCalledWith(expect.stringContaining("anthropic-accounts.json"));
|
|
506
529
|
});
|
|
507
530
|
|
|
508
531
|
it("ignores ENOENT errors", async () => {
|
|
509
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|