Files
magpie/internal/gui/tests/plugin-move.test.cjs
T
yetone 70a6c72a2c gui: built-in subscriptions that have a community plugin are marked Deprecated, with the reason — some subscriptions are reached in ways their vendors' terms may not allow, so to keep magpie itself from being banned they are being decoupled from magpie and run by community plugins instead. The provider list's quiet "can run on community plugins" line is now a deprecation notice (an amber card with the plugin mark: the deprecated signed-in subscriptions, the reason, that moving keeps accounts, models and agents, and Review in Plugins — or Review the move for one — beside Not now, which hides it until another deprecated subscription signs in). Their rows, their Add tiles (signed in or not; /api/providers' new movable lists every built-in with a plugin, from provider.MovableIDs) and the editor's Runs on field carry an outlined Deprecated badge whose tooltip gives the reason; the Add sheet's Subscriptions end with a line saying why, with Browse plugins. A subscription already on its plugin, or one with no plugin, carries none. deprecated-subscriptions.test.cjs (Chromium and WebKit, en and zh: the badges, their reason, none on a moved or keyed provider, the notice, Not now staying hidden), plugin-move and plugin-movable-review updated to the notice. add-button, add-sheet, mobile-web, old-webkit, plugin-move-overflow, plugin-moved-signin, plugin-market, plugin-signin, plugin-plan, provider-search, provider-picks-stay, providers-off-fold pass (click-scroll's 5 failures fail on main as well). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gui/ pass.
2026-10-02 11:39:05 +08:00

192 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Run with Node's test runner and Playwright on the module path; see README.md.
// A built-in subscription a community plugin can run (Zed here). The
// providers list says so in a quiet line whose link opens its editor; the
// editor says which runs it, and "Move to the plugin" says what it is doing
// on the button. A move that fails says why right under it, in the
// reader's language, and the button becomes "Try again"; one that goes
// through turns the field over in the editor, still open where it was, and
// "Use the built-in again" posts provider/moveback. The add sheet no longer
// offers the built-in Zed once it runs on its plugin, and its editor shows
// no plugin:// endpoints to test. The Plugins tab, opened as the page
// (?view=plugins), shows its market and offers Zed's accounts on the
// plugin's card. In English and Chinese; 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");
const pkg = "@magpie-community/opencode-zed-auth";
function serve(lang, posts) {
let move = { package: pkg, state: "" };
let tries = 0;
const payload = () => {
const onPlugin = move.state === "plugin";
const zed = {
id: "zed", name: "Zed", icon: "zed", chat: onPlugin ? "plugin://zed/v1" : "", responses: "", anthropic: onPlugin ? "plugin://zed" : "", catalog: "",
// enough models that the editor scrolls to reach Runs on
models: Array.from({ length: 30 }, (_, i) => ({ id: "m" + i, name: "Model " + i, on: i === 0 })),
agents: [], fallback: [], headers: {}, keyList: [], key: {},
account: { agent: "zed", agentName: "Zed", user: "ada", logins: [{ user: "ada", active: true, on: true, own: true }, { user: "bob", on: true }] },
move,
};
return {
providers: [zed], presets: [], excluded: [], gateway: { running: true, window: true },
plugins: onPlugin ? [{ id: "zed", pid: "zed", name: "Zed", icon: "zed", spec: pkg, signedIn: true, models: 1, methods: [] }] : [],
onPlugins: onPlugin ? ["zed"] : [],
};
};
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:"${lang}",theme:"light",web:true};` });
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, theme: "light" } });
if (url.pathname === "/api/providers") return json(payload());
if (url.pathname === "/api/groups") return json({ groups: [] });
// the page asks for the market in parts (#488)
if (url.pathname === "/api/plugins/market" || url.pathname === "/api/plugins" || url.pathname === "/api/plugins/listings") {
const m = ({ listings: [{ package: pkg, name: "Zed", icon: "zed", providers: ["zed"], community: true, replaces: "zed", summary: { en: "Zed", zh: "Zed" }, npm: { version: "0.1.0" } }],
state: { bun: true, bunVersion: "1.3.0", plugins: [], movable: move.state === "plugin" ? [] : [{ id: "zed", name: "Zed", package: pkg, accounts: 2 }] } });
return json(url.pathname === "/api/plugins" ? m.state : url.pathname === "/api/plugins/listings" ? { listings: m.listings } : m);
}
if (url.pathname === "/api/provider/move" || url.pathname === "/api/provider/moveback") {
posts.push({ path: url.pathname, body: route.request().postDataJSON() });
await new Promise((r) => setTimeout(r, 400)); // long enough to read the button
// the first move can't reach npm; the second goes through
if (url.pathname.endsWith("/move") && !tries++) {
move = { package: pkg, state: "failed", error: "magpie couldn't reach npm to install the plugin.", why: { code: "offline" } };
return route.fulfill({ status: 400, json: { error: move.error, why: move.why } });
}
move = { package: pkg, state: url.pathname.endsWith("moveback") ? "back" : "plugin" };
return json(payload());
}
if (url.pathname.startsWith("/api/")) return json({});
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: "" }); }
};
}
const L = {
en: { runs: "Runs on", line: "Zed's built-in subscription is deprecated", look: "Review the move",
move: "Move to the plugin", busy: "Installing the plugin and checking each account…", again: "Try again", back: "Use the built-in again",
failed: "It stays built-in: magpie couldn't reach npm to install the plugin. Check the network or proxy, then try again.",
onPlugin: "The community Zed plugin", builtin: "magpie's built-in · or the community Zed plugin", done: "Zed now runs on its plugin — 2 accounts, 1 model.",
subs: "Subscriptions", card: "Move", own: "Zed itself stays signed in as it is." },
zh: { runs: "运行方式", line: "Zed 的内置订阅已弃用", look: "查看迁移",
move: "迁移到插件", busy: "正在安装插件并逐个检查账号…", again: "重试", back: "改回内置",
failed: "仍使用内置:无法连接 npm 安装插件,请检查网络或代理后重试。",
onPlugin: "社区 Zed 插件", builtin: "magpie 内置 · 也可改用社区 Zed 插件", done: "Zed 现在由插件运行——2 个账号,1 个模型。",
subs: "订阅", card: "迁移", own: "Zed 本身的登录保持不变。" },
};
const launch = (engine) => engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" });
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
for (const lang of ["en", "zh"]) {
test(`${engine} ${lang}: a built-in subscription moves to its plugin and back`, async (t) => {
const w = L[lang];
const browser = await launch(engine);
t.after(() => browser.close());
const page = await (await browser.newContext({ viewport: { width: 900, height: 700 }, reducedMotion: "reduce" })).newPage();
page.setDefaultTimeout(5000);
const errors = [], posts = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", serve(lang, posts));
await page.goto("http://magpie.test/?view=providers");
// the deprecation notice over the list opens Zed's editor
const line = page.locator("#movable");
await line.locator("button", { hasText: w.look }).waitFor();
assert.ok((await line.innerText()).includes(w.line), "no notice saying Zed is deprecated");
await line.locator("button", { hasText: w.look }).click();
const ed = page.locator("#modal .editor");
const btn = ed.locator(".move button");
await btn.waitFor();
assert.equal(await btn.innerText(), w.move);
const text = await ed.innerText();
assert.ok(text.includes(w.runs), "no Runs on field");
assert.ok(text.includes(w.builtin), "doesn't say it's built-in");
assert.equal(await ed.locator(".move").getAttribute("title"), null);
assert.equal(await ed.locator(".move > div").first().getAttribute("title"), pkg, "the package isn't in the tooltip");
assert.ok(!text.includes(pkg), "names the npm package in the sentence");
assert.ok(text.includes(w.own), "doesn't say Zed stays signed in");
// the reader scrolled to the field: nothing the move does moves it
const body = ed.locator(".ebody");
const top = await body.evaluate((e) => { e.scrollTop = 120; return e.scrollTop; });
assert.ok(top > 0, "the editor doesn't scroll: the test proves nothing about keeping its place");
await btn.click();
await page.waitForFunction((busy) => [...document.querySelectorAll("#modal .move button")].some((b) => b.textContent === busy), w.busy, { timeout: 300 });
assert.equal(await btn.isDisabled(), true, "the button can be pressed again mid-move");
// it fails: said under the button, the editor open where it was
const why = ed.locator(".move-why");
await why.waitFor({ state: "visible" });
assert.equal(await why.innerText(), w.failed);
assert.equal(await btn.innerText(), w.again);
assert.equal(await btn.isDisabled(), false);
assert.equal(await body.evaluate((e) => e.scrollTop), top, "the failure moved the editor");
if (process.env.ARTIFACT_DIR) await ed.screenshot({ path: path.join(process.env.ARTIFACT_DIR, `plugin-move-failed-${engine}-${lang}.png`) });
// tried again, it goes through: the same editor, turned over
await btn.click();
const back = ed.locator(".move button", { hasText: w.back });
await back.waitFor();
assert.equal(await page.locator("#modal .editor").count(), 1, "the editor closed");
assert.ok((await ed.innerText()).includes(w.onPlugin), "doesn't say the plugin runs it");
// as the built-in said: the agent's own sign-in is where it was
assert.ok((await ed.innerText()).includes(w.own), "the moved Zed no longer says Zed stays signed in");
assert.equal(await why.count() && await why.isVisible(), false, "the failure is still said");
assert.equal(await body.evaluate((e) => e.scrollTop), top, "the move moved the editor");
assert.ok((await page.locator("#status").innerText()).startsWith(w.done), "no toast saying what moved");
assert.deepEqual(posts.map((p) => [p.path, p.body.id]), [["/api/provider/move", "zed"], ["/api/provider/move", "zed"]]);
// its plugin:// URLs are magpie's own: no Endpoints, no Test
assert.equal(await ed.locator(".eps").count(), 0, "shows the plugin:// endpoints");
assert.equal(await line.isVisible(), false, "the line still offers the move");
if (process.env.ARTIFACT_DIR) await ed.screenshot({ path: path.join(process.env.ARTIFACT_DIR, `plugin-moved-${engine}-${lang}.png`) });
await page.keyboard.press("Escape");
// the add sheet offers Zed once, as the plugin's
await page.locator("#addProvider").click();
const sheet = page.locator("#addSheet");
await sheet.locator(".kind b", { hasText: w.subs }).waitFor();
assert.equal(await sheet.locator('.tile[data-pick="Zed"]').count(), 1);
await page.keyboard.press("Escape");
await page.locator(".row.provider", { hasText: "Zed" }).click();
await back.click();
for (let i = 0; i < 50 && posts.length < 3; i++) await page.waitForTimeout(50);
assert.deepEqual(posts.map((p) => p.path), ["/api/provider/move", "/api/provider/move", "/api/provider/moveback"]);
assert.deepEqual(errors, []);
});
test(`${engine} ${lang}: the Plugins tab opened as the page offers Zed's accounts`, async (t) => {
const w = L[lang];
const browser = await launch(engine);
t.after(() => browser.close());
const page = await (await browser.newContext({ viewport: { width: 980, height: 820 } })).newPage();
page.setDefaultTimeout(5000);
const errors = [], posts = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", serve(lang, posts));
await page.goto("http://magpie.test/?view=plugins");
const card = page.locator("#view-plugins button", { hasText: w.card });
await card.waitFor();
await card.click();
// the first move fails (npm unreachable): said, and the card offers it again
await page.waitForFunction((s) => document.querySelector("#status").textContent.startsWith(s), w.failed.split(":")[0] || w.failed.split(":")[0]);
await card.waitFor();
await card.click();
await page.waitForFunction((s) => document.querySelector("#status").textContent.startsWith(s), w.done.split(" — ")[0].split("——")[0]);
assert.deepEqual(posts.map((p) => [p.path, p.body.id]), [["/api/provider/move", "zed"], ["/api/provider/move", "zed"]]);
assert.deepEqual(errors, []);
});
}
}