tests: a fake program is a file macOS has already let run (testenv.Program), so a fake's first start no longer eats a test's wait under load
macOS checks a newly written executable before its first exec: 0.27s alone, 2-5s while other new programs start (a go test ./... starting its test binaries). The same file again takes 0.009s, and the pass is kept across a rename and an in-place rewrite.108852b4worked round this for one test with /bin/sh -c; the class was every test that writes a `#!/bin/sh` fake and times its answer. testenv.Program(t, path, script) replaces os.WriteFile(path, script, 0o755) for fakes. On macOS it renames a file that has run once from a pool to path and writes the script into it in place; at the test's cleanup the file goes back to the pool (only a path the test created; a stand-in it wrote over stays). Fakes still end with their tests: nothing about how they are started or waited on changed. Run (and gateway's TestMain) remove the pool at the end. TestMain's PATH stand-ins are run once in the background (testenv.StandIns). Other systems write the file as before. Converted every `#!/bin/sh` fake written by tests under internal/ (agent, gateway, gui, library, plugin, proc, provider, update). Windows, binary and node/bun fakes are unchanged. Under the same load (3 loops each starting 8 freshly written scripts at a time), 4 rounds each, before (108852b4/ origin/main) vs after: - provider TestSavedButSignedOutClaudeServed: 4/4 fail (8.01s) -> 4/4 pass - provider TestClaudeSignInOverStaleCredentials: 4/4 fail (5.00s) -> 4/4 pass - provider TestDevinSignedInOnlyInMagpie: 3/4 fail ("CLI's own []") -> 4/4 pass - gateway TestClaudeRunEndsWhenTheClientLeaves: 4/4 fail -> 4/4 pass (and 3/3 fail -> 3/3 pass at a third of that load) - gateway TestClientGoneRecordedAsCanceled: 4/4 fail -> 4/4 pass - gui TestProvidersAnswerWhileListsComeIn ("cursor found false" at 3.03s): 4/4 fail -> 4/4 pass. The same class, not bfe8bcaf's test order. - proc TestUserPath (login shell's PATH never came, 7.01s): 4/4 fail -> 4/4 pass Unloaded, every touched package passes; no pool directory or process is left after a run.
This commit is contained in:
1 parent
0c08b1d5bb
commit
06420a7121
68 files changed
+472
-212
No files matched your search
@@ -10,6 +10,8 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
func TestNewerVersions(t *testing.T) {
|
||||
@@ -51,9 +53,7 @@ func TestInstalledVersion(t *testing.T) {
|
||||
}
|
||||
p := filepath.Join(bin, "pi")
|
||||
for _, c := range []struct{ out, want string }{{"0.98.2", "0.98.2"}, {"pi 0.99.0\n", "0.99.0"}} {
|
||||
if err := os.WriteFile(p, []byte("#!/bin/sh\necho '"+c.out+"'\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, p, "#!/bin/sh\necho '"+c.out+"'\n")
|
||||
if v := a.InstalledVersion(); v != c.want {
|
||||
t.Errorf("%q: %q", c.out, v)
|
||||
}
|
||||
@@ -87,7 +87,9 @@ func file(t *testing.T, path, body string) string {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(body), 0o755); err != nil {
|
||||
if strings.HasPrefix(body, "#!") {
|
||||
testenv.Program(t, path, body)
|
||||
} else if err := os.WriteFile(path, []byte(body), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
func TestFx(t *testing.T) {
|
||||
@@ -85,9 +86,7 @@ func TestFxDetected(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
bin := t.TempDir()
|
||||
t.Setenv("PATH", bin)
|
||||
if err := os.WriteFile(filepath.Join(bin, "fx"), []byte("#!/bin/sh\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, "fx"), "#!/bin/sh\n")
|
||||
if fx(home).Detected() {
|
||||
t.Error("an fx command alone is taken for the agent")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// TestGeminiNotAntigravity: a ~/.gemini holding only what Antigravity keeps
|
||||
@@ -42,7 +44,7 @@ func TestGeminiNotAntigravity(t *testing.T) {
|
||||
return
|
||||
}
|
||||
bin := t.TempDir()
|
||||
os.WriteFile(filepath.Join(bin, "gemini"), []byte("#!/bin/sh\n"), 0o755)
|
||||
testenv.Program(t, filepath.Join(bin, "gemini"), "#!/bin/sh\n")
|
||||
t.Setenv("PATH", bin)
|
||||
if !gemini(home).Detected() {
|
||||
t.Fatal("gemini on PATH not detected")
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestGooseDetectSkipsGoGoose(t *testing.T) {
|
||||
t.Fatal("a Go goose on PATH was taken for the agent")
|
||||
}
|
||||
|
||||
os.WriteFile(filepath.Join(bin, "goose"), []byte("#!/bin/sh\necho 1.9.0\n"), 0o755)
|
||||
testenv.Program(t, filepath.Join(bin, "goose"), "#!/bin/sh\necho 1.9.0\n")
|
||||
if !a.Detected() {
|
||||
t.Fatal("Block's goose on PATH not detected")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// An agent whose settings are left but whose CLI is gone (#843: dsh
|
||||
@@ -55,9 +57,7 @@ func TestCLIMissing(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
name += ".exe"
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(local, name), []byte("#!/bin/sh\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(local, name), "#!/bin/sh\n")
|
||||
if dsh.CLIMissing() {
|
||||
t.Fatal("dsh in ~/.local/bin taken for missing")
|
||||
}
|
||||
|
||||
@@ -102,9 +102,7 @@ func TestInstallCommandsWithoutNode(t *testing.T) {
|
||||
// npm.cmd on Windows; a folder without it, or a folder named npm, isn't it.
|
||||
func TestNpmIn(t *testing.T) {
|
||||
empty, nvm, win, odd := t.TempDir(), t.TempDir(), t.TempDir(), t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(nvm, "npm"), []byte("#!/bin/sh\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(nvm, "npm"), "#!/bin/sh\n")
|
||||
if err := os.WriteFile(filepath.Join(win, "npm.cmd"), nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/access"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -75,7 +76,7 @@ func TestWSLProbeFindsBunOmp(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
os.WriteFile(filepath.Join(bun, "bun"), []byte("#!/bin/sh\necho omp/16.5.1\n"), 0o755)
|
||||
testenv.Program(t, filepath.Join(bun, "bun"), "#!/bin/sh\necho omp/16.5.1\n")
|
||||
os.WriteFile(filepath.Join(bun, "omp"), []byte("#!/usr/bin/env bun\n"), 0o755)
|
||||
if err := os.Symlink(to, filepath.Join(sys, "timeout")); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/edit"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// #304: PI_CODING_AGENT_DIR moves Pi's agent folder (config.js,
|
||||
@@ -136,9 +137,7 @@ func TestOmpDetectedWithPiDir(t *testing.T) {
|
||||
if omp(home).Detected() {
|
||||
t.Error("Pi's folder is taken for omp's")
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(bin, "omp"), []byte("#!/bin/sh\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, "omp"), "#!/bin/sh\n")
|
||||
if !omp(home).Detected() {
|
||||
t.Error("omp's command is not")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/yetone/magpie/internal/gateway"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -59,9 +60,7 @@ func noKeychain(t *testing.T) {
|
||||
return
|
||||
}
|
||||
bin := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(bin, "security"), []byte("#!/bin/sh\nexit 44\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, "security"), "#!/bin/sh\nexit 44\n")
|
||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/yetone/magpie/internal/edit"
|
||||
"github.com/yetone/magpie/internal/gateway"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// claudeDistroHome is a distro's / in a temp dir with a user whose
|
||||
@@ -332,9 +333,8 @@ func TestWSLClaudeNotFolded(t *testing.T) {
|
||||
// this machine's Claude Code signed in, as its files say, with an inert
|
||||
// claude first on PATH: the machine's own is never asked
|
||||
bin := t.TempDir()
|
||||
writeFile(t, filepath.Join(bin, "claude"), "#!/bin/sh\nexit 1\n")
|
||||
testenv.Program(t, filepath.Join(bin, "claude"), "#!/bin/sh\nexit 1\n")
|
||||
writeFile(t, filepath.Join(bin, "claude.cmd"), "@exit /b 1\r\n")
|
||||
os.Chmod(filepath.Join(bin, "claude"), 0o755)
|
||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
writeFile(t, filepath.Join(home, ".claude", ".credentials.json"), fmt.Sprintf(
|
||||
`{"claudeAiOauth":{"accessToken":"tok","refreshToken":"r","expiresAt":%d,"subscriptionType":"max"}}`, time.Now().Add(time.Hour).UnixMilli()))
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// WSL puts Windows' PATH on the distro's, so `command -v pi` there finds
|
||||
@@ -56,7 +58,7 @@ func TestWSLProbeScriptSaysWhere(t *testing.T) {
|
||||
t.Skip("no sh")
|
||||
}
|
||||
home, bin := t.TempDir(), t.TempDir()
|
||||
os.WriteFile(filepath.Join(bin, "pi"), []byte("#!/bin/sh\n"), 0o755)
|
||||
testenv.Program(t, filepath.Join(bin, "pi"), "#!/bin/sh\n")
|
||||
cmd := exec.Command(sh, "-c", wslProbeScript)
|
||||
cmd.Env = []string{"HOME=" + home, "PATH=" + bin + ":/usr/bin:/bin"}
|
||||
out, err := cmd.Output()
|
||||
|
||||
@@ -20,10 +20,7 @@ func TestZedCredentialCommand(t *testing.T) {
|
||||
bin := t.TempDir()
|
||||
app := filepath.Join(t.TempDir(), "Zed.app")
|
||||
zedBin := filepath.Join(app, "Contents", "MacOS", "zed")
|
||||
writeFile(t, zedBin, "#!/bin/sh\n")
|
||||
if err := os.Chmod(zedBin, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, zedBin, "#!/bin/sh\n")
|
||||
if err := os.Symlink(zedBin, filepath.Join(bin, "zed")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -36,9 +33,7 @@ func TestZedCredentialCommand(t *testing.T) {
|
||||
t.Setenv("ZED_TEST_ARGS", args)
|
||||
name := "security"
|
||||
script := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$ZED_TEST_ARGS\"\n"
|
||||
if err := os.WriteFile(filepath.Join(bin, name), []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, name), script)
|
||||
url := "http://127.0.0.1:7654/v1"
|
||||
if err := saveZedCredential(url); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -47,9 +42,7 @@ func TestZedCredentialCommand(t *testing.T) {
|
||||
if got := readFile(args); got != want {
|
||||
t.Fatalf("credential arguments: %q, want %q", got, want)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(bin, name), []byte("#!/bin/sh\necho locked >&2\nexit 1\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, name), "#!/bin/sh\necho locked >&2\nexit 1\n")
|
||||
if err := saveZedCredential(url); err == nil || !strings.Contains(err.Error(), "locked") {
|
||||
t.Fatalf("credential store error: %v", err)
|
||||
}
|
||||
@@ -76,10 +69,7 @@ func TestZedAppPath(t *testing.T) {
|
||||
}
|
||||
custom := filepath.Join(t.TempDir(), "Custom Zed.app")
|
||||
zedBin := filepath.Join(custom, "Contents", "MacOS", "zed")
|
||||
writeFile(t, zedBin, "#!/bin/sh\n")
|
||||
if err := os.Chmod(zedBin, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, zedBin, "#!/bin/sh\n")
|
||||
if err := os.Symlink(zedBin, filepath.Join(bin, "zed")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -92,10 +82,7 @@ func TestZedAppPath(t *testing.T) {
|
||||
}
|
||||
customEnv := filepath.Join(t.TempDir(), "ZedG.app")
|
||||
customBin := filepath.Join(customEnv, "Contents", "MacOS", "zedg")
|
||||
writeFile(t, customBin, "#!/bin/sh\n")
|
||||
if err := os.Chmod(customBin, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, customBin, "#!/bin/sh\n")
|
||||
customEnv, err = filepath.EvalSymlinks(customEnv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
"github.com/yetone/magpie/internal/wslrun"
|
||||
)
|
||||
|
||||
@@ -222,7 +223,7 @@ while read -r line; do
|
||||
echo '{"type":"result","subtype":"success","is_error":false,"result":""}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
s := New()
|
||||
@@ -380,9 +381,7 @@ for line in sys.stdin:
|
||||
send({'type':'stream_event','event':{'type':'message_delta','delta':{'stop_reason':'end_turn'}}})
|
||||
send({'type':'stream_event','event':{'type':'message_stop'}})
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
t.Setenv("CLAUDE_CODE_EXTRA_BODY", `{"model":"must-not-leak"}`)
|
||||
s := New()
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// claudeRefuses has the fake Claude Code fail as Claude Code does on a
|
||||
@@ -42,9 +43,7 @@ while read -r line; do
|
||||
done
|
||||
`
|
||||
binary := strings.Split(os.Getenv("PATH"), string(os.PathListSeparator))[0]
|
||||
if err := os.WriteFile(filepath.Join(binary, "claude"), []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(binary, "claude"), script)
|
||||
}
|
||||
|
||||
func askClaudeModel(s *Server, text string) *httptest.ResponseRecorder {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// claudeMadeFirst signs Claude Code in to a@example.com, with
|
||||
@@ -83,7 +84,7 @@ while read -r line; do
|
||||
echo '{"type":"result","subtype":"success","is_error":false,"result":""}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(bin, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(bin, "claude"), script)
|
||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
provider.ForgetAccounts()
|
||||
t.Cleanup(provider.ForgetAccounts)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// What Claude Code writes as it exits reaches the client: its output is
|
||||
@@ -32,7 +33,7 @@ echo '{"type":"stream_event","event":{"type":"message_delta","delta":{"stop_reas
|
||||
echo '{"type":"stream_event","event":{"type":"message_stop"}}'
|
||||
}
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
for i := 0; i < 5; i++ {
|
||||
s := New()
|
||||
|
||||
@@ -73,7 +73,7 @@ while read -r line; do
|
||||
echo '{"type":"result","subtype":"success","is_error":false,"result":""}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
s := New()
|
||||
t.Cleanup(s.subscription.abortAll)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// fakeClaudeCalling answers each line with a reply that calls one of the
|
||||
@@ -48,7 +49,7 @@ while read -r line; do
|
||||
echo '{"type":"stream_event","event":{"type":"message_stop"}}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
return dir
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// rewindClaude stands in for Claude Code: it logs its arguments and each
|
||||
@@ -51,7 +52,7 @@ while read -r line; do
|
||||
echo '{"type":"result","subtype":"success","is_error":false,"result":""}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
return log
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// A request with output_config.format runs Claude Code with --json-schema;
|
||||
@@ -32,7 +33,7 @@ while read -r line; do
|
||||
echo '{"type":"result","subtype":"success","is_error":false,"result":"","structured_output":{"color":"blue"}}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
s := New()
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
func TestClaudeSubscriptionPromptKeepsForeignHarnessOutOfSystem(t *testing.T) {
|
||||
@@ -96,7 +97,7 @@ while read -r line; do
|
||||
echo '{"type":"result","subtype":"success","is_error":false,"result":""}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
}
|
||||
|
||||
@@ -474,7 +475,7 @@ while read -r line; do
|
||||
echo '{"type":"result","subtype":"success","is_error":true,"result":"You'"'"'ve hit your limit · resets 3am"}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
s := New()
|
||||
p := provider.Provider{ID: "claude", Account: &provider.Account{Agent: "claude", User: "u"}}
|
||||
@@ -525,7 +526,7 @@ while read -r line; do
|
||||
echo '{"type":"result","subtype":"success","is_error":false,"result":""}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
s := New()
|
||||
p := provider.Provider{ID: "claude", Account: &provider.Account{Agent: "claude", User: "u"}}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// fakeWarmClaude is a claude that writes what it was run with to log and
|
||||
@@ -27,9 +28,7 @@ func fakeWarmClaude(t *testing.T, out string, code int) string {
|
||||
"echo \"traffic:$CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\"; echo \"telemetry:$DISABLE_TELEMETRY\"; " +
|
||||
"echo \"errors:$DISABLE_ERROR_REPORTING\"; echo \"updater:$DISABLE_AUTOUPDATER\"; } > " + log + "\n" +
|
||||
"echo '" + out + "'\nexit " + string(rune('0'+code)) + "\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
t.Setenv("ANTHROPIC_BASE_URL", "http://127.0.0.1:3425")
|
||||
return log
|
||||
@@ -108,9 +107,7 @@ func TestClaudeUsageRunsClaudeCode(t *testing.T) {
|
||||
script := "#!/bin/sh\n" +
|
||||
"{ printf 'args:'; for a in \"$@\"; do printf '[%s]' \"$a\"; done; echo; echo \"token:$CLAUDE_CODE_OAUTH_TOKEN\"; } > " + log + "\n" +
|
||||
"cat " + out + "\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
t.Setenv("CLAUDE_CODE_OAUTH_TOKEN", "leaked")
|
||||
got, err := claudeUsage(context.Background())
|
||||
|
||||
@@ -43,7 +43,7 @@ while read -r line; do
|
||||
echo '{"type":"result","subtype":"success","is_error":false,"result":""}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
s := New()
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// twoProviders: "plan" (the one agents pick) falls back to "spare".
|
||||
@@ -275,7 +276,7 @@ while read -r line; do
|
||||
fi
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
provider.ForgetAccounts()
|
||||
t.Cleanup(provider.ForgetAccounts)
|
||||
|
||||
@@ -52,11 +52,10 @@ func isolatedTests(m *testing.M) (int, error) {
|
||||
// Finding an inert CLI also prevents discovery from falling back to
|
||||
// one installed at an absolute system path. Tests can prepend their
|
||||
// own fakes, as fakeClaude does, and still use ordinary shell tools.
|
||||
for _, name := range []string{"security", "secret-tool", "claude", "codex", "cursor-agent", "devin", "grok", "kiro-cli"} {
|
||||
if err := os.WriteFile(filepath.Join(bin, name), []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
|
||||
return 1, err
|
||||
}
|
||||
if err := testenv.StandIns(bin, []string{"security", "secret-tool", "claude", "codex", "cursor-agent", "devin", "grok", "kiro-cli"}); err != nil {
|
||||
return 1, err
|
||||
}
|
||||
defer testenv.RemovePrograms()
|
||||
if err := os.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")); err != nil {
|
||||
return 1, err
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
"github.com/yetone/magpie/internal/usage"
|
||||
)
|
||||
|
||||
@@ -267,7 +268,7 @@ while read -r line; do
|
||||
fi
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
provider.ForgetAccounts()
|
||||
t.Cleanup(provider.ForgetAccounts)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
"github.com/yetone/magpie/internal/usage"
|
||||
)
|
||||
|
||||
@@ -132,7 +133,7 @@ while read -r line; do
|
||||
echo '{"type":"result","subtype":"success","is_error":true,"api_error_status":529,"result":"Server is having a moment"}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
s := New()
|
||||
p := provider.Provider{ID: "claude", Account: &provider.Account{Agent: "claude", User: "u"}}
|
||||
@@ -176,7 +177,7 @@ while read -r line; do
|
||||
echo '{"type":"result","subtype":"success","is_error":false,"result":""}'
|
||||
done
|
||||
`
|
||||
os.WriteFile(filepath.Join(dir, "claude"), []byte(script), 0o755)
|
||||
testenv.Program(t, filepath.Join(dir, "claude"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
s := New()
|
||||
p := provider.Provider{ID: "claude", Account: &provider.Account{Agent: "claude", User: "u"}}
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// The Restart button runs `codex app-server daemon restart` with the codex
|
||||
@@ -22,9 +24,7 @@ func TestCodexDaemonRestartRoute(t *testing.T) {
|
||||
t.Setenv("CODEX_HOME", "/somewhere/else")
|
||||
rec := filepath.Join(bin, "ran")
|
||||
script := "#!/bin/sh\necho \"$@|$CODEX_HOME\" > '" + rec + "'\n[ \"$FAIL\" = 1 ] && { echo 'Error: no daemon' >&2; exit 1; }\nexit 0\n"
|
||||
if err := os.WriteFile(filepath.Join(bin, "codex"), []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, "codex"), script)
|
||||
mux := http.NewServeMux()
|
||||
providerRoutes(mux, nil)
|
||||
post := func(action string) *httptest.ResponseRecorder {
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// barWin is the desktop app as a Windows; the Bar icon routes reach no
|
||||
@@ -25,9 +27,7 @@ func TestBarIconTellsTheTray(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
bin := t.TempDir()
|
||||
for _, name := range []string{"omarchy", "omarchy-shell"} {
|
||||
if err := os.WriteFile(filepath.Join(bin, name), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, name), "#!/bin/sh\nexit 0\n")
|
||||
}
|
||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
priorOm, priorHypr, priorHook := onOmarchy, hyprland, onBarIcon
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// The Providers page answers at once while an account's vendor is slow to
|
||||
@@ -23,12 +24,12 @@ func TestProvidersAnswerWhileListsComeIn(t *testing.T) {
|
||||
h := sandboxHome(t)
|
||||
t.Setenv("PATH", "/usr/bin"+string(os.PathListSeparator)+"/bin")
|
||||
exe := filepath.Join(h, "cursor-agent")
|
||||
os.WriteFile(exe, []byte(`#!/bin/sh
|
||||
testenv.Program(t, exe, `#!/bin/sh
|
||||
case "$1" in
|
||||
about) echo '{"userEmail":"me@example.com","subscriptionTier":"Pro"}' ;;
|
||||
models) sleep 4; echo 'slow-1 - Slow One' ;;
|
||||
esac
|
||||
`), 0o755)
|
||||
`)
|
||||
old := provider.CursorExecutable
|
||||
provider.CursorExecutable = func() string { return exe }
|
||||
provider.ForgetAccounts()
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
func TestRegisterScheme(t *testing.T) {
|
||||
@@ -35,9 +37,7 @@ func TestRegisterScheme(t *testing.T) {
|
||||
t.Setenv("PATH", bin)
|
||||
for _, name := range []string{"update-desktop-database", "xdg-mime"} {
|
||||
script := "#!/bin/sh\nprintf '%s\\n' '" + name + "' \"$@\" >> \"$MAGPIE_SCHEME_TEST_CALLS\"\n"
|
||||
if err := os.WriteFile(filepath.Join(bin, name), []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, name), script)
|
||||
}
|
||||
dir := filepath.Join(data, "applications")
|
||||
path := filepath.Join(dir, "magpie.desktop")
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/yetone/magpie/internal/edit"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// Picking Claude Code's model answers at once, although /api/set answers
|
||||
@@ -39,7 +40,7 @@ func TestSetAnswersWhileDevinAndProvidersHang(t *testing.T) {
|
||||
|
||||
// a devin CLI that takes 4s over every answer
|
||||
devin := filepath.Join(bin, "devin")
|
||||
os.WriteFile(devin, []byte("#!/bin/sh\nexec sleep 4\n"), 0o755)
|
||||
testenv.Program(t, devin, "#!/bin/sh\nexec sleep 4\n")
|
||||
old := provider.DevinExecutable
|
||||
provider.DevinExecutable = func() string { return devin }
|
||||
t.Cleanup(func() { provider.DevinExecutable = old })
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/tidwall/jsonc"
|
||||
|
||||
"github.com/yetone/magpie/internal/agentenv"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// sandbox is a home with every agent magpie can give the library to, and
|
||||
@@ -32,8 +33,7 @@ func sandbox(t *testing.T) string {
|
||||
t.Setenv("XDG_CONFIG_HOME", filepath.Join(h, ".config"))
|
||||
// Gemini CLI is found by its binary alone
|
||||
bin := filepath.Join(h, "bin")
|
||||
write(t, filepath.Join(bin, "gemini"), "#!/bin/sh\n")
|
||||
os.Chmod(filepath.Join(bin, "gemini"), 0o755)
|
||||
testenv.Program(t, filepath.Join(bin, "gemini"), "#!/bin/sh\n")
|
||||
write(t, filepath.Join(bin, "gemini.exe"), "")
|
||||
t.Setenv("PATH", bin)
|
||||
// never the machine's global node_modules
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/jsonc"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
func piDoc(t *testing.T, p string) map[string]any {
|
||||
@@ -184,10 +186,7 @@ func fakePi(t *testing.T, bin, out string) {
|
||||
t.Skip("a shell script for pi")
|
||||
}
|
||||
p := filepath.Join(bin, "pi")
|
||||
write(t, p, "#!/bin/sh\necho '"+out+"'\n")
|
||||
if err := os.Chmod(p, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, p, "#!/bin/sh\necho '"+out+"'\n")
|
||||
t.Setenv("PATH", bin)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// TestRTKOffPath (#601): an rtk magpie finds where rtk's script puts it,
|
||||
@@ -18,11 +20,9 @@ import (
|
||||
func TestRTKOffPath(t *testing.T) {
|
||||
h := sandbox(t)
|
||||
local := filepath.Join(h, ".local", "bin")
|
||||
write(t, filepath.Join(local, "rtk"), fakeRTK)
|
||||
os.Chmod(filepath.Join(local, "rtk"), 0o755)
|
||||
testenv.Program(t, filepath.Join(local, "rtk"), fakeRTK)
|
||||
tools := filepath.Join(h, "tools")
|
||||
write(t, filepath.Join(tools, "curl"), "#!/bin/sh\n")
|
||||
os.Chmod(filepath.Join(tools, "curl"), 0o755)
|
||||
testenv.Program(t, filepath.Join(tools, "curl"), "#!/bin/sh\n")
|
||||
t.Setenv("PATH", tools)
|
||||
|
||||
v := ReadRTK()
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// fakeRTK is an rtk on PATH that acts as rtk 0.50's installer does for
|
||||
@@ -35,10 +37,7 @@ func TestRTK(t *testing.T) {
|
||||
}
|
||||
h := sandbox(t)
|
||||
bin := filepath.Join(h, "bin")
|
||||
write(t, filepath.Join(bin, "rtk"), fakeRTK)
|
||||
if err := os.Chmod(filepath.Join(bin, "rtk"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, "rtk"), fakeRTK)
|
||||
t.Setenv("PATH", bin)
|
||||
state := func() map[string]bool {
|
||||
m := map[string]bool{}
|
||||
@@ -220,15 +219,10 @@ func TestRTKOpenCode2(t *testing.T) {
|
||||
}
|
||||
h := sandbox(t)
|
||||
bin := filepath.Join(h, "bin")
|
||||
write(t, filepath.Join(bin, "rtk"), fakeRTK)
|
||||
testenv.Program(t, filepath.Join(bin, "rtk"), fakeRTK)
|
||||
opencode := func(version string) {
|
||||
t.Helper()
|
||||
write(t, filepath.Join(bin, "opencode"), "#!/bin/sh\necho "+version+"\n")
|
||||
for _, f := range []string{"rtk", "opencode"} {
|
||||
if err := os.Chmod(filepath.Join(bin, f), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, "opencode"), "#!/bin/sh\necho "+version+"\n")
|
||||
}
|
||||
t.Setenv("PATH", bin)
|
||||
plugin := filepath.Join(h, ".config", "opencode", "plugins", "rtk.ts")
|
||||
@@ -304,10 +298,7 @@ func TestRTKCodexOld(t *testing.T) {
|
||||
bin := filepath.Join(h, "bin")
|
||||
rtk := func(version, init string) {
|
||||
t.Helper()
|
||||
write(t, filepath.Join(bin, "rtk"), "#!/bin/sh\ncase \"$1\" in\n--version) echo \"rtk "+version+"\"; exit 0 ;;\ngain) exit 1 ;;\nesac\n"+init+"\n")
|
||||
if err := os.Chmod(filepath.Join(bin, "rtk"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, "rtk"), "#!/bin/sh\ncase \"$1\" in\n--version) echo \"rtk "+version+"\"; exit 0 ;;\ngain) exit 1 ;;\nesac\n"+init+"\n")
|
||||
}
|
||||
t.Setenv("PATH", bin)
|
||||
write(t, filepath.Join(codex, "config.toml"), "")
|
||||
@@ -361,8 +352,7 @@ func TestRTKGainRead(t *testing.T) {
|
||||
bin := filepath.Join(h, "bin")
|
||||
gain := func(body string) {
|
||||
t.Helper()
|
||||
write(t, filepath.Join(bin, "rtk"), "#!/bin/sh\ncase \"$1\" in\n--version) echo 'rtk 0.51.0'; exit 0 ;;\ngain) "+body+" ;;\nesac\nexit 2\n")
|
||||
os.Chmod(filepath.Join(bin, "rtk"), 0o755)
|
||||
testenv.Program(t, filepath.Join(bin, "rtk"), "#!/bin/sh\ncase \"$1\" in\n--version) echo 'rtk 0.51.0'; exit 0 ;;\ngain) "+body+" ;;\nesac\nexit 2\n")
|
||||
}
|
||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+"/bin"+string(os.PathListSeparator)+"/usr/bin")
|
||||
summary := `{"summary":{"total_commands":42,"total_input":9000,"total_output":1000,"total_saved":8000,"avg_savings_pct":88.9,"total_time_ms":10,"avg_time_ms":0},"daily":[{"date":"2026-10-03","commands":42,"input_tokens":9000,"output_tokens":1000,"saved_tokens":8000,"savings_pct":88.9,"total_time_ms":10,"avg_time_ms":0}]}`
|
||||
@@ -419,8 +409,7 @@ func TestCodexSandbox(t *testing.T) {
|
||||
func TestRTKNoHookListed(t *testing.T) {
|
||||
h := sandbox(t)
|
||||
bin := filepath.Join(h, "bin")
|
||||
write(t, filepath.Join(bin, "dsh"), "#!/bin/sh\n")
|
||||
os.Chmod(filepath.Join(bin, "dsh"), 0o755)
|
||||
testenv.Program(t, filepath.Join(bin, "dsh"), "#!/bin/sh\n")
|
||||
write(t, filepath.Join(bin, "dsh.exe"), "")
|
||||
var dsh *RTKAgent
|
||||
for _, a := range ReadRTK().Agents {
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// versionedRTK is an rtk whose version is in the file VERSION names, with
|
||||
@@ -69,7 +71,7 @@ func TestRTKUpgradeBrew(t *testing.T) {
|
||||
h := sandbox(t)
|
||||
latestServer(t, "v0.50.0")
|
||||
cellar := filepath.Join(h, "brew", "Cellar", "rtk", "0.28.2", "bin")
|
||||
write(t, filepath.Join(cellar, "rtk"), strings.ReplaceAll(versionedRTK, "VERSION", filepath.Join(cellar, "VERSION")))
|
||||
testenv.Program(t, filepath.Join(cellar, "rtk"), strings.ReplaceAll(versionedRTK, "VERSION", filepath.Join(cellar, "VERSION")))
|
||||
write(t, filepath.Join(cellar, "VERSION"), "0.28.2")
|
||||
bin := filepath.Join(h, "brew", "bin")
|
||||
os.MkdirAll(bin, 0o755)
|
||||
@@ -77,12 +79,10 @@ func TestRTKUpgradeBrew(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Homebrew so far has 0.49.0
|
||||
write(t, filepath.Join(bin, "brew"), `#!/bin/sh
|
||||
testenv.Program(t, filepath.Join(bin, "brew"), `#!/bin/sh
|
||||
[ "$*" = "upgrade rtk" ] || exit 2
|
||||
echo 0.49.0 > "`+filepath.Join(cellar, "VERSION")+`"
|
||||
`)
|
||||
os.Chmod(filepath.Join(cellar, "rtk"), 0o755)
|
||||
os.Chmod(filepath.Join(bin, "brew"), 0o755)
|
||||
t.Setenv("PATH", bin)
|
||||
|
||||
v := ReadRTK()
|
||||
@@ -122,15 +122,13 @@ func TestRTKUpgradeScript(t *testing.T) {
|
||||
h := sandbox(t)
|
||||
latestServer(t, "v0.50.0")
|
||||
dir := filepath.Join(h, ".local", "bin")
|
||||
write(t, filepath.Join(dir, "rtk"), strings.ReplaceAll(versionedRTK, "VERSION", filepath.Join(dir, "VERSION")))
|
||||
testenv.Program(t, filepath.Join(dir, "rtk"), strings.ReplaceAll(versionedRTK, "VERSION", filepath.Join(dir, "VERSION")))
|
||||
write(t, filepath.Join(dir, "VERSION"), "0.40.1")
|
||||
os.Chmod(filepath.Join(dir, "rtk"), 0o755)
|
||||
tools := filepath.Join(h, "tools")
|
||||
// curl hands over an installer that puts 0.50.0 in RTK_INSTALL_DIR
|
||||
write(t, filepath.Join(tools, "curl"), `#!/bin/sh
|
||||
testenv.Program(t, filepath.Join(tools, "curl"), `#!/bin/sh
|
||||
echo 'echo 0.50.0 > "$RTK_INSTALL_DIR/VERSION"'
|
||||
`)
|
||||
os.Chmod(filepath.Join(tools, "curl"), 0o755)
|
||||
t.Setenv("PATH", tools+string(os.PathListSeparator)+"/bin"+string(os.PathListSeparator)+"/usr/bin")
|
||||
|
||||
v := ReadRTK()
|
||||
@@ -210,15 +208,14 @@ func TestRTKWaitsForWinget(t *testing.T) {
|
||||
h := sandbox(t)
|
||||
latestServer(t, "v0.51.0")
|
||||
links := filepath.Join(h, "AppData", "Local", "Microsoft", "WinGet", "Links")
|
||||
write(t, filepath.Join(links, "rtk"), strings.ReplaceAll(versionedRTK, "VERSION", filepath.Join(links, "VERSION")))
|
||||
testenv.Program(t, filepath.Join(links, "rtk"), strings.ReplaceAll(versionedRTK, "VERSION", filepath.Join(links, "VERSION")))
|
||||
write(t, filepath.Join(links, "VERSION"), "0.50.0")
|
||||
os.Chmod(filepath.Join(links, "rtk"), 0o755)
|
||||
has, broken := filepath.Join(h, "winget-has"), filepath.Join(h, "winget-broken")
|
||||
write(t, has, "0.50.0")
|
||||
tools := filepath.Join(h, "tools")
|
||||
// winget show, as Windows in Chinese prints it; winget upgrade installs
|
||||
// what it has, when that is newer, and says when it isn't
|
||||
write(t, filepath.Join(tools, "winget"), `#!/bin/sh
|
||||
testenv.Program(t, filepath.Join(tools, "winget"), `#!/bin/sh
|
||||
[ -e "`+broken+`" ] && { echo "Failed when searching source: winget"; exit 1; }
|
||||
v=$(/bin/cat "`+has+`")
|
||||
case "$1" in
|
||||
@@ -229,7 +226,6 @@ upgrade)
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
`)
|
||||
os.Chmod(filepath.Join(tools, "winget"), 0o755)
|
||||
t.Setenv("PATH", links+string(os.PathListSeparator)+tools)
|
||||
up := upgraderOf
|
||||
upgraderOf = func(string) []string {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/agent"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// A file where an agent keeps its folder (~/.dsh, another tool's) is not
|
||||
@@ -20,8 +21,7 @@ func TestAgentHomeTakenByAFile(t *testing.T) {
|
||||
write(t, filepath.Join(h, ".dsh"), "ls -la\n")
|
||||
if runtime.GOOS != "windows" {
|
||||
bin := filepath.Join(h, "bin")
|
||||
write(t, filepath.Join(bin, "dsh"), "#!/bin/sh\n")
|
||||
os.Chmod(filepath.Join(bin, "dsh"), 0o755)
|
||||
testenv.Program(t, filepath.Join(bin, "dsh"), "#!/bin/sh\n")
|
||||
t.Setenv("PATH", bin)
|
||||
}
|
||||
a, err := agent.Find("dsh")
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// bunReleases serves Bun's releases as GitHub does: each version's zip for
|
||||
@@ -243,9 +245,7 @@ func TestHostFallsBackWhenTheNewBunDies(t *testing.T) {
|
||||
if err := os.MkdirAll(bunDirOf("9.9.9"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(bunExeOf("9.9.9"), []byte("#!/bin/sh\nexit 3\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, bunExeOf("9.9.9"), "#!/bin/sh\nexit 3\n")
|
||||
if err := writeBunState(bunState{Current: "9.9.9", Previous: BunVersion}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/agentenv"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// A desktop app with launchd's PATH finds a claude installed under a custom
|
||||
@@ -22,9 +23,9 @@ func TestUserPath(t *testing.T) {
|
||||
t.Setenv("USERPROFILE", home)
|
||||
npm := filepath.Join(home, ".npm-global", "bin")
|
||||
os.MkdirAll(npm, 0o755)
|
||||
os.WriteFile(filepath.Join(npm, "claude"), []byte("#!/bin/sh\n"), 0o755)
|
||||
testenv.Program(t, filepath.Join(npm, "claude"), "#!/bin/sh\n")
|
||||
sh := filepath.Join(home, "sh")
|
||||
os.WriteFile(sh, []byte("#!/bin/sh\necho 'welcome back!'\nPATH=/from/profile:$PATH\neval \"$2\"\necho bye\n"), 0o755)
|
||||
testenv.Program(t, sh, "#!/bin/sh\necho 'welcome back!'\nPATH=/from/profile:$PATH\neval \"$2\"\necho bye\n")
|
||||
t.Setenv("SHELL", sh)
|
||||
t.Setenv("PATH", "/usr/bin:/bin")
|
||||
|
||||
@@ -54,9 +55,9 @@ func TestUserPathTakesAgentVars(t *testing.T) {
|
||||
t.Setenv("USERPROFILE", home)
|
||||
pi := filepath.Join(home, "pi agent") // a space survives
|
||||
sh := filepath.Join(home, "sh")
|
||||
os.WriteFile(sh, []byte("#!/bin/sh\necho 'a profile that talks'\n"+
|
||||
testenv.Program(t, sh, "#!/bin/sh\necho 'a profile that talks'\n"+
|
||||
"export PI_CODING_AGENT_DIR='"+pi+"'\nexport CODEX_HOME=/from/profile/codex\nexport GROK_HOME=\n"+
|
||||
"eval \"$2\"\necho bye\n"), 0o755)
|
||||
"eval \"$2\"\necho bye\n")
|
||||
t.Setenv("SHELL", sh)
|
||||
t.Setenv("PATH", "/usr/bin:/bin")
|
||||
for _, v := range []string{"PI_CODING_AGENT_DIR", "GROK_HOME", "CLAUDE_CONFIG_DIR"} {
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// probeScript is a CLI that is a script starting another program and
|
||||
@@ -20,7 +22,7 @@ import (
|
||||
func probeScript(t *testing.T) (sh, pidFile string) {
|
||||
dir := t.TempDir()
|
||||
sh, pidFile = filepath.Join(dir, "cli"), filepath.Join(dir, "child.pid")
|
||||
os.WriteFile(sh, []byte("#!/bin/sh\nsleep 30 &\necho $! > "+pidFile+"\nwait\n"), 0o755)
|
||||
testenv.Program(t, sh, "#!/bin/sh\nsleep 30 &\necho $! > "+pidFile+"\nwait\n")
|
||||
old := waitDelay
|
||||
waitDelay = 200 * time.Millisecond
|
||||
t.Cleanup(func() { waitDelay = old })
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// A CLI is asked its version once per binary: one that fails to run (macOS
|
||||
@@ -22,9 +24,7 @@ func TestVersionRunsOnce(t *testing.T) {
|
||||
runs := filepath.Join(dir, "runs")
|
||||
bin := filepath.Join(dir, "codex")
|
||||
write := func(body string) {
|
||||
if err := os.WriteFile(bin, []byte("#!/bin/sh\necho x >> '"+runs+"'\n"+body), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, bin, "#!/bin/sh\necho x >> '"+runs+"'\n"+body)
|
||||
}
|
||||
count := func() int {
|
||||
b, _ := os.ReadFile(runs)
|
||||
@@ -51,7 +51,7 @@ func TestVersionRunsOnce(t *testing.T) {
|
||||
|
||||
// a timeout is asked again
|
||||
other := filepath.Join(dir, "slow")
|
||||
os.WriteFile(other, []byte("#!/bin/sh\n"), 0o755)
|
||||
testenv.Program(t, other, "#!/bin/sh\n")
|
||||
old := runVersion
|
||||
t.Cleanup(func() { runVersion = old })
|
||||
asked := 0
|
||||
|
||||
@@ -4,17 +4,18 @@ package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// a CLI that is a script starting another program: its timeout ends the
|
||||
// call, though what it started still holds the output
|
||||
func TestCommandContextTimeoutWithChild(t *testing.T) {
|
||||
sh := filepath.Join(t.TempDir(), "cli")
|
||||
os.WriteFile(sh, []byte("#!/bin/sh\nsleep 30\n"), 0o755)
|
||||
testenv.Program(t, sh, "#!/bin/sh\nsleep 30\n")
|
||||
old := waitDelay
|
||||
waitDelay = 200 * time.Millisecond
|
||||
t.Cleanup(func() { waitDelay = old })
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// fakeJWT is a token whose payload is the given claims; nobody checks the
|
||||
@@ -516,7 +517,7 @@ func TestClaudeSignedOut(t *testing.T) {
|
||||
status := `{"loggedIn": true, "email": "me@example.com", "subscriptionType": "max"}`
|
||||
exe := filepath.Join(home, "claude")
|
||||
fake := func() {
|
||||
os.WriteFile(exe, []byte("#!/bin/sh\ncat <<'X'\n"+status+"\nX\n"), 0o755)
|
||||
testenv.Program(t, exe, "#!/bin/sh\ncat <<'X'\n"+status+"\nX\n")
|
||||
forgetClaudeStatus()
|
||||
}
|
||||
claudeExecutable = func() string { return exe }
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
func claudeAuthFixture(t *testing.T, lapse string) (Provider, savedLogin) {
|
||||
@@ -134,9 +136,7 @@ func TestClaudeClearedKeychainDoesNotFallBackToStaleFile(t *testing.T) {
|
||||
writeFile(t, cleared, map[string]any{"claudeAiOauth": map[string]any{
|
||||
"accessToken": "", "refreshToken": "", "expiresAt": 0, "subscriptionType": "max",
|
||||
}})
|
||||
if err := os.WriteFile(filepath.Join(bin, "security"), []byte("#!/bin/sh\ncat '"+cleared+"'\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(bin, "security"), "#!/bin/sh\ncat '"+cleared+"'\n")
|
||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
claudeKeychain = true
|
||||
if _, err := sideToken(t, p, side.User); err == nil {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// Claude Code installed off magpie's PATH, in ~/.local/bin as its
|
||||
@@ -24,7 +26,7 @@ func TestClaudeFoundInLocalBin(t *testing.T) {
|
||||
bin := filepath.Join(h, ".local", "bin")
|
||||
os.MkdirAll(bin, 0o755)
|
||||
exe := filepath.Join(bin, name)
|
||||
os.WriteFile(exe, []byte("#!/bin/sh\n"), 0o755)
|
||||
testenv.Program(t, exe, "#!/bin/sh\n")
|
||||
if got := claudeExecutable(); got != exe {
|
||||
t.Fatalf("claude: %q, want %q", got, exe)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// With magpie wired into Claude Code, `claude auth status` tells of magpie's
|
||||
@@ -32,7 +34,7 @@ func TestClaudeAccountNamedAsItsLogin(t *testing.T) {
|
||||
})
|
||||
dir := t.TempDir()
|
||||
exe, env := filepath.Join(dir, "claude"), filepath.Join(dir, "env")
|
||||
os.WriteFile(exe, []byte("#!/bin/sh\necho \"token=$ANTHROPIC_AUTH_TOKEN\" > "+env+"\necho '{\"loggedIn\":true,\"authMethod\":\"oauth_token\",\"apiProvider\":\"firstParty\"}'\n"), 0o755)
|
||||
testenv.Program(t, exe, "#!/bin/sh\necho \"token=$ANTHROPIC_AUTH_TOKEN\" > "+env+"\necho '{\"loggedIn\":true,\"authMethod\":\"oauth_token\",\"apiProvider\":\"firstParty\"}'\n")
|
||||
claudeExecutable = func() string { return exe }
|
||||
loginsMu.Lock()
|
||||
loginsSeenAt = time.Time{}
|
||||
@@ -61,7 +63,7 @@ func TestClaudeIdentityServedWhileAsked(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
exe, who := filepath.Join(dir, "claude"), filepath.Join(dir, "who")
|
||||
os.WriteFile(who, []byte("a@example.com"), 0o600)
|
||||
os.WriteFile(exe, []byte("#!/bin/sh\nsleep 1\nprintf '{\"loggedIn\":true,\"email\":\"%s\",\"subscriptionType\":\"max\"}' \"$(cat "+who+")\"\n"), 0o755)
|
||||
testenv.Program(t, exe, "#!/bin/sh\nsleep 1\nprintf '{\"loggedIn\":true,\"email\":\"%s\",\"subscriptionType\":\"max\"}' \"$(cat "+who+")\"\n")
|
||||
old := claudeExecutable
|
||||
claudeExecutable = func() string { return exe }
|
||||
t.Cleanup(func() { claudeExecutable = old; forgetClaudeStatus() })
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/proc"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
func TestIsManagedDaemon(t *testing.T) {
|
||||
@@ -180,9 +181,7 @@ func fakeCodex(t *testing.T, exit int) (out string) {
|
||||
script += "echo 'Error: background server socket is stale or unreachable' >&2\nexit " + string(rune('0'+exit)) + "\n"
|
||||
}
|
||||
exe := filepath.Join(dir, "codex")
|
||||
if err := os.WriteFile(exe, []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, exe, script)
|
||||
old := codexExecutable
|
||||
t.Cleanup(func() { codexExecutable = old })
|
||||
codexExecutable = func() string { return exe }
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// A ChatGPT account lists the models of its own plan, asked with its own
|
||||
@@ -56,9 +58,7 @@ func TestCodexVersion(t *testing.T) {
|
||||
t.Setenv("USERPROFILE", home)
|
||||
writeFile(t, filepath.Join(home, ".codex", "models_cache.json"), map[string]any{"client_version": "0.160.0"})
|
||||
exe := filepath.Join(t.TempDir(), "codex")
|
||||
if err := os.WriteFile(exe, []byte("#!/bin/sh\necho codex-cli 0.161.1\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, exe, "#!/bin/sh\necho codex-cli 0.161.1\n")
|
||||
old := codexExecutable
|
||||
t.Cleanup(func() { codexExecutable = old; codexVersionCache.at = time.Time{}; codexSeen = seenVersion{} })
|
||||
codexSeen = seenVersion{}
|
||||
@@ -158,9 +158,7 @@ func TestCodexExecutableOffPath(t *testing.T) {
|
||||
os.WriteFile(filepath.Join(home, ".npmrc"), []byte("registry=https://registry.npmjs.org/\nprefix = ~/custom-npm\n"), 0o644)
|
||||
exe := filepath.Join(home, dir, "codex")
|
||||
os.MkdirAll(filepath.Dir(exe), 0o755)
|
||||
if err := os.WriteFile(exe, []byte("#!/bin/sh\necho codex-cli 0.161.0\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, exe, "#!/bin/sh\necho codex-cli 0.161.0\n")
|
||||
if got := codexExecutable(); got != exe {
|
||||
t.Fatalf("found %q, want %q", got, exe)
|
||||
}
|
||||
@@ -193,9 +191,7 @@ func TestCodexVersionNotRerun(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
runs := filepath.Join(dir, "runs")
|
||||
exe := filepath.Join(dir, "codex")
|
||||
if err := os.WriteFile(exe, []byte("#!/bin/sh\necho x >> '"+runs+"'\nexit 137\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, exe, "#!/bin/sh\necho x >> '"+runs+"'\nexit 137\n")
|
||||
old := codexExecutable
|
||||
t.Cleanup(func() { codexExecutable = old; codexVersionCache.at = time.Time{}; codexSeen = seenVersion{} })
|
||||
codexSeen = seenVersion{}
|
||||
@@ -213,7 +209,7 @@ func TestCodexVersionNotRerun(t *testing.T) {
|
||||
pkg := filepath.Join(dir, "node_modules", "@openai", "codex")
|
||||
os.MkdirAll(filepath.Join(pkg, "bin"), 0o755)
|
||||
os.WriteFile(filepath.Join(pkg, "package.json"), []byte(`{"version":"0.199.0"}`), 0o644)
|
||||
os.WriteFile(filepath.Join(pkg, "bin", "codex.js"), []byte("#!/bin/sh\necho x >> '"+runs+"'\necho codex-cli 0.199.0\n"), 0o755)
|
||||
testenv.Program(t, filepath.Join(pkg, "bin", "codex.js"), "#!/bin/sh\necho x >> '"+runs+"'\necho codex-cli 0.199.0\n")
|
||||
link := filepath.Join(dir, "bin", "codex")
|
||||
os.MkdirAll(filepath.Dir(link), 0o755)
|
||||
os.Symlink(filepath.Join(pkg, "bin", "codex.js"), link)
|
||||
|
||||
@@ -2,11 +2,12 @@ package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// an ask that couldn't tell leaves the account served, on disk too, where
|
||||
@@ -97,9 +98,7 @@ func TestAskCursorStatus(t *testing.T) {
|
||||
{"prints no JSON", `echo 'Something went wrong'`, false, "", false},
|
||||
{"fails, token gone", `exit 1`, true, "", true},
|
||||
} {
|
||||
if err := os.WriteFile(exe, []byte("#!/bin/sh\n"+c.script+"\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, exe, "#!/bin/sh\n"+c.script+"\n")
|
||||
signedOut = c.out
|
||||
u, _, ok, err := askCursorStatus()
|
||||
if u != c.user || ok != (c.user != "") || (err == nil) != c.sure {
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// TestDevinSignedInOnlyInMagpie is 蓝猫's Devin on Discord: the CLI kept a
|
||||
@@ -33,7 +35,7 @@ func TestDevinSignedInOnlyInMagpie(t *testing.T) {
|
||||
// an old account's, one Devin refused, or the account signed in (by
|
||||
// magpie, or by `devin auth login` after)
|
||||
exe := filepath.Join(home, "devin")
|
||||
os.WriteFile(exe, []byte(`#!/bin/sh
|
||||
testenv.Program(t, exe, `#!/bin/sh
|
||||
f="$XDG_DATA_HOME/devin/credentials.toml"
|
||||
[ -f "$f" ] || { echo "Not logged in"; exit 1; }
|
||||
if grep -q refused "$f"; then
|
||||
@@ -47,7 +49,7 @@ if [ "$1" = models ]; then
|
||||
fi
|
||||
if grep -q old "$f"; then who=old@example.com; else who=dev@example.com; fi
|
||||
printf 'Logged in (via Devin).\n\nUser:\n Email: %s\n\nAccount:\n Tier: Devin Pro\n' "$who"
|
||||
`), 0o755)
|
||||
`)
|
||||
fakeDevin(t, exe)
|
||||
devinFamiliesCached(nil)
|
||||
t.Cleanup(func() { devinFamiliesCached(nil) })
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// Devin's own ids models.dev doesn't know (swe-2, gpt-6-1-sol) take images,
|
||||
@@ -58,7 +59,7 @@ func TestDevinImagesFromModelConfigs(t *testing.T) {
|
||||
t.Setenv("WINDSURF_API_SERVER_URL", fake.URL)
|
||||
|
||||
exe := filepath.Join(home, "devin")
|
||||
os.WriteFile(exe, []byte(`#!/bin/sh
|
||||
testenv.Program(t, exe, `#!/bin/sh
|
||||
cat <<'X'
|
||||
{"families":[
|
||||
{"family_label":"SWE-2","family_uid":"swe-2","aliases":["swe"],"variants":[
|
||||
@@ -67,7 +68,7 @@ cat <<'X'
|
||||
{"model_uid":"glm-5-2","label":"GLM-5.2 High"},{"model_uid":"glm-5-2-max","label":"GLM-5.2 Max"}]},
|
||||
{"family_label":"New","family_uid":"new-model","variants":[{"model_uid":"new-model-high","label":"New High"}]}]}
|
||||
X
|
||||
`), 0o755)
|
||||
`)
|
||||
fakeDevin(t, exe)
|
||||
|
||||
families, err := askDevinFamiliesAt(context.Background(), "")
|
||||
|
||||
@@ -3,6 +3,7 @@ package provider
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -183,7 +184,7 @@ func TestDevinSignIn(t *testing.T) {
|
||||
|
||||
// a CLI that answers `auth status` once the exchange wrote its file
|
||||
exe := filepath.Join(home, "devin")
|
||||
os.WriteFile(exe, []byte("#!/bin/sh\ncat <<'X'\nLogged in (via Devin).\n\nUser:\n Email: dev@example.com\n\nAccount:\n Tier: Devin Pro\nX\n"), 0o755)
|
||||
testenv.Program(t, exe, "#!/bin/sh\ncat <<'X'\nLogged in (via Devin).\n\nUser:\n Email: dev@example.com\n\nAccount:\n Tier: Devin Pro\nX\n")
|
||||
oldExe := DevinExecutable
|
||||
DevinExecutable = func() string { return exe }
|
||||
t.Cleanup(func() { DevinExecutable = oldExe })
|
||||
@@ -258,7 +259,7 @@ func TestDevinSeveralAccounts(t *testing.T) {
|
||||
|
||||
// a CLI that says whose key is in the credentials.toml of its data folder
|
||||
exe := filepath.Join(home, "devin")
|
||||
os.WriteFile(exe, []byte(`#!/bin/sh
|
||||
testenv.Program(t, exe, `#!/bin/sh
|
||||
f="$XDG_DATA_HOME/devin/credentials.toml"
|
||||
[ -f "$f" ] || { echo "Not logged in"; exit 1; }
|
||||
if grep -q own "$f"; then who=dev@example.com; tier="Devin Pro"; else who=two@example.com; tier="Devin Max"; fi
|
||||
@@ -270,7 +271,7 @@ User:
|
||||
Account:
|
||||
Tier: %s
|
||||
' "$who" "$tier"
|
||||
`), 0o755)
|
||||
`)
|
||||
oldExe := DevinExecutable
|
||||
DevinExecutable = func() string { return exe }
|
||||
t.Cleanup(func() { DevinExecutable = oldExe })
|
||||
@@ -427,9 +428,7 @@ func TestAskDevinStatus(t *testing.T) {
|
||||
} else if err := os.Remove(creds); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(exe, []byte("#!/bin/sh\n"+c.script+"\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, exe, "#!/bin/sh\n"+c.script+"\n")
|
||||
u, _, ok, err := askDevinStatus()
|
||||
if u != c.user || ok != (c.user != "") || (err == nil) != c.sure {
|
||||
t.Errorf("%s: %q %v %v", c.name, u, ok, err)
|
||||
@@ -455,9 +454,7 @@ func TestDevinStatusKeepsTheAccount(t *testing.T) {
|
||||
// signed in once, then asked again behind what is served
|
||||
ask := func(script string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(exe, []byte("#!/bin/sh\n"+script+"\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, exe, "#!/bin/sh\n"+script+"\n")
|
||||
devinStatus.Lock()
|
||||
devinStatus.at = time.Now().Add(-2 * time.Minute)
|
||||
done := devinStatus.refresh()
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// TestDevinUsage: magpie's built-in Devin account showed no usage while the
|
||||
@@ -24,9 +26,9 @@ func TestDevinUsage(t *testing.T) {
|
||||
os.MkdirAll(filepath.Join(data, "devin"), 0o700)
|
||||
os.WriteFile(filepath.Join(data, "devin", "credentials.toml"), devinCredentials("devin-session-token$good", "", "", ""), 0o600)
|
||||
exe := filepath.Join(home, "devin")
|
||||
os.WriteFile(exe, []byte(`#!/bin/sh
|
||||
testenv.Program(t, exe, `#!/bin/sh
|
||||
printf 'Logged in (via Devin).\n\nUser:\n Email: dev@example.com\n\nAccount:\n Tier: Devin Pro\n'
|
||||
`), 0o755)
|
||||
`)
|
||||
fakeDevin(t, exe)
|
||||
forgetDevinStatus()
|
||||
old := firstAsk
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/plugin"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// magpie in Docker (xugui on Discord: "docker版登不上grok和command code"):
|
||||
@@ -260,7 +261,7 @@ export const P = async () => ({
|
||||
runInstaller = func(ctx context.Context, c agentCLI) ([]byte, error) {
|
||||
ran = c.Name
|
||||
os.MkdirAll(filepath.Dir(exe), 0o755)
|
||||
return nil, os.WriteFile(exe, []byte("#!/bin/sh\n"), 0o755)
|
||||
return nil, testenv.WriteProgram(exe, "#!/bin/sh\n")
|
||||
}
|
||||
t.Cleanup(func() { runInstaller = oldRun })
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
func TestParseGrokModels(t *testing.T) {
|
||||
@@ -230,9 +232,7 @@ func TestGrokExecutableFinds(t *testing.T) {
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte("#!/bin/sh\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, p, "#!/bin/sh\n")
|
||||
return p
|
||||
}
|
||||
npm := filepath.Join(home, "npm")
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// A request that only reads builds the catalog once for all its look-ups
|
||||
@@ -146,9 +147,7 @@ func TestCursorKeychainReadOnce(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
log := filepath.Join(dir, "runs")
|
||||
script := "#!/bin/sh\necho run >> " + log + "\necho tok-$(wc -l < " + log + " | tr -d ' ')\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "security"), []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, filepath.Join(dir, "security"), script)
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
forgetCursorStatus()
|
||||
t.Cleanup(forgetCursorStatus)
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// a sign-in that needs a CLI this machine lacks installs it, then goes on
|
||||
@@ -35,7 +37,7 @@ func TestSignInInstallsMissingCLI(t *testing.T) {
|
||||
ran = c.sh
|
||||
<-release
|
||||
os.MkdirAll(filepath.Dir(exe), 0o755)
|
||||
os.WriteFile(exe, []byte("#!/bin/sh\nprintf 'Logged in (via Devin).\\n Email: dev@example.com\\n Tier: Devin Pro\\n'\n"), 0o755)
|
||||
testenv.Program(t, exe, "#!/bin/sh\nprintf 'Logged in (via Devin).\\n Email: dev@example.com\\n Tier: Devin Pro\\n'\n")
|
||||
// as Devin's does: `devin setup` gives up on its sign-in without a terminal
|
||||
return []byte("Installed devin\nError: Login canceled\n"), errors.New("exit status 1")
|
||||
}
|
||||
@@ -126,7 +128,7 @@ func TestDevinSignInUnseen(t *testing.T) {
|
||||
home := claudeHome(t)
|
||||
t.Setenv("XDG_DATA_HOME", filepath.Join(home, "data"))
|
||||
exe := filepath.Join(home, "devin")
|
||||
os.WriteFile(exe, []byte("#!/bin/sh\necho 'Not logged in.'\n"), 0o755)
|
||||
testenv.Program(t, exe, "#!/bin/sh\necho 'Not logged in.'\n")
|
||||
oldExe := DevinExecutable
|
||||
DevinExecutable = func() string { return exe }
|
||||
t.Cleanup(func() { DevinExecutable = oldExe })
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
"github.com/yetone/magpie/internal/update"
|
||||
)
|
||||
|
||||
@@ -100,7 +101,7 @@ func TestMoveDevin(t *testing.T) {
|
||||
os.MkdirAll(filepath.Join(data, "devin"), 0o700)
|
||||
os.WriteFile(DevinCredentialsPath(), devinCredentials("key-own", "", "", ""), 0o600)
|
||||
exe := filepath.Join(home, "devin")
|
||||
os.WriteFile(exe, []byte("#!/bin/sh\n"+devinSigned+"\n"), 0o755)
|
||||
testenv.Program(t, exe, "#!/bin/sh\n"+devinSigned+"\n")
|
||||
fakeDevin(t, exe)
|
||||
// the CLI's account is read by running it, which a loaded machine may
|
||||
// take longer than a look's first wait for
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// prefsHome is a sandbox home where two providers serve the same model,
|
||||
@@ -32,7 +33,7 @@ func prefsHome(t *testing.T) {
|
||||
// a `security` that finds nothing: no Keychain of the user's is read
|
||||
if runtime.GOOS != "windows" {
|
||||
bin := t.TempDir()
|
||||
os.WriteFile(filepath.Join(bin, "security"), []byte("#!/bin/sh\nexit 44\n"), 0o755)
|
||||
testenv.Program(t, filepath.Join(bin, "security"), "#!/bin/sh\nexit 44\n")
|
||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
}
|
||||
os.MkdirAll(filepath.Dir(catalog.CachePath()), 0o755)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/yetone/magpie/internal/plugin"
|
||||
"github.com/yetone/magpie/internal/settings"
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// movedPlugin sets up a built-in moved onto its plugin, id's accounts as
|
||||
@@ -230,7 +231,7 @@ func TestMovedSignInAsBuiltIn(t *testing.T) {
|
||||
runInstaller = func(ctx context.Context, c agentCLI) ([]byte, error) {
|
||||
<-release
|
||||
os.MkdirAll(filepath.Dir(exe), 0o755)
|
||||
return nil, os.WriteFile(exe, []byte("#!/bin/sh\n"), 0o755)
|
||||
return nil, testenv.WriteProgram(exe, "#!/bin/sh\n")
|
||||
}
|
||||
t.Cleanup(func() { runInstaller = oldRun })
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// Saved accounts of an agent not signed in where magpie looks (a magpie
|
||||
@@ -79,7 +81,7 @@ func TestSavedButSignedOutClaudeServed(t *testing.T) {
|
||||
shellFakes(t)
|
||||
claudeSignIn(t, home, time.Now().Add(time.Hour))
|
||||
exe := filepath.Join(home, "claude")
|
||||
os.WriteFile(exe, []byte("#!/bin/sh\necho '{\"loggedIn\": false}'\n"), 0o755)
|
||||
testenv.Program(t, exe, "#!/bin/sh\necho '{\"loggedIn\": false}'\n")
|
||||
claudeExecutable = func() string { return exe }
|
||||
check("auth status signed out")
|
||||
}
|
||||
@@ -125,12 +127,12 @@ func TestClaudeKeychainReadsItsAccount(t *testing.T) {
|
||||
bin := filepath.Join(home, "bin")
|
||||
os.MkdirAll(bin, 0o755)
|
||||
// the leftover comes first unless the account is asked for
|
||||
os.WriteFile(filepath.Join(bin, "security"), []byte(`#!/bin/sh
|
||||
testenv.Program(t, filepath.Join(bin, "security"), `#!/bin/sh
|
||||
case "$*" in
|
||||
*"-a tester"*) echo '{"claudeAiOauth":{"accessToken":"live","subscriptionType":"max"}}' ;;
|
||||
*) echo '{}' ;;
|
||||
esac
|
||||
`), 0o755)
|
||||
`)
|
||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
claudeKeychain = true
|
||||
forgetClaudeCredential()
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// fakeClaudeAccount is who a fake `claude auth login` signs in to.
|
||||
@@ -79,15 +81,13 @@ EOF
|
||||
echo '{"loggedIn":true,"email":"%[8]s","subscriptionType":"%[7]s"}' > "$CLAUDE_CONFIG_DIR/status.json"
|
||||
echo "Login successful."
|
||||
`, own, opened, printed, goFile, a.refresh, time.Now().Add(time.Hour).UnixMilli(), a.plan, a.email, a.orgUUID, a.org, a.name)
|
||||
if err := os.WriteFile(exe, []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, exe, script)
|
||||
browser := filepath.Join(dir, "browser")
|
||||
body := "#!/bin/sh\nexit 1\n"
|
||||
if opens {
|
||||
body = "#!/bin/sh\nprintf %s \"$1\" > \"$" + openedURLEnv + "\"\n"
|
||||
}
|
||||
os.WriteFile(browser, []byte(body), 0o755)
|
||||
testenv.Program(t, browser, body)
|
||||
oldExe, oldOpener := claudeExecutable, claudeURLOpener
|
||||
claudeExecutable = func() string { return exe }
|
||||
claudeURLOpener = func() string { return browser }
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package testenv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Program writes script, a `#!` script, at path as a program a test runs,
|
||||
// where os.WriteFile(path, script, 0o755) would. On macOS the file is one
|
||||
// the system has already let run: macOS checks a newly written executable
|
||||
// before its first exec, a quarter of a second alone and several seconds
|
||||
// while other new programs start (a go test ./... starting its test
|
||||
// binaries), which ate the wait of every test that times a fake's answer
|
||||
// (TestDevinSignedInOnlyInMagpie, TestClaudeRunEndsWhenTheClientLeaves).
|
||||
// The check is made once a file: one already run keeps its pass when it is
|
||||
// renamed or written over in place. So Program takes a file that has run
|
||||
// from a pool, renames it to path and writes the script into it; when the
|
||||
// test ends, the file goes back to the pool for the next test.
|
||||
func Program[S ~string | ~[]byte](t testing.TB, path string, script S) {
|
||||
t.Helper()
|
||||
_, err := os.Lstat(path)
|
||||
existed := err == nil
|
||||
if err := WriteProgram(path, script); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// a file the test only wrote over (a stand-in TestMain made) stays,
|
||||
// as it would have
|
||||
if !existed {
|
||||
t.Cleanup(func() { reclaim(path) })
|
||||
}
|
||||
}
|
||||
|
||||
// WriteProgram is Program for a fake that lasts the whole test binary, made
|
||||
// from TestMain, or written by a stand-in for the code under test: it is
|
||||
// not taken back when a test ends.
|
||||
func WriteProgram[S ~string | ~[]byte](path string, script S) error {
|
||||
b := []byte(script)
|
||||
if !bytes.HasPrefix(b, []byte("#!")) {
|
||||
// a binary written into a file that passed as a script is no test's
|
||||
return fmt.Errorf("testenv.Program: %s is no #! script", path)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime.GOOS != "darwin" {
|
||||
return os.WriteFile(path, b, 0o755)
|
||||
}
|
||||
if !ran(path) {
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := warmAt(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// written over in place: the file keeps the pass of its first run
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.Write(b); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(path, 0o755)
|
||||
}
|
||||
|
||||
// RemovePrograms deletes the pool, after the tests: Run does it, and a
|
||||
// TestMain of its own calls it.
|
||||
func RemovePrograms() {
|
||||
warming.Wait()
|
||||
pool.Lock()
|
||||
defer pool.Unlock()
|
||||
if pool.dir != "" {
|
||||
os.RemoveAll(pool.dir)
|
||||
}
|
||||
pool.dir, pool.free, pool.ran = "", nil, nil
|
||||
}
|
||||
|
||||
// pool holds the files that have run once: the free ones in dir, and in ran
|
||||
// each one at a program's path.
|
||||
var pool struct {
|
||||
sync.Mutex
|
||||
dir string
|
||||
n int
|
||||
free []string
|
||||
ran map[string]os.FileInfo
|
||||
}
|
||||
|
||||
// ran says whether path is a file that has run once, put there by
|
||||
// WriteProgram and still in place.
|
||||
func ran(path string) bool {
|
||||
pool.Lock()
|
||||
fi, ok := pool.ran[path]
|
||||
pool.Unlock()
|
||||
now, err := os.Stat(path)
|
||||
return ok && err == nil && os.SameFile(fi, now)
|
||||
}
|
||||
|
||||
// warmAt puts a file that has run once at path: a free one from the pool,
|
||||
// or a new one run there now.
|
||||
func warmAt(path string) error {
|
||||
pool.Lock()
|
||||
for len(pool.free) > 0 {
|
||||
f := pool.free[0]
|
||||
pool.free = pool.free[1:]
|
||||
if os.Rename(f, path) == nil {
|
||||
pool.Unlock()
|
||||
return record(path)
|
||||
}
|
||||
os.Remove(f) // another file system: the pool is no help here
|
||||
}
|
||||
pool.Unlock()
|
||||
if err := os.WriteFile(path, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
// run as a test's code runs it (proc.Command is out of reach: proc's
|
||||
// own tests use this package), on macOS only, so no console to hide
|
||||
p, err := os.StartProcess(path, []string{path}, &os.ProcAttr{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("testenv.Program: a first run of %s: %v", path, err)
|
||||
}
|
||||
if st, err := p.Wait(); err != nil || !st.Success() {
|
||||
return fmt.Errorf("testenv.Program: a first run of %s: %v %v", path, st, err)
|
||||
}
|
||||
return record(path)
|
||||
}
|
||||
|
||||
// record notes path as a file that has run once.
|
||||
func record(path string) error {
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pool.Lock()
|
||||
defer pool.Unlock()
|
||||
if pool.ran == nil {
|
||||
pool.ran = map[string]os.FileInfo{}
|
||||
}
|
||||
pool.ran[path] = fi
|
||||
return nil
|
||||
}
|
||||
|
||||
// reclaim takes path back into the pool when it is still the file Program
|
||||
// put there: one the code under test replaced, or that is gone, is not.
|
||||
func reclaim(path string) {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return
|
||||
}
|
||||
pool.Lock()
|
||||
defer pool.Unlock()
|
||||
fi, ok := pool.ran[path]
|
||||
delete(pool.ran, path)
|
||||
if now, err := os.Stat(path); !ok || err != nil || !os.SameFile(fi, now) {
|
||||
return
|
||||
}
|
||||
if pool.dir == "" {
|
||||
d, err := os.MkdirTemp("", "magpie-test-programs-")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pool.dir = d
|
||||
}
|
||||
pool.n++
|
||||
f := filepath.Join(pool.dir, strconv.Itoa(pool.n))
|
||||
if os.Rename(path, f) == nil {
|
||||
pool.free = append(pool.free, f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package testenv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A program Program writes runs what it was given; when its test ends, the
|
||||
// file goes back to the pool and the next test's program is that same
|
||||
// file, which macOS has let run already. A file that was there before, such
|
||||
// as a stand-in TestMain made, stays when the test that wrote over it ends.
|
||||
func TestProgram(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("shell scripts")
|
||||
}
|
||||
t.Cleanup(RemovePrograms) // this package has no TestMain to do it
|
||||
var first os.FileInfo
|
||||
t.Run("first", func(t *testing.T) {
|
||||
p := filepath.Join(t.TempDir(), "fake")
|
||||
Program(t, p, "#!/bin/sh\necho one \"$@\"\n")
|
||||
if out, err := exec.Command(p, "a").Output(); err != nil || string(out) != "one a\n" {
|
||||
t.Fatalf("ran: %q %v", out, err)
|
||||
}
|
||||
// written again within the test: the new script runs
|
||||
Program(t, p, []byte("#!/bin/sh\necho two\n"))
|
||||
if out, err := exec.Command(p).Output(); err != nil || string(out) != "two\n" {
|
||||
t.Fatalf("ran again: %q %v", out, err)
|
||||
}
|
||||
fi, err := os.Stat(p)
|
||||
if err != nil || fi.Mode().Perm() != 0o755 {
|
||||
t.Fatalf("mode: %v %v", fi, err)
|
||||
}
|
||||
first = fi
|
||||
})
|
||||
p := filepath.Join(t.TempDir(), "next")
|
||||
Program(t, p, "#!/bin/sh\necho next\n")
|
||||
if out, err := exec.Command(p).Output(); err != nil || string(out) != "next\n" {
|
||||
t.Fatalf("the next test's program: %q %v", out, err)
|
||||
}
|
||||
fi, _ := os.Stat(p)
|
||||
if runtime.GOOS == "darwin" && !os.SameFile(first, fi) {
|
||||
t.Fatal("the next test's program is a new file, which macOS checks before its first run")
|
||||
}
|
||||
|
||||
standIn := filepath.Join(t.TempDir(), "stand-in")
|
||||
if err := WriteProgram(standIn, "#!/bin/sh\nexit 1\n"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Run("over a stand-in", func(t *testing.T) {
|
||||
Program(t, standIn, "#!/bin/sh\necho test\n")
|
||||
})
|
||||
if out, _ := exec.Command(standIn).Output(); string(out) != "test\n" {
|
||||
t.Fatalf("a stand-in written over is gone or changed back: %q", out)
|
||||
}
|
||||
|
||||
if err := WriteProgram(filepath.Join(t.TempDir(), "bin"), "MZ"); err == nil {
|
||||
t.Fatal("a binary written as a script")
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/agentenv"
|
||||
@@ -66,6 +67,7 @@ func RunIn(m *testing.M, setup func(home string)) int {
|
||||
if setup != nil {
|
||||
setup(home)
|
||||
}
|
||||
defer RemovePrograms()
|
||||
return m.Run()
|
||||
}
|
||||
home, err := Isolate()
|
||||
@@ -74,6 +76,7 @@ func RunIn(m *testing.M, setup func(home string)) int {
|
||||
return 1
|
||||
}
|
||||
defer remove(home)
|
||||
defer RemovePrograms()
|
||||
if setup != nil {
|
||||
setup(home)
|
||||
}
|
||||
@@ -174,10 +177,10 @@ func inertTools(home string) error {
|
||||
if err := os.Mkdir(bin, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := StandIns(bin, tools); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range tools {
|
||||
if err := os.WriteFile(filepath.Join(bin, name), []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
// Windows finds a program by PATHEXT; a .bat comes before the
|
||||
// user's claude.cmd further down PATH
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -189,6 +192,33 @@ func inertTools(home string) error {
|
||||
return os.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
}
|
||||
|
||||
// StandIns writes a failing program for each name in bin. On macOS each
|
||||
// is run once now, in the background, so that the check macOS makes of a
|
||||
// new program's first run (Program) is made while the tests start rather
|
||||
// than in the first test to reach it, and without holding them up.
|
||||
func StandIns(bin string, names []string) error {
|
||||
for _, name := range names {
|
||||
p := filepath.Join(bin, name)
|
||||
if err := os.WriteFile(p, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
warming.Add(1)
|
||||
go func() {
|
||||
defer warming.Done()
|
||||
if proc, err := os.StartProcess(p, []string{p}, &os.ProcAttr{}); err == nil {
|
||||
proc.Wait()
|
||||
record(p)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// warming is the stand-ins' first runs under way.
|
||||
var warming sync.WaitGroup
|
||||
|
||||
// within says whether path lies in dir.
|
||||
func within(path, dir string) bool {
|
||||
rel, err := filepath.Rel(dir, path)
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// Settings' Command line (PAMI on Discord): which magpie each shell runs,
|
||||
@@ -27,9 +29,7 @@ func TestCLIOnPath(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(exe, []byte("#!/bin/sh\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, exe, "#!/bin/sh\n")
|
||||
paths := map[string][]string{
|
||||
"/bin/zsh": {bin, "/usr/bin"},
|
||||
"/bin/fish": {other, "/usr/bin"},
|
||||
@@ -81,7 +81,7 @@ func TestCLIOnPath(t *testing.T) {
|
||||
|
||||
// another copy that runs first is replaced by the link, as install.sh's ln -sf
|
||||
copied := filepath.Join(other, "magpie")
|
||||
os.WriteFile(copied, []byte("#!/bin/sh\necho old\n"), 0o755)
|
||||
testenv.Program(t, copied, "#!/bin/sh\necho old\n")
|
||||
paths["/bin/zsh"] = []string{other, bin}
|
||||
if v = ReadCLI(); v.Ours || v.Command != copied || v.Dir != other {
|
||||
t.Fatalf("shadowed: %+v", v)
|
||||
@@ -121,7 +121,7 @@ func TestCLIOnPath(t *testing.T) {
|
||||
// a magpie.app elsewhere says its version without being run
|
||||
app := filepath.Join(h, "Old", "magpie.app", "Contents")
|
||||
os.MkdirAll(filepath.Join(app, "MacOS"), 0o755)
|
||||
os.WriteFile(filepath.Join(app, "MacOS", "magpie"), []byte("#!/bin/sh\nexit 1\n"), 0o755)
|
||||
testenv.Program(t, filepath.Join(app, "MacOS", "magpie"), "#!/bin/sh\nexit 1\n")
|
||||
os.WriteFile(filepath.Join(app, "Info.plist"), []byte("<dict><key>CFBundleShortVersionString</key>\n<string>0.1.500</string></dict>"), 0o644)
|
||||
if got := otherVersion(filepath.Join(app, "MacOS", "magpie")); got != "0.1.500" {
|
||||
t.Fatalf("version = %q", got)
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/testenv"
|
||||
)
|
||||
|
||||
// #275: a restart to update from the open window (Windows, Linux) came
|
||||
@@ -51,9 +53,7 @@ func TestRelaunchBinaryArgs(t *testing.T) {
|
||||
out := filepath.Join(dir, "out")
|
||||
exe := filepath.Join(dir, "magpie")
|
||||
script := "#!/bin/sh\necho \"$* $MAGPIE_REPLACES\" > " + out + ".tmp && mv " + out + ".tmp " + out + "\n"
|
||||
if err := os.WriteFile(exe, []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testenv.Program(t, exe, script)
|
||||
if err := RelaunchBinary(exe, "", c.window, c.view); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user