Skip to content

Commit 7b716ab

Browse files
committed
Merge branch 'release/pr-328' into release/integration-1.2.2
# Conflicts: # test/accounts-load-from-disk.test.ts
2 parents 0c0cfcb + 83d934a commit 7b716ab

3 files changed

Lines changed: 97 additions & 8 deletions

File tree

lib/accounts.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,20 +285,24 @@ export class AccountManager {
285285
const cached = cache.get(email);
286286
if (!cached) continue;
287287

288-
if (typeof cached.expiresAt === "number" && cached.expiresAt <= now) {
289-
continue;
290-
}
288+
const cachedAccessUsable =
289+
typeof cached.expiresAt !== "number" || cached.expiresAt > now;
291290

292291
const missingOrExpired =
293292
!account.access || account.expires === undefined || account.expires <= now;
294-
if (missingOrExpired) {
293+
if (missingOrExpired && cachedAccessUsable) {
295294
account.access = cached.accessToken;
296295
if (typeof cached.expiresAt === "number") {
297296
account.expires = cached.expiresAt;
298297
}
299298
changed = true;
300299
}
301300

301+
if (cached.refreshToken && !account.refreshToken) {
302+
account.refreshToken = cached.refreshToken;
303+
changed = true;
304+
}
305+
302306
if (
303307
!account.accountId &&
304308
cached.accountId &&

test/accounts-edge.test.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,14 @@ describe("accounts edge branches", () => {
161161
email: "match@example.com",
162162
accessToken: "refreshed-access",
163163
expiresAt: now + 300_000,
164+
refreshToken: "refreshed-refresh",
164165
accountId: "account-from-cache",
165166
},
166167
{
167168
email: "expired@example.com",
168169
accessToken: "expired-access",
169170
expiresAt: now - 1,
171+
refreshToken: "expired-refresh-updated",
170172
accountId: "expired-id",
171173
},
172174
{
@@ -187,12 +189,94 @@ describe("accounts edge branches", () => {
187189
const snapshot = manager.getAccountsSnapshot();
188190
const updated = snapshot[0];
189191
expect(updated?.access).toBe("refreshed-access");
192+
expect(updated?.refreshToken).toBe("refresh-1");
190193
expect(updated?.accountId).toBe("account-from-cache");
191194
expect(updated?.accountIdSource).toBe("token");
192195

193196
const expired = snapshot[1];
194197
expect(expired?.access).toBe("existing-access");
195-
expect(expired?.accountId).toBeUndefined();
198+
expect(expired?.refreshToken).toBe("refresh-2");
199+
expect(expired?.accountId).toBe("expired-id");
200+
expect(expired?.accountIdSource).toBe("token");
201+
});
202+
203+
it("does not overwrite a local refresh token with a stale usable CLI cache token", async () => {
204+
const now = Date.now();
205+
const stored = buildStored([
206+
buildStoredAccount({
207+
refreshToken: "local-refresh-new",
208+
email: "match@example.com",
209+
accessToken: "local-access",
210+
expiresAt: now + 120_000,
211+
}),
212+
]);
213+
214+
const { AccountManager } = await importAccountsModule();
215+
const manager = new AccountManager(undefined, stored as never);
216+
217+
mockLoadCodexCliState.mockResolvedValue({
218+
sourceUpdatedAtMs: now - 60_000,
219+
accounts: [
220+
{
221+
email: "match@example.com",
222+
accessToken: "cached-access-old",
223+
expiresAt: now + 300_000,
224+
refreshToken: "cached-refresh-old",
225+
},
226+
],
227+
});
228+
229+
const hydrate = getPrivate<() => Promise<void>>(
230+
manager as object,
231+
"hydrateFromCodexCli",
232+
);
233+
await hydrate.call(manager);
234+
235+
const snapshot = manager.getAccountsSnapshot();
236+
expect(snapshot[0]?.refreshToken).toBe("local-refresh-new");
237+
expect(snapshot[0]?.access).toBe("local-access");
238+
expect(mockSaveAccounts).not.toHaveBeenCalled();
239+
});
240+
241+
it("hydrates a missing local refresh token from an expired CLI cache entry", async () => {
242+
const now = Date.now();
243+
const stored = buildStored([
244+
buildStoredAccount({
245+
refreshToken: "local-refresh-placeholder",
246+
email: "expired@example.com",
247+
accessToken: "local-access",
248+
expiresAt: now + 120_000,
249+
}),
250+
]);
251+
252+
const { AccountManager } = await importAccountsModule();
253+
const manager = new AccountManager(undefined, stored as never);
254+
const account = manager.getAccountByIndex(0)!;
255+
account.refreshToken = "";
256+
257+
mockLoadCodexCliState.mockResolvedValue({
258+
sourceUpdatedAtMs: now - 60_000,
259+
accounts: [
260+
{
261+
email: "expired@example.com",
262+
accessToken: "cached-access-old",
263+
expiresAt: now - 1,
264+
refreshToken: "cached-refresh-restored",
265+
accountId: "expired-account-id",
266+
},
267+
],
268+
});
269+
270+
const hydrate = getPrivate<() => Promise<void>>(
271+
manager as object,
272+
"hydrateFromCodexCli",
273+
);
274+
await hydrate.call(manager);
275+
276+
const snapshot = manager.getAccountsSnapshot();
277+
expect(snapshot[0]?.refreshToken).toBe("cached-refresh-restored");
278+
expect(snapshot[0]?.access).toBe("local-access");
279+
expect(snapshot[0]?.accountId).toBe("expired-account-id");
196280
});
197281

198282
it("returns early when Codex CLI state has no usable cache entries", async () => {

test/accounts-load-from-disk.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ describe("AccountManager loadFromDisk", () => {
140140
expect(mockSaveAccounts).toHaveBeenCalledTimes(1);
141141
});
142142

143-
it("skips expired Codex CLI cache entries and does not persist", async () => {
143+
it("skips expired access hydration but backfills missing account identity", async () => {
144144
const now = Date.now();
145145
mockLoadAccounts.mockResolvedValue({
146146
version: 3 as const,
@@ -170,8 +170,9 @@ describe("AccountManager loadFromDisk", () => {
170170
const account = manager.getCurrentAccount();
171171

172172
expect(account?.access).toBeUndefined();
173-
expect(account?.accountId).toBeUndefined();
174-
expect(mockSaveAccounts).not.toHaveBeenCalled();
173+
expect(account?.accountId).toBe("acct-expired");
174+
expect(account?.accountIdSource).toBe("token");
175+
expect(mockSaveAccounts).toHaveBeenCalledTimes(1);
175176
});
176177

177178
it("syncCodexCliActiveSelectionForIndex ignores invalid indices and syncs a valid one", async () => {

0 commit comments

Comments
 (0)