fix(antigravity): separate weekly and short-window quotas and clean up redundant rows
- Track both weekly and 5-hour session buckets in parseWeeklyQuotaSummary, distinguishing sliding-window limits from multi-day weekly limits - Preserve disabled session buckets at 0% rather than dropping them when weekly limits are reached - Target 5-hour session rows (not weekly rows) during family exhaustion reconciliation in getAntigravityUsage - Suppress synthesized per-model duplicate rows in dashboard normalization when family summaries are present - Add unit test coverage for multi-bucket extraction, reconciliation isolation, and dashboard deduplication
This commit is contained in:
@@ -25,10 +25,18 @@ export function _clearWeeklyCache() {
|
||||
weeklyCache.clear();
|
||||
}
|
||||
|
||||
// — Group-name to stable key mapping ——————————————————————
|
||||
const GROUP_MATCHERS = [
|
||||
{ pattern: /gemini/i, key: "gemini_weekly", displayName: "Gemini (Weekly)" },
|
||||
{ pattern: /claude|gpt/i, key: "claude_gpt_weekly", displayName: "Claude & GPT (Weekly)" },
|
||||
// — Group-name and window to stable key mapping ——————————————————————
|
||||
const GROUP_CONFIGS = [
|
||||
{
|
||||
pattern: /gemini/i,
|
||||
weekly: { key: "gemini_weekly", displayName: "Gemini (Weekly)" },
|
||||
session: { key: "gemini_session", displayName: "Gemini (5h)" },
|
||||
},
|
||||
{
|
||||
pattern: /claude|gpt/i,
|
||||
weekly: { key: "claude_gpt_weekly", displayName: "Claude & GPT (Weekly)" },
|
||||
session: { key: "claude_gpt_session", displayName: "Claude & GPT (5h)" },
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -60,32 +68,40 @@ export function parseWeeklyQuotaSummary(data) {
|
||||
for (const bucket of buckets) {
|
||||
if (!bucket || typeof bucket !== "object") continue;
|
||||
|
||||
// Identify weekly buckets by checking bucketId + displayName for "weekly"
|
||||
const windowType = String(bucket.window || "").toLowerCase();
|
||||
const bucketText = `${bucket.bucketId || ""} ${bucket.displayName || ""}`.toLowerCase();
|
||||
if (!bucketText.includes("weekly")) continue;
|
||||
const isWeekly = windowType === "weekly" || bucketText.includes("weekly");
|
||||
const isSession = windowType === "5h" || bucketText.includes("five hour") || bucketText.includes("5h") || bucketText.includes("daily") || windowType === "daily";
|
||||
|
||||
// Skip disabled buckets
|
||||
if (bucket.disabled === true) continue;
|
||||
if (!isWeekly && !isSession) continue;
|
||||
|
||||
const remainingFraction = Number(bucket.remainingFraction);
|
||||
// If a session (5h) bucket is marked disabled by upstream (because weekly was hit),
|
||||
// keep it so the UI shows the 5h row, but with remainingFraction: 0.
|
||||
// Disabled weekly buckets are truly disabled and skipped.
|
||||
if (bucket.disabled === true && isWeekly) continue;
|
||||
|
||||
const remainingFraction = bucket.disabled === true ? 0 : Number(bucket.remainingFraction);
|
||||
if (!Number.isFinite(remainingFraction)) continue;
|
||||
|
||||
// Match group to a known family
|
||||
for (const matcher of GROUP_MATCHERS) {
|
||||
if (matcher.pattern.test(displayName)) {
|
||||
for (const config of GROUP_CONFIGS) {
|
||||
if (config.pattern.test(displayName)) {
|
||||
const target = isWeekly ? config.weekly : config.session;
|
||||
if (result[target.key]) break; // first matching bucket per type wins
|
||||
|
||||
const total = 1000;
|
||||
const remaining = Math.round(total * remainingFraction);
|
||||
const used = Math.max(0, total - remaining);
|
||||
|
||||
result[matcher.key] = {
|
||||
result[target.key] = {
|
||||
used,
|
||||
total,
|
||||
resetAt: parseResetTime(bucket.resetTime),
|
||||
remainingPercentage: remainingFraction * 100,
|
||||
unlimited: false,
|
||||
displayName: matcher.displayName,
|
||||
displayName: target.displayName,
|
||||
};
|
||||
break; // first matching bucket per family wins
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,39 +228,37 @@ export async function getAntigravityUsage(accessToken, providerSpecificData, pro
|
||||
proxyOptions
|
||||
);
|
||||
|
||||
// Reconcile weekly quota against model family status:
|
||||
// Reconcile short-window session quota if models are exhausted:
|
||||
// If every model in a family is locked/exhausted (remainingPercentage === 0)
|
||||
// until a future reset time, the weekly limit cannot be 100% available.
|
||||
// On Google's Free Starter tier, retrieveUserQuotaSummary buggily reports
|
||||
// remainingFraction: 1 even after the starter quota is depleted and all models 429.
|
||||
// until a future reset time, update the 5h session row (not the weekly row).
|
||||
const entries = Object.entries(quotas);
|
||||
const geminiModels = entries.filter(([k]) => k.startsWith("gemini-") && !k.includes("image"));
|
||||
const claudeModels = entries.filter(([k]) => k.startsWith("claude-"));
|
||||
|
||||
if (weeklyQuotas.gemini_weekly && geminiModels.length > 0) {
|
||||
if (weeklyQuotas.gemini_session && geminiModels.length > 0) {
|
||||
const allGeminiExhausted = geminiModels.every(([, q]) => (q.remainingPercentage ?? 0) === 0);
|
||||
if (allGeminiExhausted && weeklyQuotas.gemini_weekly.remainingPercentage > 0) {
|
||||
if (allGeminiExhausted && weeklyQuotas.gemini_session.remainingPercentage > 0) {
|
||||
const maxResetAt = geminiModels.reduce((max, [, q]) =>
|
||||
!max || (q.resetAt && new Date(q.resetAt) > new Date(max)) ? q.resetAt : max, null
|
||||
);
|
||||
weeklyQuotas.gemini_weekly.used = weeklyQuotas.gemini_weekly.total;
|
||||
weeklyQuotas.gemini_weekly.remainingPercentage = 0;
|
||||
weeklyQuotas.gemini_session.used = weeklyQuotas.gemini_session.total;
|
||||
weeklyQuotas.gemini_session.remainingPercentage = 0;
|
||||
if (maxResetAt) {
|
||||
weeklyQuotas.gemini_weekly.resetAt = maxResetAt;
|
||||
weeklyQuotas.gemini_session.resetAt = maxResetAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (weeklyQuotas.claude_gpt_weekly && claudeModels.length > 0) {
|
||||
if (weeklyQuotas.claude_gpt_session && claudeModels.length > 0) {
|
||||
const allClaudeExhausted = claudeModels.every(([, q]) => (q.remainingPercentage ?? 0) === 0);
|
||||
if (allClaudeExhausted && weeklyQuotas.claude_gpt_weekly.remainingPercentage > 0) {
|
||||
if (allClaudeExhausted && weeklyQuotas.claude_gpt_session.remainingPercentage > 0) {
|
||||
const maxResetAt = claudeModels.reduce((max, [, q]) =>
|
||||
!max || (q.resetAt && new Date(q.resetAt) > new Date(max)) ? q.resetAt : max, null
|
||||
);
|
||||
weeklyQuotas.claude_gpt_weekly.used = weeklyQuotas.claude_gpt_weekly.total;
|
||||
weeklyQuotas.claude_gpt_weekly.remainingPercentage = 0;
|
||||
weeklyQuotas.claude_gpt_session.used = weeklyQuotas.claude_gpt_session.total;
|
||||
weeklyQuotas.claude_gpt_session.remainingPercentage = 0;
|
||||
if (maxResetAt) {
|
||||
weeklyQuotas.claude_gpt_weekly.resetAt = maxResetAt;
|
||||
weeklyQuotas.claude_gpt_session.resetAt = maxResetAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,51 +377,112 @@ export function parseQuotaData(provider, data) {
|
||||
if (data.quotas) {
|
||||
const entries = Object.entries(data.quotas);
|
||||
const weeklyKeys = new Set(["gemini_weekly", "claude_gpt_weekly"]);
|
||||
const sessionKeys = new Set(["gemini_session", "claude_gpt_session"]);
|
||||
const summaryKeys = new Set([...weeklyKeys, ...sessionKeys]);
|
||||
const geminiModels = entries.filter(([k]) => k.startsWith("gemini-") && !k.includes("image"));
|
||||
const claudeModels = entries.filter(([k]) => k.startsWith("claude-"));
|
||||
const imageModels = entries.filter(([k]) => k.includes("image"));
|
||||
const weeklyModels = entries.filter(([k]) => weeklyKeys.has(k));
|
||||
const otherModels = entries.filter(([k]) => !k.startsWith("gemini-") && !k.startsWith("claude-") && !k.includes("image") && !weeklyKeys.has(k));
|
||||
const summaryModels = entries.filter(([k]) => summaryKeys.has(k));
|
||||
const otherModels = entries.filter(([k]) => !k.startsWith("gemini-") && !k.startsWith("claude-") && !k.includes("image") && !summaryKeys.has(k));
|
||||
|
||||
if (geminiModels.length > 0) {
|
||||
// Summary keys from retrieveUserQuotaSummary
|
||||
const hasGeminiWeekly = Boolean(data.quotas.gemini_weekly);
|
||||
const hasGeminiSession = Boolean(data.quotas.gemini_session);
|
||||
const hasClaudeWeekly = Boolean(data.quotas.claude_gpt_weekly);
|
||||
const hasClaudeSession = Boolean(data.quotas.claude_gpt_session);
|
||||
|
||||
// 1. Gemini Family:
|
||||
if (hasGeminiSession) {
|
||||
summaryModels.filter(([k]) => k === "gemini_session").forEach(([modelKey, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
name: quota.displayName || modelKey,
|
||||
modelKey,
|
||||
used: quota.used || 0,
|
||||
total: quota.total || 0,
|
||||
resetAt: quota.resetAt || null,
|
||||
remainingPercentage: quota.remainingPercentage,
|
||||
});
|
||||
});
|
||||
} else if (geminiModels.length > 0) {
|
||||
const rep = geminiModels.reduce((min, cur) =>
|
||||
(cur[1].remainingPercentage ?? 100) < (min[1].remainingPercentage ?? 100) ? cur : min
|
||||
)[1];
|
||||
normalizedQuotas.push({
|
||||
name: "Gemini (Flash / Pro)",
|
||||
modelKey: "gemini",
|
||||
used: rep.used || 0,
|
||||
total: rep.total || 0,
|
||||
resetAt: rep.resetAt || null,
|
||||
remainingPercentage: rep.remainingPercentage,
|
||||
// Only show synthesized Gemini row if its resetAt differs from weekly (i.e. it represents a separate 5h window)
|
||||
const weeklyResetAt = data.quotas.gemini_weekly?.resetAt;
|
||||
const isDuplicateOfWeekly = hasGeminiWeekly && rep.resetAt === weeklyResetAt && (rep.remainingPercentage ?? 0) === 0;
|
||||
|
||||
if (!isDuplicateOfWeekly) {
|
||||
normalizedQuotas.push({
|
||||
name: "Gemini (Flash / Pro)",
|
||||
modelKey: "gemini",
|
||||
used: rep.used || 0,
|
||||
total: rep.total || 0,
|
||||
resetAt: rep.resetAt || null,
|
||||
remainingPercentage: rep.remainingPercentage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Show Gemini weekly row if present
|
||||
if (hasGeminiWeekly) {
|
||||
summaryModels.filter(([k]) => k === "gemini_weekly").forEach(([modelKey, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
name: quota.displayName || modelKey,
|
||||
modelKey,
|
||||
used: quota.used || 0,
|
||||
total: quota.total || 0,
|
||||
resetAt: quota.resetAt || null,
|
||||
remainingPercentage: quota.remainingPercentage,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (claudeModels.length > 0) {
|
||||
// 2. Claude & GPT Family:
|
||||
if (hasClaudeSession) {
|
||||
summaryModels.filter(([k]) => k === "claude_gpt_session").forEach(([modelKey, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
name: quota.displayName || modelKey,
|
||||
modelKey,
|
||||
used: quota.used || 0,
|
||||
total: quota.total || 0,
|
||||
resetAt: quota.resetAt || null,
|
||||
remainingPercentage: quota.remainingPercentage,
|
||||
});
|
||||
});
|
||||
} else if (claudeModels.length > 0) {
|
||||
const rep = claudeModels.reduce((min, cur) =>
|
||||
(cur[1].remainingPercentage ?? 100) < (min[1].remainingPercentage ?? 100) ? cur : min
|
||||
)[1];
|
||||
normalizedQuotas.push({
|
||||
name: "Claude (Sonnet / Opus)",
|
||||
modelKey: "claude",
|
||||
used: rep.used || 0,
|
||||
total: rep.total || 0,
|
||||
resetAt: rep.resetAt || null,
|
||||
remainingPercentage: rep.remainingPercentage,
|
||||
const weeklyResetAt = data.quotas.claude_gpt_weekly?.resetAt;
|
||||
const isDuplicateOfWeekly = hasClaudeWeekly && rep.resetAt === weeklyResetAt && (rep.remainingPercentage ?? 0) === 0;
|
||||
|
||||
if (!isDuplicateOfWeekly) {
|
||||
normalizedQuotas.push({
|
||||
name: "Claude (Sonnet / Opus)",
|
||||
modelKey: "claude",
|
||||
used: rep.used || 0,
|
||||
total: rep.total || 0,
|
||||
resetAt: rep.resetAt || null,
|
||||
remainingPercentage: rep.remainingPercentage,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Show Claude & GPT weekly row if present
|
||||
if (hasClaudeWeekly) {
|
||||
summaryModels.filter(([k]) => k === "claude_gpt_weekly").forEach(([modelKey, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
name: quota.displayName || modelKey,
|
||||
modelKey,
|
||||
used: quota.used || 0,
|
||||
total: quota.total || 0,
|
||||
resetAt: quota.resetAt || null,
|
||||
remainingPercentage: quota.remainingPercentage,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
weeklyModels.forEach(([modelKey, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
name: quota.displayName || modelKey,
|
||||
modelKey,
|
||||
used: quota.used || 0,
|
||||
total: quota.total || 0,
|
||||
resetAt: quota.resetAt || null,
|
||||
remainingPercentage: quota.remainingPercentage,
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Standalone Image Generation Models (unique usage)
|
||||
imageModels.forEach(([modelKey, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
name: quota.displayName || modelKey,
|
||||
@@ -433,16 +494,23 @@ export function parseQuotaData(provider, data) {
|
||||
});
|
||||
});
|
||||
|
||||
otherModels.forEach(([modelKey, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
name: quota.displayName || modelKey,
|
||||
modelKey,
|
||||
used: quota.used || 0,
|
||||
total: quota.total || 0,
|
||||
resetAt: quota.resetAt || null,
|
||||
remainingPercentage: quota.remainingPercentage,
|
||||
// 4. Other models:
|
||||
// In Antigravity, GPT-OSS is explicitly documented by Google as part of the "Claude and GPT models" group:
|
||||
// ("Models within this group: Claude Opus, Claude Sonnet, GPT-OSS").
|
||||
// When summary quotas (claude_gpt_session / claude_gpt_weekly) are present, GPT-OSS is already represented
|
||||
// by the "Claude & GPT" family rows. We only include otherModels if no summary exists for that pool.
|
||||
if (!hasClaudeWeekly && !hasClaudeSession) {
|
||||
otherModels.forEach(([modelKey, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
name: quota.displayName || modelKey,
|
||||
modelKey,
|
||||
used: quota.used || 0,
|
||||
total: quota.total || 0,
|
||||
resetAt: quota.resetAt || null,
|
||||
remainingPercentage: quota.remainingPercentage,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -702,10 +770,10 @@ export function parseQuotaData(provider, data) {
|
||||
// Use modelKey for antigravity (mapped to family anchor), otherwise use name
|
||||
let keyA = a.modelKey || a.name;
|
||||
let keyB = b.modelKey || b.name;
|
||||
if (keyA === "gemini") keyA = "gemini-3.8-flash-high";
|
||||
if (keyA === "claude") keyA = "claude-sonnet-4-6";
|
||||
if (keyB === "gemini") keyB = "gemini-3.8-flash-high";
|
||||
if (keyB === "claude") keyB = "claude-sonnet-4-6";
|
||||
if (keyA === "gemini" || keyA === "gemini_session") keyA = "gemini-3.8-flash-high";
|
||||
if (keyA === "claude" || keyA === "claude_gpt_session") keyA = "claude-sonnet-4-6";
|
||||
if (keyB === "gemini" || keyB === "gemini_session") keyB = "gemini-3.8-flash-high";
|
||||
if (keyB === "claude" || keyB === "claude_gpt_session") keyB = "claude-sonnet-4-6";
|
||||
const orderA = orderMap.get(keyA) ?? 999;
|
||||
const orderB = orderMap.get(keyB) ?? 999;
|
||||
return orderA - orderB;
|
||||
|
||||
@@ -82,17 +82,97 @@ describe("Antigravity dashboard normalization with weekly quotas", () => {
|
||||
expect(weeklyRows[1].name).toMatch(/Weekly/);
|
||||
});
|
||||
|
||||
it("order: gemini family, claude family, weekly, then other", () => {
|
||||
const quotas = parseQuotaData("antigravity", data);
|
||||
it("order: session, weekly, then other models", () => {
|
||||
const dataWithBoth = {
|
||||
quotas: {
|
||||
gemini_session: {
|
||||
displayName: "Gemini (5h)",
|
||||
used: 100,
|
||||
total: 1000,
|
||||
resetAt: "2026-09-08T05:00:00Z",
|
||||
remainingPercentage: 90,
|
||||
},
|
||||
gemini_weekly: {
|
||||
displayName: "Gemini (Weekly)",
|
||||
used: 250,
|
||||
total: 1000,
|
||||
resetAt: "2026-09-15T00:00:00Z",
|
||||
remainingPercentage: 75,
|
||||
},
|
||||
claude_gpt_session: {
|
||||
displayName: "Claude & GPT (5h)",
|
||||
used: 50,
|
||||
total: 1000,
|
||||
resetAt: "2026-09-08T05:00:00Z",
|
||||
remainingPercentage: 95,
|
||||
},
|
||||
claude_gpt_weekly: {
|
||||
displayName: "Claude & GPT (Weekly)",
|
||||
used: 500,
|
||||
total: 1000,
|
||||
resetAt: "2026-09-14T00:00:00Z",
|
||||
remainingPercentage: 50,
|
||||
},
|
||||
},
|
||||
};
|
||||
const quotas = parseQuotaData("antigravity", dataWithBoth);
|
||||
const keys = quotas.map((q) => q.modelKey);
|
||||
|
||||
const geminiIdx = keys.indexOf("gemini");
|
||||
const claudeIdx = keys.indexOf("claude");
|
||||
const geminiSessionIdx = keys.indexOf("gemini_session");
|
||||
const geminiWeeklyIdx = keys.indexOf("gemini_weekly");
|
||||
const claudeSessionIdx = keys.indexOf("claude_gpt_session");
|
||||
const claudeWeeklyIdx = keys.indexOf("claude_gpt_weekly");
|
||||
|
||||
expect(geminiIdx).toBeLessThan(geminiWeeklyIdx);
|
||||
expect(claudeIdx).toBeLessThan(claudeWeeklyIdx);
|
||||
expect(geminiSessionIdx).toBeLessThan(geminiWeeklyIdx);
|
||||
expect(claudeSessionIdx).toBeLessThan(claudeWeeklyIdx);
|
||||
});
|
||||
|
||||
it("excludes redundant duplicates when individual models mirror weekly reset and summary is present", () => {
|
||||
// Exact scenario from Christian's account:
|
||||
const liveLikeData = {
|
||||
quotas: {
|
||||
"gemini-3.8-flash-high": { used: 1000, total: 1000, remainingPercentage: 0, resetAt: "2026-09-23T06:00:17Z", displayName: "Gemini 3.8 Flash (High)" },
|
||||
"claude-sonnet-4-6": { used: 1000, total: 1000, remainingPercentage: 0, resetAt: "2026-09-20T19:00:21Z", displayName: "Claude 3.7 Sonnet" },
|
||||
"gpt-oss-120b-medium": { used: 1000, total: 1000, remainingPercentage: 0, resetAt: "2026-09-20T19:00:21Z", displayName: "GPT-OSS 120B (Medium)" },
|
||||
"gemini-3.1-flash-image": { used: 1000, total: 1000, remainingPercentage: 0, resetAt: "2026-09-23T06:00:17Z", displayName: "Gemini 3.1 Flash Image" },
|
||||
gemini_weekly: {
|
||||
displayName: "Gemini (Weekly)",
|
||||
used: 1000,
|
||||
total: 1000,
|
||||
resetAt: "2026-09-23T06:00:17Z",
|
||||
remainingPercentage: 0,
|
||||
},
|
||||
claude_gpt_weekly: {
|
||||
displayName: "Claude & GPT (Weekly)",
|
||||
used: 807,
|
||||
total: 1000,
|
||||
resetAt: "2026-09-24T18:09:46Z",
|
||||
remainingPercentage: 19.3,
|
||||
},
|
||||
claude_gpt_session: {
|
||||
displayName: "Claude & GPT (5h)",
|
||||
used: 1000,
|
||||
total: 1000,
|
||||
resetAt: "2026-09-20T19:00:21Z",
|
||||
remainingPercentage: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const quotas = parseQuotaData("antigravity", liveLikeData);
|
||||
const names = quotas.map((q) => q.name);
|
||||
|
||||
// Should contain unique usages:
|
||||
expect(names).toContain("Claude & GPT (5h)");
|
||||
expect(names).toContain("Claude & GPT (Weekly)");
|
||||
expect(names).toContain("Gemini (Weekly)");
|
||||
expect(names).toContain("Gemini 3.1 Flash Image");
|
||||
|
||||
// Should NOT contain redundant duplicate entries:
|
||||
expect(names).not.toContain("Gemini (Flash / Pro)"); // duplicate of Gemini (Weekly)
|
||||
expect(names).not.toContain("Claude (Sonnet / Opus)"); // duplicate of Claude & GPT (5h)
|
||||
expect(names).not.toContain("GPT-OSS 120B (Medium)"); // covered by Claude & GPT family
|
||||
expect(quotas).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("works with no weekly keys present (backward compat)", () => {
|
||||
|
||||
@@ -53,7 +53,7 @@ const NESTED_RESPONSE = {
|
||||
|
||||
// — parseWeeklyQuotaSummary ———————————————————————————————
|
||||
describe("parseWeeklyQuotaSummary", () => {
|
||||
it("extracts Gemini weekly quota from top-level groups", () => {
|
||||
it("extracts Gemini weekly and session quotas from top-level groups", () => {
|
||||
const result = parseWeeklyQuotaSummary(FULL_RESPONSE);
|
||||
expect(result.gemini_weekly).toMatchObject({
|
||||
used: 250,
|
||||
@@ -63,6 +63,14 @@ describe("parseWeeklyQuotaSummary", () => {
|
||||
unlimited: false,
|
||||
});
|
||||
expect(result.gemini_weekly.resetAt).toBe("2026-09-15T00:00:00.000Z");
|
||||
expect(result.gemini_session).toMatchObject({
|
||||
used: 100,
|
||||
total: 1000,
|
||||
remainingPercentage: 90,
|
||||
displayName: "Gemini (5h)",
|
||||
unlimited: false,
|
||||
});
|
||||
expect(result.gemini_session.resetAt).toBe("2026-09-09T00:00:00.000Z");
|
||||
});
|
||||
|
||||
it("extracts Claude & GPT weekly quota", () => {
|
||||
@@ -85,14 +93,14 @@ describe("parseWeeklyQuotaSummary", () => {
|
||||
expect(result.claude_gpt_weekly.remainingPercentage).toBe(50);
|
||||
});
|
||||
|
||||
it("skips non-weekly buckets", () => {
|
||||
it("skips unrecognized non-weekly non-session buckets", () => {
|
||||
const data = {
|
||||
groups: [{
|
||||
displayName: "Gemini Models",
|
||||
buckets: [
|
||||
{
|
||||
bucketId: "gemini-daily-bucket",
|
||||
displayName: "Daily Limit",
|
||||
bucketId: "gemini-monthly-bucket",
|
||||
displayName: "Monthly Limit",
|
||||
remainingFraction: 0.9,
|
||||
resetTime: "2026-09-09T00:00:00Z",
|
||||
},
|
||||
@@ -404,7 +412,7 @@ describe("weekly quota isolation from existing quota", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("reconciles weekly quota to 0% when all paid-tier family models are exhausted", async () => {
|
||||
it("reconciles 5h session quota to 0% when all paid-tier family models are exhausted without clobbering weekly quota", async () => {
|
||||
proxyAwareFetch.mockImplementation(async (url) => {
|
||||
if (url.includes(":loadCodeAssist")) {
|
||||
return {
|
||||
@@ -421,7 +429,7 @@ describe("weekly quota isolation from existing quota", () => {
|
||||
models: {
|
||||
"gemini-3.8-flash-high": {
|
||||
displayName: "Gemini 3.8 Flash (High)",
|
||||
// Exhausted model: no remainingFraction, future resetTime
|
||||
// Exhausted model: no remainingFraction, future resetTime (5h window reset)
|
||||
quotaInfo: { resetTime: "2026-09-13T12:00:00Z" },
|
||||
},
|
||||
},
|
||||
@@ -435,30 +443,49 @@ describe("weekly quota isolation from existing quota", () => {
|
||||
json: async () => ({
|
||||
groups: [{
|
||||
displayName: "Gemini Models",
|
||||
buckets: [{
|
||||
bucketId: "gemini-weekly",
|
||||
displayName: "Weekly Limit Remaining",
|
||||
remainingFraction: 1,
|
||||
resetTime: "2026-09-15T00:00:00Z",
|
||||
}],
|
||||
buckets: [
|
||||
{
|
||||
bucketId: "gemini-weekly",
|
||||
displayName: "Weekly Limit Remaining",
|
||||
window: "weekly",
|
||||
remainingFraction: 0.75,
|
||||
resetTime: "2026-09-15T00:00:00Z",
|
||||
},
|
||||
{
|
||||
bucketId: "gemini-5h",
|
||||
displayName: "Five Hour Limit Remaining",
|
||||
window: "5h",
|
||||
remainingFraction: 1,
|
||||
resetTime: "2026-09-13T11:00:00Z",
|
||||
},
|
||||
],
|
||||
}],
|
||||
}),
|
||||
};
|
||||
}
|
||||
return { ok: false, status: 404 };
|
||||
return { ok: true, status: 200, json: async () => ({}) };
|
||||
});
|
||||
|
||||
const { getAntigravityUsage } = await import("../../open-sse/services/usage/google.js");
|
||||
const result = await getAntigravityUsage("token", {});
|
||||
const result = await getAntigravityUsage("token-exhausted", null);
|
||||
|
||||
// Per-model quota should show exhausted
|
||||
expect(result.quotas["gemini-3.8-flash-high"].remainingPercentage).toBe(0);
|
||||
// Weekly quota should be reconciled to 0% with the family reset time
|
||||
expect(result.quotas.gemini_weekly).toMatchObject({
|
||||
|
||||
// 5h session quota should be reconciled to 0% with the family reset time
|
||||
expect(result.quotas.gemini_session).toMatchObject({
|
||||
used: 1000,
|
||||
total: 1000,
|
||||
remainingPercentage: 0,
|
||||
resetAt: "2026-09-13T12:00:00.000Z",
|
||||
});
|
||||
|
||||
// Weekly quota should remain intact and NOT be clobbered to 0% or steal the 5h resetAt
|
||||
expect(result.quotas.gemini_weekly).toMatchObject({
|
||||
used: 250,
|
||||
total: 1000,
|
||||
remainingPercentage: 75,
|
||||
resetAt: "2026-09-15T00:00:00.000Z",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user