quota: pool windows stand in for their models' in the alerts and the CLI (#523)

PooledWindows is how the usage page shows an Antigravity account's windows:
a pool's 5-hour and weekly ones, named with their pool ("Gemini · 7 days"),
in place of the per-model windows drawing on it. dueAlerts, the CLI's
accountRows and QuotaReport walked the raw per-model windows instead: a
pool raised an alert for every model in it and again for its 7-day
window, "magpie accounts" and "magpie quota" (its --json and the
gateway's /v1/magpie/quotas with it) listed a row per model, and whether
a model's name carried the pool in front depended on the name's own
words ("Gemini 3 Flash" did, "Claude Opus 4.6 (Thinking)" didn't). All
three go through PooledWindows now — one alert per pool span, one cell
per pool window, the pool already in the name. A pool's weekly window is
aside (using it up doesn't stop the account) but is still told of; the
pools' 5-hour asides aren't. The alert key keeps its shape: the pooled
name in it keeps two pools apart, and a window with no pool keys as it
did, so what an earlier build already said isn't said again — the pool
suffix the last draft appended is gone.

QuotaReport's card assembly is quotaReport(subs, plans, balances, now),
which tests feed directly, and LoginUsageVia (as UsageClaudeVia) stands
in for LoginUsage's readings, so accountRows is testable too.

TestDueAlertsPoolWindows feeds dueAlerts the windows Antigravity really
reports (the shapes TestAntigravityQuotaPools records): every model's
own, then each pool's 5-hour and weekly ones. On the old code it fails
with six alerts where two belong, per model with the pool sometimes in
front; now the two weekly pools are told of once each, "Gemini · 7 days"
and "Claude & GPT · 7 days", their marks distinct, a second reading
saying nothing, and a mark an earlier build wrote of a window with no
pool still holding. TestQuotaReportPoolsAntigravity fails on the raw
windows with eight spans where four pooled ones belong; TestQuotaCell,
TestAccountRowsPoolsAntigravity and TestQuotaCell read pooled names.
QuotaSpan keeps its Pool for magpie quota --json.

go vet ./... and builds -tags nogui (windows, GOOS=linux, GOOS=darwin)
pass; go test -tags nogui -count=1 ./... under a temp HOME passes for
every package this touches (provider, the gateway and the CLI and TUI
packages among them) — agent, gateway, library and sessions fail a few
of their own tests on this Windows machine the same way on main, paths
and WSL probes its Linux and macOS runs don't see.

Co-authored-by: werldl517-cyber <werldl517-cyber@users.noreply.github.com>
This commit is contained in:
werldl517-cyberandwerldl517-cyber authored and GitHub committed 2026-10-04 01:44:27 +08:00
1 parent 2719dd6ac3
commit 29a14c9a26
7 files changed
+157 -7

No files matched your search

+4 -2
View File
@@ -232,8 +232,10 @@ func accountRows(ls []provider.Login, now time.Time) []accountRow {
r.Plan = q.Plan
}
r.Error, r.Resets, r.AsOf = q.Error, q.Resets, q.AsOf
for _, w := range q.Windows {
s := quotaSpan{Name: w.Name, Used: w.Used, Remaining: max(0, 100-w.Used), ResetsAt: w.ResetsAt, Display: w.Display}
// a pool's own windows stand in for the models' drawing on it,
// as the usage page shows them
for _, w := range provider.PooledWindows(q.Windows) {
s := quotaSpan{Name: w.Name, Used: w.Used, Remaining: max(0, 100-w.Used), ResetsAt: w.ResetsAt, Display: w.Display, Pool: w.Pool}
if s.ResetsAt == nil && w.ResetSecs > 0 {
t := now.Add(time.Duration(w.ResetSecs) * time.Second)
s.ResetsAt = &t
+41
View File
@@ -122,4 +122,45 @@ func TestQuotaCell(t *testing.T) {
if !strings.HasPrefix(got, "5h 42%") || !strings.Contains(got, "↻2h13m "+provider.ResetClock(at, time.Now())) {
t.Fatalf("%q", got)
}
// a pool's own window comes in named with its pool (PooledWindows) and
// reads compactly, without the " · "
pooled := quotaCell(quotaSpan{Name: "Gemini · 7 days", Pool: "Gemini", Used: 80})
if !strings.HasPrefix(pooled, "Gemini 7d 80%") {
t.Fatalf("pooled: %q", pooled)
}
}
// accountRows shows a pool's own windows in place of its models', as the
// usage page does; reverting it to the raw per-model windows fails here.
func TestAccountRowsPoolsAntigravity(t *testing.T) {
now := time.Date(2026, 10, 1, 9, 0, 0, 0, time.UTC)
reset := now.Add(48 * time.Hour)
five, week := 5*time.Hour, 7*24*time.Hour
provider.LoginUsageVia(func(context.Context, string) map[string]provider.SubscriptionQuota {
return map[string]provider.SubscriptionQuota{"u@x.com": {
Provider: "antigravity", Name: "Antigravity", User: "u@x.com",
Windows: []provider.QuotaWindow{
{Name: "Gemini 3 Flash", Model: "gemini-3-flash", Family: "Gemini", Pool: "Gemini", Used: 85, ResetsAt: &reset},
{Name: "Gemini 3.1 Pro (High)", Model: "gemini-3.1-pro-high", Family: "Gemini", Pool: "Gemini", Used: 85, ResetsAt: &reset},
{Name: "Claude Opus 4.6 (Thinking)", Model: "claude-opus-4-6-thinking", Family: "Claude", Pool: "Claude & GPT", Used: 90},
{Name: "GPT-OSS 120B (Medium)", Model: "gpt-oss-120b-medium", Family: "GPT-OSS", Pool: "Claude & GPT", Used: 90},
{Name: "7 days", Pool: "Gemini", Span: week, Aside: true, Used: 85, ResetsAt: &reset},
{Name: "5 hours", Pool: "Gemini", Span: five, Aside: true, Used: 95, ResetsAt: &reset},
{Name: "7 days", Pool: "Claude & GPT", Span: week, Aside: true, Used: 90, ResetsAt: &reset},
{Name: "5 hours", Pool: "Claude & GPT", Span: five, Aside: true, Used: 90, ResetsAt: &reset},
},
}}
})
t.Cleanup(func() { provider.LoginUsageVia(nil) })
rows := accountRows([]provider.Login{{Agent: "antigravity", User: "u@x.com", Active: true, On: true}}, now)
if len(rows) != 1 {
t.Fatalf("%d rows", len(rows))
}
var names []string
for _, w := range rows[0].Windows {
names = append(names, w.Name)
}
if strings.Join(names, ",") != "Gemini · 7 days,Gemini · 5 hours,Claude & GPT · 7 days,Claude & GPT · 5 hours" {
t.Fatalf("windows %v", names)
}
}
+12
View File
@@ -22,10 +22,22 @@ type loginUsageEntry struct {
q SubscriptionQuota
}
// loginUsageFor, when set, stands in for LoginUsage's readings (tests),
// as UsageClaudeVia does for Claude's own.
var loginUsageFor func(ctx context.Context, agent string) map[string]SubscriptionQuota
// LoginUsageVia has tests stand in for LoginUsage's readings.
func LoginUsageVia(f func(ctx context.Context, agent string) map[string]SubscriptionQuota) {
loginUsageFor = f
}
// LoginUsage is the allowance used by each of an agent's accounts, by
// user. What was fetched less than a minute ago comes from the cache; the
// rest is asked for at once, as long as ctx allows.
func LoginUsage(ctx context.Context, agent string) map[string]SubscriptionQuota {
if loginUsageFor != nil {
return loginUsageFor(ctx, agent)
}
out := map[string]SubscriptionQuota{}
if agent == "grok" {
if _, ok := pluginOfAgent(agent); !ok {
+4 -2
View File
@@ -68,8 +68,10 @@ func dueAlerts(qs []SubscriptionQuota, marks map[string]alertMark, pct int, bal
}
continue
}
for _, w := range q.Windows {
if w.Aside {
// the pools' own windows stand in for the models' drawing on them,
// so a pool is told of once, not once per model
for _, w := range PooledWindows(q.Windows) {
if w.Aside && !(w.Pool != "" && w.Span == 7*24*time.Hour) {
continue // using it up doesn't stop the account
}
key := alertPrefix(q) + "w|" + w.Name + "|" + w.Model
+51
View File
@@ -146,3 +146,54 @@ func TestCheckAlertsKeepsMarks(t *testing.T) {
t.Fatalf("told again after the file was read back: %+v", got)
}
}
// the windows Antigravity really reports (TestAntigravityQuotaPools): every
// model's own beside the pools' 5-hour and weekly ones. A pool's weekly
// window is what should be told of, once per pool and named with it — not
// once for every model drawing on it, and not the 5-hour asides.
func TestDueAlertsPoolWindows(t *testing.T) {
now := time.Date(2026, 10, 1, 9, 0, 0, 0, time.UTC)
reset := now.Add(48 * time.Hour)
five, week := 5*time.Hour, 7*24*time.Hour
qs := []SubscriptionQuota{{
Provider: "antigravity",
Name: "Antigravity",
User: "u@x.com",
Windows: []QuotaWindow{
// the models' own, as fetchAvailableModels reports them, each
// drawing on its pool
{Name: "Gemini 3 Flash", Model: "gemini-3-flash", Family: "Gemini", Pool: "Gemini", Used: 85, ResetsAt: &reset},
{Name: "Gemini 3.1 Pro (High)", Model: "gemini-3.1-pro-high", Family: "Gemini", Pool: "Gemini", Used: 85, ResetsAt: &reset},
{Name: "Claude Opus 4.6 (Thinking)", Model: "claude-opus-4-6-thinking", Family: "Claude", Pool: "Claude & GPT", Used: 90},
{Name: "GPT-OSS 120B (Medium)", Model: "gpt-oss-120b-medium", Family: "GPT-OSS", Pool: "Claude & GPT", Used: 90},
// the pools' own, as retrieveUserQuotaSummary reports them:
// aside, naming no model
{Name: "7 days", Pool: "Gemini", Span: week, Aside: true, Used: 85, ResetsAt: &reset},
{Name: "5 hours", Pool: "Gemini", Span: five, Aside: true, Used: 95, ResetsAt: &reset},
{Name: "7 days", Pool: "Claude & GPT", Span: week, Aside: true, Used: 90, ResetsAt: &reset},
{Name: "5 hours", Pool: "Claude & GPT", Span: five, Aside: true, Used: 90, ResetsAt: &reset},
},
}}
marks := map[string]alertMark{}
got := dueAlerts(qs, marks, 80, 0, now)
if len(got) != 2 {
t.Fatalf("got %d alerts, want one per pool: %+v", len(got), got)
}
if got[0].Window != "Gemini · 7 days" || got[1].Window != "Claude & GPT · 7 days" {
t.Errorf("named %q, %q", got[0].Window, got[1].Window)
}
if len(marks) != 2 {
t.Errorf("the pools' marks ran together: %v", marks)
}
// told once: the same readings again say nothing
if again := dueAlerts(qs, marks, 80, 0, now); len(again) != 0 {
t.Errorf("told again: %+v", again)
}
// a mark an earlier build wrote of a window with no pool still holds
legacy := map[string]alertMark{"codex|c@x.com|w|5 hours|": {At: now, Until: &reset}}
codex := []SubscriptionQuota{{Provider: "codex", User: "c@x.com",
Windows: []QuotaWindow{{Name: "5 hours", Used: 90, ResetsAt: &reset}}}}
if told := dueAlerts(codex, legacy, 80, 0, now); len(told) != 0 {
t.Errorf("the old mark no longer holds: %+v", told)
}
}
+11 -3
View File
@@ -100,11 +100,17 @@ type QuotaSpan struct {
Amount float64 `json:"amount,omitempty"`
Limit float64 `json:"limit,omitempty"`
Unit string `json:"unit,omitempty"`
Pool string `json:"pool,omitempty"` // the shared pool the window draws on ("Gemini")
}
// QuotaReport is Quotas as Quota, the reset times made absolute from now.
func QuotaReport(ctx context.Context, now time.Time) []Quota {
subs, plans, balances := quotas(ctx)
return withServed(quotaReport(subs, plans, balances, now), LastServed())
}
// quotaReport assembles the cards from readings the tests feed directly.
func quotaReport(subs, plans, balances []SubscriptionQuota, now time.Time) []Quota {
out := []Quota{}
for _, g := range []struct {
kind string
@@ -113,9 +119,11 @@ func QuotaReport(ctx context.Context, now time.Time) []Quota {
for _, q := range g.qs {
r := Quota{Provider: q.Provider, Name: q.Name, Kind: g.kind, Plan: q.Plan, User: q.User, AsOf: q.AsOf,
Windows: []QuotaSpan{}, Balance: q.Balance, Error: q.Error, Until: q.Until, Renew: q.Renew, Resets: q.Resets}
for _, w := range q.Windows {
// a pool's own windows stand in for the models' drawing on it,
// as the usage page shows them
for _, w := range PooledWindows(q.Windows) {
s := QuotaSpan{Unlimited: w.Unlimited, Name: w.Name, Used: w.Used, Remaining: max(0, 100-w.Used), ResetsAt: w.ResetsAt, Display: w.Display,
Amount: w.Amount, Limit: w.Limit, Unit: w.Unit}
Amount: w.Amount, Limit: w.Limit, Unit: w.Unit, Pool: w.Pool}
if s.ResetsAt == nil && w.ResetSecs > 0 {
t := now.Add(time.Duration(w.ResetSecs) * time.Second)
s.ResetsAt = &t
@@ -125,7 +133,7 @@ func QuotaReport(ctx context.Context, now time.Time) []Quota {
out = append(out, r)
}
}
return withServed(out, LastServed())
return out
}
// ResetClock is when a window starts again, on the clock: "14:30" today,
+34
View File
@@ -1,6 +1,7 @@
package provider
import (
"strings"
"testing"
"time"
)
@@ -49,3 +50,36 @@ func TestPlanLeftOutOfACopy(t *testing.T) {
t.Fatalf("%+v", plans)
}
}
// magpie quota, its --json and the gateway show a pool's own windows in
// place of the models' drawing on it (the shapes TestAntigravityQuotaPools
// records): one span per pool window, the pool in its name.
func TestQuotaReportPoolsAntigravity(t *testing.T) {
now := time.Date(2026, 10, 1, 9, 0, 0, 0, time.UTC)
reset := now.Add(48 * time.Hour)
five, week := 5*time.Hour, 7*24*time.Hour
antigravity := SubscriptionQuota{Provider: "antigravity", Name: "Antigravity", User: "u@x.com",
Windows: []QuotaWindow{
{Name: "Gemini 3 Flash", Model: "gemini-3-flash", Family: "Gemini", Pool: "Gemini", Used: 85, ResetsAt: &reset},
{Name: "Gemini 3.1 Pro (High)", Model: "gemini-3.1-pro-high", Family: "Gemini", Pool: "Gemini", Used: 85, ResetsAt: &reset},
{Name: "Claude Opus 4.6 (Thinking)", Model: "claude-opus-4-6-thinking", Family: "Claude", Pool: "Claude & GPT", Used: 90},
{Name: "GPT-OSS 120B (Medium)", Model: "gpt-oss-120b-medium", Family: "GPT-OSS", Pool: "Claude & GPT", Used: 90},
{Name: "7 days", Pool: "Gemini", Span: week, Aside: true, Used: 85, ResetsAt: &reset},
{Name: "5 hours", Pool: "Gemini", Span: five, Aside: true, Used: 95, ResetsAt: &reset},
{Name: "7 days", Pool: "Claude & GPT", Span: week, Aside: true, Used: 90, ResetsAt: &reset},
{Name: "5 hours", Pool: "Claude & GPT", Span: five, Aside: true, Used: 90, ResetsAt: &reset},
}}
got := quotaReport([]SubscriptionQuota{antigravity}, nil, nil, now)
var names []string
for _, w := range got[0].Windows {
names = append(names, w.Name)
}
if strings.Join(names, ",") != "Gemini · 7 days,Gemini · 5 hours,Claude & GPT · 7 days,Claude & GPT · 5 hours" {
t.Fatalf("windows %v", names)
}
// the pool's own use, label and reset reach the span
w := got[0].Windows[0]
if w.Used != 85 || w.Pool != "Gemini" || w.Remaining != 15 || w.ResetsAt == nil || !w.ResetsAt.Equal(reset) {
t.Fatalf("span %+v", w)
}
}