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:
1 parent
2719dd6ac3
commit
29a14c9a26
7 files changed
+157
-7
No files matched your search
+4
-2
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user