What: Update, Update all, the hourly auto-update and the move's install ask npm for the plugin's newest version and have bun install that exact version (installAt), then check it is the one installed. When bun won't, the error says why, reason first for the status pill: "<pkg> 0.1.21 isn't installed yet: Bun's minimumReleaseAge (in a .bunfig.toml) holds back versions published less than 2 days ago, and magpie installs it once it is older (bun's own words)". Another bun error (a registry of the user's own without the version) is "<pkg> <v> isn't installed: <bun's error: lines>". Specs stay as they were (name@latest); pinned and git plugins, and npm not answering, install as their spec says, as before. Why: the Factory card stayed at 0.1.18 with 0.1.21 out, through clicking update and restarts. bun add pkg@latest resolves "latest" by bun's own config: a minimumReleaseAge in the user's ~/.bunfig.toml or $XDG_CONFIG_HOME/.bunfig.toml makes it install the newest version older than that and exit 0. 172800s (2 days) gives exactly 0.1.18 (0.1.19-21 were published 10-08). So every update path succeeded with nothing changed, and the page said "updated to v0.1.21". Asked for 0.1.21 itself, bun fails with "blocked by minimum-release-age: 172800 seconds". The same code serves every moved plugin (Cursor, Devin, Kiro, Qoder, ...), so all of them had it. npm view: 0.1.21 is latest; npmmirror has it too. Run: - TestUpdateBunHoldsBackSaysWhy (new): real bun 1.4.2 against a fake registry with 0.1.18 (3 days old) and 0.1.21 (1 hour), a bunfig with minimumReleaseAge = 172800. On the old code it fails: "Update with 0.1.21 held back by Bun: <nil>, want bun's reason (installed 0.1.18)". Breaking Update alone fails at "Update all: <nil>", breaking the hourly update alone at "the hourly update: <nil>". Without the bunfig it installs 0.1.21 and keeps the spec @latest. - Real use: magpie web (nogui build) under a sandbox HOME, real npm, with the bunfig: add factory-auth@0.1.18, spec @latest, POST /api/plugins/upgrade and /update return the reason above, the card stays 0.1.18; bunfig removed, upgrade installs 0.1.21. - Ruled out on macOS and the Windows box: plain upgrade 0.1.18 -> 0.1.21 works there without a bunfig, host running or not. - gofmt -l; go vet (darwin, GOOS=linux, GOOS=windows, -tags nogui) on internal/plugin, internal/provider, internal/gui; go build (darwin), GOOS=linux and GOOS=windows -tags nogui builds; go test -tags nogui ./... under a temp HOME, all ok.
169 lines
5.6 KiB
Go
169 lines
5.6 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// installedAt fakes the package installed at version v.
|
|
func installedAt(t *testing.T, pkg, v string) {
|
|
t.Helper()
|
|
dir := Target(pkg)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := json.Marshal(map[string]string{"name": pkg, "version": v})
|
|
if err := os.WriteFile(filepath.Join(dir, "package.json"), b, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// The community's plugins update by themselves; anyone else's, or one
|
|
// pinned to a version, waits for the reader, and waits no more once
|
|
// updated; one switched off is left alone.
|
|
func TestCheckUpdates(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("HOME", dir)
|
|
t.Setenv("USERPROFILE", dir)
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(dir, "config"))
|
|
const (
|
|
ours = "@magpie-community/opencode-zed-auth"
|
|
pinned = "@magpie-community/opencode-kiro-auth"
|
|
theirs = "opencode-gemini-auth"
|
|
off = "@magpie-community/opencode-qoder-auth"
|
|
fresh = "@magpie-community/opencode-grok-auth"
|
|
)
|
|
if err := save(List{Plugins: []Entry{{Spec: ours + "@latest"}, {Spec: pinned + "@0.1.2"}, {Spec: theirs + "@latest"}, {Spec: off + "@latest", Off: true}, {Spec: fresh}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, p := range []string{ours, pinned, theirs, off} {
|
|
installedAt(t, p, "0.1.2")
|
|
}
|
|
installedAt(t, fresh, "0.2.0")
|
|
latest := map[string]string{ours: "0.1.4", pinned: "0.1.4", theirs: "1.0.0", off: "0.1.4", fresh: "0.2.0"}
|
|
var installed []string
|
|
was, wasInstall := latestOf, installNewest
|
|
t.Cleanup(func() { latestOf, installNewest = was, wasInstall })
|
|
latestOf = func(context.Context, []string) map[string]string { return latest }
|
|
installNewest = func(_ context.Context, pkg, _ string) error {
|
|
installed = append(installed, pkg)
|
|
installedAt(t, pkg, latest[pkg])
|
|
return nil
|
|
}
|
|
|
|
u, err := CheckUpdates(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(installed, ",") != ours {
|
|
t.Fatalf("installed %v, want only %s", installed, ours)
|
|
}
|
|
if Installed(ours+"@latest") != "0.1.4" || len(u.Updated) != 1 || u.Updated[0].From != "0.1.2" || u.Updated[0].To != "0.1.4" {
|
|
t.Fatalf("Updated = %+v", u.Updated)
|
|
}
|
|
p := PendingUpdates()
|
|
var waiting []string
|
|
for _, w := range p.Waiting {
|
|
waiting = append(waiting, w.Package+"@"+w.Latest)
|
|
}
|
|
if strings.Join(waiting, ",") != pinned+"@0.1.4,"+theirs+"@1.0.0" {
|
|
t.Fatalf("Waiting = %v", waiting)
|
|
}
|
|
if x, ok := LastUpdated(ours, time.Now().Add(-time.Hour)); !ok || x.To != "0.1.4" {
|
|
t.Fatalf("LastUpdated = %+v, %v", x, ok)
|
|
}
|
|
if _, ok := LastUpdated(ours, time.Now().Add(time.Hour)); ok {
|
|
t.Fatal("an update older than asked for was told")
|
|
}
|
|
|
|
// the reader updates theirs: it waits no more, before magpie looks again
|
|
installedAt(t, theirs, "1.0.0")
|
|
if p := PendingUpdates(); len(p.Waiting) != 1 || p.Waiting[0].Package != pinned {
|
|
t.Fatalf("after updating %s, Waiting = %+v", theirs, p.Waiting)
|
|
}
|
|
// looking again updates nothing more, and keeps what it updated
|
|
installed = nil
|
|
if u, err := CheckUpdates(context.Background()); err != nil || len(installed) != 0 || len(u.Updated) != 1 {
|
|
t.Fatalf("looking again: installed %v, %+v, %v", installed, u, err)
|
|
}
|
|
}
|
|
|
|
func TestPinned(t *testing.T) {
|
|
for spec, want := range map[string]bool{
|
|
"@magpie-community/opencode-zed-auth": false,
|
|
"@magpie-community/opencode-zed-auth@latest": false,
|
|
"@magpie-community/opencode-zed-auth@0.1.2": true,
|
|
"@magpie-community/opencode-zed-auth@^0.1.0": true,
|
|
"opencode-gemini-auth@next": true,
|
|
"opencode-gemini-auth": false,
|
|
filepath.Join(os.TempDir(), "plugin/index.js"): false,
|
|
} {
|
|
if Pinned(spec) != want {
|
|
t.Errorf("Pinned(%q) = %v, want %v", spec, !want, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A reply streaming through a plugin when the plugins are loaded again
|
|
// (one updated) goes on to its end, while the next request goes to the
|
|
// host started anew.
|
|
func TestRestartFinishesTheReply(t *testing.T) {
|
|
sandbox(t)
|
|
release := make(chan struct{})
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
io.WriteString(w, "data: first\n\n")
|
|
w.(http.Flusher).Flush()
|
|
if r.URL.Query().Get("slow") != "" {
|
|
<-release
|
|
}
|
|
io.WriteString(w, "data: last\n\n")
|
|
}))
|
|
defer srv.Close()
|
|
t.Setenv("FAKE_BASE", srv.URL+"/v1")
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
|
defer cancel()
|
|
abs, _ := filepath.Abs("testdata/fake/index.js")
|
|
if _, err := Add(ctx, abs); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := APIKey(ctx, "fakeco", 0, nil, "k1", NewAccount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fetch := func(q string) (*http.Response, error) {
|
|
return Fetch(ctx, FetchRequest{Provider: "fakeco", Model: "fake-1", NPM: "@ai-sdk/openai-compatible",
|
|
URL: srv.URL + "/v1/chat/completions" + q, Method: "POST", Body: []byte(`{}`)})
|
|
}
|
|
res, err := fetch("?slow=1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer res.Body.Close()
|
|
before := generation.Load()
|
|
Restart()
|
|
// the host the reply streams through is still answering it
|
|
time.Sleep(3 * time.Second)
|
|
next, err := fetch("")
|
|
if err != nil {
|
|
t.Fatalf("a request after the restart: %v", err)
|
|
}
|
|
b, _ := io.ReadAll(next.Body)
|
|
next.Body.Close()
|
|
if generation.Load() == before || !strings.Contains(string(b), "last") {
|
|
t.Fatalf("after the restart: a new host %v, %q", generation.Load() != before, b)
|
|
}
|
|
close(release)
|
|
b, err = io.ReadAll(res.Body)
|
|
if err != nil || !strings.Contains(string(b), "data: last") {
|
|
t.Fatalf("the reply streaming through the restart: %q, %v", b, err)
|
|
}
|
|
}
|