diff --git a/accounts_plugin_cli_test.go b/accounts_plugin_cli_test.go index 1dae1f3d..71cb0fb8 100644 --- a/accounts_plugin_cli_test.go +++ b/accounts_plugin_cli_test.go @@ -10,12 +10,13 @@ import ( "github.com/yetone/magpie/internal/plugin" "github.com/yetone/magpie/internal/provider" + "github.com/yetone/magpie/internal/testenv" ) // magpie accounts lists, switches and forgets a plugin's accounts by its // provider's id, as a built-in subscription's. func TestAccountsOfAPlugin(t *testing.T) { - bun := pathBun(t) + bun := testenv.Bun(t) home := t.TempDir() t.Setenv("HOME", home) t.Setenv("USERPROFILE", home) diff --git a/internal/gateway/plugin_failover_test.go b/internal/gateway/plugin_failover_test.go index b37e1059..e45bc71a 100644 --- a/internal/gateway/plugin_failover_test.go +++ b/internal/gateway/plugin_failover_test.go @@ -6,7 +6,6 @@ import ( "io" "net/http" "net/http/httptest" - "os/exec" "path/filepath" "strings" "sync" @@ -15,6 +14,7 @@ import ( "github.com/yetone/magpie/internal/plugin" "github.com/yetone/magpie/internal/provider" + "github.com/yetone/magpie/internal/testenv" ) // A plugin's provider with several accounts fails over as a built-in's @@ -24,10 +24,7 @@ import ( // and a routing group goes on to its next member when every account of // the plugin is out. func TestPluginFailover(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) fresh(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -255,10 +252,7 @@ func contains(xs []string, x string) bool { // lapsed, as the built-in's did; a 401 the built-in didn't take for a // refused sign-in leaves it be. The agent never sees the header. func TestPluginSaysSignIn(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) fresh(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/gateway/plugin_grok_parity_test.go b/internal/gateway/plugin_grok_parity_test.go index c7aaf54c..5851ac40 100644 --- a/internal/gateway/plugin_grok_parity_test.go +++ b/internal/gateway/plugin_grok_parity_test.go @@ -7,7 +7,6 @@ import ( "io" "net/http" "net/http/httptest" - "os/exec" "path/filepath" "strings" "sync" @@ -16,6 +15,7 @@ import ( "github.com/yetone/magpie/internal/plugin" "github.com/yetone/magpie/internal/provider" + "github.com/yetone/magpie/internal/testenv" ) // besideFake installs the fake plugin as the plugin of the built-in id, @@ -23,10 +23,7 @@ import ( // key, its requests going to up; it gives the provider's id. func besideFake(t *testing.T, id string, up http.Handler) string { t.Helper() - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) fresh(t) t.Setenv("MAGPIE_BUN", bun) t.Setenv("FAKE_ID", id) diff --git a/internal/gateway/plugin_listing_proxy_test.go b/internal/gateway/plugin_listing_proxy_test.go index 99665ca8..e3c11ff3 100644 --- a/internal/gateway/plugin_listing_proxy_test.go +++ b/internal/gateway/plugin_listing_proxy_test.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "net/http/httptest" - "os/exec" "path/filepath" "slices" "testing" @@ -13,16 +12,14 @@ import ( "github.com/yetone/magpie/internal/plugin" "github.com/yetone/magpie/internal/provider" + "github.com/yetone/magpie/internal/testenv" ) // A plugin account's model list is asked through the account's own proxy, // as a built-in account's is fetched through its: each of two accounts // behind its own proxy is told the list its proxy carried. func TestPluginModelListThroughAccountProxy(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) fresh(t) for _, k := range []string{"HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy", "NO_PROXY", "no_proxy"} { t.Setenv(k, "") diff --git a/internal/gateway/plugin_parity_test.go b/internal/gateway/plugin_parity_test.go index 24d21d21..d658af4c 100644 --- a/internal/gateway/plugin_parity_test.go +++ b/internal/gateway/plugin_parity_test.go @@ -6,7 +6,6 @@ import ( "net/http" "net/http/httptest" "os" - "os/exec" "path/filepath" "strings" "sync" @@ -15,16 +14,14 @@ import ( "github.com/yetone/magpie/internal/plugin" "github.com/yetone/magpie/internal/provider" + "github.com/yetone/magpie/internal/testenv" ) // movedFake is the fake plugin signed in as the built-in id it was moved // onto, its requests going to up. func movedFake(t *testing.T, id string, up http.Handler) { t.Helper() - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) fresh(t) t.Setenv("MAGPIE_BUN", bun) t.Setenv("FAKE_ID", id) diff --git a/internal/gateway/plugin_plan_lacks_test.go b/internal/gateway/plugin_plan_lacks_test.go index a133549b..be950e88 100644 --- a/internal/gateway/plugin_plan_lacks_test.go +++ b/internal/gateway/plugin_plan_lacks_test.go @@ -5,7 +5,6 @@ import ( "io" "net/http" "net/http/httptest" - "os/exec" "path/filepath" "strings" "sync" @@ -14,16 +13,14 @@ import ( "github.com/yetone/magpie/internal/plugin" "github.com/yetone/magpie/internal/provider" + "github.com/yetone/magpie/internal/testenv" ) // A plugin's account whose plan lacks the model is tried after the ones // that list it, as a Codex account's is (a Free one behind a Plus): the // plugin's first account, told only fake-1, isn't sent fake-resp. func TestPluginAccountLackingTheModelGoesLast(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) fresh(t) t.Setenv("MAGPIE_BUN", bun) t.Setenv("FAKE_RESPONSES", "1") diff --git a/internal/gateway/plugin_proxy_down_test.go b/internal/gateway/plugin_proxy_down_test.go index 8ff05133..3a7add57 100644 --- a/internal/gateway/plugin_proxy_down_test.go +++ b/internal/gateway/plugin_proxy_down_test.go @@ -7,7 +7,6 @@ import ( "net" "net/http" "net/http/httptest" - "os/exec" "path/filepath" "strings" "testing" @@ -15,16 +14,14 @@ import ( "github.com/yetone/magpie/internal/plugin" "github.com/yetone/magpie/internal/provider" + "github.com/yetone/magpie/internal/testenv" ) // A plugin account whose proxy is down, HTTP or SOCKS5, never reached its // vendor: as for a built-in's (TestProxyDownDoesNotRest), the next account // answers and the first isn't set aside. func TestPluginProxyDownDoesNotRest(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) for _, scheme := range []string{"http", "socks5"} { t.Run(scheme, func(t *testing.T) { fresh(t) diff --git a/internal/gateway/plugin_test.go b/internal/gateway/plugin_test.go index 44208a05..a02c1974 100644 --- a/internal/gateway/plugin_test.go +++ b/internal/gateway/plugin_test.go @@ -6,7 +6,6 @@ import ( "io" "net/http" "net/http/httptest" - "os/exec" "path/filepath" "slices" "strings" @@ -16,6 +15,7 @@ import ( "github.com/yetone/magpie/internal/plugin" "github.com/yetone/magpie/internal/provider" + "github.com/yetone/magpie/internal/testenv" ) // A plugin's provider signed in to is a provider like the others: each of @@ -23,10 +23,7 @@ import ( // base URL the plugin's loader gives, and the plugin's fetch carries the // request with the sign-in it keeps. func TestPluginProvider(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) fresh(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/gui/plugins_clash_test.go b/internal/gui/plugins_clash_test.go index 72cecd72..dd0790e7 100644 --- a/internal/gui/plugins_clash_test.go +++ b/internal/gui/plugins_clash_test.go @@ -6,7 +6,6 @@ import ( "net/http" "net/http/httptest" "os" - "os/exec" "path/filepath" "testing" @@ -19,10 +18,7 @@ import ( // /api/plugins carries the clash, naming the plugin that serves it and // the provider as it is listed, and prefer hands it to the other one. func TestPluginsClashRows(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) dir := t.TempDir() testenv.SetHome(t, dir) t.Setenv("XDG_CONFIG_HOME", filepath.Join(dir, "config")) diff --git a/internal/plugin/bunupdate_test.go b/internal/plugin/bunupdate_test.go index 87eed266..b436dc6e 100644 --- a/internal/plugin/bunupdate_test.go +++ b/internal/plugin/bunupdate_test.go @@ -11,7 +11,6 @@ import ( "net/http" "net/http/httptest" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -211,10 +210,7 @@ func TestCheckBunSetsAsideOneThatFailsItsTry(t *testing.T) { // The real try: the Bun on PATH passes it, as its own version. func TestTryBunOnRealBun(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) v := bunReported(bun) if err := tryBun(context.Background(), bun, v); err != nil { t.Fatalf("bun %s: %v", v, err) @@ -230,10 +226,7 @@ func TestHostFallsBackWhenTheNewBunDies(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("the dying bun is a shell script") } - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) bunHome(t, "9.9.9", time.Now().Add(-72*time.Hour)) t.Cleanup(Settle) if err := os.MkdirAll(bunDirOf(BunVersion), 0o755); err != nil { diff --git a/internal/plugin/plugin_test.go b/internal/plugin/plugin_test.go index 63c5189e..33b579b4 100644 --- a/internal/plugin/plugin_test.go +++ b/internal/plugin/plugin_test.go @@ -9,7 +9,6 @@ import ( "net/http" "net/http/httptest" "os" - "os/exec" "path/filepath" "reflect" "runtime" @@ -20,16 +19,14 @@ import ( "time" "github.com/yetone/magpie/internal/netproxy" + "github.com/yetone/magpie/internal/testenv" ) // sandbox gives the test its own magpie folders and the Bun on PATH; a // machine without Bun skips it. func sandbox(t *testing.T) { t.Helper() - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) dir := t.TempDir() t.Setenv("HOME", dir) t.Setenv("USERPROFILE", dir) diff --git a/internal/provider/account_order_plugin_test.go b/internal/provider/account_order_plugin_test.go index 8227ab6b..07337a2e 100644 --- a/internal/provider/account_order_plugin_test.go +++ b/internal/provider/account_order_plugin_test.go @@ -2,23 +2,20 @@ package provider import ( "context" - "os/exec" "path/filepath" "reflect" "testing" "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // A plugin provider's accounts are arranged like a built-in's: the page // names them by the provider's id, magpie keeps them as plugin:, and // the gateway tries them in the order dragged. func TestAccountOrderPluginAccounts(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/provider/docker_signin_test.go b/internal/provider/docker_signin_test.go index 299ec167..b6c44f7a 100644 --- a/internal/provider/docker_signin_test.go +++ b/internal/provider/docker_signin_test.go @@ -208,10 +208,7 @@ func TestCommandCodeSignInPastedKey(t *testing.T) { // bash" in Docker, where there's no shell to run it: the CLI is installed // first, as it is for a moved one. func TestGrokPluginSignInInstallsCLI(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) home := claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/provider/forget_account_test.go b/internal/provider/forget_account_test.go index 5950f478..7b5136cd 100644 --- a/internal/provider/forget_account_test.go +++ b/internal/provider/forget_account_test.go @@ -2,13 +2,13 @@ package provider import ( "context" - "os/exec" "path/filepath" "slices" "testing" "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // #694: a plugin's subscription removed from magpie was listed among the @@ -17,10 +17,7 @@ import ( // ForgetAccount signs its accounts out of plugin-auth.json and logins.json: // it leaves the removed list, and a sign-in later starts afresh. func TestForgetRemovedPluginAccount(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/provider/migrate_lapse_test.go b/internal/provider/migrate_lapse_test.go index 24640943..b4a9e32b 100644 --- a/internal/provider/migrate_lapse_test.go +++ b/internal/provider/migrate_lapse_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // An account the vendor refused while it ran on the plugin goes back to @@ -18,10 +19,7 @@ import ( // (every real mover clears the mark there): the built-in had marked it // the same way, and it would show signed in until its next refusal. func TestMoveBackKeepsLapse(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -76,10 +74,7 @@ func TestMoveBackKeepsLapse(t *testing.T) { // on a refused model token) moves along marked, as the built-in would // have marked it on its own model read, and doesn't stop the move. func TestMoveRefusedWhileListing(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -123,10 +118,7 @@ func TestMoveRefusedWhileListing(t *testing.T) { // the whole move. A plugin failing an account for any other reason still // does (TestMove's r-dead). func TestMoveExpiredInWords(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/provider/migrate_test.go b/internal/provider/migrate_test.go index 8b6f174d..266f79db 100644 --- a/internal/provider/migrate_test.go +++ b/internal/provider/migrate_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "os" - "os/exec" "path/filepath" "slices" "strings" @@ -12,6 +11,7 @@ import ( "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" "github.com/yetone/magpie/internal/zed" ) @@ -77,10 +77,7 @@ func fakeSaved(t *testing.T) map[string]savedLogin { // Moved, the plugin has the accounts in the user's order; moved back, the // built-in has them again, fresh tokens and order included. func TestMoveToPlugin(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -128,7 +125,7 @@ func TestMoveToPlugin(t *testing.T) { // an account the vendor refuses: nothing moves, and the other's token, // spent by the plugin, comes back renewed reset(fakeLogin("a@fake", "rot-a", false, true), fakeLogin("c@fake", "r-dead", false, true)) - err = Move(ctx, "fakeco") + err := Move(ctx, "fakeco") if err == nil || !strings.Contains(err.Error(), "c@fake") { t.Fatalf("Move with a refused account = %v", err) } @@ -282,10 +279,7 @@ func TestMoveToPlugin(t *testing.T) { // A move cut short (magpie quit mid-way) is put back before it is tried // again, and a failed one waits before magpie tries it by itself again. func TestMoveCutShort(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -361,10 +355,7 @@ func mustPlugin(t *testing.T) plugin.Provider { // when it renewed one, the built-in's when that renewed it meanwhile (a // request went to it), none when neither did. func TestPutBackKeepsNewer(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -470,10 +461,7 @@ func TestKeepMovedCurrent(t *testing.T) { // the built-in back first: its accounts go back to it rather than out of // sight with the plugin. func TestReleasePlugin(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) for _, op := range []string{"off", "remove"} { t.Run(op, func(t *testing.T) { claudeHome(t) @@ -502,6 +490,7 @@ func TestReleasePlugin(t *testing.T) { if got := MovedOnto(abs); len(got) != 1 || got[0] != "fakeco" { t.Fatalf("moved onto the plugin: %v", got) } + var err error if op == "off" { err = SetPluginOff(ctx, abs, true) } else { diff --git a/internal/provider/migrate_ux_test.go b/internal/provider/migrate_ux_test.go index eb1b1786..c577f38d 100644 --- a/internal/provider/migrate_ux_test.go +++ b/internal/provider/migrate_ux_test.go @@ -4,13 +4,13 @@ import ( "context" "encoding/json" "os" - "os/exec" "path/filepath" "strings" "testing" "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" "github.com/yetone/magpie/internal/zed" ) @@ -18,10 +18,7 @@ import ( // the plugin not yet installed: the move's install adds it. func fakePlugin(t *testing.T, inUse *[]string) (context.Context, string, *int) { t.Helper() - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/provider/plugin_checkin_test.go b/internal/provider/plugin_checkin_test.go index dfb8997d..804e8179 100644 --- a/internal/provider/plugin_checkin_test.go +++ b/internal/provider/plugin_checkin_test.go @@ -3,7 +3,6 @@ package provider import ( "context" "os" - "os/exec" "path/filepath" "slices" "strings" @@ -12,16 +11,14 @@ import ( "github.com/yetone/magpie/internal/plugin" "github.com/yetone/magpie/internal/settings" + "github.com/yetone/magpie/internal/testenv" ) // pluginCheckinSandbox is the fake plugin with its own check-in, signed in // with each key; it says the file each press is written to. func pluginCheckinSandbox(t *testing.T, keys ...string) string { t.Helper() - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) home := claudeHome(t) t.Setenv("MAGPIE_BUN", bun) presses := filepath.Join(home, "presses") @@ -175,10 +172,7 @@ func TestPluginCheckinRunsAsTheBuiltinsDo(t *testing.T) { // through its fetch, so an account isn't checked in twice; and its switch // follows the vendor's, which it took over, until set on its own. func TestPluginCheckinTakesOverTheVendors(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Setenv("FAKE_ID", "qoder") diff --git a/internal/provider/plugin_fetch_lapse_test.go b/internal/provider/plugin_fetch_lapse_test.go index a4460a8d..108e1fc1 100644 --- a/internal/provider/plugin_fetch_lapse_test.go +++ b/internal/provider/plugin_fetch_lapse_test.go @@ -3,23 +3,20 @@ package provider import ( "context" "net/http" - "os/exec" "path/filepath" "strings" "testing" "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // A plugin's fetch hook that throws on its sign-in (the vendor turned the // refresh away) marks the account lapsed, as a built-in's refused refresh // marked it; the other account is left alone. func TestPluginFetchSignInExpiredLapses(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Setenv("FAKE_BASE", "http://127.0.0.1:9/v1") diff --git a/internal/provider/plugin_fetchnew_test.go b/internal/provider/plugin_fetchnew_test.go index ba65eca3..9c236992 100644 --- a/internal/provider/plugin_fetchnew_test.go +++ b/internal/provider/plugin_fetchnew_test.go @@ -6,13 +6,13 @@ import ( "net/http" "net/http/httptest" "os" - "os/exec" "path/filepath" "sync/atomic" "testing" "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // A plugin's account listed with the plugin's providers is not asked for @@ -20,10 +20,7 @@ import ( // the Providers page built its state through FetchNew, and every ten // minutes asked every plugin's vendors for their lists again. func TestFetchNewLeavesListedPlugins(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -64,10 +61,7 @@ func TestFetchNewLeavesListedPlugins(t *testing.T) { // plugin: (routing) is read from the vendor once, as a built-in's, // asked for by one name, is. func TestPluginUsageReadOnceWhicheverName(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/provider/plugin_id_custom_test.go b/internal/provider/plugin_id_custom_test.go index 8e976ded..e9196aa3 100644 --- a/internal/provider/plugin_id_custom_test.go +++ b/internal/provider/plugin_id_custom_test.go @@ -2,12 +2,12 @@ package provider import ( "context" - "os/exec" "path/filepath" "testing" "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // #867: a provider of the user's own saved before a plugin with its id was @@ -15,10 +15,7 @@ import ( // subscription once signed in — "signed in, but magpie can't list it". The // plugin's is id-plugin then, and the user's own keeps its id. func TestPluginIDBesideCustomProvider(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/provider/plugin_id_reserved_test.go b/internal/provider/plugin_id_reserved_test.go index 72ff51d1..f499cafd 100644 --- a/internal/provider/plugin_id_reserved_test.go +++ b/internal/provider/plugin_id_reserved_test.go @@ -2,22 +2,19 @@ package provider import ( "context" - "os/exec" "path/filepath" "testing" "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // An installed plugin's provider id is kept for it signed in or not, as a // built-in subscription's is: a provider of the user's own saved on it, or // renamed onto it, would hide the plugin's subscription once signed in. func TestPluginIDReservedSignedOut(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/provider/plugin_moved_test.go b/internal/provider/plugin_moved_test.go index 04a61143..64a85bdb 100644 --- a/internal/provider/plugin_moved_test.go +++ b/internal/provider/plugin_moved_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "os" - "os/exec" "path/filepath" "strings" "testing" @@ -170,10 +169,7 @@ func TestAllLoginsMoved(t *testing.T) { // installed first when it isn't there, the code the page asks about shown // to copy, and the account it signs in to named as the one in use. func TestMovedSignInAsBuiltIn(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) home := claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/provider/plugin_usage_test.go b/internal/provider/plugin_usage_test.go index abcc45ff..4380ffff 100644 --- a/internal/provider/plugin_usage_test.go +++ b/internal/provider/plugin_usage_test.go @@ -5,23 +5,20 @@ import ( "fmt" "net/http" "net/http/httptest" - "os/exec" "path/filepath" "strings" "testing" "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // A plugin's accounts show their allowance as the built-ins' do: beside // each account, on the usage page, and to the gateway, a window counting // only some models holding back those alone. func TestPluginUsage(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -165,10 +162,7 @@ func TestPluginUsage(t *testing.T) { // built-ins' did, and a clean one takes the mark off; one the network // failed shows the last reading, as a built-in's did. func TestPluginUsageLapse(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -229,10 +223,7 @@ func TestPluginUsageLapse(t *testing.T) { // marks or clears as before. TestPluginFailover's refused account lost // its mark to a reading of its allowance begun just before the 401. func TestPluginUsageReadOlderThanAnswer(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -320,10 +311,7 @@ func TestPluginUsageReadOlderThanAnswer(t *testing.T) { // serves, as each of a built-in's accounts had: a key that has fewer // doesn't take on the first account's. func TestPluginAccountModels(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -379,10 +367,7 @@ func TestMovedProviderName(t *testing.T) { // A models hook that finds the sign-in refused and says so marks the // account, as a built-in whose model list the vendor refused marked it. func TestPluginModelsSayExpired(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/provider/pluginsignin_paste_test.go b/internal/provider/pluginsignin_paste_test.go index 4fc1b32f..4f1e9581 100644 --- a/internal/provider/pluginsignin_paste_test.go +++ b/internal/provider/pluginsignin_paste_test.go @@ -3,13 +3,13 @@ package provider import ( "context" "net/url" - "os/exec" "path/filepath" "strings" "testing" "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // A plugin's browser sign-in that comes back to a port of the plugin's on @@ -18,10 +18,7 @@ import ( // only this sign-in's port is taken, a page that sends the browser on is // the next page to open, a wrong code fails, the right one signs in. func TestPluginSignInTakesAPastedCallback(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Setenv("FAKE_LOOPBACK", "1") diff --git a/internal/provider/pluginsignin_test.go b/internal/provider/pluginsignin_test.go index dd0d253b..11fecc3c 100644 --- a/internal/provider/pluginsignin_test.go +++ b/internal/provider/pluginsignin_test.go @@ -2,7 +2,6 @@ package provider import ( "context" - "os/exec" "path/filepath" "slices" "strings" @@ -10,6 +9,7 @@ import ( "time" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // A plugin's sign-in is followed like a built-in one: the page, then the @@ -17,10 +17,7 @@ import ( // done, its provider is there, and removing it signs out. A key signs in // at once. func TestPluginSignIn(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) @@ -102,10 +99,7 @@ func TestPluginSignIn(t *testing.T) { // subscription does: each signed in beside the others, one first and the // rest on behind it or off, any put first, and removing one signs it out. func TestPluginAccounts(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/provider/signin_removed_test.go b/internal/provider/signin_removed_test.go index a3710a68..4673d7b8 100644 --- a/internal/provider/signin_removed_test.go +++ b/internal/provider/signin_removed_test.go @@ -6,7 +6,6 @@ import ( "net" "net/http" "net/http/httptest" - "os/exec" "path/filepath" "sync" "testing" @@ -14,6 +13,7 @@ import ( "github.com/yetone/magpie/internal/catalog" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // Signing in to an account removed from magpie shows it again before the @@ -62,10 +62,7 @@ func TestSignInDoneOnceTheAccountIsBack(t *testing.T) { }, { agent: "fakeco", setUp: func(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) claudeHome(t) t.Setenv("MAGPIE_BUN", bun) t.Cleanup(plugin.Settle) diff --git a/internal/testenv/bun.go b/internal/testenv/bun.go new file mode 100644 index 00000000..0fd254c9 --- /dev/null +++ b/internal/testenv/bun.go @@ -0,0 +1,27 @@ +package testenv + +import ( + "os/exec" + "sync" + "testing" +) + +// bunsRun holds a *sync.Once for each bun Bun has run. +var bunsRun sync.Map + +// Bun is the bun on PATH for a test that runs plugins with it (as +// MAGPIE_BUN), run once before the test opens its deadline; a machine +// without one skips the test. A bun just installed or upgraded hasn't run +// on this machine yet, and macOS checks a program before its first exec, +// which can take tens of seconds on a busy machine: that check must not eat +// the minute a test gives its plugin calls (ggbdpq, #1306). +func Bun(t testing.TB) string { + t.Helper() + bun, err := exec.LookPath("bun") + if err != nil { + t.Skip("no bun on PATH") + } + once, _ := bunsRun.LoadOrStore(bun, new(sync.Once)) + once.(*sync.Once).Do(func() { exec.Command(bun, "--version").Run() }) + return bun +} diff --git a/plugin_clash_cli_test.go b/plugin_clash_cli_test.go index 3c3699f1..8a34805c 100644 --- a/plugin_clash_cli_test.go +++ b/plugin_clash_cli_test.go @@ -3,22 +3,19 @@ package main import ( "context" "os" - "os/exec" "path/filepath" "strings" "testing" "github.com/yetone/magpie/internal/plugin" + "github.com/yetone/magpie/internal/testenv" ) // magpie plugin lists both plugins that sign in to one provider (neiko on // Discord: a third-party plugin and their own), says which one serves it // and how to have the other one serve it; `magpie plugin use` does that. func TestPluginListSaysClash(t *testing.T) { - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } + bun := testenv.Bun(t) groupsHome(t) t.Setenv("MAGPIE_BUN", bun) t.Setenv("MAGPIE_PLUGIN_MARKET", "off") diff --git a/plugin_login_cli_test.go b/plugin_login_cli_test.go index ae279d55..8926d9ab 100644 --- a/plugin_login_cli_test.go +++ b/plugin_login_cli_test.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "net/http/httptest" - "os/exec" "path/filepath" "sync/atomic" "testing" @@ -13,6 +12,7 @@ import ( "github.com/yetone/magpie/internal/plugin" "github.com/yetone/magpie/internal/provider" + "github.com/yetone/magpie/internal/testenv" ) // magpie accounts add, signing in to a plugin's provider removed from @@ -101,7 +101,7 @@ func TestPluginLoginReadsUsageAfresh(t *testing.T) { // plugin (FakeCo) added, as TestAccountsOfAPlugin sets one up. func fakeCoHome(t *testing.T) context.Context { t.Helper() - bun := pathBun(t) + bun := testenv.Bun(t) home := t.TempDir() t.Setenv("HOME", home) t.Setenv("USERPROFILE", home) @@ -122,21 +122,6 @@ func fakeCoHome(t *testing.T) context.Context { return ctx } -// pathBun is the bun on PATH, run once before the caller opens its -// one-minute context. A bun just installed or upgraded hasn't run on this -// machine yet, and macOS checks a program before its first exec, which can -// take tens of seconds on a busy machine: that check must not eat the -// minute the plugin calls have (ggbdpq, #1306; 7ddea2c2 for the copied bun). -func pathBun(t *testing.T) string { - t.Helper() - bun, err := exec.LookPath("bun") - if err != nil { - t.Skip("no bun on PATH") - } - exec.Command(bun, "--version").Run() - return bun -} - // noBrowser has the sign-in pages magpie opens go nowhere. It swaps the // opener rather than putting a do-nothing open first on PATH: a script // written now is a program macOS checks on its first run, which took 25 to