Files
magpie/internal/agent/codex_connected_test.go
forever a7c4e5ddde provider: a Codex account the Codex app still sends for keeps Codex signed in — spend cap or overage with credits, limit_reached alone; a hold survives a failed read (#996)
* provider: a Codex account at a spend cap or past its overage with credits left keeps Codex signed in, as the Codex app's composer still sends for it

#966 held such an account (and one whose limit_reached alone was true),
following the readiness check Tht in ChatGPT.app 26.930. Tht has one
caller, the work-credit-restriction-recovery poller; it isn't the
composer's gate. The composer's submit is disabled only by hP (search
"n.rate_limit?.allowed!==!1||Jpe(n)||SIe(") and hardBlocked (search
"hardBlocked:r.rate_limit?.allowed===!1"): both read rate_limit.allowed
alone and let credits (has_credits or unlimited) through before any spend
cap or overage. So the app sends for that account, and holding it gains
nothing: CodexUsedUp only moves Codex to magpie as provider while Codex is
on a magpie model, whose turns already reach magpie through
openai_base_url. It only takes the app out of ChatGPT (durable threads,
remote control), the loss #966 set out to stop.

codexHeld is back to that gate: held when allowed is false, no credits,
and not a workspace within its overage (no spend cap, an ordinary
reason). Its comment names the build, both gates and why Tht isn't one,
each with a string to search for. The spend-cap and overage subcases now
expect not held; new subcases pin allowed as the only trigger. Five
subcases fail on the merged codexHeld. agent-wiring.md and reference.md
say the same.

Follow-up to #966.

* provider: a Sci, Law, FinServ or Enterprise trial/view-only workspace within its overage keeps Codex signed in

codexHeld lets a workspace with no credits through while it is within
its overage, as the Codex app's composer does, but the workspace list
came from codex-rs's PlanType alone. The app's own plan_type switch for
its usage banner (ChatGPT.app 26.930.61225, app-initial) also names sci,
law, finserv, enterprise_cbp_trial and enterprise_cbp_view_only, so such
an account read held and magpie took Codex off its ChatGPT sign-in while
the app still sent. They are added; the comment cites that switch. guest
there is a personal plan and stays out, and a plan not listed still
counts as no workspace (held).

Verification: "Law without credits, within its overage" in
TestCodexUsedUpIsWhatTheAppHolds fails without the list change
(CodexUsedUp = true, want false) and passes with it.

* provider: a held Codex reading with no window at 100% stays held through a failed read

A reading kept for when /wham/usage fails dropped Held whenever none of
its windows read 100%. That is right for a hold that came with a used-up
window which has since started again, but a hold with no window used up
(the backend says not allowed, no credits, every window under 100%) has
no reset to wait for: the first 503 after it read not held and put Codex
back on its ChatGPT sign-in for an account its app sends nothing for.
Held is now cleared only when the saved reading had a window used up and
none is after the passed resets are applied; any other hold stands till
a fresh read.

Verification: TestCodexHeldWithNoWindowUpKeptTillAFreshRead (the real Pro
reply, credits spent, its window at 40%: a good read, then a 503) fails
without the change ("CodexUsedUp = false on the kept held reading, no
window used up") and passes with it. TestCodexHeldKeptOnlyTillTheReset
still passes; both share their setup now. -race -count=10 passes.

* agent, gui, docs: Codex's sign-in notes say magpie steps in while the Codex app blocks the account, not whenever it is used up

#981's sign-in notes said magpie becomes Codex's provider "while its
account is used up". Since codexHeld, a ChatGPT account at 100% with
credits left keeps Codex signed in; magpie steps in only while the Codex
app blocks the account (out of allowance, no credits left), and the notes
now say that, Always ChatGPT's to match, in English, Chinese, Japanese
and German. agent-wiring.md's sign-in line and reference.md's Codex
paragraph say when the app holds an account as codexHeld reads it: OpenAI
no longer allows it and it has no credits left, a workspace still within
its overage excepted; at 100% with credits Codex stays signed in.

Verification: codex-signin-always (Chromium and WebKit, en/zh/ja/de, 1100
and 440px) 8/8, i18n-keys 1/1, gui-ja 3/3, gui-de 3/3. git grep finds the
old English notes nowhere under internal/gui/tests or internal/gui/assets.
2026-10-06 15:28:41 +08:00

83 lines
3.1 KiB
Go

package agent
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/yetone/magpie/internal/edit"
)
// Codex signed in with ChatGPT, connected on one of magpie's models beside
// its sign-in, stays connected when Codex writes one of its own models in
// (its /model, or a Codex app still running on its old pick): its own
// models are in its list beside magpie's, as when it joined (#940: Codex
// read as not connected then, its config still on magpie's gateway).
// Disconnect leaves it on that model. Made magpie's provider (the Codex
// app blocking its account), it is not beside the sign-in.
func TestCodexOwnPickAfterMagpieModelStaysConnected(t *testing.T) {
home, read := codexHome(t, `{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`, "model = \"gpt-5.5\"\n")
cx := codex(home)
if err := cx.Apply("model", "fake/m1"); err != nil {
t.Fatal(err)
}
if !cx.Wired() {
t.Fatalf("not connected on fake/m1:\n%s", read())
}
if err := edit.SetTOMLTop(filepath.Join(home, ".codex", "config.toml"), edit.KV{Path: "model", Value: "gpt-6.1-sol"}); err != nil {
t.Fatal(err)
}
if !cx.Wired() {
t.Fatalf("Codex's own pick disconnected it:\n%s", read())
}
// told as a change from outside, the row still connected
if d := cx.Drift(); d == nil || d.Kind != "replaced" || d.Now != "gpt-6.1-sol" {
t.Fatalf("drift: %+v", d)
}
if err := cx.Disconnect(); err != nil {
t.Fatal(err)
}
if cfg := read(); cx.Wired() || strings.Contains(cfg, "openai_base_url") || !strings.Contains(cfg, `model = "gpt-6.1-sol"`) {
t.Fatalf("disconnected:\n%s", cfg)
}
// out of its allowance, magpie is its provider: an own model written
// back is a change from outside, not a pick beside magpie's
codexUsedUp = func() bool { return true }
if err := cx.Fields[0].Set("fake/m1"); err != nil {
t.Fatal(err)
}
path := filepath.Join(home, ".codex", "config.toml")
if err := edit.SetTOMLTop(path, edit.KV{Path: "model", Value: "gpt-6.1-sol"}); err != nil {
t.Fatal(err)
}
if err := edit.DelTOMLTop(path, "model_provider"); err != nil {
t.Fatal(err)
}
if cx.Wired() {
t.Fatalf("still beside the sign-in after magpie became its provider:\n%s", read())
}
}
// Connected while Codex can't join (the Codex app blocking its ChatGPT
// account, or a provider of its own in config.toml), Codex stays on the
// model it was on as magpie serves it on the ChatGPT account, not the first
// of magpie's other models (#940: it went to Grok 4.7, "magpie doesn't
// serve the model it was on").
func TestCodexConnectKeepsItsOwnModelThroughMagpie(t *testing.T) {
home, read := codexHome(t, `{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`, "model = \"gpt-b\"\n")
os.WriteFile(filepath.Join(home, ".codex", "models_cache.json"), []byte(`{"models":[
{"slug":"gpt-a","display_name":"A","priority":1},
{"slug":"gpt-b","display_name":"B","priority":2}]}`), 0o644)
codexUsedUp = func() bool { return true }
cx := codex(home)
c, err := cx.ConnectHow()
if err != nil {
t.Fatal(err)
}
if c.How != "same" || c.Value != "codex/gpt-b" || !strings.Contains(read(), `model = "codex/gpt-b"`) {
t.Fatalf("connected %+v:\n%s", c, read())
}
}