gui: an Antigravity card keeps its order when a refresh leaves out a pool's 5 hours (#860, huoranxuanyuan: 刷新额度有时会让5h和七天的顺序颠倒)

retrieveUserQuotaSummary sometimes sends a pool without its 5-hour bucket
(the reporter's Gemini, its week used up). Since #745 the pool's models then
keep their own 5-hour windows, shown as a family row; poolWindows put that
row after every pool, so the card read Gemini · 7 days, Claude & GPT · 5
hours, Claude & GPT · 7 days, Gemini, and on a refresh that sent the 5 hours,
Gemini · 5 hours, Gemini · 7 days, … — in the card's two columns the 5 hours
and the weeks swapped sides. poolWindows now puts a 5-hour-less pool's
family rows in its 5-hour place, before its week; a pool read whole is as
before.

quota-pool-order.test.cjs is the reporter's card (Gemini's week only,
Claude & GPT both): on the old code the Usage page lists Gemini last
(fails in Chromium and WebKit, en and zh); it passes now, with the panel's
rings Gemini, Gemini, Claude, Claude. quota-pools, quota-families,
usage-quota-head and routing-quota-left pass in both engines. Not tried
with a real Antigravity account answering a partial summary.
This commit is contained in:
yetone committed 2026-10-07 00:29:47 +08:00
1 parent 92469ee9f9
commit 8ef53f2ec8
2 files changed
+108 -4

No files matched your search

+13 -4
View File
@@ -11290,14 +11290,23 @@ function poolWindows(ws) {
return m ? m[1] * { hour: 1, day: 24, week: 168 }[m[2].toLowerCase()] : Infinity;
};
const first = (p) => { const i = FAMILY_FIRST.indexOf(p.split(/[ &]/)[0]); return i < 0 ? FAMILY_FIRST.length : i; };
const rest = ws.filter((w) => !isPool(w) && !(w.pool && pools.has(w.pool)));
let fam = familyWindows(rest);
const out = [];
for (const pool of [...pools.keys()].sort((a, b) => first(a) - first(b))) {
const members = ws.filter((w) => w.family && w.pool === pool);
for (const w of pools.get(pool).sort((a, b) => hours(a) - hours(b)))
out.push({ ...w, name: pool + " · " + t(w.name), window: w.name, members });
const own = pools.get(pool).sort((a, b) => hours(a) - hours(b));
// a pool read without its 5 hours (#745) keeps its models' own 5-hour
// windows as families: they go in the pool's place, before its week,
// so the card reads in the same order whether or not the summary sent
// the 5 hours this time (#860: 5h and 7d swapped after a refresh)
if (!own.some((w) => hours(w) === 5)) {
const mine = (w) => { const f = w.tiers ? w.name : w.family; return !!f && pool.toLowerCase().includes(f.toLowerCase()); };
out.push(...fam.filter(mine));
fam = fam.filter((w) => !mine(w));
}
for (const w of own) out.push({ ...w, name: pool + " · " + t(w.name), window: w.name, members });
}
const rest = ws.filter((w) => !isPool(w) && !(w.pool && pools.has(w.pool)));
const fam = familyWindows(rest);
return out.concat(fam);
}
// pooledModels: the models a whole account's card lists under "Every model"
@@ -0,0 +1,95 @@
// Run with Node's test runner and Playwright on the module path; see README.md.
// An Antigravity card keeps its order from one refresh to the next (#860,
// huoranxuanyuan: 刷新额度有时会让5h和七天的顺序颠倒). retrieveUserQuotaSummary
// sometimes leaves out a pool's 5-hour bucket (Gemini's, here, its week used
// up); magpie then keeps the Gemini models' own 5-hour windows as a family
// row (#745). That row used to go last, after Claude & GPT, so the card read
// Gemini · 7 days, Claude & GPT · 5 hours, Claude & GPT · 7 days, Gemini, and
// with the 5 hours back, Gemini · 5 hours, Gemini · 7 days, … — in the card's
// two columns the 5 hours and the weeks swapped sides. Now the family row
// takes the pool's 5-hour place, on the Usage page and in the panel's rings.
// Chromium and WebKit, English and Chinese; the API is faked here.
const assert = require("node:assert/strict");
const fs = require("node:fs/promises");
const path = require("node:path");
const { test } = require("node:test");
const { chromium, webkit } = require("playwright");
const assets = path.resolve(__dirname, "../assets");
const at = (h) => new Date(Date.now() + h * 36e5).toISOString();
const model = (name, family, pool, used, h) => ({ name, family, ...(pool ? { pool } : {}), used, resetsAt: at(h) });
const pool = (pool, name, used, h) => ({ name, pool, used, resetsAt: at(h) });
// the reporter's card: Gemini's week used up and no Gemini 5 hours in the
// summary, so the Gemini models carry no pool; Claude & GPT read whole
const partial = {
provider: "antigravity", name: "Antigravity", icon: "antigravity-color", user: "ada@example.com", plan: "Google AI Pro",
windows: [
model("Claude Opus 4.6 (Thinking)", "Claude", "Claude & GPT", 0, 5), model("Claude Sonnet 4.6", "Claude", "Claude & GPT", 0, 5),
model("Gemini 3 Flash", "Gemini", "", 100, 100), model("Gemini 3.1 Pro (High)", "Gemini", "", 100, 100),
model("GPT-OSS 120B (Medium)", "GPT-OSS", "Claude & GPT", 0, 5),
pool("Gemini", "7 days", 100, 100),
pool("Claude & GPT", "7 days", 0, 168), pool("Claude & GPT", "5 hours", 0, 5),
],
};
const antigravity = {
id: "antigravity", name: "Antigravity", icon: "antigravity-color", chat: "", responses: "", anthropic: "", catalog: "",
models: [{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", on: true }], agents: [], fallback: [], headers: {}, keyList: [],
account: { agent: "antigravity", agentName: "Antigravity", user: partial.user, plan: partial.plan, logins: [{ user: partial.user, plan: partial.plan, active: true, on: true }] },
};
function serve(lang, panel) {
const settings = { theme: "light", lang, tray: "panel", quotaLeft: false, currency: "usd" };
return async (route) => {
const url = new URL(route.request().url());
const json = (data) => route.fulfill({ json: data });
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:${!panel}};` });
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
if (url.pathname === "/api/state") return json({ agents: [], profiles: [], settings });
if (url.pathname === "/api/settings") return json(settings);
if (url.pathname === "/api/providers") return json({ providers: [antigravity], presets: [], excluded: [], gateway: { running: true, window: true } });
if (url.pathname === "/api/usage/quotas") return json([partial]);
if (url.pathname === "/api/login/usage") return json({ [partial.user]: partial });
if (url.pathname === "/api/groups") return json({ groups: [], models: [] });
if (url.pathname === "/api/plugins") return json({ plugins: [] });
if (url.pathname.startsWith("/api/")) return json({});
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
try { await route.fulfill({ body: await fs.readFile(file), contentType }); } catch { await route.fulfill({ status: 404, body: "" }); }
};
}
const words = { en: { five: "5 hours", week: "7 days" }, zh: { five: "5 小时", week: "7 天" } };
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
for (const lang of ["en", "zh"]) {
const w = words[lang];
test(`${engine} ${lang}: a pool without its 5 hours keeps its place`, async (t) => {
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
t.after(() => browser.close());
const errors = [];
const open = async (url, viewport) => {
const page = await (await browser.newContext({ viewport, reducedMotion: "reduce" })).newPage();
page.setDefaultTimeout(5000);
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", serve(lang, url.includes("mode=panel")));
await page.goto(url);
return page;
};
const page = await open("http://magpie.test/?view=usage", { width: 900, height: 700 });
const box = page.locator(".subscription-card", { hasText: "Antigravity" }).locator(".quota-windows").first();
await box.waitFor();
assert.deepEqual(await box.locator(".quota-labels > span:first-child").allTextContents(),
["Gemini", `Gemini · ${w.week}`, `Claude & GPT · ${w.five}`, `Claude & GPT · ${w.week}`],
"Gemini's 5-hour reading first, then its week, then Claude & GPT's");
const panel = await open("http://magpie.test/?mode=panel", { width: 440, height: 640 });
await panel.locator('#ptabs [data-ptab="usage"]').click();
const card = panel.locator(".pq-group", { hasText: "Antigravity" }).locator(".pq-card").first();
await card.locator(".pq-ring").first().waitFor();
const rings = await card.locator(".pq-rn").evaluateAll((es) => es.map((e) => e.textContent));
assert.equal(rings.length, 4);
assert(/^Gemini/.test(rings[0]) && /^Gemini/.test(rings[1]) && /^Claude/.test(rings[2]) && /^Claude/.test(rings[3]), "rings Gemini, Gemini, Claude, Claude: " + rings);
assert.deepEqual(errors, []);
});
}
}