Skip to content

Commit fe4d83c

Browse files
committed
Scope pool lastUsed preservation
1 parent 9715ca0 commit fe4d83c

3 files changed

Lines changed: 106 additions & 3 deletions

File tree

lib/runtime/account-pool.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type TokenSuccessWithAccount = Extract<
1616
export async function persistAccountPoolResults(params: {
1717
results: TokenSuccessWithAccount[];
1818
replaceAll?: boolean;
19+
preserveLastUsedOnUpdate?: boolean;
1920
modelFamilies: readonly ModelFamily[];
2021
withAccountStorageTransaction: <T>(
2122
handler: (
@@ -31,7 +32,11 @@ export async function persistAccountPoolResults(params: {
3132
) => string | undefined;
3233
sanitizeEmail: (email: string | undefined) => string | undefined;
3334
}): Promise<void> {
34-
const { results, replaceAll = false } = params;
35+
const {
36+
results,
37+
replaceAll = false,
38+
preserveLastUsedOnUpdate = false,
39+
} = params;
3540
if (results.length === 0) return;
3641

3742
await params.withAccountStorageTransaction(async (loadedStorage, persist) => {
@@ -161,7 +166,9 @@ export async function persistAccountPoolResults(params: {
161166
refreshToken: result.refresh,
162167
accessToken: result.access,
163168
expiresAt: result.expires,
164-
lastUsed: existing.lastUsed ?? now,
169+
lastUsed: preserveLastUsedOnUpdate
170+
? (existing.lastUsed ?? now)
171+
: now,
165172
workspaces: mergedWorkspaces,
166173
currentWorkspaceIndex: nextCurrentWorkspaceIndex,
167174
};

test/account-pool.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,68 @@ describe("account pool helper", () => {
5353
});
5454
});
5555

56-
it("preserves lastUsed when updating an existing account", async () => {
56+
it("advances lastUsed when updating an existing account by default", async () => {
57+
const persist = vi.fn(async () => undefined);
58+
const originalLastUsed = 456;
59+
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(789);
60+
61+
try {
62+
await persistAccountPoolResults({
63+
results: [
64+
{
65+
type: "success",
66+
access: "access-token-next",
67+
refresh: "refresh-token",
68+
expires: 999,
69+
},
70+
],
71+
replaceAll: false,
72+
modelFamilies: ["codex"],
73+
withAccountStorageTransaction: async (handler) =>
74+
handler(
75+
{
76+
version: 3,
77+
activeIndex: 0,
78+
activeIndexByFamily: { codex: 0 },
79+
accounts: [
80+
{
81+
accountId: "acct_1",
82+
email: "user@example.com",
83+
refreshToken: "refresh-token",
84+
accessToken: "access-token-old",
85+
expiresAt: 123,
86+
addedAt: 111,
87+
lastUsed: originalLastUsed,
88+
enabled: true,
89+
},
90+
],
91+
},
92+
persist,
93+
),
94+
findMatchingAccountIndex: () => 0,
95+
extractAccountId: () => "acct_1",
96+
extractAccountEmail: () => "user@example.com",
97+
sanitizeEmail: (email) => email,
98+
});
99+
} finally {
100+
nowSpy.mockRestore();
101+
}
102+
103+
expect(persist).toHaveBeenCalledWith(
104+
expect.objectContaining({
105+
accounts: [
106+
expect.objectContaining({
107+
refreshToken: "refresh-token",
108+
accessToken: "access-token-next",
109+
expiresAt: 999,
110+
lastUsed: 789,
111+
}),
112+
],
113+
}),
114+
);
115+
});
116+
117+
it("preserves lastUsed for report-style updates when requested", async () => {
57118
const persist = vi.fn(async () => undefined);
58119
const originalLastUsed = 456;
59120

@@ -67,6 +128,7 @@ describe("account pool helper", () => {
67128
},
68129
],
69130
replaceAll: false,
131+
preserveLastUsedOnUpdate: true,
70132
modelFamilies: ["codex"],
71133
withAccountStorageTransaction: async (handler) =>
72134
handler(

test/codex-manager-report-command.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,40 @@ describe("runReportCommand", () => {
224224
});
225225
});
226226

227+
it("records probe error when usable token exists but account id is missing", async () => {
228+
const deps = createDeps({
229+
hasUsableAccessToken: vi.fn(() => true),
230+
loadAccounts: vi.fn(async () =>
231+
createStorage([
232+
{
233+
email: "missing-id@example.com",
234+
refreshToken: "refresh-token-1",
235+
accessToken: "not-a-jwt",
236+
expiresAt: 5_000,
237+
addedAt: 1,
238+
lastUsed: 1,
239+
enabled: true,
240+
},
241+
]),
242+
),
243+
});
244+
245+
const result = await runReportCommand(["--live", "--json"], deps);
246+
247+
expect(result).toBe(0);
248+
expect(deps.queuedRefresh).not.toHaveBeenCalled();
249+
expect(deps.saveAccounts).not.toHaveBeenCalled();
250+
expect(deps.fetchCodexQuotaSnapshot).not.toHaveBeenCalled();
251+
const jsonOutput = JSON.parse(
252+
(deps.logInfo as ReturnType<typeof vi.fn>).mock.calls.at(-1)?.[0] ?? "{}",
253+
) as { forecast: { probeErrors: string[] } };
254+
expect(jsonOutput.forecast.probeErrors).toEqual(
255+
expect.arrayContaining([
256+
expect.stringContaining("missing accountId for live probe"),
257+
]),
258+
);
259+
});
260+
227261
it("persists refreshed probe tokens before report live probes", async () => {
228262
const storage = createStorage([
229263
{

0 commit comments

Comments
 (0)