Files
magpie/internal/agent/cursorlocal_test.go
yetone 5361a2178e agent, gateway: Cursor Private Inference's row has an effort, which magpie's gateway asks its requests for (#1003, gjf7) — other agents' rows pick a reasoning effort that magpie writes into their config, but Cursor Private Inference reads only its base URL and key from the environment, and its app offers no Reasoning control for most of magpie's models, so there was no way to set one short of a routing group with a member fixed at a level, which the reporter used as a workaround. Its row now has an effort field like the others': the default (as Cursor asks) first, then every level the models it lists have (cursorLocalEfforts: their levels in MemberEfforts order, low/medium/high where a model reasons with levels unknown, none at all when nothing it lists reasons or it isn't connected). The pick is kept in magpie, settings' agentEfforts by agent (provider.AgentEffort / SetAgentEffort, a level of MemberEfforts or "" for none), and the gateway's candidate loop asks a cursor-local request for it with withFixedEffort, fitted to the model's levels and bounded by a group's, on a model that reasons (the new Provider.Thinks: levels, its list's Reasoning, or models.dev's). Precedence: a group member's fixed effort, then a :effort suffix, then a routing rule's turn pick, then the agent's pick, then the request's own; the pick wins over a level Cursor sent, as Cursor sends its own default as often as a choice, and Default hands the choice back to Cursor. A model that doesn't reason and every other agent's requests go as they came. The Settings page's save carries agentEfforts over as it does fastPicks (TestSettingsSaveKeepsWhatItDoesNotSend's fixture names it). In the window the field sits under "Settings" rather than "New sessions" for this agent alone, as it applies to every request from then on, not to what a session starts on; no new t() strings (Settings, effort and default are translated in zh, ja and de already). TestAgentEffortOnCursorLocal (Chat and Responses with and without a level of their own, Claude on Messages made to think, OpenCode's request untouched, a non-reasoning model untouched, a member fixed at low kept, the pick taken off) fails without the gateway change and passes; TestCursorLocalEffort covers the options (none with no reasoning model, the union of levels, the low/medium/high fallback, none while disconnected), Apply and a bad level refused; both pass -race (-count=20 gateway, -count=10 agent). cursor-local-effort.test.cjs (the window at 560px and the tray panel at 440px: the field under Settings, the slider's five stops with Default first, a pick posting the field, no sideways scroll) passes on Chromium and WebKit in en, zh, ja and de; claude-effort, subagent-effort, agent-field-names, agent-connect, agent-layout, agent-tweak-fold, agent-models, cursor-local-launch and i18n pass on both. go test -tags nogui ./internal/..., go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried with the real Cursor Private Inference: whether it sends a reasoning level of its own on models it shows a Reasoning control for is unverified, which the precedence makes moot. docs/subsystems/gateway-routing.md and agent-wiring.md say it.
2026-10-06 17:29:28 +08:00

218 lines
8.2 KiB
Go

package agent
import (
"context"
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"testing"
"github.com/yetone/magpie/internal/catalog"
"github.com/yetone/magpie/internal/gateway"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/usage"
)
// Cursor Private Inference (#299) is found by its product.json among the
// apps, beside a regular Cursor of the same app name that isn't it, and its
// row gives the command that starts it on magpie, with its own key; its
// requests are its own in the usage, not Cursor's.
func TestCursorLocal(t *testing.T) {
root := t.TempDir()
app := func(dir, nameShort, exe string) {
t.Helper()
res := filepath.Join(root, dir, "Contents", "Resources", "app")
if err := os.MkdirAll(res, 0o755); err != nil {
t.Fatal(err)
}
os.WriteFile(filepath.Join(res, "product.json"), []byte(`{"nameShort":"`+nameShort+`","applicationName":"cursor","dataFolderName":".cursor"}`), 0o644)
if exe != "" {
os.WriteFile(filepath.Join(root, dir, "Contents", "Info.plist"), []byte("<plist><dict>\n\t<key>CFBundleExecutable</key>\n\t<string>"+exe+"</string>\n</dict></plist>"), 0o644)
}
}
app("Cursor.app", "Cursor", "Cursor")
if got := findCursorLocal([]string{root, filepath.Join(root, "none")}); got != "" {
t.Fatalf("a regular Cursor found as it: %q", got)
}
app("Cursor Private Inference.app", "Cursor Private Inference", "Cursor PI")
want := filepath.Join(root, "Cursor Private Inference.app", "Contents", "MacOS", "Cursor PI")
if got := findCursorLocal([]string{root}); got != want {
t.Fatalf("found %q, want %q", got, want)
}
os.Remove(filepath.Join(root, "Cursor Private Inference.app", "Contents", "Info.plist"))
if got := findCursorLocal([]string{root}); filepath.Base(got) != "Cursor" || !strings.Contains(got, "Private Inference.app") {
t.Fatalf("without an Info.plist: %q", got)
}
// the command: the gateway's /v1 and the agent's key, the app quoted
cmd := CursorLocalLaunch("/Applications/It's.app/Contents/MacOS/Cursor", "http://127.0.0.1:3425")
for _, s := range []string{"CURSOR_LOCAL_AGENT_BASE_URL", "http://127.0.0.1:3425/v1", "CURSOR_LOCAL_AGENT_API_KEY", gateway.TokenFor("cursor-local")} {
if !strings.Contains(cmd, s) {
t.Errorf("%q lacks %q", cmd, s)
}
}
if runtime.GOOS != "windows" && !strings.HasSuffix(cmd, ` '/Applications/It'\''s.app/Contents/MacOS/Cursor'`) {
t.Errorf("app not quoted: %q", cmd)
}
// the agent: there and launchable when the app is, else not there
old := cursorLocalApp
t.Cleanup(func() { cursorLocalApp = old })
a, err := Find(CursorLocalID)
if err != nil {
t.Fatal(err)
}
cursorLocalApp = func() string { return want }
if !a.detect() || !strings.Contains(a.Launch(), want) {
t.Errorf("installed: detect %v, launch %q", a.detect(), a.Launch())
}
cursorLocalApp = func() string { return "" }
if a.detect() || a.Launch() != "" {
t.Error("not installed, yet there")
}
if got := usage.AgentOf("cursor-local"); got != CursorLocalID {
t.Errorf("AgentOf(cursor-local) = %q", got)
}
if got := usage.AgentOf("Cursor/3.2"); got != "cursor" {
t.Errorf("AgentOf(Cursor/3.2) = %q", got)
}
}
// Its row connects as other agents' do, with no command to run (mamba on
// Discord): magpie sets CURSOR_LOCAL_AGENT_BASE_URL and _API_KEY for the
// user, which only this build reads, notes it, sets them again when the
// gateway is served (the Mac's launchd forgets them at a restart, and the
// port may change), and clears them on disconnect.
func TestCursorLocalConnects(t *testing.T) {
h := t.TempDir()
t.Setenv("HOME", h)
t.Setenv("USERPROFILE", h)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(h, ".config"))
var set []map[string]string
oldEnv, oldApp := cursorLocalUserEnv, cursorLocalApp
t.Cleanup(func() { cursorLocalUserEnv, cursorLocalApp = oldEnv, oldApp })
cursorLocalUserEnv = func(env map[string]string) error { set = append(set, env); return nil }
cursorLocalApp = func() string { return "/Applications/Cursor Private Inference.app/Contents/MacOS/Cursor" }
a, err := Find(CursorLocalID)
if err != nil {
t.Fatal(err)
}
if len(a.Fields) == 0 || a.Wired() {
t.Fatalf("fields %d, wired %v before connecting", len(a.Fields), a.Wired())
}
how, err := a.ConnectHow()
if err != nil || how.How != "magpie" || !a.Wired() {
t.Fatalf("connect: %+v %v, wired %v", how, err, a.Wired())
}
want := map[string]string{"CURSOR_LOCAL_AGENT_BASE_URL": gateway.URL() + "/v1", "CURSOR_LOCAL_AGENT_API_KEY": gateway.TokenFor(CursorLocalID)}
if len(set) != 1 || set[0]["CURSOR_LOCAL_AGENT_BASE_URL"] != want["CURSOR_LOCAL_AGENT_BASE_URL"] || set[0]["CURSOR_LOCAL_AGENT_API_KEY"] != want["CURSOR_LOCAL_AGENT_API_KEY"] {
t.Fatalf("set %v, want %v", set, want)
}
if a.Launch() == "" {
t.Error("the command to start it from a shell is gone")
}
// served again: set again
KeepCursorLocalEnv(context.Background())
if len(set) != 2 || set[1]["CURSOR_LOCAL_AGENT_API_KEY"] != want["CURSOR_LOCAL_AGENT_API_KEY"] {
t.Fatalf("not set again when served: %v", set)
}
if err := a.Disconnect(); err != nil || a.Wired() {
t.Fatalf("disconnect: %v, wired %v", err, a.Wired())
}
if len(set) != 3 || set[2] != nil {
t.Fatalf("not cleared: %v", set)
}
// off, a serve sets nothing
KeepCursorLocalEnv(context.Background())
if len(set) != 3 {
t.Fatalf("set while off: %v", set)
}
}
// Its effort (#1003), which neither its environment nor, for most models,
// its app can say, is picked in its row among the levels the models it
// lists have, the default first: kept in magpie for the gateway to ask, and
// offered only while it is connected and something it lists reasons.
func TestCursorLocalEffort(t *testing.T) {
syncHome(t)
oldEnv, oldApp := cursorLocalUserEnv, cursorLocalApp
t.Cleanup(func() { cursorLocalUserEnv, cursorLocalApp = oldEnv, oldApp })
cursorLocalUserEnv = func(map[string]string) error { return nil }
cursorLocalApp = func() string { return "/Applications/Cursor Private Inference.app/Contents/MacOS/Cursor" }
if err := provider.Save(provider.Provider{ID: "plain", Name: "Plain", Chat: "https://plain.test/v1", Key: "key", Models: []string{"gpt-4o-mini"}}); err != nil {
t.Fatal(err)
}
a, err := Find(CursorLocalID)
if err != nil {
t.Fatal(err)
}
if _, err := a.ConnectHow(); err != nil || !a.Wired() {
t.Fatalf("connect: %v, wired %v", err, a.Wired())
}
field := func() Field {
t.Helper()
for _, f := range a.Fields {
if f.Key == "effort" {
return f
}
}
t.Fatal("no effort field")
return Field{}
}
values := func() []string {
var out []string
for _, o := range field().Options(a.Values()) {
out = append(out, o.Value)
}
return out
}
if got := values(); got != nil {
t.Fatalf("offered %q with no model that reasons", got)
}
if err := provider.Save(provider.Provider{ID: "think", Name: "Think", Chat: "https://think.test/v1", Key: "key", Models: []string{"gpt-5.5", "switch-only"}}); err != nil {
t.Fatal(err)
}
if err := catalog.SaveLive("think", "https://think.test/v1", []catalog.Model{
{ID: "gpt-5.5", Efforts: []string{"xhigh", "minimal", "low", "medium", "high", "ultra"}},
{ID: "switch-only", Reasoning: true},
}); err != nil {
t.Fatal(err)
}
if got := values(); !slices.Equal(got, []string{"", "minimal", "low", "medium", "high", "xhigh"}) {
t.Fatalf("offered %q", got)
}
// a model that reasons with no levels known: the three every vendor takes
if err := provider.SetHiddenModels(CursorLocalID, []string{"think/gpt-5.5"}); err != nil {
t.Fatal(err)
}
if got := values(); !slices.Equal(got, []string{"", "low", "medium", "high"}) {
t.Fatalf("with gpt-5.5 hidden, offered %q", got)
}
if err := a.Apply("effort", "high"); err != nil {
t.Fatal(err)
}
if got, kept := a.Values()["effort"], provider.AgentEffort(CursorLocalID); got != "high" || kept != "high" {
t.Fatalf("picked high: shown %q, kept %q", got, kept)
}
if err := field().Set("hard"); err == nil {
t.Fatal("a word that isn't a level was kept")
}
if err := a.Apply("effort", ""); err != nil || provider.AgentEffort(CursorLocalID) != "" {
t.Fatalf("back to the default: %v, kept %q", err, provider.AgentEffort(CursorLocalID))
}
// not connected: nothing to pick
if err := a.Disconnect(); err != nil {
t.Fatal(err)
}
if got := values(); got != nil {
t.Fatalf("offered %q while not connected", got)
}
}