Files
yetone 2a990175e5 upstream: the vendors' own status pages say when their API is down (#971, emo172: an Anthropic or OpenAI outage looked like a sign-in or quota problem) — a provider that calls Anthropic's, OpenAI's or Moonshot's API (by its base URL's host: api.anthropic.com, which the Claude subscription calls too; api.openai.com and chatgpt.com, Codex's; api.moonshot.cn/.ai) carries "API outage" (red), "API degraded" or "API maintenance" beside its name on its Providers row and its Usage card while the vendor's page says so; its title names the parts and the open incidents, and a click opens the incident (or the page), not the row. internal/upstream reads each page's Statuspage-style summary.json (status.claude.com; status.openai.com, incident.io's; status.moonshot.cn), checked by hand on 2026-10-06, and only the API's own parts (Claude API and Claude Code, not claude.ai; Chat Completions, Responses, Codex, Codex CLI, not "Codex in ChatGPT Desktop", which was degraded that day); an incident naming no parts (OpenAI's) counts only while those parts are affected. Pages are read only when the Providers or Usage page asks (GET /api/upstream), at most once in five minutes, never on a request's way; a page that fails, isn't JSON or no longer lists the API's parts is unknown and marks nothing. A relay serving the same models is never marked. DeepSeek's page (Flashduty) has nothing readable and is only known for its link; OpenRouter, xAI, Mistral and z.ai have no page read. The gateway's failure messages, TUI, CLI and tray aren't changed. zh/ja/de added. upstream_test (fake pages from the real summaries: API down, claude.ai down isn't the API, Claude Code degraded, OpenAI's app isn't the API, 503/HTML/changed page unknown, read once in Fresh and Of never waits, hosts; -race -count=5) and gui's TestUpstreamRoute pass; upstream-status (Anthropic down: its provider and the Claude subscription marked, relay/OpenAI/unread Moonshot not, click opens the incident not the row, all well marks nothing; 900 and 440px, the badge whole, the name giving way) passes in Chromium and WebKit, en and zh, and fails 8/8 without the app.js change. i18n-keys, gui-ja, gui-de, click-scroll, account-return, account-models, usage-keys-fold, deprecated-subscriptions and add-sheet pass; go vet, go test ./internal/gui/, linux/windows/nogui builds pass.
2026-10-06 12:36:05 +08:00

72 lines
2.7 KiB
Go

package gui
import (
"encoding/json"
"net/http"
"net/http/httptest"
"slices"
"testing"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/upstream"
)
// /api/upstream says what the vendor's page says for the providers that
// call its API, and nothing for a relay serving the same models (#971)
func TestUpstreamRoute(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("XDG_CACHE_HOME", t.TempDir())
page := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"components":[{"id":"api","name":"Claude API (api.anthropic.com)","status":"major_outage","group":false},
{"id":"web","name":"claude.ai","status":"operational","group":false}],
"incidents":[{"name":"Claude API errors","status":"investigating","impact":"critical","shortlink":"https://stspg.io/z","components":[{"id":"api"}]}]}`))
}))
defer page.Close()
was := slices.Clone(upstream.Vendors)
defer func() { upstream.Vendors = was; upstream.Forget() }()
for i := range upstream.Vendors {
upstream.Vendors[i].Summary = "" // no other page is asked
if upstream.Vendors[i].ID == "anthropic" {
upstream.Vendors[i].Summary = page.URL
}
}
upstream.Forget()
for _, p := range []provider.Provider{
{ID: "anth", Name: "Anthropic", Anthropic: "https://api.anthropic.com", Key: "k"},
{ID: "relay", Name: "Relay", Anthropic: "https://relay.example.invalid", Key: "k"},
{ID: "ds", Name: "DeepSeek", Chat: "https://api.deepseek.com/v1", Key: "k"},
} {
if err := provider.Save(p); err != nil {
t.Fatal(err)
}
}
mux := http.NewServeMux()
providerRoutes(mux, nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, httptest.NewRequest("GET", "/api/upstream", nil))
if w.Code != 200 {
t.Fatalf("%d %s", w.Code, w.Body)
}
var got struct {
Vendors []upstream.Status
Providers map[string]string
}
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatal(err)
}
if got.Providers["anth"] != "anthropic" || got.Providers["ds"] != "deepseek" || got.Providers["relay"] != "" {
t.Fatalf("providers %v", got.Providers)
}
i := slices.IndexFunc(got.Vendors, func(s upstream.Status) bool { return s.Vendor == "anthropic" })
if i < 0 || got.Vendors[i].Level != "major" || len(got.Vendors[i].Incidents) != 1 || got.Vendors[i].Incidents[0].URL != "https://stspg.io/z" {
t.Fatalf("vendors %+v", got.Vendors)
}
j := slices.IndexFunc(got.Vendors, func(s upstream.Status) bool { return s.Vendor == "deepseek" })
if j < 0 || got.Vendors[j].Level != "" || got.Vendors[j].Page != "https://status.deepseek.com" {
t.Fatalf("deepseek %+v", got.Vendors)
}
}