Files
magpie/internal/gateway/codex_models_wait_test.go
yetone cb7f442785 gateway: Codex's /models builds magpie's providers once, and cursor-agent's Keychain token is read once a while (#746, sperwe: /backend-api/codex/models called All() over and over, rescanning accounts and running /usr/bin/security some 200 times, 5.3–5.6 s against about 1 s straight to the backend, past Codex's 5 s model refresh, so Codex fell back to the official list) — after the ChatGPT backend's list, codexModels read magpie's: CodexListed (CatalogFor's catalog, and AutoVision through codexListed's descriptions), CodexNativeWindow, CodexNativePicked and CodexListTag (CodexListed again, codexWindowsTag), seven builds of All() in a test with one provider, each going through Accounts() and every agent's sign-in; a plugin's accounts ask pluginOwnUser, and for a Cursor moved onto its plugin that is cursorSignedOut, a security find-generic-password each time. All() now goes through the hold the GUI's reads use (heldOf, dff14ff4): while a request holds the catalog it is built once (a clone each call, so a caller's change stays its own; resolveIn's own heldOf("all", All) is plain All() now), and codexModels takes the hold (provider.Hold) for what follows the backend's answer. A hold is dropped by a write, as before (store → catalog.Touched → Changed, a plugin's sign-in), and now also by writeLogins and forgetAccountCaches, so an account signed in, switched or removed while one is held is seen at once; a sign-in changed outside magpie shows within the hold's second (heldFor). cursor-agent's Keychain token (readCursorToken, cursorToken) is read at most once in 10 s (cursorTokenTTL, as claudeCacheTTL); forgetCursorStatus, which a sign-in or out in magpie calls, drops it, and CursorToken reads it afresh after asking cursor-agent to renew it, so a renewed or new token isn't missed. AllBuilt counts builds for tests. TestCodexModelListBuildsProvidersOnce counts one build for one /models request (7 on the old code), TestHoldBuildsProvidersOnce one build for two All() calls under a hold, a caller's change not leaking, a provider saved and logins written while held being seen, and TestCursorKeychainReadOnce, with a fake security on PATH, one run for three reads, a fresh one after forgetCursorStatus and after a fresh read. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not timed with the reporter's accounts: here the request built the providers 7 times and builds them once, so their ~200 security runs should come to about a seventh, Cursor's from one a build to one in 10 s; the GUI's held reads (GET /api/*) now share one All() too.
2026-10-04 12:15:40 +08:00

85 lines
2.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gateway
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/yetone/magpie/internal/provider"
)
// A ChatGPT backend slow to hand over its model list doesn't hold magpie's
// answer past the 5 s Codex waits for it: Codex then kept the list it was
// built with, none of magpie's models in it (#539). The answer comes in
// time, with magpie's models and Codex's own from its last list.
func TestCodexModelListBackendSlow(t *testing.T) {
setup(t, provider.Chat, &fake{t: t})
release := make(chan struct{})
up := chatgpt(t, func(w http.ResponseWriter, r *http.Request) {
select {
case <-release:
case <-r.Context().Done():
}
})
t.Cleanup(func() { close(release); up.CloseClientConnections() })
home, _ := os.UserHomeDir()
os.MkdirAll(filepath.Join(home, ".codex"), 0o755)
os.WriteFile(filepath.Join(home, ".codex", "models_cache.json"),
[]byte(`{"etag":"\"v1\"","models":[{"slug":"gpt-5.5","priority":1,"base_instructions":"x"}]}`), 0o644)
was := codexModelsWait
codexModelsWait = 200 * time.Millisecond
t.Cleanup(func() { codexModelsWait = was })
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", CodexPath+"/models?client_version=0.159.2", nil)
req.Header.Set("Authorization", "Bearer chatgpt-token")
done := make(chan struct{})
go func() { New().Handler().ServeHTTP(rec, req); close(done) }()
select {
case <-done:
case <-time.After(3 * time.Second):
t.Fatal("magpie's model list waited on the backend past the time Codex gives it")
}
var list struct {
Models []map[string]any `json:"models"`
}
json.Unmarshal(rec.Body.Bytes(), &list)
has := map[string]bool{}
for _, m := range list.Models {
s, _ := m["slug"].(string)
has[s] = true
}
if rec.Code != 200 || !has["fake/m1"] || !has["gpt-5.5"] {
t.Fatalf("%d %s", rec.Code, rec.Body.String())
}
}
// One /models request from Codex builds magpie's providers once (#746,
// sperwe: seven builds, each reading every agent's sign-in, some 200
// keychain reads, took 5.3–5.6 s, past the 5 s Codex waits, and Codex kept
// its own list): the catalog, the codex provider's picks and windows and
// the list's tag all read the one build.
func TestCodexModelListBuildsProvidersOnce(t *testing.T) {
setup(t, provider.Chat, &fake{t: t})
chatgpt(t, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"models":[{"slug":"gpt-5.5","priority":1}]}`))
})
h := New().Handler()
n := provider.AllBuilt.Load()
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", CodexPath+"/models?client_version=0.159.2", nil)
req.Header.Set("Authorization", "Bearer chatgpt-token")
h.ServeHTTP(rec, req)
if rec.Code != 200 || !strings.Contains(rec.Body.String(), `"fake/m1"`) || !strings.Contains(rec.Body.String(), `"gpt-5.5"`) {
t.Fatalf("%d %s", rec.Code, rec.Body.String())
}
if got := provider.AllBuilt.Load() - n; got != 1 {
t.Fatalf("the providers were built %d times for one list, want 1", got)
}
}