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:
1 parent
92469ee9f9
commit
8ef53f2ec8
2 files changed
+108
-4
No files matched your search
@@ -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, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user