providers: an account whose list couldn't be had is asked again a minute on, and opening the panel asks (#422, Q1CHENL: after the in-app update Kiro still offered Auto alone until magpie was restarted by hand) — the update restarts magpie as a new process, whose one try for an account still without its vendor's list was made at start-up, or by the Providers page with 8 seconds for it; one that failed left the account on its fallback (Kiro's Auto) for ten minutes, and only the Providers page asked again, never the panel and its model picker. The retry is now a minute, and GET /api/state (the panel, the window's Agents page) starts FetchNew in the background, no more than once in 15s and never two at once, so the page doesn't wait on vendors. TestFetchNewRetriedSoon fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.

This commit is contained in:
yetone committed 2026-10-01 21:49:14 +08:00
1 parent bb1aa3acbd
commit 58aca0eeb8
3 files changed
+161 -2

No files matched your search

+4
View File
@@ -401,6 +401,10 @@ func Handler(w Windows, gw *gateway.Server) http.Handler {
})
omarchyRoutes(mux, w)
mux.HandleFunc("GET /api/state", func(rw http.ResponseWriter, r *http.Request) {
// the panel and its model picker load from here: an account still
// without its vendor's list (one whose try at start-up failed) is
// asked again in the background, not only from the Providers page
provider.FetchNewSoon(8 * time.Second)
writeJSON(rw, state())
})
mux.HandleFunc("POST /api/set", func(rw http.ResponseWriter, r *http.Request) {
+123
View File
@@ -0,0 +1,123 @@
package provider
import (
"net/http"
"net/http/httptest"
"path/filepath"
"sync/atomic"
"testing"
"time"
"github.com/yetone/magpie/internal/catalog"
)
// An account whose list couldn't be had is asked again a minute on, and
// opening the panel asks without the page waiting (#422: after an update
// Kiro offered Auto alone until magpie was restarted by hand — its one try
// had failed, and it wasn't asked again for ten minutes, and then only by
// the Providers page).
func TestFetchNewRetriedSoon(t *testing.T) {
isolate(t)
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
wbTokens.Lock()
wbTokens.m = map[string]wbCreds{}
wbTokens.Unlock()
newFetches.Lock()
newFetches.m = map[string]time.Time{}
newFetches.Unlock()
newSoonAt.Store(0)
var asked atomic.Int32
var down atomic.Bool
down.Store(true)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v3/config" {
w.WriteHeader(404)
return
}
asked.Add(1)
if down.Load() {
w.WriteHeader(502)
return
}
w.Write([]byte(`{"code":0,"data":{"agents":[{"name":"cli","models":["hy4-preview","glm-5.3"]}],
"models":[{"id":"hy4-preview","name":"Hy4 preview"},{"id":"glm-5.3","name":"GLM-5.3"}]}}`))
}))
defer srv.Close()
old := wbEndpoint
wbEndpoint = srv.URL
defer func() { wbEndpoint = old }()
future := time.Now().Add(24 * time.Hour).UnixMilli()
writeFile(t, wbAuthFile(home), map[string]any{
"account": map[string]any{"uid": "u1", "nickname": "旅行者"},
"auth": map[string]any{"accessToken": "a", "refreshToken": "r",
"expiresAt": future, "refreshExpiresAt": future, "domain": "www.codebuddy.cn"},
})
forgetAccountCaches()
listed := func() bool {
p, err := Find("workbuddy")
if err != nil {
t.Fatal(err)
}
for _, m := range p.Available() {
if m.ID == "hy4-preview" {
return true
}
}
return false
}
// the try at start-up fails
FetchNew(5 * time.Second)
if n := asked.Load(); n != 1 {
t.Fatalf("asked %d times at start-up", n)
}
down.Store(false)
FetchNew(5 * time.Second)
if n := asked.Load(); n != 1 {
t.Fatalf("a failed list asked again at once: %d", n)
}
// opened again straight away: nothing more is started
FetchNewSoon(5 * time.Second)
newSoonAt.Store(0)
waitNewSoon()
if n := asked.Load(); n != 1 {
t.Fatalf("a failed list asked again at once by the panel: %d", n)
}
// a minute and a bit later the panel is opened: asked again, without
// the page waiting for it
newFetches.Lock()
newFetches.m["workbuddy"] = time.Now().Add(-61 * time.Second)
newFetches.Unlock()
FetchNewSoon(5 * time.Second)
waitNewSoon()
if !listed() || asked.Load() != 2 {
t.Fatalf("not asked again a minute on: asked %d times, listed %v", asked.Load(), listed())
}
// and the panel opened again within newSoonEvery starts nothing
if err := catalog.SaveLive("workbuddy", "", nil); err != nil {
t.Fatal(err)
}
newFetches.Lock()
newFetches.m = map[string]time.Time{}
newFetches.Unlock()
FetchNewSoon(5 * time.Second)
waitNewSoon()
if n := asked.Load(); n != 2 {
t.Fatalf("the panel asked again within %v: %d", newSoonEvery, n)
}
}
// waitNewSoon waits for a FetchNewSoon under way to end.
func waitNewSoon() {
for deadline := time.Now().Add(5 * time.Second); fetchingNew.Load() && time.Now().Before(deadline); {
time.Sleep(10 * time.Millisecond)
}
}
+34 -2
View File
@@ -9,6 +9,7 @@ import (
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/yetone/magpie/internal/catalog"
@@ -159,8 +160,39 @@ var newFetches = struct {
}{m: map[string]time.Time{}}
// newFetchRetry is how long FetchNew leaves an account whose list it
// couldn't get before asking again.
var newFetchRetry = 10 * time.Minute
// couldn't get before asking again. Short: until it has its list the
// account offers magpie's fallback (Kiro's Auto alone), and one try that
// failed — the first after an update, cut short by a page's 8 seconds, or
// made before the network was up — left it so for ten minutes, while only
// the Providers page asked again (#422: Auto alone until magpie was
// restarted by hand).
var newFetchRetry = time.Minute
// fetchingNew is set while a FetchNewSoon runs; newSoonAt is when the last
// one started.
var (
fetchingNew atomic.Bool
newSoonAt atomic.Int64
)
// newSoonEvery is how often FetchNewSoon starts at most.
var newSoonEvery = 15 * time.Second
// FetchNewSoon is FetchNew in the background, for a page that shouldn't
// wait on vendors (the panel, whose model picker otherwise kept an
// account's fallback list until the Providers page was opened). It does
// nothing while one runs or within newSoonEvery of the last.
func FetchNewSoon(timeout time.Duration) {
now := time.Now().UnixNano()
if now-newSoonAt.Load() < int64(newSoonEvery) || !fetchingNew.CompareAndSwap(false, true) {
return
}
newSoonAt.Store(now)
go func() {
defer fetchingNew.Store(false)
FetchNew(timeout)
}()
}
// FetchNew asks each signed-in account whose vendor list magpie hasn't
// fetched yet for it, each for at most timeout. Start-up does this for the