mirror of
https://github.com/NoeFabris/opencode-antigravity-auth.git
synced 2026-05-13 15:46:05 +00:00
PR fixes
This commit is contained in:
@@ -118,7 +118,7 @@ Tool schemas are cleaned via allowlist. Unsupported fields (`const`, `$ref`, `$d
|
||||
Accounts rotate on rate limits. Gemini has dual quota pools (Antigravity headers + Gemini CLI headers). Fingerprints are per-account and regenerated on capacity exhaustion.
|
||||
|
||||
### 6. Fingerprint System
|
||||
Per-account device fingerprints stored in `antigravity-accounts.json`. Each fingerprint includes deviceId, sessionToken, userAgent, apiClient, clientMetadata. Headers: `User-Agent`, `X-Goog-Api-Client`, `Client-Metadata`, `X-Goog-QuotaUser`, `X-Client-Device-Id`. History tracked (max 5), restorable.
|
||||
Per-account device fingerprints stored in `antigravity-accounts.json`. Each fingerprint includes deviceId, sessionToken, userAgent, and a reduced clientMetadata (ideType, platform, pluginType — no osVersion, arch, or sqmId). The only header composed is `User-Agent`, built by `buildFingerprintHeaders()` in `fingerprint.ts` and applied on the antigravity request path in `request.ts`. History tracked (max 5), restorable.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
@@ -847,7 +847,7 @@ async function persistAccountPool(
|
||||
: (typeof stored?.activeIndex === "number" && Number.isFinite(stored.activeIndex) ? stored.activeIndex : 0);
|
||||
|
||||
await saveAccounts({
|
||||
version: 3,
|
||||
version: 4,
|
||||
accounts,
|
||||
activeIndex: clampInt(activeIndex, 0, accounts.length - 1),
|
||||
activeIndexByFamily: {
|
||||
@@ -2929,7 +2929,7 @@ export const createAntigravityPlugin = (providerId: string) => async (
|
||||
);
|
||||
// Use saveAccountsReplace to bypass merge (otherwise deleted account gets merged back)
|
||||
await saveAccountsReplace({
|
||||
version: 3,
|
||||
version: 4,
|
||||
accounts: updatedAccounts,
|
||||
activeIndex: 0,
|
||||
activeIndexByFamily: { claude: 0, gemini: 0 },
|
||||
@@ -3145,7 +3145,7 @@ export const createAntigravityPlugin = (providerId: string) => async (
|
||||
lastUsed: Date.now(),
|
||||
};
|
||||
await saveAccounts({
|
||||
version: 3,
|
||||
version: 4,
|
||||
accounts: updatedAccounts,
|
||||
activeIndex: currentStorage.activeIndex,
|
||||
activeIndexByFamily: currentStorage.activeIndexByFamily,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { AccountManager, type ModelFamily, type HeaderStyle, parseRateLimitReason, calculateBackoffMs, type RateLimitReason, resolveQuotaGroup } from "./accounts";
|
||||
import type { AccountStorageV3 } from "./storage";
|
||||
import type { AccountStorageV4 } from "./storage";
|
||||
import type { OAuthAuthDetails } from "./types";
|
||||
|
||||
// Mock storage to prevent test data from leaking to real config files
|
||||
@@ -28,8 +28,8 @@ describe("AccountManager", () => {
|
||||
expires: 123,
|
||||
};
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [],
|
||||
activeIndex: 0,
|
||||
};
|
||||
@@ -39,8 +39,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("returns current account when not rate-limited for family", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -58,8 +58,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("switches to next account when current is rate-limited for family", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -78,8 +78,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("returns null when all accounts are rate-limited for family", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -101,8 +101,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -123,8 +123,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -143,8 +143,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("tracks rate limits per model family independently", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -172,8 +172,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -206,8 +206,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -237,8 +237,8 @@ describe("AccountManager", () => {
|
||||
expires: 123,
|
||||
};
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -259,8 +259,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -282,8 +282,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("header style fallback for Gemini", () => {
|
||||
it("tracks rate limits separately for each header style", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -300,8 +300,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("getAvailableHeaderStyle returns antigravity first for Gemini", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -315,8 +315,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("getAvailableHeaderStyle returns gemini-cli when antigravity is rate-limited", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -332,8 +332,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("getAvailableHeaderStyle returns null when both header styles are rate-limited", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -350,8 +350,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("getAvailableHeaderStyle always returns antigravity for Claude", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -365,8 +365,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("getAvailableHeaderStyle returns null for Claude when rate-limited", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -385,8 +385,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -411,8 +411,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -434,8 +434,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("per-family account tracking", () => {
|
||||
it("tracks current account independently per model family", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -458,8 +458,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("switching Claude account does not affect Gemini account selection", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -486,8 +486,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("persists per-family indices to storage", async () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -506,8 +506,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("loads per-family indices from storage", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -527,8 +527,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("falls back to activeIndex when activeIndexByFamily is not present", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -545,8 +545,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("account cooldown (non-429 errors)", () => {
|
||||
it("marks account as cooling down with reason", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -565,8 +565,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -586,8 +586,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("clearAccountCooldown removes cooldown state", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -605,8 +605,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("cooling down account is skipped in getCurrentOrNextForFamily", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -624,8 +624,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("cooldown is independent from rate limits", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -646,8 +646,8 @@ describe("AccountManager", () => {
|
||||
describe("account selection strategies", () => {
|
||||
describe("sticky strategy (default)", () => {
|
||||
it("returns same account on consecutive calls", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -667,8 +667,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("switches account only when current is rate-limited", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -690,8 +690,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("round-robin strategy", () => {
|
||||
it("rotates to next account on each call", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -712,8 +712,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("skips rate-limited accounts", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -736,8 +736,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("hybrid strategy", () => {
|
||||
it("returns fresh (untouched) accounts first", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -759,8 +759,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("continues to return valid accounts after all touched", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -785,8 +785,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("hybrid strategy with token bucket", () => {
|
||||
it("returns account based on health and token availability", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -803,8 +803,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("skips rate-limited accounts", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -821,8 +821,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("skips cooling down accounts", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -842,8 +842,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -860,8 +860,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(5000));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -884,8 +884,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(1000));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -901,8 +901,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("isFreshForQuota returns true for untouched accounts", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -919,8 +919,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(1000));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -939,8 +939,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(1000));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -962,8 +962,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("consecutiveFailures tracking", () => {
|
||||
it("initializes consecutiveFailures as undefined", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -977,8 +977,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("can increment and reset consecutiveFailures", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1001,8 +1001,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("Issue #147: headerStyle-aware account selection", () => {
|
||||
it("skips account when requested headerStyle is rate-limited even if other style is available", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -1037,8 +1037,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("returns same account when a different headerStyle is rate-limited", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -1070,8 +1070,8 @@ describe("AccountManager", () => {
|
||||
it("requestSaveToDisk coalesces multiple calls into one write", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1097,8 +1097,8 @@ describe("AccountManager", () => {
|
||||
it("flushSaveToDisk waits for pending save to complete", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1123,8 +1123,8 @@ describe("AccountManager", () => {
|
||||
it("does not save again if no new requestSaveToDisk after flush", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1152,8 +1152,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1248,8 +1248,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(1000);
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1284,8 +1284,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(1000);
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1306,8 +1306,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("markRequestSuccess", () => {
|
||||
it("resets consecutive failure counter", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1328,8 +1328,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(10_000);
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0, rateLimitResetTimes: { "gemini-antigravity": 11_500, "gemini-cli": 11_500 } },
|
||||
],
|
||||
@@ -1346,8 +1346,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(10_000);
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0, rateLimitResetTimes: { "gemini-antigravity": 15_000, "gemini-cli": 15_000 } },
|
||||
],
|
||||
@@ -1361,8 +1361,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("shouldTryOptimisticReset returns false when accounts are available", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1377,8 +1377,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(10_000);
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0, rateLimitResetTimes: { "gemini-antigravity": 70_000, "gemini-cli": 80_000 } },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 2, lastUsed: 0, rateLimitResetTimes: { "gemini-antigravity": 90_000 } },
|
||||
@@ -1409,8 +1409,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1439,8 +1439,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1467,8 +1467,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("Fingerprint History", () => {
|
||||
it("regenerateAccountFingerprint saves old fingerprint to history", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1493,8 +1493,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(1000)); // Start at 1000 to avoid 0 being falsy
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1533,8 +1533,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("getAccountFingerprintHistory returns empty array for new account", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1548,8 +1548,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("limits fingerprint history to MAX_FINGERPRINT_HISTORY", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1570,8 +1570,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("soft quota threshold", () => {
|
||||
it("skips account over soft quota threshold in sticky mode", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 2, lastUsed: 0 },
|
||||
@@ -1587,8 +1587,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("allows account under soft quota threshold", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1603,8 +1603,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("threshold of 100 disables soft quota protection", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1619,8 +1619,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("returns null when all accounts over threshold", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 2, lastUsed: 0 },
|
||||
@@ -1637,8 +1637,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("skips account over threshold in round-robin mode", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 2, lastUsed: 0 },
|
||||
@@ -1654,8 +1654,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("account without cached quota is not skipped", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1669,8 +1669,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("handles remainingFraction of 0 (fully exhausted)", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 2, lastUsed: 0 },
|
||||
@@ -1689,8 +1689,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(0));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1709,8 +1709,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("fails open when cachedQuotaUpdatedAt is missing", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1729,8 +1729,8 @@ describe("AccountManager", () => {
|
||||
|
||||
describe("getMinWaitTimeForSoftQuota", () => {
|
||||
it("returns 0 when accounts are available (under threshold)", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1745,8 +1745,8 @@ describe("AccountManager", () => {
|
||||
});
|
||||
|
||||
it("returns null when no resetTime available", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1764,8 +1764,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-28T10:00:00Z"));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1791,8 +1791,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-28T16:00:00Z"));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
],
|
||||
@@ -1818,8 +1818,8 @@ describe("AccountManager", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-28T10:00:00Z"));
|
||||
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 2, lastUsed: 0 },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { formatRefreshParts, parseRefreshParts } from "./auth";
|
||||
import { loadAccounts, saveAccounts, type AccountStorageV3, type AccountMetadataV3, type RateLimitStateV3, type ModelFamily, type HeaderStyle, type CooldownReason } from "./storage";
|
||||
import { loadAccounts, saveAccounts, type AccountStorageV4, type AccountMetadataV3, type RateLimitStateV3, type ModelFamily, type HeaderStyle, type CooldownReason } from "./storage";
|
||||
import type { OAuthAuthDetails, RefreshParts } from "./types";
|
||||
import type { AccountSelectionStrategy } from "./config/schema";
|
||||
import { getHealthTracker, getTokenTracker, selectHybridAccount, type AccountWithMetrics } from "./rotation";
|
||||
@@ -318,7 +318,7 @@ export class AccountManager {
|
||||
return new AccountManager(authFallback, stored);
|
||||
}
|
||||
|
||||
constructor(authFallback?: OAuthAuthDetails, stored?: AccountStorageV3 | null) {
|
||||
constructor(authFallback?: OAuthAuthDetails, stored?: AccountStorageV4 | null) {
|
||||
const authParts = authFallback ? parseRefreshParts(authFallback.refresh) : null;
|
||||
|
||||
if (stored && stored.accounts.length === 0) {
|
||||
@@ -359,8 +359,8 @@ export class AccountManager {
|
||||
coolingDownUntil: acc.coolingDownUntil,
|
||||
cooldownReason: acc.cooldownReason,
|
||||
touchedForQuota: {},
|
||||
// Always regenerate fingerprint on load to pick up alignment changes
|
||||
fingerprint: generateFingerprint(),
|
||||
fingerprint: acc.fingerprint ?? generateFingerprint(),
|
||||
fingerprintHistory: acc.fingerprintHistory ?? [],
|
||||
cachedQuota: acc.cachedQuota as Partial<Record<QuotaGroup, QuotaGroupSummary>> | undefined,
|
||||
cachedQuotaUpdatedAt: acc.cachedQuotaUpdatedAt,
|
||||
verificationRequired: acc.verificationRequired,
|
||||
@@ -977,8 +977,8 @@ export class AccountManager {
|
||||
const claudeIndex = Math.max(0, this.currentAccountIndexByFamily.claude);
|
||||
const geminiIndex = Math.max(0, this.currentAccountIndexByFamily.gemini);
|
||||
|
||||
const storage: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const storage: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: this.accounts.map((a) => ({
|
||||
email: a.email,
|
||||
refreshToken: a.parts.refreshToken,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { AccountManager, type ModelFamily, type HeaderStyle } from "./accounts";
|
||||
import type { AccountStorageV3 } from "./storage";
|
||||
import type { AccountStorageV4 } from "./storage";
|
||||
|
||||
/**
|
||||
* Test: Antigravity-first fallback logic
|
||||
@@ -21,8 +21,8 @@ describe("Antigravity-first fallback", () => {
|
||||
|
||||
describe("hasOtherAccountWithAntigravityAvailable", () => {
|
||||
it("returns true when another account has antigravity available", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -47,8 +47,8 @@ describe("Antigravity-first fallback", () => {
|
||||
});
|
||||
|
||||
it("returns false when all other accounts are also rate-limited for antigravity", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -73,8 +73,8 @@ describe("Antigravity-first fallback", () => {
|
||||
});
|
||||
|
||||
it("skips disabled accounts", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0, enabled: false },
|
||||
@@ -99,8 +99,8 @@ describe("Antigravity-first fallback", () => {
|
||||
});
|
||||
|
||||
it("skips cooling down accounts", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -126,8 +126,8 @@ describe("Antigravity-first fallback", () => {
|
||||
});
|
||||
|
||||
it("works with model-specific rate limits", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -152,8 +152,8 @@ describe("Antigravity-first fallback", () => {
|
||||
});
|
||||
|
||||
it("returns false for Claude family (no gemini-cli fallback)", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -177,8 +177,8 @@ describe("Antigravity-first fallback", () => {
|
||||
|
||||
describe("Pre-check fallback logic", () => {
|
||||
it("should switch to account with antigravity rather than fall back to gemini-cli", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
@@ -211,8 +211,8 @@ describe("Antigravity-first fallback", () => {
|
||||
});
|
||||
|
||||
it("should only fall back to gemini-cli when ALL accounts exhausted antigravity", () => {
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
|
||||
{ refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 },
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { promises as fs } from "node:fs";
|
||||
import * as storageModule from "./storage";
|
||||
import type { AccountStorageV3, AccountMetadataV3 } from "./storage";
|
||||
import type { AccountStorageV4, AccountMetadataV3 } from "./storage";
|
||||
|
||||
vi.mock("proper-lockfile", () => ({
|
||||
default: {
|
||||
@@ -46,9 +46,9 @@ function createMockAccount(overrides: Partial<AccountMetadataV3> = {}): AccountM
|
||||
};
|
||||
}
|
||||
|
||||
function createMockStorage(accounts: AccountMetadataV3[], activeIndex = 0): AccountStorageV3 {
|
||||
function createMockStorage(accounts: AccountMetadataV3[], activeIndex = 0): AccountStorageV4 {
|
||||
return {
|
||||
version: 3,
|
||||
version: 4,
|
||||
accounts,
|
||||
activeIndex,
|
||||
};
|
||||
@@ -83,7 +83,7 @@ describe("loadAccounts", () => {
|
||||
const result = await storageModule.loadAccounts();
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.version).toBe(3);
|
||||
expect(result?.version).toBe(4);
|
||||
expect(result?.accounts).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -141,7 +141,7 @@ describe("loadAccounts", () => {
|
||||
});
|
||||
|
||||
it("returns null on invalid storage format", async () => {
|
||||
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({ version: 3, notAccounts: [] }));
|
||||
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({ version: 4, notAccounts: [] }));
|
||||
|
||||
const result = await storageModule.loadAccounts();
|
||||
|
||||
@@ -175,7 +175,7 @@ describe("loadAccounts", () => {
|
||||
|
||||
const result = await storageModule.loadAccounts();
|
||||
|
||||
expect(result?.version).toBe(3);
|
||||
expect(result?.version).toBe(4);
|
||||
expect(result?.accounts).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -197,7 +197,7 @@ describe("saveAccounts", () => {
|
||||
const writtenContent = vi.mocked(fs.writeFile).mock.calls[0]?.[1];
|
||||
expect(writtenContent).toBeDefined();
|
||||
const parsed = JSON.parse(writtenContent as string);
|
||||
expect(parsed.version).toBe(3);
|
||||
expect(parsed.version).toBe(4);
|
||||
expect(parsed.accounts).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ProactiveRefreshQueue } from "./refresh-queue";
|
||||
import { AccountManager } from "./accounts";
|
||||
import type { AccountStorageV3 } from "./storage";
|
||||
import type { AccountStorageV4 } from "./storage";
|
||||
import type { PluginClient } from "./types";
|
||||
|
||||
// Mock PluginClient
|
||||
@@ -22,8 +22,8 @@ describe("ProactiveRefreshQueue", () => {
|
||||
describe("getAccountsNeedingRefresh", () => {
|
||||
it("skips disabled accounts", () => {
|
||||
const now = Date.now();
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{
|
||||
refreshToken: "r1",
|
||||
@@ -75,8 +75,8 @@ describe("ProactiveRefreshQueue", () => {
|
||||
|
||||
it("includes accounts with undefined enabled (default to enabled)", () => {
|
||||
const now = Date.now();
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{
|
||||
refreshToken: "r1",
|
||||
@@ -109,8 +109,8 @@ describe("ProactiveRefreshQueue", () => {
|
||||
|
||||
it("skips expired accounts", () => {
|
||||
const now = Date.now();
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{
|
||||
refreshToken: "r1",
|
||||
@@ -142,8 +142,8 @@ describe("ProactiveRefreshQueue", () => {
|
||||
|
||||
it("skips accounts that don't need refresh yet", () => {
|
||||
const now = Date.now();
|
||||
const stored: AccountStorageV3 = {
|
||||
version: 3,
|
||||
const stored: AccountStorageV4 = {
|
||||
version: 4,
|
||||
accounts: [
|
||||
{
|
||||
refreshToken: "r1",
|
||||
|
||||
@@ -373,7 +373,7 @@ describe("Storage Migration", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("migrates V2 storage on load and persists V3", async () => {
|
||||
it("migrates V2 storage on load and persists V4", async () => {
|
||||
const v2Data = {
|
||||
version: 2,
|
||||
accounts: [
|
||||
@@ -402,7 +402,7 @@ describe("Storage Migration", () => {
|
||||
const result = await loadAccounts();
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.version).toBe(3);
|
||||
expect(result?.version).toBe(4);
|
||||
|
||||
const account = result?.accounts[0];
|
||||
if (!account) throw new Error("Account not found");
|
||||
@@ -419,7 +419,7 @@ describe("Storage Migration", () => {
|
||||
if (!saveCall) throw new Error("saveAccounts was not called (tmp file not found)");
|
||||
|
||||
const savedContent = JSON.parse(saveCall[1] as string);
|
||||
expect(savedContent.version).toBe(3);
|
||||
expect(savedContent.version).toBe(4);
|
||||
expect(savedContent.accounts[0].rateLimitResetTimes).toEqual({
|
||||
"gemini-antigravity": future,
|
||||
});
|
||||
|
||||
@@ -193,6 +193,7 @@ export interface AccountMetadataV3 {
|
||||
cooldownReason?: CooldownReason;
|
||||
/** Per-account device fingerprint for rate limit mitigation */
|
||||
fingerprint?: import("./fingerprint").Fingerprint;
|
||||
fingerprintHistory?: import("./fingerprint").FingerprintVersion[];
|
||||
/** Set when Google asks the user to verify this account before requests can continue. */
|
||||
verificationRequired?: boolean;
|
||||
verificationRequiredAt?: number;
|
||||
@@ -213,7 +214,21 @@ export interface AccountStorageV3 {
|
||||
};
|
||||
}
|
||||
|
||||
type AnyAccountStorage = AccountStorageV1 | AccountStorage | AccountStorageV3;
|
||||
export interface AccountStorageV4 {
|
||||
version: 4;
|
||||
accounts: AccountMetadataV3[];
|
||||
activeIndex: number;
|
||||
activeIndexByFamily?: {
|
||||
claude?: number;
|
||||
gemini?: number;
|
||||
};
|
||||
}
|
||||
|
||||
type AnyAccountStorage =
|
||||
| AccountStorageV1
|
||||
| AccountStorage
|
||||
| AccountStorageV3
|
||||
| AccountStorageV4;
|
||||
|
||||
/**
|
||||
* Gets the legacy Windows config directory (%APPDATA%\opencode).
|
||||
@@ -361,7 +376,7 @@ async function ensureFileExists(path: string): Promise<void> {
|
||||
await fs.mkdir(dirname(path), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path,
|
||||
JSON.stringify({ version: 3, accounts: [], activeIndex: 0 }, null, 2),
|
||||
JSON.stringify({ version: 4, accounts: [], activeIndex: 0 }, null, 2),
|
||||
{ encoding: "utf-8", mode: 0o600 },
|
||||
);
|
||||
}
|
||||
@@ -385,9 +400,9 @@ async function withFileLock<T>(path: string, fn: () => Promise<T>): Promise<T> {
|
||||
}
|
||||
|
||||
function mergeAccountStorage(
|
||||
existing: AccountStorageV3,
|
||||
incoming: AccountStorageV3,
|
||||
): AccountStorageV3 {
|
||||
existing: AccountStorageV4,
|
||||
incoming: AccountStorageV4,
|
||||
): AccountStorageV4 {
|
||||
const accountMap = new Map<string, AccountMetadataV3>();
|
||||
|
||||
for (const acc of existing.accounts) {
|
||||
@@ -419,7 +434,7 @@ function mergeAccountStorage(
|
||||
}
|
||||
|
||||
return {
|
||||
version: 3,
|
||||
version: 4,
|
||||
accounts: Array.from(accountMap.values()),
|
||||
activeIndex: incoming.activeIndex,
|
||||
activeIndexByFamily: incoming.activeIndexByFamily,
|
||||
@@ -558,7 +573,20 @@ export function migrateV2ToV3(v2: AccountStorage): AccountStorageV3 {
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadAccounts(): Promise<AccountStorageV3 | null> {
|
||||
export function migrateV3ToV4(v3: AccountStorageV3): AccountStorageV4 {
|
||||
return {
|
||||
version: 4,
|
||||
accounts: v3.accounts.map((acc) => ({
|
||||
...acc,
|
||||
fingerprint: undefined,
|
||||
fingerprintHistory: undefined,
|
||||
})),
|
||||
activeIndex: v3.activeIndex,
|
||||
activeIndexByFamily: v3.activeIndexByFamily,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadAccounts(): Promise<AccountStorageV4 | null> {
|
||||
try {
|
||||
const path = getStoragePath();
|
||||
// Ensure permissions are correct on load (fixes existing files)
|
||||
@@ -572,32 +600,45 @@ export async function loadAccounts(): Promise<AccountStorageV3 | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
let storage: AccountStorageV3;
|
||||
let storage: AccountStorageV4;
|
||||
|
||||
if (data.version === 1) {
|
||||
log.info("Migrating account storage from v1 to v3");
|
||||
log.info("Migrating account storage from v1 to v4");
|
||||
const v2 = migrateV1ToV2(data);
|
||||
storage = migrateV2ToV3(v2);
|
||||
const v3 = migrateV2ToV3(v2);
|
||||
storage = migrateV3ToV4(v3);
|
||||
try {
|
||||
await saveAccounts(storage);
|
||||
log.info("Migration to v3 complete");
|
||||
log.info("Migration to v4 complete");
|
||||
} catch (saveError) {
|
||||
log.warn("Failed to persist migrated storage", {
|
||||
error: String(saveError),
|
||||
});
|
||||
}
|
||||
} else if (data.version === 2) {
|
||||
log.info("Migrating account storage from v2 to v3");
|
||||
storage = migrateV2ToV3(data);
|
||||
log.info("Migrating account storage from v2 to v4");
|
||||
const v3 = migrateV2ToV3(data);
|
||||
storage = migrateV3ToV4(v3);
|
||||
try {
|
||||
await saveAccounts(storage);
|
||||
log.info("Migration to v3 complete");
|
||||
log.info("Migration to v4 complete");
|
||||
} catch (saveError) {
|
||||
log.warn("Failed to persist migrated storage", {
|
||||
error: String(saveError),
|
||||
});
|
||||
}
|
||||
} else if (data.version === 3) {
|
||||
log.info("Migrating account storage from v3 to v4");
|
||||
storage = migrateV3ToV4(data);
|
||||
try {
|
||||
await saveAccounts(storage);
|
||||
log.info("Migration to v4 complete");
|
||||
} catch (saveError) {
|
||||
log.warn("Failed to persist migrated storage", {
|
||||
error: String(saveError),
|
||||
});
|
||||
}
|
||||
} else if (data.version === 4) {
|
||||
storage = data;
|
||||
} else {
|
||||
log.warn("Unknown storage version, ignoring", {
|
||||
@@ -634,7 +675,7 @@ export async function loadAccounts(): Promise<AccountStorageV3 | null> {
|
||||
}
|
||||
|
||||
return {
|
||||
version: 3,
|
||||
version: 4,
|
||||
accounts: deduplicatedAccounts,
|
||||
activeIndex,
|
||||
};
|
||||
@@ -648,7 +689,7 @@ export async function loadAccounts(): Promise<AccountStorageV3 | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveAccounts(storage: AccountStorageV3): Promise<void> {
|
||||
export async function saveAccounts(storage: AccountStorageV4): Promise<void> {
|
||||
const path = getStoragePath();
|
||||
const configDir = dirname(path);
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
@@ -681,7 +722,7 @@ export async function saveAccounts(storage: AccountStorageV3): Promise<void> {
|
||||
* Use this for destructive operations like delete where we need to
|
||||
* remove accounts that would otherwise be merged back from existing storage.
|
||||
*/
|
||||
export async function saveAccountsReplace(storage: AccountStorageV3): Promise<void> {
|
||||
export async function saveAccountsReplace(storage: AccountStorageV4): Promise<void> {
|
||||
const path = getStoragePath();
|
||||
const configDir = dirname(path);
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
@@ -705,7 +746,7 @@ export async function saveAccountsReplace(storage: AccountStorageV3): Promise<vo
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAccountsUnsafe(): Promise<AccountStorageV3 | null> {
|
||||
async function loadAccountsUnsafe(): Promise<AccountStorageV4 | null> {
|
||||
try {
|
||||
const path = getStoragePath();
|
||||
// Ensure permissions are correct on load (fixes existing files)
|
||||
@@ -715,10 +756,13 @@ async function loadAccountsUnsafe(): Promise<AccountStorageV3 | null> {
|
||||
const parsed = JSON.parse(content);
|
||||
|
||||
if (parsed.version === 1) {
|
||||
return migrateV2ToV3(migrateV1ToV2(parsed));
|
||||
return migrateV3ToV4(migrateV2ToV3(migrateV1ToV2(parsed)));
|
||||
}
|
||||
if (parsed.version === 2) {
|
||||
return migrateV2ToV3(parsed);
|
||||
return migrateV3ToV4(migrateV2ToV3(parsed));
|
||||
}
|
||||
if (parsed.version === 3) {
|
||||
return migrateV3ToV4(parsed);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user