@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
@@ -2,108 +2,108 @@ import { describe, expect, it } from "vitest";
2
2
  import { adjustActiveIndexAfterRemoval, applyOAuthCredentials, resetAccountTracking } from "./account-state.js";
3
3
 
4
4
  describe("resetAccountTracking", () => {
5
- it("resets rate-limit and failure fields", () => {
6
- const account = {
7
- rateLimitResetTimes: { anthropic: Date.now() + 60_000 },
8
- consecutiveFailures: 7,
9
- lastFailureTime: Date.now(),
10
- };
5
+ it("resets rate-limit and failure fields", () => {
6
+ const account = {
7
+ rateLimitResetTimes: { anthropic: Date.now() + 60_000 },
8
+ consecutiveFailures: 7,
9
+ lastFailureTime: Date.now(),
10
+ };
11
11
 
12
- resetAccountTracking(account);
12
+ resetAccountTracking(account);
13
13
 
14
- expect(account.rateLimitResetTimes).toEqual({});
15
- expect(account.consecutiveFailures).toBe(0);
16
- expect(account.lastFailureTime).toBeNull();
17
- });
14
+ expect(account.rateLimitResetTimes).toEqual({});
15
+ expect(account.consecutiveFailures).toBe(0);
16
+ expect(account.lastFailureTime).toBeNull();
17
+ });
18
18
  });
19
19
 
20
20
  describe("applyOAuthCredentials", () => {
21
- it("applies refresh/access/expiry and optional email", () => {
22
- const account = {
23
- refreshToken: "old-refresh",
24
- access: "old-access",
25
- expires: 1,
26
- email: "old@example.com",
27
- };
21
+ it("applies refresh/access/expiry and optional email", () => {
22
+ const account = {
23
+ refreshToken: "old-refresh",
24
+ access: "old-access",
25
+ expires: 1,
26
+ email: "old@example.com",
27
+ };
28
28
 
29
- applyOAuthCredentials(account, {
30
- refresh: "new-refresh",
31
- access: "new-access",
32
- expires: 123,
33
- email: "new@example.com",
34
- });
29
+ applyOAuthCredentials(account, {
30
+ refresh: "new-refresh",
31
+ access: "new-access",
32
+ expires: 123,
33
+ email: "new@example.com",
34
+ });
35
35
 
36
- expect(account).toEqual({
37
- refreshToken: "new-refresh",
38
- access: "new-access",
39
- expires: 123,
40
- token_updated_at: expect.any(Number),
41
- email: "new@example.com",
36
+ expect(account).toEqual({
37
+ refreshToken: "new-refresh",
38
+ access: "new-access",
39
+ expires: 123,
40
+ token_updated_at: expect.any(Number),
41
+ email: "new@example.com",
42
+ });
42
43
  });
43
- });
44
44
 
45
- it("preserves existing email when credentials omit email", () => {
46
- const account = {
47
- refreshToken: "old-refresh",
48
- access: "old-access",
49
- expires: 1,
50
- email: "old@example.com",
51
- };
45
+ it("preserves existing email when credentials omit email", () => {
46
+ const account = {
47
+ refreshToken: "old-refresh",
48
+ access: "old-access",
49
+ expires: 1,
50
+ email: "old@example.com",
51
+ };
52
52
 
53
- applyOAuthCredentials(account, {
54
- refresh: "new-refresh",
55
- access: "new-access",
56
- expires: 456,
57
- });
53
+ applyOAuthCredentials(account, {
54
+ refresh: "new-refresh",
55
+ access: "new-access",
56
+ expires: 456,
57
+ });
58
58
 
59
- expect(account.email).toBe("old@example.com");
60
- expect(account.refreshToken).toBe("new-refresh");
61
- expect(account.access).toBe("new-access");
62
- expect(account.expires).toBe(456);
63
- expect(account.token_updated_at).toEqual(expect.any(Number));
64
- });
59
+ expect(account.email).toBe("old@example.com");
60
+ expect(account.refreshToken).toBe("new-refresh");
61
+ expect(account.access).toBe("new-access");
62
+ expect(account.expires).toBe(456);
63
+ expect(account.token_updated_at).toEqual(expect.any(Number));
64
+ });
65
65
  });
66
66
 
67
67
  describe("adjustActiveIndexAfterRemoval", () => {
68
- it("resets activeIndex to 0 when no accounts remain", () => {
69
- const storage = { accounts: [], activeIndex: 3 };
70
- adjustActiveIndexAfterRemoval(storage, 0);
71
- expect(storage.activeIndex).toBe(0);
72
- });
68
+ it("resets activeIndex to 0 when no accounts remain", () => {
69
+ const storage = { accounts: [], activeIndex: 3 };
70
+ adjustActiveIndexAfterRemoval(storage, 0);
71
+ expect(storage.activeIndex).toBe(0);
72
+ });
73
73
 
74
- it("clamps activeIndex when it falls out of range", () => {
75
- const storage = {
76
- accounts: [{ id: "a" }, { id: "b" }],
77
- activeIndex: 2,
78
- };
79
- adjustActiveIndexAfterRemoval(storage, 0);
80
- expect(storage.activeIndex).toBe(1);
81
- });
74
+ it("clamps activeIndex when it falls out of range", () => {
75
+ const storage = {
76
+ accounts: [{ id: "a" }, { id: "b" }],
77
+ activeIndex: 2,
78
+ };
79
+ adjustActiveIndexAfterRemoval(storage, 0);
80
+ expect(storage.activeIndex).toBe(1);
81
+ });
82
82
 
83
- it("decrements activeIndex when removed index is before active", () => {
84
- const storage = {
85
- accounts: [{ id: "a" }, { id: "b" }, { id: "c" }],
86
- activeIndex: 2,
87
- };
88
- adjustActiveIndexAfterRemoval(storage, 0);
89
- expect(storage.activeIndex).toBe(1);
90
- });
83
+ it("decrements activeIndex when removed index is before active", () => {
84
+ const storage = {
85
+ accounts: [{ id: "a" }, { id: "b" }, { id: "c" }],
86
+ activeIndex: 2,
87
+ };
88
+ adjustActiveIndexAfterRemoval(storage, 0);
89
+ expect(storage.activeIndex).toBe(1);
90
+ });
91
91
 
92
- it("keeps activeIndex when removed index is after active", () => {
93
- const storage = {
94
- accounts: [{ id: "a" }, { id: "b" }, { id: "c" }],
95
- activeIndex: 0,
96
- };
97
- adjustActiveIndexAfterRemoval(storage, 2);
98
- expect(storage.activeIndex).toBe(0);
99
- });
92
+ it("keeps activeIndex when removed index is after active", () => {
93
+ const storage = {
94
+ accounts: [{ id: "a" }, { id: "b" }, { id: "c" }],
95
+ activeIndex: 0,
96
+ };
97
+ adjustActiveIndexAfterRemoval(storage, 2);
98
+ expect(storage.activeIndex).toBe(0);
99
+ });
100
100
 
101
- it("keeps activeIndex when removed index was the active slot", () => {
102
- const storage = {
103
- accounts: [{ id: "a" }, { id: "b" }, { id: "c" }],
104
- activeIndex: 1,
105
- };
106
- adjustActiveIndexAfterRemoval(storage, 1);
107
- expect(storage.activeIndex).toBe(1);
108
- });
101
+ it("keeps activeIndex when removed index was the active slot", () => {
102
+ const storage = {
103
+ accounts: [{ id: "a" }, { id: "b" }, { id: "c" }],
104
+ activeIndex: 1,
105
+ };
106
+ adjustActiveIndexAfterRemoval(storage, 1);
107
+ expect(storage.activeIndex).toBe(1);
108
+ });
109
109
  });
@@ -4,46 +4,46 @@ import type { AccountMetadata, AccountStorage } from "./storage.js";
4
4
  * Reset transient account tracking fields.
5
5
  */
6
6
  export function resetAccountTracking(account: AccountMetadata): void {
7
- account.rateLimitResetTimes = {};
8
- account.consecutiveFailures = 0;
9
- account.lastFailureTime = null;
7
+ account.rateLimitResetTimes = {};
8
+ account.consecutiveFailures = 0;
9
+ account.lastFailureTime = null;
10
10
  }
11
11
 
12
12
  /**
13
13
  * Normalize active index after removing one account.
14
14
  */
15
15
  export function adjustActiveIndexAfterRemoval(storage: AccountStorage, removedIndex: number): void {
16
- if (storage.accounts.length === 0) {
17
- storage.activeIndex = 0;
18
- return;
19
- }
16
+ if (storage.accounts.length === 0) {
17
+ storage.activeIndex = 0;
18
+ return;
19
+ }
20
20
 
21
- if (storage.activeIndex >= storage.accounts.length) {
22
- storage.activeIndex = storage.accounts.length - 1;
23
- return;
24
- }
21
+ if (storage.activeIndex >= storage.accounts.length) {
22
+ storage.activeIndex = storage.accounts.length - 1;
23
+ return;
24
+ }
25
25
 
26
- if (storage.activeIndex > removedIndex) {
27
- storage.activeIndex -= 1;
28
- }
26
+ if (storage.activeIndex > removedIndex) {
27
+ storage.activeIndex -= 1;
28
+ }
29
29
  }
30
30
 
31
31
  export interface OAuthCredentials {
32
- refresh: string;
33
- access: string;
34
- expires: number;
35
- email?: string;
32
+ refresh: string;
33
+ access: string;
34
+ expires: number;
35
+ email?: string;
36
36
  }
37
37
 
38
38
  /**
39
39
  * Apply OAuth credentials to an existing account record.
40
40
  */
41
41
  export function applyOAuthCredentials(account: AccountMetadata, credentials: OAuthCredentials): void {
42
- account.refreshToken = credentials.refresh;
43
- account.access = credentials.access;
44
- account.expires = credentials.expires;
45
- account.token_updated_at = Date.now();
46
- if (credentials.email) {
47
- account.email = credentials.email;
48
- }
42
+ account.refreshToken = credentials.refresh;
43
+ account.access = credentials.access;
44
+ account.expires = credentials.expires;
45
+ account.token_updated_at = Date.now();
46
+ if (credentials.email) {
47
+ account.email = credentials.email;
48
+ }
49
49
  }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Direct unit tests for accounts/matching.ts
3
+ *
4
+ * Tests identity resolution, managed account creation, matching,
5
+ * reindexing, and storage-to-memory account updating.
6
+ */
7
+
8
+ import { describe, expect, it } from "vitest";
9
+
10
+ import type { AccountIdentity } from "../account-identity.js";
11
+ import type { ManagedAccount } from "../accounts.js";
12
+ import type { AccountMetadata } from "../storage.js";
13
+ import {
14
+ createManagedAccount,
15
+ findMatchingManagedAccount,
16
+ reindexManagedAccounts,
17
+ resolveManagedAccountIdentity,
18
+ updateManagedAccountFromStorage,
19
+ } from "./matching.js";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // resolveManagedAccountIdentity
23
+ // ---------------------------------------------------------------------------
24
+
25
+ describe("resolveManagedAccountIdentity", () => {
26
+ it("returns provided identity when present", () => {
27
+ const identity: AccountIdentity = { kind: "oauth", email: "a@b.com" };
28
+ const result = resolveManagedAccountIdentity({
29
+ refreshToken: "tok",
30
+ identity,
31
+ });
32
+ expect(result).toEqual(identity);
33
+ });
34
+
35
+ it("returns cc identity for cc-keychain source with label", () => {
36
+ const result = resolveManagedAccountIdentity({
37
+ refreshToken: "tok",
38
+ source: "cc-keychain",
39
+ label: "my-label",
40
+ });
41
+ expect(result).toEqual({
42
+ kind: "cc",
43
+ source: "cc-keychain",
44
+ label: "my-label",
45
+ });
46
+ });
47
+
48
+ it("returns cc identity for cc-file source with label", () => {
49
+ const result = resolveManagedAccountIdentity({
50
+ refreshToken: "tok",
51
+ source: "cc-file",
52
+ label: "file-label",
53
+ });
54
+ expect(result).toEqual({
55
+ kind: "cc",
56
+ source: "cc-file",
57
+ label: "file-label",
58
+ });
59
+ });
60
+
61
+ it("returns oauth identity when email is provided", () => {
62
+ const result = resolveManagedAccountIdentity({
63
+ refreshToken: "tok",
64
+ email: "user@test.com",
65
+ });
66
+ expect(result).toEqual({
67
+ kind: "oauth",
68
+ email: "user@test.com",
69
+ });
70
+ });
71
+
72
+ it("returns legacy identity as fallback", () => {
73
+ const result = resolveManagedAccountIdentity({
74
+ refreshToken: "my-refresh-token",
75
+ });
76
+ expect(result).toEqual({
77
+ kind: "legacy",
78
+ refreshToken: "my-refresh-token",
79
+ });
80
+ });
81
+
82
+ it("prefers explicit identity over email", () => {
83
+ const identity: AccountIdentity = { kind: "legacy", refreshToken: "x" };
84
+ const result = resolveManagedAccountIdentity({
85
+ refreshToken: "tok",
86
+ email: "user@test.com",
87
+ identity,
88
+ });
89
+ expect(result).not.toBeUndefined();
90
+ expect(result!.kind).toBe("legacy");
91
+ });
92
+
93
+ it("prefers cc source over email when source is cc-keychain", () => {
94
+ const result = resolveManagedAccountIdentity({
95
+ refreshToken: "tok",
96
+ email: "user@test.com",
97
+ source: "cc-keychain",
98
+ label: "cc-label",
99
+ });
100
+ expect(result).not.toBeUndefined();
101
+ expect(result!.kind).toBe("cc");
102
+ });
103
+ });
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // createManagedAccount
107
+ // ---------------------------------------------------------------------------
108
+
109
+ describe("createManagedAccount", () => {
110
+ it("creates account with minimal fields", () => {
111
+ const account = createManagedAccount({
112
+ index: 0,
113
+ refreshToken: "refresh-abc123xyz",
114
+ });
115
+
116
+ expect(account.index).toBe(0);
117
+ expect(account.refreshToken).toBe("refresh-abc123xyz");
118
+ expect(account.enabled).toBe(true);
119
+ expect(account.consecutiveFailures).toBe(0);
120
+ expect(account.lastFailureTime).toBeNull();
121
+ expect(account.source).toBe("oauth");
122
+ expect(account.id).toContain("refresh-abc1");
123
+ });
124
+
125
+ it("creates account with full override fields", () => {
126
+ const now = 1000;
127
+ const account = createManagedAccount({
128
+ id: "custom-id",
129
+ index: 2,
130
+ email: "test@example.com",
131
+ refreshToken: "tok",
132
+ access: "access-tok",
133
+ expires: 9999,
134
+ enabled: false,
135
+ consecutiveFailures: 3,
136
+ lastFailureTime: 500,
137
+ source: "oauth",
138
+ now,
139
+ });
140
+
141
+ expect(account.id).toBe("custom-id");
142
+ expect(account.index).toBe(2);
143
+ expect(account.email).toBe("test@example.com");
144
+ expect(account.access).toBe("access-tok");
145
+ expect(account.expires).toBe(9999);
146
+ expect(account.enabled).toBe(false);
147
+ expect(account.consecutiveFailures).toBe(3);
148
+ expect(account.lastFailureTime).toBe(500);
149
+ });
150
+
151
+ it("copies rateLimitResetTimes instead of sharing reference", () => {
152
+ const times = { "429": 5000 };
153
+ const account = createManagedAccount({
154
+ index: 0,
155
+ refreshToken: "tok",
156
+ rateLimitResetTimes: times,
157
+ });
158
+
159
+ times["429"] = 9999;
160
+ expect(account.rateLimitResetTimes["429"]).toBe(5000);
161
+ });
162
+
163
+ it("assigns cc source when identity is cc", () => {
164
+ const account = createManagedAccount({
165
+ index: 0,
166
+ refreshToken: "tok",
167
+ source: "cc-keychain",
168
+ label: "my-cc-label",
169
+ });
170
+
171
+ expect(account.source).toBe("cc-keychain");
172
+ expect(account.label).toBe("my-cc-label");
173
+ expect(account.identity?.kind).toBe("cc");
174
+ });
175
+ });
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // findMatchingManagedAccount
179
+ // ---------------------------------------------------------------------------
180
+
181
+ describe("findMatchingManagedAccount", () => {
182
+ function makeAccount(overrides: Partial<ManagedAccount> = {}): ManagedAccount {
183
+ return createManagedAccount({
184
+ index: 0,
185
+ refreshToken: "default-token",
186
+ now: 1000,
187
+ ...overrides,
188
+ });
189
+ }
190
+
191
+ it("finds by id", () => {
192
+ const a = makeAccount({ id: "match-id" });
193
+ const b = makeAccount({ id: "other-id", refreshToken: "tok2" });
194
+ const result = findMatchingManagedAccount([a, b], { id: "match-id" });
195
+ expect(result).toBe(a);
196
+ });
197
+
198
+ it("finds by identity", () => {
199
+ const identity: AccountIdentity = { kind: "oauth", email: "user@x.com" };
200
+ const a = makeAccount({ email: "user@x.com", identity });
201
+ const result = findMatchingManagedAccount([a], { identity });
202
+ expect(result).toBe(a);
203
+ });
204
+
205
+ it("finds by refreshToken as fallback", () => {
206
+ const a = makeAccount({ refreshToken: "my-token-abcdef" });
207
+ const result = findMatchingManagedAccount([a], { refreshToken: "my-token-abcdef" });
208
+ expect(result).toBe(a);
209
+ });
210
+
211
+ it("returns null when no match", () => {
212
+ const a = makeAccount({ id: "no-match" });
213
+ const result = findMatchingManagedAccount([a], { id: "different" });
214
+ expect(result).toBeNull();
215
+ });
216
+
217
+ it("returns null for empty accounts array", () => {
218
+ const result = findMatchingManagedAccount([], { id: "x" });
219
+ expect(result).toBeNull();
220
+ });
221
+
222
+ it("prefers id match over identity match", () => {
223
+ const identity: AccountIdentity = { kind: "oauth", email: "user@x.com" };
224
+ const a = makeAccount({ id: "id-match", email: "other@x.com" });
225
+ const b = makeAccount({ id: "other", email: "user@x.com", identity, refreshToken: "tok2" });
226
+ const result = findMatchingManagedAccount([a, b], { id: "id-match", identity });
227
+ expect(result).toBe(a);
228
+ });
229
+ });
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // reindexManagedAccounts
233
+ // ---------------------------------------------------------------------------
234
+
235
+ describe("reindexManagedAccounts", () => {
236
+ it("reassigns sequential indices starting from 0", () => {
237
+ const accounts = [
238
+ createManagedAccount({ index: 5, refreshToken: "a" }),
239
+ createManagedAccount({ index: 10, refreshToken: "b" }),
240
+ createManagedAccount({ index: 0, refreshToken: "c" }),
241
+ ];
242
+
243
+ reindexManagedAccounts(accounts);
244
+
245
+ expect(accounts[0].index).toBe(0);
246
+ expect(accounts[1].index).toBe(1);
247
+ expect(accounts[2].index).toBe(2);
248
+ });
249
+
250
+ it("handles empty array", () => {
251
+ const accounts: ManagedAccount[] = [];
252
+ reindexManagedAccounts(accounts);
253
+ expect(accounts).toHaveLength(0);
254
+ });
255
+ });
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // updateManagedAccountFromStorage
259
+ // ---------------------------------------------------------------------------
260
+
261
+ describe("updateManagedAccountFromStorage", () => {
262
+ function makeExisting(): ManagedAccount {
263
+ return createManagedAccount({
264
+ id: "existing-id",
265
+ index: 0,
266
+ email: "old@test.com",
267
+ refreshToken: "old-token",
268
+ access: "old-access",
269
+ expires: 1000,
270
+ source: "oauth",
271
+ });
272
+ }
273
+
274
+ function makeStorageAccount(overrides: Partial<AccountMetadata> = {}): AccountMetadata {
275
+ return {
276
+ id: "storage-id",
277
+ refreshToken: "new-token",
278
+ token_updated_at: 2000,
279
+ addedAt: 2000,
280
+ lastUsed: 3000,
281
+ enabled: true,
282
+ rateLimitResetTimes: {},
283
+ consecutiveFailures: 0,
284
+ lastFailureTime: null,
285
+ stats: {
286
+ requests: 10,
287
+ inputTokens: 100,
288
+ outputTokens: 50,
289
+ cacheReadTokens: 0,
290
+ cacheWriteTokens: 0,
291
+ lastReset: 1000,
292
+ },
293
+ ...overrides,
294
+ };
295
+ }
296
+
297
+ it("updates index from parameter", () => {
298
+ const existing = makeExisting();
299
+ updateManagedAccountFromStorage(existing, makeStorageAccount(), 5);
300
+ expect(existing.index).toBe(5);
301
+ });
302
+
303
+ it("updates refreshToken from storage", () => {
304
+ const existing = makeExisting();
305
+ updateManagedAccountFromStorage(existing, makeStorageAccount({ refreshToken: "updated-tok" }), 0);
306
+ expect(existing.refreshToken).toBe("updated-tok");
307
+ });
308
+
309
+ it("preserves email from existing when storage has none", () => {
310
+ const existing = makeExisting();
311
+ updateManagedAccountFromStorage(existing, makeStorageAccount(), 0);
312
+ expect(existing.email).toBe("old@test.com");
313
+ });
314
+
315
+ it("updates email from storage when provided", () => {
316
+ const existing = makeExisting();
317
+ updateManagedAccountFromStorage(existing, makeStorageAccount({ email: "new@test.com" }), 0);
318
+ expect(existing.email).toBe("new@test.com");
319
+ });
320
+
321
+ it("copies rateLimitResetTimes (not reference)", () => {
322
+ const existing = makeExisting();
323
+ const times = { "429": 9999 };
324
+ updateManagedAccountFromStorage(existing, makeStorageAccount({ rateLimitResetTimes: times }), 0);
325
+
326
+ times["429"] = 0;
327
+ expect(existing.rateLimitResetTimes["429"]).toBe(9999);
328
+ });
329
+
330
+ it("updates enabled status", () => {
331
+ const existing = makeExisting();
332
+ updateManagedAccountFromStorage(existing, makeStorageAccount({ enabled: false }), 0);
333
+ expect(existing.enabled).toBe(false);
334
+ });
335
+ });