From f56d1054955fabc06bea03317c8d3cfdb68e2fe5 Mon Sep 17 00:00:00 2001 From: Bo Zou Date: Thu, 1 Oct 2026 14:43:13 +0800 Subject: [PATCH] provider: a Copilot account signed in from magpie gets its allowance card, the editors' sign-in or not (#386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Usage page's Copilot card was asked only when copilotLogin(cfg) found the editors' or the standalone CLI's own GitHub sign-in (~/.config/github-copilot/apps.json or hosts.json, else ~/.copilot/config.json). An account signed in from magpie itself is kept in logins.json, and copilotLogin never looks there: with no editor and no Copilot CLI signed in on the machine, no Copilot allowance was fetched at all. The Usage page, the menu bar's usage picker and the gateway's GET /v1/magpie/quotas had no Copilot card, while `magpie accounts` did show one (that path reads copilotLoginList through builtinLogins, login_usage.go) and quotas.json kept a fresh reading written by it — the account was known and readable, only never asked where the page looks. copilotLoginList() covers both the editors'/CLI's own account and the accounts magpie signed in (copilot_accounts.go), and loginQuota resolves each account's own token by user, so the fetch is scheduled from that list now, as Kiro's, ZCode's, Qoder's, Zed's and WorkBuddy's already are. The single-account branch this replaces called copilotSubscriptionUsage with the editors' token for the same account, which perLogin reaches the same way. TestCopilotQuotaWithoutEditorsSignIn fails without the change ("no Copilot card: an account signed in from magpie alone was left out") and passes with it. go vet ./internal/provider/ and the internal/provider package's tests pass. --- internal/provider/subscription_usage.go | 19 ++++--- internal/provider/subscription_usage_test.go | 55 ++++++++++++++++++++ 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/internal/provider/subscription_usage.go b/internal/provider/subscription_usage.go index c2dc2007..7ee6803e 100644 --- a/internal/provider/subscription_usage.go +++ b/internal/provider/subscription_usage.go @@ -252,16 +252,15 @@ func fetchSubscriptionUsage() []SubscriptionQuota { fetches = append(fetches, withUser(p.Account.User, func() SubscriptionQuota { return codexSubscriptionUsage(viaLogin("codex", p.Account.User), auth) })) } } - cfg := os.Getenv("XDG_CONFIG_HOME") - if cfg == "" { - cfg = filepath.Join(home, ".config") - } - if app, ok := copilotLogin(cfg); ok && !hidden["copilot"] { - if ls := copilotLoginList(); len(ls) > 1 { - fetches = append(fetches, perLogin(via("copilot"), ls, "Copilot", "githubcopilot")...) - } else { - fetches = append(fetches, withUser(app.User, func() SubscriptionQuota { return copilotSubscriptionUsage(viaLogin("copilot", app.User), app.Token) })) - } + // Every Copilot account magpie knows, the editors' or the CLI's own + // sign-in or not: one signed in from magpie alone is enough + // (copilotLoginList reads both, copilot_accounts.go). Asking only + // when copilotLogin found the editors' token left an account magpie + // signed in itself without a card on the Usage page, and out of the + // gateway's GET /v1/magpie/quotas, however fresh its reading was + // (subscription_usage_test.go, TestCopilotQuotaWithoutEditorsSignIn). + if ls := copilotLoginList(); len(ls) > 0 && !hidden["copilot"] { + fetches = append(fetches, perLogin(via("copilot"), ls, "Copilot", "githubcopilot")...) } } if moved("kiro") { diff --git a/internal/provider/subscription_usage_test.go b/internal/provider/subscription_usage_test.go index 76adc2fe..d13b1c16 100644 --- a/internal/provider/subscription_usage_test.go +++ b/internal/provider/subscription_usage_test.go @@ -2,6 +2,10 @@ package provider import ( "context" + "net/http" + "net/http/httptest" + "os" + "path/filepath" "testing" "time" ) @@ -119,3 +123,54 @@ func TestChosenWindows(t *testing.T) { t.Fatalf("antigravity: %+v", got) } } + +// A Copilot account magpie signed in itself gets its card even when the +// editors' own sign-in isn't on the machine: every account magpie knows is +// asked, not only the one the editors' or the CLI's token belongs to. +func TestCopilotQuotaWithoutEditorsSignIn(t *testing.T) { + home := signIn(t) + t.Setenv("COPILOT_HOME", filepath.Join(home, ".copilot")) // not the machine's own CLI, when it has one + // the editors' own sign-in gone: what magpie keeps is all that's left + for _, dir := range []string{filepath.Join(home, ".config", "github-copilot"), filepath.Join(home, ".copilot")} { + if err := os.RemoveAll(dir); err != nil { + t.Fatal(err) + } + } + if _, ok := copilotLogin(filepath.Join(home, ".config")); ok { + t.Fatal("the editors' own sign-in is still readable") + } + if err := addCopilotLogin("hubot", "Pro+", "ghu_hubot"); err != nil { + t.Fatal(err) + } + if ls := copilotLoginList(); len(ls) != 1 || ls[0].User != "hubot" { + t.Fatalf("copilotLoginList = %+v", ls) + } + + old := CopilotUserURL + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "token ghu_hubot" { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.Write([]byte(`{"copilot_plan":"individual_pro","quota_snapshots":{"premium_interactions":{"has_quota":true,"entitlement":7000,"quota_remaining":6996}}}`)) + })) + defer srv.Close() + CopilotUserURL = srv.URL + t.Cleanup(func() { CopilotUserURL = old }) + + var card *SubscriptionQuota + for _, q := range fetchSubscriptionUsage() { + if q.Provider == "copilot" { + card = &q + } + } + if card == nil { + t.Fatal("no Copilot card: an account signed in from magpie alone was left out") + } + if card.User != "hubot" || card.Plan != "individual_pro" || len(card.Windows) != 1 { + t.Fatalf("Copilot card = %+v", *card) + } + if w := card.Windows[0]; w.Name != "Premium requests" || w.Display != "4 / 7000" { + t.Fatalf("window = %+v", w) + } +}