Files
magpie/internal/settings/settings_test.go
T
yetone 575e694ed7 settings: a Text size preference (文字大小) that makes everything in magpie's windows larger, 100/110/125/150%, with Ctrl/Cmd + = − 0 in the window and the tray panel, and Windows' text drawn in its own UI faces (Discord, Clarence Dan: the font size is hard on the eyes on Windows) — Settings → Preferences gains a Text size row under Language whose pick posts to a new POST /api/settings/text-size (saved as settings.TextSize, validated against 100/110/125/150, 0 read as 100, and kept by the Settings page's general save, which never sends it). The size is each webview's own zoom, as a browser zooms, not CSS zoom: CSS zoom leaves getBoundingClientRect and clientX in zoomed pixels while offsets, scrollTop and 100vh are not, and app.js, routing.js and library.js place menus, popovers and the panel's height by those measures; under the webview's zoom the page is laid out again in fewer, larger CSS pixels and every measure agrees. Windows and Linux use Wails' SetZoom (WebView2's zoom factor, WebKitGTK's zoom level; the Zoom window option at creation, so the pages open at the saved size), the Mac WKWebView.pageZoom through cgo (macOS 11+), since Wails' Mac zoom is magnification, which lays nothing out again. None is under 100% because Wails clamps SetZoom below 1 on Windows and Linux. The windows grow around the pages: the panel's width and the height its page asks for (still in CSS pixels) are multiplied by the zoom, capped at the screen's work area (textsize.go panelFrame), and resized at once when the size changes; the main window's minimum is 560×420 CSS pixels at the size, capped by the screen, so a window smaller than that grows. Runtime changes wait for the app to be ready (whenReady), and at startup the Mac applies the zoom once its webviews exist. The Mac header, drawn under the native traffic lights which do not grow, divides its height, left padding and the modal's top by --zoom (set from boot.js before the first paint and from the settings), so it stays 50 points tall and level with them. The shortcuts are Cmd on the Mac and Ctrl elsewhere (the other modifier does nothing), step through the sizes and 0 goes back to 100%, with a "Text size 125%" toast; in magpie web the row is hidden and the keys are left to the browser's own zoom. On Windows the page names its faces (html.win): Segoe UI Variable Text, Segoe UI, then Microsoft YaHei UI for Chinese rather than whatever WebView2 falls back to; the Mac and Linux stack is unchanged, and there is no automatic size bump. New strings in en and zh. Tested: internal/gui/tests/text-size.test.cjs in Chromium and WebKit, en and zh, light and dark — a pick posts and does not scroll the settings page, stays picked after a reload with --zoom from boot.js, the Settings page's other saves leave it, the shortcuts step and reset in the window and the panel, and at 150% (the smallest window, 840×630 points, as 560×420 CSS pixels at device scale 1.5) no view, the header or the panel runs off to the side, the Mac header is 50 points; screenshots at 100% and 150% looked at. currency.test.cjs's viewport is 480 tall instead of 420, as settings-groups' is: the new row put the Currency control under the footer, where WebKit's hover could not reach it. Go: TestTextSizeFrames (panel and window sizes) and TestTextSizeSetting (the endpoint saves, zooms the windows, rejects 133, survives the general save, boot.js carries it) in internal/gui, and the settings round trip. go build -tags nogui (and GOOS=linux, GOOS=windows, plain darwin), go test -tags nogui ./... (TestUserPath flaked once under load and passes alone) and the whole GUI suite with --test-concurrency=2 pass. Not tried in the real Wails window on any platform: the Mac pageZoom, WebView2's zoom factor with the resized panel and the Windows font are untested there.
2026-09-30 02:51:17 +08:00

181 lines
6.2 KiB
Go

package settings
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/yetone/magpie/internal/redact"
)
func TestRoundTrip(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
if s := Load(); s.Theme != "system" || s.Lang != "system" {
t.Fatalf("defaults: %+v", s)
}
if err := Save(Settings{Theme: "dark", Lang: "zh"}); err != nil {
t.Fatal(err)
}
if s := Load(); s.Theme != "dark" || s.Lang != "zh" {
t.Fatalf("saved: %+v", s)
}
if err := Save(Settings{Theme: "sepia"}); err == nil {
t.Fatal("bad theme accepted")
}
if Save(Settings{}) != nil || Load().Theme != "system" {
t.Fatal("empty means system")
}
// costs show in dollars unless cny is chosen
if s := Load(); s.Currency != "usd" {
t.Fatalf("default currency: %+v", s)
}
if err := Save(Settings{Currency: "cny"}); err != nil {
t.Fatal(err)
}
if s := Load(); s.Currency != "cny" {
t.Fatalf("saved currency: %+v", s)
}
if Save(Settings{Currency: "eur"}) == nil {
t.Fatal("bad currency accepted")
}
if Load().SessionTerminal != "" {
t.Fatal("the system handler should be the default")
}
if err := Save(Settings{SessionTerminal: "com.mitchellh.ghostty"}); err != nil || Load().SessionTerminal != "com.mitchellh.ghostty" {
t.Fatalf("session terminal not kept: %v", err)
}
if err := Save(Settings{SessionTerminal: "system"}); err != nil || Load().SessionTerminal != "system" {
t.Fatalf("system terminal not kept: %v", err)
}
if Save(Settings{SessionTerminal: "Ghostty; rm -rf /"}) == nil {
t.Fatal("bad terminal app id accepted")
}
// the text size is 100% until one of the sizes is chosen
if Save(Settings{}) != nil || Load().TextSize != 100 {
t.Fatalf("default text size: %+v", Load())
}
if Save(Settings{TextSize: 125}) != nil || Load().TextSize != 125 {
t.Fatal("text size not kept")
}
for _, bad := range []int{90, 120, 300, -1} {
if Save(Settings{TextSize: bad}) == nil {
t.Fatalf("text size %d accepted", bad)
}
}
if filepath.Base(Path()) != "settings.json" {
t.Fatal(Path())
}
if Save(Settings{CodexWarmup: "week"}) != nil || Load().CodexWarmup != "week" {
t.Fatal("codex warm-up not kept")
}
if Save(Settings{CodexWarmup: "off"}) != nil || Load().CodexWarmup != "" {
t.Fatal("off is off")
}
if Save(Settings{CodexWarmup: "daily"}) == nil {
t.Fatal("bad codex warm-up accepted")
}
if Save(Settings{ClaudeWarmup: "all"}) != nil || Load().ClaudeWarmup != "all" {
t.Fatal("claude warm-up not kept")
}
if Save(Settings{ClaudeWarmup: "hourly"}) == nil {
t.Fatal("bad claude warm-up accepted")
}
// a time of day to start the 5-hour windows at, as 06:00 however given
if Save(Settings{CodexWarmAt: "6:00", ClaudeWarmAt: "21:30:00"}) != nil || Load().CodexWarmAt != "06:00" || Load().ClaudeWarmAt != "21:30" {
t.Fatalf("warm-up times not kept: %+v", Load())
}
for _, bad := range []string{"25:00", "6am", "06:60"} {
if Save(Settings{CodexWarmAt: bad}) == nil {
t.Fatalf("bad time %q accepted", bad)
}
}
if Save(Settings{}) != nil || Load().CodexWarmAt != "" {
t.Fatal("no time is off")
}
// the menu bar's usage: every 3 minutes unless told, left or used
if s := Load(); s.TrayUsageEvery != 3 || s.QuotaLeft {
t.Fatalf("tray defaults: %+v", s)
}
if Save(Settings{TrayUsageEvery: 10, QuotaLeft: true}) != nil || Load().TrayUsageEvery != 10 || !Load().QuotaLeft {
t.Fatal("tray refresh or left not kept")
}
if Save(Settings{TrayUsageEvery: 7}) == nil {
t.Fatal("bad tray refresh accepted")
}
// the user's masking rules: kept tidied, and one that doesn't compile
// said, not saved and left out (#195)
if err := Save(Settings{RedactRules: []redact.Rule{{Kind: "gw key", Prefix: " oc_sk_ "}, {Kind: "ns", Regex: `ns-[0-9a-f]{12}`}}}); err != nil {
t.Fatal(err)
}
if r := Load().RedactRules; len(r) != 2 || r[0] != (redact.Rule{Kind: "GW_KEY", Prefix: "oc_sk_"}) || r[1].Kind != "NS" {
t.Fatalf("rules not kept: %+v", r)
}
if err := Save(Settings{RedactRules: []redact.Rule{{Kind: "bad", Regex: `ns-[0-9a-f`}}}); err == nil || !strings.Contains(err.Error(), "BAD") {
t.Fatalf("bad regex: %v", err)
}
if len(Load().RedactRules) != 2 {
t.Fatal("a failed save changed the rules")
}
}
func TestMigrate(t *testing.T) {
cfg, cache := t.TempDir(), t.TempDir()
t.Setenv("XDG_CONFIG_HOME", cfg)
t.Setenv("XDG_CACHE_HOME", cache)
old := filepath.Join(cfg, "dial")
if err := os.MkdirAll(filepath.Join(old, "sub"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(old, "providers.json"), []byte("{}"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(old, "sub", "x"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
Migrate()
fi, err := os.Stat(filepath.Join(cfg, "magpie", "providers.json"))
if err != nil || fi.Mode().Perm() != 0o600 {
t.Fatalf("providers.json not copied with its mode: %v %v", fi, err)
}
if _, err := os.Stat(filepath.Join(cfg, "magpie", "sub", "x")); err != nil {
t.Fatal("nested file not copied")
}
if _, err := os.Stat(old); err != nil {
t.Fatal("the old folder must stay")
}
if _, err := os.Stat(filepath.Join(cache, "magpie")); err == nil {
t.Fatal("no old cache, so no new one")
}
// a second run must not touch an existing folder
if err := os.WriteFile(filepath.Join(cfg, "magpie", "providers.json"), []byte("new"), 0o600); err != nil {
t.Fatal(err)
}
Migrate()
if b, _ := os.ReadFile(filepath.Join(cfg, "magpie", "providers.json")); string(b) != "new" {
t.Fatal("existing folder overwritten")
}
}
func TestArrange(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
s := Settings{AgentOrder: []string{"codex", "gone", "claude", "codex"}, AgentsHidden: []string{"crush"}}
if err := Save(s); err != nil {
t.Fatal(err)
}
s = Load()
if strings.Join(s.AgentOrder, ",") != "codex,gone,claude" {
t.Fatalf("order kept as %v", s.AgentOrder)
}
// an agent the order doesn't name follows, in the order it came
shown, hidden := Arrange(s, []string{"claude", "gemini", "crush", "codex", "pi"}, func(x string) string { return x })
if strings.Join(shown, ",") != "codex,claude,gemini,pi" || strings.Join(hidden, ",") != "crush" {
t.Fatalf("arranged %v, hidden %v", shown, hidden)
}
// the rest of the settings leave it as it is
s.Theme = "dark"
if Save(s) != nil || len(Load().AgentsHidden) != 1 {
t.Fatal("arrangement lost")
}
}