233 lines
8.5 KiB
Go
233 lines
8.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/yetone/magpie/internal/provider"
|
|
)
|
|
|
|
func codexHome(t *testing.T, auth, config string) (home string, read func() string) {
|
|
t.Helper()
|
|
home = t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
|
|
t.Setenv("CODEX_HOME", "")
|
|
noKeychain(t)
|
|
usedUp := codexUsedUp
|
|
codexUsedUp = func() bool { return false }
|
|
t.Cleanup(func() { codexUsedUp = usedUp })
|
|
dir := filepath.Join(home, ".codex")
|
|
os.MkdirAll(dir, 0o755)
|
|
if auth != "" {
|
|
os.WriteFile(filepath.Join(dir, "auth.json"), []byte(auth), 0o600)
|
|
}
|
|
os.WriteFile(filepath.Join(dir, "config.toml"), []byte(config), 0o644)
|
|
if err := provider.Save(provider.Provider{ID: "fake", Name: "Fake", Key: "k", Chat: "http://127.0.0.1:1/v1", Models: []string{"m1"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return home, func() string {
|
|
b, _ := os.ReadFile(filepath.Join(dir, "config.toml"))
|
|
return string(b)
|
|
}
|
|
}
|
|
|
|
// Signed in, a magpie model points Codex's built-in OpenAI provider at
|
|
// magpie and leaves the provider as it is; one of Codex's own models takes
|
|
// the base URL away again and brings back what was there.
|
|
func TestCodexSignedInRoutesByBaseURL(t *testing.T) {
|
|
home, read := codexHome(t, `{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`,
|
|
"model = \"gpt-5.5\"\nmodel_reasoning_effort = \"xhigh\"\n\n[projects.\"/x\"]\ntrust_level = \"trusted\"\n")
|
|
cx := codex(home)
|
|
if err := cx.Fields[0].Set("fake/m1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := read()
|
|
if !strings.Contains(cfg, `openai_base_url = "http://127.0.0.1:`) || !strings.Contains(cfg, `/backend-api/codex"`) ||
|
|
!strings.Contains(cfg, `model = "fake/m1"`) || strings.Contains(cfg, "model_provider") ||
|
|
strings.Contains(cfg, "model_catalog_json") || strings.Contains(cfg, "[model_providers") {
|
|
t.Fatalf("magpie model:\n%s", cfg)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(home, ".codex", "magpie-models.json")); err == nil {
|
|
t.Error("catalog file written")
|
|
}
|
|
if err := cx.Fields[0].Set("gpt-5.4"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg = read()
|
|
if strings.Contains(cfg, "openai_base_url") || !strings.Contains(cfg, `model = "gpt-5.4"`) ||
|
|
!strings.Contains(cfg, `model_reasoning_effort = "xhigh"`) || !strings.Contains(cfg, `trust_level = "trusted"`) {
|
|
t.Fatalf("own model:\n%s", cfg)
|
|
}
|
|
}
|
|
|
|
// A magpie set up as a provider of Codex's, from before, moves to the base
|
|
// URL the next time a magpie model is picked.
|
|
func TestCodexSignedInLeavesProviderTable(t *testing.T) {
|
|
home, read := codexHome(t, `{"OPENAI_API_KEY":"sk-x"}`,
|
|
"model = \"fake/m1\"\nmodel_provider = \"magpie\"\nmodel_catalog_json = \"/x/magpie-models.json\"\n\n[model_providers.magpie]\nname = \"magpie\"\nbase_url = \"http://127.0.0.1:3425/v1\"\nwire_api = \"responses\"\n")
|
|
if err := codex(home).Fields[0].Set("fake/m1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := read()
|
|
if !strings.Contains(cfg, "openai_base_url") || strings.Contains(cfg, "model_provider") || strings.Contains(cfg, "model_catalog_json") {
|
|
t.Fatalf("\n%s", cfg)
|
|
}
|
|
}
|
|
|
|
// A provider table the user wrote with spaces around the dots — valid TOML —
|
|
// is the same table to magpie: it is reused in place, and taken away again
|
|
// when Codex steps back to its own models, as if magpie had written it.
|
|
func TestCodexSpacedProviderTable(t *testing.T) {
|
|
home, read := codexHome(t, "", "model = \"gpt-5.5\"\n\n[ model_providers . magpie ]\nname = \"magpie\"\nbase_url = \"http://127.0.0.1:1/v1\"\n\n[[skills.config]]\npath = \"/skill\"\n")
|
|
cx := codex(home)
|
|
if err := cx.Fields[0].Set("fake/m1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := read()
|
|
if strings.Count(cfg, "model_providers") != 1 || !strings.Contains(cfg, `model_provider = "magpie"`) {
|
|
t.Fatalf("provider table was not reused:\n%s", cfg)
|
|
}
|
|
if err := cx.Fields[0].Set(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg = read(); strings.Contains(cfg, "magpie") || !strings.Contains(cfg, "[[skills.config]]") {
|
|
t.Fatalf("reset:\n%s", cfg)
|
|
}
|
|
}
|
|
|
|
// Not signed in, Codex's OpenAI provider can't run, so magpie is a provider
|
|
// of its own.
|
|
func TestCodexSignedOutUsesProvider(t *testing.T) {
|
|
home, read := codexHome(t, "", "")
|
|
cx := codex(home)
|
|
if err := cx.Fields[0].Set("fake/m1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := read()
|
|
if strings.Contains(cfg, "openai_base_url") || !strings.Contains(cfg, `model_provider = "magpie"`) ||
|
|
!strings.Contains(cfg, "[model_providers.magpie]") || !strings.Contains(cfg, "model_catalog_json") {
|
|
t.Fatalf("\n%s", cfg)
|
|
}
|
|
if err := cx.Fields[0].Set(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg = read(); strings.Contains(cfg, "magpie") || strings.Contains(cfg, "model") {
|
|
t.Fatalf("reset:\n%s", cfg)
|
|
}
|
|
}
|
|
|
|
// A ChatGPT account that has used its allowance up keeps the Codex app from
|
|
// sending, so magpie is a provider of Codex's then, as when signed out.
|
|
func TestCodexUsedUpUsesProvider(t *testing.T) {
|
|
home, read := codexHome(t, `{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`, "")
|
|
codexUsedUp = func() bool { return true }
|
|
if err := codex(home).Fields[0].Set("fake/m1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := read()
|
|
if strings.Contains(cfg, "openai_base_url") || !strings.Contains(cfg, `model_provider = "magpie"`) ||
|
|
!strings.Contains(cfg, "[model_providers.magpie]") || !strings.Contains(cfg, `model = "fake/m1"`) {
|
|
t.Fatalf("\n%s", cfg)
|
|
}
|
|
}
|
|
|
|
// Subagents can be given a magpie model of their own once Codex runs through
|
|
// magpie; it goes with magpie when Codex steps back to its own models, where
|
|
// a user's own choice of one of those stays.
|
|
func TestCodexSubagentModel(t *testing.T) {
|
|
home, read := codexHome(t, `{"tokens":{"access_token":"x","id_token":"x.e30.x"}}`,
|
|
"model = \"gpt-5.5\"\n\n[agents]\nmax_threads = 6\n")
|
|
cx := codex(home)
|
|
sub := cx.Field("subagent")
|
|
if sub == nil {
|
|
t.Fatal("no subagent field")
|
|
}
|
|
if err := sub.Set("fake/m1"); err == nil {
|
|
t.Error("a magpie model was taken before Codex runs through magpie")
|
|
}
|
|
for _, o := range sub.Options(nil) {
|
|
if isMagpie(o.Value) {
|
|
t.Errorf("magpie model offered before Codex runs through magpie: %s", o.Value)
|
|
}
|
|
}
|
|
if err := cx.Fields[0].Set("fake/m1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := sub.Set("fake/m1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := sub.Get(); got != "fake/m1" {
|
|
t.Fatalf("subagent = %q\n%s", got, read())
|
|
}
|
|
if cfg := read(); !strings.Contains(cfg, "max_threads = 6") {
|
|
t.Fatalf("user's [agents] key lost:\n%s", cfg)
|
|
}
|
|
if err := cx.Fields[0].Set("gpt-5.4"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg := read(); strings.Contains(cfg, "default_subagent_model") || !strings.Contains(cfg, "max_threads = 6") {
|
|
t.Fatalf("stepping out of magpie:\n%s", cfg)
|
|
}
|
|
if err := sub.Set("gpt-5.4-mini"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cx.Fields[0].Set(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := sub.Get(); got != "gpt-5.4-mini" {
|
|
t.Fatalf("own subagent model dropped: %q", got)
|
|
}
|
|
if err := sub.Set(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg := read(); strings.Contains(cfg, "default_subagent_model") {
|
|
t.Fatalf("\n%s", cfg)
|
|
}
|
|
}
|
|
|
|
func TestCodexSubagentReadErrorStopsChanges(t *testing.T) {
|
|
const input = "model = \"fake/m1\"\nmodel_provider = \"magpie\"\n\n[agents]\ndefault_subagent_model = [\n"
|
|
for _, model := range []string{"", "gpt-native"} {
|
|
t.Run("model="+model, func(t *testing.T) {
|
|
home := t.TempDir()
|
|
path := filepath.Join(home, ".codex", "config.toml")
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(input), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err := codex(home).Field("model").Set(model)
|
|
if err == nil || !strings.HasPrefix(err.Error(), path+": ") {
|
|
t.Fatalf("expected the config path in the error, got %v", err)
|
|
}
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != input {
|
|
t.Fatalf("changed config after a subagent read error:\n%s", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCodexCheckReportsTOMLParseErrors(t *testing.T) {
|
|
for _, profile := range []string{"", "profile = \"work\"\n"} {
|
|
input := "model = \"fake/m1\"\nmodel_provider = \"magpie\"\n" + profile + "\n[model_providers.magpie]\ninvalid = [\n"
|
|
home, read := codexHome(t, "", input)
|
|
path := filepath.Join(home, ".codex", "config.toml")
|
|
message := codex(home).Check()
|
|
if !strings.HasPrefix(message, path+": line ") || !strings.Contains(message, "column") || !strings.Contains(message, "array is incomplete") {
|
|
t.Fatalf("check did not report the parse location: %q", message)
|
|
}
|
|
if read() != input {
|
|
t.Fatal("check changed the config")
|
|
}
|
|
}
|
|
}
|