@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.
- package/README.md +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
+
resetAccountTracking(account);
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
applyOAuthCredentials(account, {
|
|
30
|
+
refresh: "new-refresh",
|
|
31
|
+
access: "new-access",
|
|
32
|
+
expires: 123,
|
|
33
|
+
email: "new@example.com",
|
|
34
|
+
});
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
applyOAuthCredentials(account, {
|
|
54
|
+
refresh: "new-refresh",
|
|
55
|
+
access: "new-access",
|
|
56
|
+
expires: 456,
|
|
57
|
+
});
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
});
|
package/src/account-state.ts
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
if (storage.accounts.length === 0) {
|
|
17
|
+
storage.activeIndex = 0;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
if (storage.activeIndex >= storage.accounts.length) {
|
|
22
|
+
storage.activeIndex = storage.accounts.length - 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (storage.activeIndex > removedIndex) {
|
|
27
|
+
storage.activeIndex -= 1;
|
|
28
|
+
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface OAuthCredentials {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
});
|