Files
magpie/internal/provider/codex_switch_order_test.go
T
yetone 817a5f50d5 routing: In order keeps sending to the first account until it is used up or refused, and a tick keeps Codex or Claude Code signed in to the first account (#530: an account In order at 98% of its week, its five hours untouched, stopped getting requests; #524: "不允许切换首选账号的登录状态", the agent's own sign-in must stay on the account made first while the gateway still spreads requests) — since #209 magpie signs Codex and Claude Code in to the next ticked account with room once the one they are on reaches 98% (provider.SpentShare, the share Smart counts an account spent at), and since #408 back once the first is under 90%. The account the agent is signed in to is the one every routing tries first (the others stand behind it, AlsoOn), so In order the 98% switch put the second account ahead of the first, which is what #530 saw as the first being dropped; and the gateway counted an account at 98% as full whatever the routing, so a passing failure on it (a 502) rested it until its week renewed (39 hours there), not for the usual backoff. Now SpentShareOf gives 100 In order and 98 otherwise: the login switch moves the agent off an account In order only once a window of it is used up, the gateway's full (the rest after a failure, the quota rest's window) and affinity's "spent" go by the same share, and a vendor's 429 or usage-limit refusal still rests the account until it says it's back, as Smart does; a window used up is out, as before. Smart, In turn and Least used are unchanged. For #524, the subscription keeps KeepLogin (providers.json, beside Routing, kept across an editor Save; POST provider/keeplogin, only for Codex and Claude Code): with it on, magpie never moves the agent off the account it is signed in to, however used up, while the gateway still routes requests through magpie to the other ticked accounts; a pending return to the first (#408) still happens, that being the first. The tick sits at the foot of the Accounts list, under the rows where Make first is, shown only for Codex and Claude Code with more than one account on: "Keep {agent} signed in to the first account" / "{agent} 始终登录首选账号", its tooltip saying requests through magpie still go to the other ticked accounts. The Routing note follows it: kept, it says the agent stays signed in to the first whatever it has left; In order, that magpie moves it on once it is used up; otherwise the 98% wording as before. TestCodexLoginOrderedAndKept (In order stays at 98% and moves at 100%, back once renewed; Smart moves at 98%; kept, it stays at 100% and the setting survives a SetRouting; let go, it moves) and TestOrderedKeepsAnAccountAt98 (In order an account at 98% isn't full and a 502 rests it by backoff, Smart rests it to its week, a refusal rests it either way) fail without the change. keep-login.test.cjs (en and zh, Chromium and WebKit: the tick, its tooltip and Chinese strings, the post, the note when kept and In order, no tick with one account, the page doesn't move) fails without the UI and passes with it (12/12, Playwright 1.61); account-return, routing-account-order and account-arrange pass; click-scroll fails in Chromium here on origin/main too and passes in WebKit. No request was made to any vendor and no live agent config was touched. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeRunEndsWhenTheClientLeaves, TestClaudeAccountNamedAsItsLogin and TestClaudeIdentityServedWhileAsked failed once under the full run and pass alone). Turned on after magpie has moved the agent off the first, it goes back to it at once rather than once that has room again.
2026-10-02 15:48:16 +08:00

131 lines
3.9 KiB
Go

package provider
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
// In order, Codex stays signed in to the account it is on at 98% of its
// allowance, as the gateway still sends that one requests first, and moves
// on once it is used up (#530); Smart moves it at 98%, as before. Kept on
// the first (KeepLogin), it stays however used up that is, and moves on
// again once that is off (#524).
func TestCodexLoginOrderedAndKept(t *testing.T) {
home := signIn(t) // me@example.com, acct-1
rememberLogins(true)
codexSignIn(t, home, "spare@example.com", "r-spare")
rememberLogins(true)
codexSignIn(t, home, "work@example.com", "r-work")
rememberLogins(true)
if err := SetLoginOn("codex", "spare@example.com", true); err != nil {
t.Fatal(err)
}
used := map[string]float64{"acct-work@example.com": 98, "acct-1": 0, "acct-spare@example.com": 20}
fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{"plan_type": "plus", "rate_limit": map[string]any{
"primary_window": map[string]any{"used_percent": 0, "limit_window_seconds": 18000},
"secondary_window": map[string]any{"used_percent": used[r.Header.Get("chatgpt-account-id")], "limit_window_seconds": 604800}}})
}))
defer fake.Close()
old := CodexBase
CodexBase = fake.URL + "/backend-api/codex"
t.Cleanup(func() { CodexBase = old })
switched := func() string {
t.Helper()
loginUsageCache.Lock()
loginUsageCache.m = nil
loginUsageCache.Unlock()
to, err := SwitchWhenSpent(context.Background(), "codex")
if err != nil {
t.Fatal(err)
}
return to
}
active := func() string {
t.Helper()
for _, l := range Logins("codex") {
if l.Active {
return l.User
}
}
return ""
}
if active() != "work@example.com" {
t.Fatalf("signed in to %s", active())
}
// in order: at 98% it stays, used up it moves on
if err := SetRouting("codex", Ordered); err != nil {
t.Fatal(err)
}
if to := switched(); to != "" {
t.Fatalf("in order, moved off an account at 98%%, to %s", to)
}
used["acct-work@example.com"] = 100
if to := switched(); to != "spare@example.com" {
t.Fatalf("in order, used up: moved to %q", to)
}
// renewed, back on the first
used["acct-work@example.com"] = 0
if to := switched(); to != "work@example.com" {
t.Fatalf("back to %q", to)
}
// smart: at 98% it moves on, as before
if err := SetRouting("codex", ""); err != nil {
t.Fatal(err)
}
used["acct-work@example.com"] = 98
if to := switched(); to != "spare@example.com" {
t.Fatalf("smart at 98%%: moved to %q", to)
}
used["acct-work@example.com"] = 0
if to := switched(); to != "work@example.com" {
t.Fatalf("back to %q", to)
}
// kept on the first: it stays, used up or not; the setting outlives a
// save of the provider's picks
if err := SetKeepLogin("codex", true); err != nil {
t.Fatal(err)
}
if err := SetRouting("codex", ""); err != nil {
t.Fatal(err)
}
if p, err := Find("codex"); err != nil || !p.KeepLogin {
t.Fatalf("kept on the first: %+v %v", p, err)
}
used["acct-work@example.com"] = 100
if to := switched(); to != "" {
t.Fatalf("kept on the first, moved to %s", to)
}
if active() != "work@example.com" {
t.Fatalf("kept on the first, signed in to %s", active())
}
// let go: it moves on again
if err := SetKeepLogin("codex", false); err != nil {
t.Fatal(err)
}
if to := switched(); to != "spare@example.com" {
t.Fatalf("let go: moved to %q", to)
}
// kept on the first again: back on it at once, used up as it is
if err := SetKeepLogin("codex", true); err != nil {
t.Fatal(err)
}
if to := switched(); to != "work@example.com" {
t.Fatalf("kept on the first again: moved to %q", to)
}
if to := switched(); to != "" {
t.Fatalf("kept on the first again, then moved to %s", to)
}
// only Codex's and Claude Code's sign-ins are moved
if err := SetKeepLogin("nonesuch", true); err == nil {
t.Fatal("kept a provider that isn't there")
}
}