Files
magpie/internal/agent/codex_thread_test.go
T
yetone 528d6c12dc agent, gui: Codex left on model_provider = "magpie" with no [model_providers.magpie] loads its config again, and connecting with nothing in magpie says what to do, in the reader's language (Tystem on Discord) — the Codex app said "无法加载 config.toml … Model provider magpie not found", magpie showed Codex as not connected on "deepseek-flash", and its switch answered only "Codex can't be connected to magpie", in English. Codex looks model_provider up in [model_providers.*] as it loads config.toml (codex-rs: "Model provider {id} not found"), so the key without the table keeps it from loading any config, a new thread too, not just an old one. magpie never writes one without the other: it takes model_provider out whenever it steps out as the provider (disconnect, Unwire, a model of Codex's own, Join) and keeps the table so the threads started on magpie still open. That is unchanged, and TestCodexSteppingOutLeavesNoHalfState now covers it. Another tool or a hand edit had taken the table away. Sync, at magpie's start and whenever the catalog changes, now writes the table back whenever the config names magpie as its provider, at the top or in a profile, and has no table, whether Codex is connected or not. Check reports that state too. A config magpie can't parse is still only reported, never written to. A Connect that finds none of magpie's models because no provider or subscription has been added answers agent.NoModelsError, "Add a provider or subscription in magpie first, then connect Codex". /api answers it with code no_models and the agent's name, which app.js says through t() (en/zh/ja/de). docs/reference.md no longer says the table goes with a native model. TestCodexProviderWithoutTable (top and profile: Check says so, Sync writes the table) and TestConnectWithNoModels in internal/agent, TestAgentConnectNoModels in internal/gui, and agent-connect-no-models.test.cjs (WebKit, en/zh/ja/de: the toast in the reader's language, the switch back off, no scroll, no native dialog) are new and fail without the change; the latter's en case passes either way, as the server's message is English. Chromium wouldn't launch on this machine for this test or the existing agent-connect.test.cjs. TestCodexSyncAddsBaseURLToProvider now expects the table added to a hand-made provider that had none. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. In the first full run, internal/gui's TestProvidersAnswerWhileListsComeIn and internal/proc's TestUserPath failed under load; both passed alone and in the second full run.
2026-10-05 21:09:13 +08:00

124 lines
4.4 KiB
Go

package agent
import (
"os"
"path/filepath"
"strings"
"testing"
)
// #259: magpie as Codex's provider, a thread started on Codex's built-in
// provider is opened on it again (Codex keeps a thread's provider), and a
// magpie model picked there went to the ChatGPT backend, which refused it
// ("… is not supported when using Codex with a ChatGPT account"). The base
// URL is magpie's now in that state too, so the built-in provider's request
// reaches magpie; back on Codex's own model, it goes — but on magpie API,
// where Codex's own model goes through magpie on its ChatGPT account and
// magpie stays wired (#701).
func TestCodexProviderAlsoSetsBaseURL(t *testing.T) {
for _, tc := range []struct {
name, auth string
api bool
}{
{"signed out", "", false},
{"api", `{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`, true},
} {
t.Run(tc.name, func(t *testing.T) {
home, read := codexHome(t, tc.auth, "model = \"gpt-5.5\"\n")
cx := codex(home)
if tc.api {
os.WriteFile(filepath.Join(home, ".codex", "models_cache.json"), []byte(`{"models":[{"slug":"gpt-5.4","display_name":"5.4","priority":1}]}`), 0o644)
if err := cx.Field("login").Set("api"); err != nil {
t.Fatal(err)
}
}
if err := cx.Fields[0].Set("fake/m1"); err != nil {
t.Fatal(err)
}
cfg := read()
if !strings.Contains(cfg, `model_provider = "magpie"`) || !strings.Contains(cfg, "model_catalog_json") ||
!strings.Contains(cfg, `openai_base_url = "`+codexGatewayURL()+`"`) {
t.Fatalf("\n%s", cfg)
}
if msg := cx.Check(); msg != "" {
t.Fatalf("check: %s", msg)
}
if err := cx.Fields[0].Set("gpt-5.4"); err != nil {
t.Fatal(err)
}
if tc.api {
if cfg = read(); !strings.Contains(cfg, `openai_base_url = "`+codexGatewayURL()+`"`) ||
!strings.Contains(cfg, `model_provider = "magpie"`) || !strings.Contains(cfg, `model = "codex/gpt-5.4"`) {
t.Fatalf("own model on magpie API:\n%s", cfg)
}
return
}
if cfg = read(); strings.Contains(cfg, "openai_base_url") || strings.Contains(cfg, "model_provider =") ||
!strings.Contains(cfg, `model = "gpt-5.4"`) {
t.Fatalf("own model:\n%s", cfg)
}
})
}
}
// A base URL of the user's own — a proxy of theirs in front of OpenAI — is
// left as it is when magpie becomes the provider, and after.
func TestCodexProviderKeepsOwnBaseURL(t *testing.T) {
const own = `openai_base_url = "https://proxy.example/v1"`
home, read := codexHome(t, "", "model = \"gpt-5.5\"\n"+own+"\n")
cx := codex(home)
if err := cx.Fields[0].Set("fake/m1"); err != nil {
t.Fatal(err)
}
if cfg := read(); !strings.Contains(cfg, own) || strings.Count(cfg, "openai_base_url") != 1 ||
!strings.Contains(cfg, `model_provider = "magpie"`) {
t.Fatalf("\n%s", cfg)
}
if err := cx.Fields[0].Set(""); err != nil {
t.Fatal(err)
}
if cfg := read(); !strings.Contains(cfg, own) || strings.Contains(cfg, "model_provider =") {
t.Fatalf("reset:\n%s", cfg)
}
}
// A Codex that an older magpie made its provider, with no base URL, gets
// magpie's at the next sync; a provider set up by hand with a catalog of
// the user's own keeps its keys, and only the table it names comes back
// (Codex loads no config while it is missing).
func TestCodexSyncAddsBaseURLToProvider(t *testing.T) {
home, read := codexHome(t, "", "")
cx := codex(home)
if err := cx.Fields[0].Set("fake/m1"); err != nil {
t.Fatal(err)
}
// as an older magpie left it
cfg := read()
i := strings.Index(cfg, "openai_base_url")
if i < 0 {
t.Fatalf("\n%s", cfg)
}
j := strings.Index(cfg[i:], "\n")
if err := os.WriteFile(filepath.Join(home, ".codex", "config.toml"), []byte(cfg[:i]+cfg[i+j+1:]), 0o644); err != nil {
t.Fatal(err)
}
if strings.Contains(read(), "openai_base_url") {
t.Fatal("base URL still there")
}
if err := cx.Sync(); err != nil {
t.Fatal(err)
}
if cfg = read(); !strings.Contains(cfg, `openai_base_url = "`+codexGatewayURL()+`"`) || !strings.Contains(cfg, `model_provider = "magpie"`) {
t.Fatalf("sync:\n%s", cfg)
}
const mine = "model = \"fake/m1\"\nmodel_provider = \"magpie\"\nmodel_catalog_json = \"/elsewhere/models.json\"\n"
home, read = codexHome(t, "", mine)
if err := codex(home).Sync(); err != nil {
t.Fatal(err)
}
if cfg = read(); !strings.HasPrefix(cfg, mine) || strings.Contains(cfg, "openai_base_url") || !strings.Contains(cfg, "[model_providers.magpie]") {
t.Fatalf("hand-made provider changed:\n%s", cfg)
}
}