The Plugins dot comes from update.Newer, and the Installed rows (chip,
Update, Update all) from plugins.js's own newer(). They disagreed:
- newer() read only x.y.z, so 2.0.0-beta.1 with 2.0.0 out had the dot
and no row to update it from. Real case: opencode-antigravity-auth,
whose latest is 1.6.0 after 1.6.0-beta.0..2.
- update.Newer compared pre-releases as strings, so 1.0.0-beta.10 came
before 1.0.0-beta.9 (and 1.6.0-10 before 1.6.0-9): a missed update,
and the other way round, an "update" to an older beta.
- update.Newer didn't parse build metadata, so 1.0.1+build.1 was never
newer than anything.
Both now order as semver §11: no pre-release comes after any;
identifiers compared one by one, numbers as numbers, a number before a
word, and a shorter list before a longer one it starts. Build metadata
is ignored. A version that isn't x.y.z is never newer, as before.
Tests:
- internal/update TestNewer gains the pairs. On the old Newer 4 fail
(beta.10/beta.9 both ways, 1.6.0-10/-9, +build).
- internal/gui/tests/plugin-prerelease.test.cjs: seven installed
plugins, the dot's waiting list as update.Newer gives it, and each
row's chip and Update button, plus the Update all count, must match.
On the old plugins.js it fails at "v2.0.0 out" in both engines. No
strings change, so it's English only.
Reproduced in a sandbox HOME with the real package:
`magpie plugin add opencode-antigravity-auth@1.6.0-beta.2`, then Check
for updates. Before: /api/plugins/updates listed it, and the Plugins
nav had the dot, but the row had no chip and no Update, and there was no
Update all. After: the row shows "v1.6.0 out", Update, and Update all (1).
Update all then installed 1.6.0 and the waiting list emptied.
Run: gofmt; go vet -tags nogui for darwin, linux and windows on
internal/update, internal/plugin, internal/gui and the root package; go
test -tags nogui of the root package, internal/{gui,plugin,provider,
stats,update} under a temp HOME; GUI tests plugin-prerelease,
plugin-updates, plugin-card-update, plugin-check-updates, plugin-market,
plugin-release, plugin-git and plugin-local in Chromium and WebKit.
83 lines
5.4 KiB
JavaScript
83 lines
5.4 KiB
JavaScript
// Run with Node's test runner and Playwright on the module path; see README.md.
|
|
// Plugin versions with a pre-release: the Installed rows say an update is
|
|
// out for exactly the plugins update.Newer puts the dot on Plugins for.
|
|
// "2.0.0" comes after "2.0.0-beta.1", "1.0.0-beta.10" after "1.0.0-beta.9",
|
|
// and build metadata is no update. Before, the rows read only x.y.z, so a
|
|
// beta with its release out had the dot and no row. No strings change, so
|
|
// English only; the API is faked here.
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs/promises");
|
|
const path = require("node:path");
|
|
const { test } = require("node:test");
|
|
const { chromium, webkit } = require("playwright");
|
|
|
|
const assets = path.resolve(__dirname, "../assets");
|
|
|
|
// the installed plugins, with whether Go's update.Newer(latest, version)
|
|
// is true for each (internal/update TestNewer has the same pairs)
|
|
const cases = [
|
|
{ spec: "opencode-release-out", version: "2.0.0-beta.1", latest: "2.0.0", up: true },
|
|
{ spec: "opencode-beta-ten", version: "1.0.0-beta.9", latest: "1.0.0-beta.10", up: true },
|
|
{ spec: "opencode-rc-after-beta", version: "1.0.0-beta.11", latest: "1.0.0-rc.1", up: true },
|
|
{ spec: "opencode-longer-pre", version: "1.0.0-alpha", latest: "1.0.0-alpha.1", up: true },
|
|
{ spec: "opencode-beta-back", version: "1.0.0-beta.10", latest: "1.0.0-beta.9", up: false },
|
|
{ spec: "opencode-build-only", version: "1.0.0", latest: "1.0.0+build.2", up: false },
|
|
{ spec: "opencode-same", version: "0.3.0", latest: "0.3.0", up: false },
|
|
];
|
|
|
|
function server() {
|
|
const installed = cases.map((c) => ({ spec: c.spec, providers: [], version: c.version, latest: c.latest }));
|
|
const waiting = cases.filter((c) => c.up).map((c) => ({ spec: c.spec, package: c.spec, version: c.version, latest: c.latest }));
|
|
return async (route) => {
|
|
const url = new URL(route.request().url());
|
|
const json = (data) => route.fulfill({ json: data });
|
|
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"en",theme:"light",web:false};` });
|
|
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
|
|
if (url.pathname === "/api/state") return json({ agents: [], profiles: [], settings: { lang: "en", theme: "light" } });
|
|
if (url.pathname === "/api/providers") return json({ providers: [], presets: [], excluded: [], gateway: { running: true, window: true }, plugins: [] });
|
|
if (url.pathname === "/api/groups") return json({ groups: [], pools: [] });
|
|
if (url.pathname === "/api/gateway/trace") return json({ routes: [] });
|
|
if (url.pathname === "/api/plugins/updates") return json({ checked: "2026-10-01T00:00:00Z", waiting, updated: [] });
|
|
if (url.pathname === "/api/plugins/market" || url.pathname === "/api/plugins" || url.pathname === "/api/plugins/listings") { const m = { listings: [], state: { bun: true, bunVersion: "1.3.0", plugins: installed } }; return json(url.pathname === "/api/plugins" ? m.state : url.pathname === "/api/plugins/listings" ? { listings: m.listings } : m); }
|
|
if (url.pathname.startsWith("/api/")) return json({});
|
|
if (!/^\/[\w./-]*$/.test(url.pathname) || url.host !== "magpie.test") return route.fulfill({ status: 404, body: "" });
|
|
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
|
|
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
|
|
try { await route.fulfill({ body: await fs.readFile(file), contentType }); } catch { await route.fulfill({ status: 404, body: "" }); }
|
|
};
|
|
}
|
|
|
|
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
|
test(engine + ": rows and the Plugins dot agree on pre-releases", async (t) => {
|
|
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
|
t.after(() => browser.close());
|
|
const page = await (await browser.newContext({ viewport: { width: 980, height: 820 } })).newPage();
|
|
page.setDefaultTimeout(5000);
|
|
const errors = [];
|
|
page.on("pageerror", (e) => errors.push(e.message));
|
|
await page.route("**/*", server());
|
|
|
|
await page.goto("http://magpie.test/?view=providers");
|
|
await page.waitForFunction(() => document.querySelector('#nav button[data-view="plugins"]')?.classList.contains("has-dot"));
|
|
await page.locator('#nav button[data-view="plugins"]').click();
|
|
const view = page.locator("#view-plugins");
|
|
await view.locator(".lib-tabs .opt", { hasText: "Installed" }).click();
|
|
await view.locator(".pm-row").nth(cases.length - 1).waitFor();
|
|
|
|
const rows = await view.locator(".pm-row").evaluateAll((rs) => rs.map((r) => ({
|
|
name: r.querySelector(".name span")?.textContent,
|
|
chip: r.querySelector(".pm-chip.up")?.textContent || null,
|
|
update: [...r.querySelectorAll(".val button")].some((b) => b.textContent === "Update"),
|
|
})));
|
|
for (const c of cases) {
|
|
const r = rows.find((x) => x.name === c.spec);
|
|
assert.ok(r, c.spec + " has a row");
|
|
assert.equal(r.chip, c.up ? `v${c.latest} out` : null, `${c.spec}: ${c.version} → ${c.latest}`);
|
|
assert.equal(r.update, c.up, `${c.spec}: Update button`);
|
|
}
|
|
const n = cases.filter((c) => c.up).length;
|
|
assert.equal(await view.getByRole("button", { name: `Update all (${n})`, exact: true }).count(), 1);
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
}
|