@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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { findByIdentity, type AccountIdentity } from "../account-identity.js";
|
|
2
|
+
import { createDefaultStats, type AccountMetadata, type AccountStats } from "../storage.js";
|
|
3
|
+
import type { ManagedAccount } from "../accounts.js";
|
|
4
|
+
|
|
5
|
+
type ManagedAccountSource = ManagedAccount["source"];
|
|
6
|
+
|
|
7
|
+
type ManagedAccountInit = {
|
|
8
|
+
id?: string;
|
|
9
|
+
index: number;
|
|
10
|
+
email?: string;
|
|
11
|
+
identity?: AccountIdentity;
|
|
12
|
+
label?: string;
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
access?: string;
|
|
15
|
+
expires?: number;
|
|
16
|
+
tokenUpdatedAt?: number;
|
|
17
|
+
addedAt?: number;
|
|
18
|
+
lastUsed?: number;
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
rateLimitResetTimes?: Record<string, number>;
|
|
21
|
+
consecutiveFailures?: number;
|
|
22
|
+
lastFailureTime?: number | null;
|
|
23
|
+
lastSwitchReason?: string;
|
|
24
|
+
stats?: AccountStats;
|
|
25
|
+
source?: ManagedAccountSource;
|
|
26
|
+
now?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function resolveManagedAccountIdentity(params: {
|
|
30
|
+
refreshToken: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
identity?: AccountIdentity;
|
|
33
|
+
label?: string;
|
|
34
|
+
source?: ManagedAccountSource;
|
|
35
|
+
}): AccountIdentity {
|
|
36
|
+
if (params.identity) {
|
|
37
|
+
return params.identity;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if ((params.source === "cc-keychain" || params.source === "cc-file") && params.label) {
|
|
41
|
+
return {
|
|
42
|
+
kind: "cc",
|
|
43
|
+
source: params.source,
|
|
44
|
+
label: params.label,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (params.email) {
|
|
49
|
+
return {
|
|
50
|
+
kind: "oauth",
|
|
51
|
+
email: params.email,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
kind: "legacy",
|
|
57
|
+
refreshToken: params.refreshToken,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createManagedAccount(init: ManagedAccountInit): ManagedAccount {
|
|
62
|
+
const now = init.now ?? Date.now();
|
|
63
|
+
const addedAt = init.addedAt ?? now;
|
|
64
|
+
const tokenUpdatedAt = init.tokenUpdatedAt ?? addedAt;
|
|
65
|
+
const identity = resolveManagedAccountIdentity({
|
|
66
|
+
refreshToken: init.refreshToken,
|
|
67
|
+
email: init.email,
|
|
68
|
+
identity: init.identity,
|
|
69
|
+
label: init.label,
|
|
70
|
+
source: init.source,
|
|
71
|
+
});
|
|
72
|
+
const email = init.email ?? (identity.kind === "oauth" ? identity.email : undefined);
|
|
73
|
+
const label = init.label ?? (identity.kind === "cc" ? identity.label : undefined);
|
|
74
|
+
const source = init.source ?? (identity.kind === "cc" ? identity.source : "oauth");
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
id: init.id ?? `${addedAt}:${init.refreshToken.slice(0, 12)}`,
|
|
78
|
+
index: init.index,
|
|
79
|
+
email,
|
|
80
|
+
identity,
|
|
81
|
+
label,
|
|
82
|
+
refreshToken: init.refreshToken,
|
|
83
|
+
access: init.access,
|
|
84
|
+
expires: init.expires,
|
|
85
|
+
tokenUpdatedAt,
|
|
86
|
+
addedAt,
|
|
87
|
+
lastUsed: init.lastUsed ?? 0,
|
|
88
|
+
enabled: init.enabled ?? true,
|
|
89
|
+
rateLimitResetTimes: { ...(init.rateLimitResetTimes ?? {}) },
|
|
90
|
+
consecutiveFailures: init.consecutiveFailures ?? 0,
|
|
91
|
+
lastFailureTime: init.lastFailureTime ?? null,
|
|
92
|
+
lastSwitchReason: init.lastSwitchReason ?? "initial",
|
|
93
|
+
stats: init.stats ?? createDefaultStats(addedAt),
|
|
94
|
+
source,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function findMatchingManagedAccount(
|
|
99
|
+
accounts: ManagedAccount[],
|
|
100
|
+
params: {
|
|
101
|
+
id?: string;
|
|
102
|
+
identity?: AccountIdentity;
|
|
103
|
+
refreshToken?: string;
|
|
104
|
+
},
|
|
105
|
+
): ManagedAccount | null {
|
|
106
|
+
if (params.id) {
|
|
107
|
+
const byId = accounts.find((account) => account.id === params.id);
|
|
108
|
+
if (byId) return byId;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (params.identity) {
|
|
112
|
+
const byIdentity = findByIdentity(accounts, params.identity);
|
|
113
|
+
if (byIdentity) return byIdentity;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (params.refreshToken) {
|
|
117
|
+
return accounts.find((account) => account.refreshToken === params.refreshToken) ?? null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function reindexManagedAccounts(accounts: ManagedAccount[]): void {
|
|
124
|
+
accounts.forEach((account, index) => {
|
|
125
|
+
account.index = index;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function updateManagedAccountFromStorage(
|
|
130
|
+
existing: ManagedAccount,
|
|
131
|
+
account: AccountMetadata,
|
|
132
|
+
index: number,
|
|
133
|
+
): void {
|
|
134
|
+
// Prefer existing source when it is CC and disk source is not — protects a
|
|
135
|
+
// healthy in-memory CC row from being downgraded by a malformed disk row
|
|
136
|
+
// during syncActiveIndexFromDisk.
|
|
137
|
+
const isExistingCC = existing.source === "cc-keychain" || existing.source === "cc-file";
|
|
138
|
+
const isDiskCC = account.source === "cc-keychain" || account.source === "cc-file";
|
|
139
|
+
const source = isExistingCC && !isDiskCC ? existing.source : account.source || existing.source || "oauth";
|
|
140
|
+
const label = account.label ?? existing.label;
|
|
141
|
+
const email = account.email ?? existing.email;
|
|
142
|
+
|
|
143
|
+
existing.id = account.id || existing.id || `${account.addedAt}:${account.refreshToken.slice(0, 12)}`;
|
|
144
|
+
existing.index = index;
|
|
145
|
+
existing.email = email;
|
|
146
|
+
existing.label = label;
|
|
147
|
+
existing.identity = resolveManagedAccountIdentity({
|
|
148
|
+
refreshToken: account.refreshToken,
|
|
149
|
+
email,
|
|
150
|
+
identity: account.identity ?? existing.identity,
|
|
151
|
+
label,
|
|
152
|
+
source,
|
|
153
|
+
});
|
|
154
|
+
existing.refreshToken = account.refreshToken;
|
|
155
|
+
existing.access = account.access ?? existing.access;
|
|
156
|
+
existing.expires = account.expires ?? existing.expires;
|
|
157
|
+
existing.tokenUpdatedAt = account.token_updated_at ?? existing.tokenUpdatedAt ?? account.addedAt;
|
|
158
|
+
existing.addedAt = account.addedAt;
|
|
159
|
+
existing.lastUsed = account.lastUsed;
|
|
160
|
+
existing.enabled = account.enabled;
|
|
161
|
+
existing.rateLimitResetTimes = { ...account.rateLimitResetTimes };
|
|
162
|
+
existing.consecutiveFailures = account.consecutiveFailures;
|
|
163
|
+
existing.lastFailureTime = account.lastFailureTime;
|
|
164
|
+
existing.lastSwitchReason = account.lastSwitchReason || existing.lastSwitchReason || "initial";
|
|
165
|
+
existing.stats = account.stats ?? existing.stats ?? createDefaultStats(account.addedAt);
|
|
166
|
+
existing.source = source;
|
|
167
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct unit tests for accounts/persistence.ts
|
|
3
|
+
*
|
|
4
|
+
* Tests account loading from storage, auth fallback merging,
|
|
5
|
+
* bootstrap account creation, storage preparation, and reconciliation.
|
|
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, AccountStorage } from "../storage.js";
|
|
13
|
+
import { createManagedAccount } from "./matching.js";
|
|
14
|
+
import {
|
|
15
|
+
createBootstrapAccountFromFallback,
|
|
16
|
+
loadManagedAccountsFromStorage,
|
|
17
|
+
mergeAuthFallbackIntoAccounts,
|
|
18
|
+
prepareStorageForSave,
|
|
19
|
+
reconcileManagedAccountsWithStorage,
|
|
20
|
+
type AuthFallback,
|
|
21
|
+
} from "./persistence.js";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
function makeStoredAccount(overrides: Partial<AccountMetadata> = {}, idx = 0): AccountMetadata {
|
|
28
|
+
return {
|
|
29
|
+
id: `acct-${idx}`,
|
|
30
|
+
refreshToken: `refresh-${idx}`,
|
|
31
|
+
token_updated_at: (idx + 1) * 1000,
|
|
32
|
+
addedAt: (idx + 1) * 1000,
|
|
33
|
+
lastUsed: 0,
|
|
34
|
+
enabled: true,
|
|
35
|
+
rateLimitResetTimes: {},
|
|
36
|
+
consecutiveFailures: 0,
|
|
37
|
+
lastFailureTime: null,
|
|
38
|
+
stats: {
|
|
39
|
+
requests: 0,
|
|
40
|
+
inputTokens: 0,
|
|
41
|
+
outputTokens: 0,
|
|
42
|
+
cacheReadTokens: 0,
|
|
43
|
+
cacheWriteTokens: 0,
|
|
44
|
+
lastReset: 0,
|
|
45
|
+
},
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeStorage(accountOverrides: Partial<AccountMetadata>[] = [{}], activeIndex = 0): AccountStorage {
|
|
51
|
+
return {
|
|
52
|
+
version: 1,
|
|
53
|
+
accounts: accountOverrides.map((o, i) => makeStoredAccount(o, i)),
|
|
54
|
+
activeIndex,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeManagedAccount(overrides: Partial<Parameters<typeof createManagedAccount>[0]> = {}): ManagedAccount {
|
|
59
|
+
return createManagedAccount({
|
|
60
|
+
index: 0,
|
|
61
|
+
refreshToken: "managed-refresh",
|
|
62
|
+
now: 5000,
|
|
63
|
+
...overrides,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// loadManagedAccountsFromStorage
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
describe("loadManagedAccountsFromStorage", () => {
|
|
72
|
+
it("loads accounts from storage with correct indices", () => {
|
|
73
|
+
const storage = makeStorage([{ email: "a@b.com" }, { email: "c@d.com" }]);
|
|
74
|
+
const { accounts, currentIndex } = loadManagedAccountsFromStorage(storage);
|
|
75
|
+
|
|
76
|
+
expect(accounts).toHaveLength(2);
|
|
77
|
+
expect(accounts[0].index).toBe(0);
|
|
78
|
+
expect(accounts[0].email).toBe("a@b.com");
|
|
79
|
+
expect(accounts[1].index).toBe(1);
|
|
80
|
+
expect(accounts[1].email).toBe("c@d.com");
|
|
81
|
+
expect(currentIndex).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("clamps activeIndex to valid range", () => {
|
|
85
|
+
const storage = makeStorage([{}], 99);
|
|
86
|
+
const { currentIndex } = loadManagedAccountsFromStorage(storage);
|
|
87
|
+
expect(currentIndex).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns -1 for empty accounts", () => {
|
|
91
|
+
const storage: AccountStorage = { version: 1, accounts: [], activeIndex: 0 };
|
|
92
|
+
const { accounts, currentIndex } = loadManagedAccountsFromStorage(storage);
|
|
93
|
+
expect(accounts).toHaveLength(0);
|
|
94
|
+
expect(currentIndex).toBe(-1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("preserves enabled/disabled state", () => {
|
|
98
|
+
const storage = makeStorage([{ enabled: true }, { enabled: false }]);
|
|
99
|
+
const { accounts } = loadManagedAccountsFromStorage(storage);
|
|
100
|
+
expect(accounts[0].enabled).toBe(true);
|
|
101
|
+
expect(accounts[1].enabled).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// mergeAuthFallbackIntoAccounts
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
describe("mergeAuthFallbackIntoAccounts", () => {
|
|
110
|
+
it("does nothing with empty accounts array", () => {
|
|
111
|
+
const accounts: ManagedAccount[] = [];
|
|
112
|
+
mergeAuthFallbackIntoAccounts(accounts, { refresh: "tok" });
|
|
113
|
+
expect(accounts).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("does nothing when fallback does not match any account", () => {
|
|
117
|
+
const accounts = [makeManagedAccount({ refreshToken: "existing-token" })];
|
|
118
|
+
const origAccess = accounts[0].access;
|
|
119
|
+
|
|
120
|
+
mergeAuthFallbackIntoAccounts(accounts, {
|
|
121
|
+
refresh: "completely-different-token",
|
|
122
|
+
access: "new-access",
|
|
123
|
+
expires: Date.now() + 60_000,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(accounts[0].access).toBe(origAccess);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("adopts fresh fallback credentials when they match and are fresher", () => {
|
|
130
|
+
const futureExpiry = Date.now() + 120_000;
|
|
131
|
+
const accounts = [makeManagedAccount({ refreshToken: "shared-token", access: undefined, expires: undefined })];
|
|
132
|
+
|
|
133
|
+
mergeAuthFallbackIntoAccounts(accounts, {
|
|
134
|
+
refresh: "shared-token",
|
|
135
|
+
access: "fresh-access",
|
|
136
|
+
expires: futureExpiry,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(accounts[0].access).toBe("fresh-access");
|
|
140
|
+
expect(accounts[0].expires).toBe(futureExpiry);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("does not overwrite with expired fallback", () => {
|
|
144
|
+
const accounts = [
|
|
145
|
+
makeManagedAccount({
|
|
146
|
+
refreshToken: "shared-token",
|
|
147
|
+
access: "current-access",
|
|
148
|
+
expires: Date.now() + 60_000,
|
|
149
|
+
}),
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
mergeAuthFallbackIntoAccounts(accounts, {
|
|
153
|
+
refresh: "shared-token",
|
|
154
|
+
access: "stale-access",
|
|
155
|
+
expires: Date.now() - 1000, // expired
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(accounts[0].access).toBe("current-access");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// createBootstrapAccountFromFallback
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
describe("createBootstrapAccountFromFallback", () => {
|
|
167
|
+
it("creates a managed account from auth fallback", () => {
|
|
168
|
+
const now = 10_000;
|
|
169
|
+
const fallback: AuthFallback = {
|
|
170
|
+
refresh: "bootstrap-refresh",
|
|
171
|
+
access: "bootstrap-access",
|
|
172
|
+
expires: 99_000,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const account = createBootstrapAccountFromFallback(fallback, now);
|
|
176
|
+
|
|
177
|
+
expect(account.refreshToken).toBe("bootstrap-refresh");
|
|
178
|
+
expect(account.access).toBe("bootstrap-access");
|
|
179
|
+
expect(account.expires).toBe(99_000);
|
|
180
|
+
expect(account.index).toBe(0);
|
|
181
|
+
expect(account.enabled).toBe(true);
|
|
182
|
+
expect(account.source).toBe("oauth");
|
|
183
|
+
expect(account.addedAt).toBe(now);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("generates id from now + refresh prefix", () => {
|
|
187
|
+
const account = createBootstrapAccountFromFallback({ refresh: "abcdef123456789" }, 5000);
|
|
188
|
+
expect(account.id).toContain("5000:");
|
|
189
|
+
expect(account.id).toContain("abcdef123456");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// prepareStorageForSave
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
describe("prepareStorageForSave", () => {
|
|
198
|
+
it("converts managed accounts back to storage format", () => {
|
|
199
|
+
const accounts = [
|
|
200
|
+
makeManagedAccount({ id: "a1", email: "a@b.com", refreshToken: "tok-a" }),
|
|
201
|
+
makeManagedAccount({ id: "a2", email: "c@d.com", refreshToken: "tok-b", index: 1 }),
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const result = prepareStorageForSave({
|
|
205
|
+
accounts,
|
|
206
|
+
currentIndex: 0,
|
|
207
|
+
statsDeltas: new Map(),
|
|
208
|
+
diskData: null,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(result.storage.version).toBe(1);
|
|
212
|
+
expect(result.storage.accounts).toHaveLength(2);
|
|
213
|
+
expect(result.storage.accounts[0].id).toBe("a1");
|
|
214
|
+
expect(result.storage.accounts[1].id).toBe("a2");
|
|
215
|
+
expect(result.storage.activeIndex).toBe(0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("preserves active index by matching account id", () => {
|
|
219
|
+
const accounts = [
|
|
220
|
+
makeManagedAccount({ id: "first", refreshToken: "tok1" }),
|
|
221
|
+
makeManagedAccount({ id: "second", refreshToken: "tok2", index: 1 }),
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const result = prepareStorageForSave({
|
|
225
|
+
accounts,
|
|
226
|
+
currentIndex: 1,
|
|
227
|
+
statsDeltas: new Map(),
|
|
228
|
+
diskData: null,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(result.storage.activeIndex).toBe(1);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("returns persisted state map keyed by account id", () => {
|
|
235
|
+
const accounts = [makeManagedAccount({ id: "test-id", refreshToken: "tok" })];
|
|
236
|
+
|
|
237
|
+
const result = prepareStorageForSave({
|
|
238
|
+
accounts,
|
|
239
|
+
currentIndex: 0,
|
|
240
|
+
statsDeltas: new Map(),
|
|
241
|
+
diskData: null,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(result.persistedStateById.has("test-id")).toBe(true);
|
|
245
|
+
const state = result.persistedStateById.get("test-id")!;
|
|
246
|
+
expect(state.refreshToken).toBe("tok");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("filters out disabled accounts with no disk match", () => {
|
|
250
|
+
const accounts = [
|
|
251
|
+
makeManagedAccount({ id: "enabled-id", refreshToken: "tok1", enabled: true }),
|
|
252
|
+
makeManagedAccount({ id: "disabled-id", refreshToken: "tok2", enabled: false, index: 1 }),
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
const result = prepareStorageForSave({
|
|
256
|
+
accounts,
|
|
257
|
+
currentIndex: 0,
|
|
258
|
+
statsDeltas: new Map(),
|
|
259
|
+
diskData: null,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(result.storage.accounts).toHaveLength(1);
|
|
263
|
+
expect(result.storage.accounts[0].id).toBe("enabled-id");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// reconcileManagedAccountsWithStorage
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
describe("reconcileManagedAccountsWithStorage", () => {
|
|
272
|
+
it("matches existing accounts by id", () => {
|
|
273
|
+
const existing = makeManagedAccount({ id: "acct-0", refreshToken: "refresh-0" });
|
|
274
|
+
const stored = makeStorage([{ id: "acct-0", refreshToken: "refresh-0", email: "updated@test.com" }]);
|
|
275
|
+
|
|
276
|
+
const result = reconcileManagedAccountsWithStorage({
|
|
277
|
+
accounts: [existing],
|
|
278
|
+
stored,
|
|
279
|
+
currentIndex: 0,
|
|
280
|
+
statsDeltaIds: [],
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(result.accounts).toHaveLength(1);
|
|
284
|
+
expect(result.accounts[0].email).toBe("updated@test.com");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("adds new accounts from storage not in memory", () => {
|
|
288
|
+
const existing = makeManagedAccount({ id: "acct-0", refreshToken: "refresh-0" });
|
|
289
|
+
const stored = makeStorage([{ id: "acct-0" }, { id: "acct-new", email: "new@test.com" }]);
|
|
290
|
+
|
|
291
|
+
const result = reconcileManagedAccountsWithStorage({
|
|
292
|
+
accounts: [existing],
|
|
293
|
+
stored,
|
|
294
|
+
currentIndex: 0,
|
|
295
|
+
statsDeltaIds: [],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(result.accounts.length).toBeGreaterThanOrEqual(2);
|
|
299
|
+
expect(result.shouldRebuildTrackers).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("disables memory-only accounts not found in storage", () => {
|
|
303
|
+
const inMemory = makeManagedAccount({ id: "orphan", refreshToken: "orphan-tok" });
|
|
304
|
+
const stored = makeStorage([{ id: "different", refreshToken: "diff-tok" }]);
|
|
305
|
+
|
|
306
|
+
const result = reconcileManagedAccountsWithStorage({
|
|
307
|
+
accounts: [inMemory],
|
|
308
|
+
stored,
|
|
309
|
+
currentIndex: 0,
|
|
310
|
+
statsDeltaIds: [],
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const orphan = result.accounts.find((a) => a.id === "orphan");
|
|
314
|
+
expect(orphan?.enabled).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("returns -1 currentIndex when no enabled accounts remain", () => {
|
|
318
|
+
const stored: AccountStorage = { version: 1, accounts: [], activeIndex: 0 };
|
|
319
|
+
|
|
320
|
+
const result = reconcileManagedAccountsWithStorage({
|
|
321
|
+
accounts: [],
|
|
322
|
+
stored,
|
|
323
|
+
currentIndex: -1,
|
|
324
|
+
statsDeltaIds: [],
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(result.currentIndex).toBe(-1);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("identifies stale delta ids no longer in reconciled set", () => {
|
|
331
|
+
const stored = makeStorage([{ id: "acct-0" }]);
|
|
332
|
+
const existing = makeManagedAccount({ id: "acct-0", refreshToken: "refresh-0" });
|
|
333
|
+
|
|
334
|
+
const result = reconcileManagedAccountsWithStorage({
|
|
335
|
+
accounts: [existing],
|
|
336
|
+
stored,
|
|
337
|
+
currentIndex: 0,
|
|
338
|
+
statsDeltaIds: ["acct-0", "stale-id-1", "stale-id-2"],
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(result.staleDeltaIds).toContain("stale-id-1");
|
|
342
|
+
expect(result.staleDeltaIds).toContain("stale-id-2");
|
|
343
|
+
expect(result.staleDeltaIds).not.toContain("acct-0");
|
|
344
|
+
});
|
|
345
|
+
});
|