mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 12:17:23 +00:00
fix(gui): 桌面版真正加载渠道插件 + 外链一律交给默认浏览器
## ① 桌面版此前**完全没有插件系统**(渠道页根本不存在) GUI 内嵌内核的 config.yaml 由 main.js 生成,唯独漏了 `plugin_dir` —— 而内核刻意不给它默认值(运维显式开启)。后果:桌面版 `plugin_dir: ""`, `/api/plugins` 返回空列表,channels 插件(多平台账号登录、凭据保管、自动 领取)在桌面端**完全不可见**。实测复现:同一份配置去掉 plugin_dir 就是 `plugins: []`,加上就有 billing/channels/quota/usage 四个。 修法: - 新 profile 直接写 `plugin_dir: <profile>/plugins`; - **老 profile 回填**(用户已有 config.yaml,不写就只有老用户永远没有插件)—— 新增 `ensurePluginDirConfigured()`,只在缺少该键时追加,其余内容不动; - 插件树由内核 SeedBundled 播种(按文件补齐,用户改过的副本优先保留)。 ## ② 升级后渠道修复**下发不到桌面用户** 内核播种规则是「文件缺失才写」,所以桌面用户升级 GUI 后 `channels.lua` 仍是旧版 —— 我们发的渠道修复对桌面用户静默失效。桌面版必须自己处理: 按 app 版本打戳(`.plugin-seed-version`),版本变化时删掉 4 个内置插件文件 让新内核重新播种,旧副本留 `.prev-<旧版本>` 可人工恢复;用户自建插件不碰。 逻辑抽到 `cmd/gui/plugin-sync.js`(无 Electron 依赖),配 6 条 node 单测。 ## ③ 登录一律拉起默认浏览器(三条路径都覆盖) 桌面版的内容区是加载内核 WebUI 的 iframe,渠道登录链接是 `target=_blank`。 此前只处理了 `setWindowOpenHandler`,另两条路径仍会把 Electron 窗口带到 第三方页面。现在三条路径统一外抛: 1. `window.open` / target=_blank → setWindowOpenHandler 2. 顶层同标签跳转 → will-navigate 3. iframe 内导航 → will-frame-navigate **loopback 例外必须保留**:channels 的服务端浏览器(noVNC)就在 `127.0.0.1:<port>` 的 iframe 里,拦掉它会让 VNC 登录失效。 实测(打包后的 GUI + Xvfb + CDP 驱动真实窗口): [gui] open in default browser: https://example.com/mr-windowopen-probe [gui] open in default browser: https://example.com/mr-navigate-probe [gui] open in default browser: https://example.com/mr-frame-probe [gui] open in default browser: https://xiaohuanxiong.com/login?code=…(真实商汤登录链接) 顶层 URL 保持在内核页面(跳转被拦下),loopback iframe 正常加载。 ## ④ 顺手 - GUI 版本号 1.9.7 → 2.0.42(与内核同版本发版;package-lock 里还是 1.9.3, 一并同步); - `make gui-test` 跑桌面壳单测。 验证:打包后的 GUI 启动即 `/api/plugins` 列出 channels(ui.page=channels), 插件目录播种 4 个内置插件;外链四类实测均走默认浏览器。
This commit is contained in:
10
Makefile
10
Makefile
@ -1,4 +1,4 @@
|
|||||||
.PHONY: all build test gui gui-dev gui-dist gui-deb gui-win gui-win-docker help clean
|
.PHONY: all build test gui gui-dev gui-dist gui-deb gui-win gui-win-docker help clean gui-test
|
||||||
|
|
||||||
# ModelRouter - single-binary gateway + optional Electron desktop GUI (embedded core)
|
# ModelRouter - single-binary gateway + optional Electron desktop GUI (embedded core)
|
||||||
export BINARY
|
export BINARY
|
||||||
@ -65,6 +65,13 @@ gui-dist-dir:
|
|||||||
test:
|
test:
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|
||||||
|
# Desktop shell unit tests (no Electron needed): plugin-tree seeding/migration.
|
||||||
|
# These guard the two things that silently break the desktop build:
|
||||||
|
# - plugin_dir missing from an embedded profile -> channels plugin invisible
|
||||||
|
# - bundled plugin files never refreshed -> old channels.lua survives upgrades
|
||||||
|
gui-test:
|
||||||
|
@cd $(GUI_DIR) && node --test plugin-sync.test.js
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIR) cmd/build cmd/gui/dist cmd/gui/build/gui-dist cmd/gui/bin
|
rm -rf $(BUILD_DIR) cmd/build cmd/gui/dist cmd/gui/build/gui-dist cmd/gui/bin
|
||||||
|
|
||||||
@ -75,6 +82,7 @@ help:
|
|||||||
@echo " make gui run desktop GUI (dev)"
|
@echo " make gui run desktop GUI (dev)"
|
||||||
@echo " make gui-dev run GUI with devtools"
|
@echo " make gui-dev run GUI with devtools"
|
||||||
@echo " make gui-dist build deb + rpm + AppImage (linux)"
|
@echo " make gui-dist build deb + rpm + AppImage (linux)"
|
||||||
|
@echo " make gui-test desktop shell unit tests (plugin tree seeding)"
|
||||||
@echo " make gui-deb build deb only"
|
@echo " make gui-deb build deb only"
|
||||||
@echo " make gui-win build Windows nsis (needs host mingw + wine)"
|
@echo " make gui-win build Windows nsis (needs host mingw + wine)"
|
||||||
@echo " make gui-win-docker build Windows nsis all-in-docker (no host mingw needed)"
|
@echo " make gui-win-docker build Windows nsis all-in-docker (no host mingw needed)"
|
||||||
|
|||||||
@ -18,6 +18,7 @@ const http = require("http");
|
|||||||
const os = require("os");
|
const os = require("os");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const { spawn, execFileSync } = require("child_process");
|
const { spawn, execFileSync } = require("child_process");
|
||||||
|
const pluginSync = require("./plugin-sync");
|
||||||
|
|
||||||
const LOG_FILE = path.join(app.getPath("userData"), "gui.log");
|
const LOG_FILE = path.join(app.getPath("userData"), "gui.log");
|
||||||
function log(msg) {
|
function log(msg) {
|
||||||
@ -40,6 +41,13 @@ console.error = function () {
|
|||||||
// Per-user profile dir: <userData>/profile
|
// Per-user profile dir: <userData>/profile
|
||||||
const PROFILE_DIR = path.join(app.getPath("userData"), "profile");
|
const PROFILE_DIR = path.join(app.getPath("userData"), "profile");
|
||||||
const CONFIG_FILE = path.join(PROFILE_DIR, "config.yaml");
|
const CONFIG_FILE = path.join(PROFILE_DIR, "config.yaml");
|
||||||
|
// Plugin tree for the embedded core. The core deliberately does NOT default
|
||||||
|
// plugin_dir (an operator opts in), so the desktop build must set it — without
|
||||||
|
// it the whole plugin system stays off and the channels plugin (multi-platform
|
||||||
|
// account login + credential vault + auto check-in) is invisible in the WebUI.
|
||||||
|
// Seeding is the core's job (SeedBundled fills MISSING files only), so a user's
|
||||||
|
// edited copy always wins.
|
||||||
|
const PLUGINS_DIR = path.join(PROFILE_DIR, "plugins");
|
||||||
// Packaged: extraResource at process.resourcesPath/bin (outside asar, spawnable).
|
// Packaged: extraResource at process.resourcesPath/bin (outside asar, spawnable).
|
||||||
// Dev: cmd/gui/bin, populated by prepare-core.js.
|
// Dev: cmd/gui/bin, populated by prepare-core.js.
|
||||||
const CORE_EXE = path.join(
|
const CORE_EXE = path.join(
|
||||||
@ -216,6 +224,9 @@ function writeDefaultEmbeddedConfig() {
|
|||||||
"listen: 127.0.0.1:" + (settings.port || DEFAULT_PORT),
|
"listen: 127.0.0.1:" + (settings.port || DEFAULT_PORT),
|
||||||
"default_model: AUTO",
|
"default_model: AUTO",
|
||||||
"adapter_dir: " + JSON.stringify(path.join(PROFILE_DIR, "adapters")),
|
"adapter_dir: " + JSON.stringify(path.join(PROFILE_DIR, "adapters")),
|
||||||
|
// Plugin tree: channels/billing/quota/usage ship inside the core binary and
|
||||||
|
// are seeded into this dir on first start. Omitting it disables plugins.
|
||||||
|
"plugin_dir: " + JSON.stringify(PLUGINS_DIR),
|
||||||
"runtime_file: " + JSON.stringify(path.join(PROFILE_DIR, "runtime.json")),
|
"runtime_file: " + JSON.stringify(path.join(PROFILE_DIR, "runtime.json")),
|
||||||
"",
|
"",
|
||||||
// Admin key as a plain `keys` entry (no seed flag) so the web UI never
|
// Admin key as a plain `keys` entry (no seed flag) so the web UI never
|
||||||
@ -268,6 +279,9 @@ function startCore() {
|
|||||||
// ensure profile dir + config + local adapters exist
|
// ensure profile dir + config + local adapters exist
|
||||||
fs.mkdirSync(PROFILE_DIR, { recursive: true });
|
fs.mkdirSync(PROFILE_DIR, { recursive: true });
|
||||||
if (!fs.existsSync(CONFIG_FILE)) writeDefaultEmbeddedConfig();
|
if (!fs.existsSync(CONFIG_FILE)) writeDefaultEmbeddedConfig();
|
||||||
|
pluginSync.ensurePluginDirConfigured(CONFIG_FILE, PLUGINS_DIR, log);
|
||||||
|
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
||||||
|
pluginSync.syncBundledPluginsOnUpgrade(PROFILE_DIR, PLUGINS_DIR, app.getVersion(), log);
|
||||||
|
|
||||||
coreProc = spawn(CORE_EXE, ["-config", CONFIG_FILE], {
|
coreProc = spawn(CORE_EXE, ["-config", CONFIG_FILE], {
|
||||||
cwd: PROFILE_DIR,
|
cwd: PROFILE_DIR,
|
||||||
@ -622,10 +636,55 @@ function createWindow() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
mainWindow.loadFile(path.join(__dirname, "renderer", "index.html"));
|
mainWindow.loadFile(path.join(__dirname, "renderer", "index.html"));
|
||||||
|
// External links ALWAYS go to the user's default browser, never an in-app
|
||||||
|
// window. Three paths have to be covered, and missing any one of them is why
|
||||||
|
// "click the login link and nothing happens / the app navigates away" shows up:
|
||||||
|
// 1. window.open / target=_blank -> setWindowOpenHandler
|
||||||
|
// 2. top-level navigation (same-tab click) -> will-navigate
|
||||||
|
// 3. navigation inside the WebUI <iframe> -> will-frame-navigate
|
||||||
|
// The embedded core's own origin (and file:// for the shell) must keep
|
||||||
|
// working, otherwise the WebUI and plugin pages stop loading.
|
||||||
|
const isInternal = (url) =>
|
||||||
|
!url ||
|
||||||
|
url === "about:blank" ||
|
||||||
|
url.startsWith("file://") ||
|
||||||
|
url.startsWith(embeddedBaseUrl()) ||
|
||||||
|
url.startsWith("devtools://");
|
||||||
|
const openExternally = (url) => {
|
||||||
|
if (url && (url.startsWith("http://") || url.startsWith("https://"))) {
|
||||||
|
log("[gui] open in default browser: " + url);
|
||||||
|
shell.openExternal(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
if (url && url.startsWith("http")) shell.openExternal(url);
|
if (!isInternal(url)) openExternally(url);
|
||||||
return { action: "deny" };
|
return { action: "deny" };
|
||||||
});
|
});
|
||||||
|
mainWindow.webContents.on("will-navigate", (e, url) => {
|
||||||
|
if (isInternal(url)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
openExternally(url);
|
||||||
|
});
|
||||||
|
// iframe navigations (the WebUI content area and the plugin pages inside it).
|
||||||
|
// The channels plugin renders its server-side login browser (noVNC) in an
|
||||||
|
// iframe on a LOOPBACK port, so loopback MUST stay in-app — blocking it would
|
||||||
|
// break VNC login. Everything non-loopback is external: open the default
|
||||||
|
// browser instead of silently replacing the gateway UI with a third-party page.
|
||||||
|
const isLoopback = (url) => {
|
||||||
|
try {
|
||||||
|
const h = new URL(url).hostname;
|
||||||
|
return h === "127.0.0.1" || h === "localhost" || h === "[::1]";
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (mainWindow.webContents.on) {
|
||||||
|
mainWindow.webContents.on("will-frame-navigate", (e) => {
|
||||||
|
if (isInternal(e.url) || isLoopback(e.url)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
openExternally(e.url);
|
||||||
|
});
|
||||||
|
}
|
||||||
// Silent start: `--silent` CLI flag or settings.silentStart keeps the app
|
// Silent start: `--silent` CLI flag or settings.silentStart keeps the app
|
||||||
// in the tray without showing the window (Clash-Verge style). First run
|
// in the tray without showing the window (Clash-Verge style). First run
|
||||||
// always shows the window so the user sees the dashboard.
|
// always shows the window so the user sees the dashboard.
|
||||||
|
|||||||
4
cmd/gui/package-lock.json
generated
4
cmd/gui/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "modelrouter-gui",
|
"name": "modelrouter-gui",
|
||||||
"version": "1.9.3",
|
"version": "2.0.42",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "modelrouter-gui",
|
"name": "modelrouter-gui",
|
||||||
"version": "1.9.3",
|
"version": "2.0.42",
|
||||||
"license": "Proprietary",
|
"license": "Proprietary",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^33.0.0",
|
"electron": "^33.0.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "modelrouter-gui",
|
"name": "modelrouter-gui",
|
||||||
"version": "1.9.7",
|
"version": "2.0.42",
|
||||||
"description": "ModelRouter Desktop — embedded ModelRouter core with tray",
|
"description": "ModelRouter Desktop — embedded ModelRouter core with tray",
|
||||||
"author": "ModelRouter",
|
"author": "ModelRouter",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
@ -81,6 +81,7 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"main.js",
|
"main.js",
|
||||||
|
"plugin-sync.js",
|
||||||
"preload.js",
|
"preload.js",
|
||||||
"renderer/**/*",
|
"renderer/**/*",
|
||||||
"build/**/*",
|
"build/**/*",
|
||||||
@ -89,4 +90,4 @@
|
|||||||
"asar": true
|
"asar": true
|
||||||
},
|
},
|
||||||
"homepage": "https://gitcode.com/JianFeeeee/ModelRouter"
|
"homepage": "https://gitcode.com/JianFeeeee/ModelRouter"
|
||||||
}
|
}
|
||||||
82
cmd/gui/plugin-sync.js
Normal file
82
cmd/gui/plugin-sync.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// Plugin-tree maintenance for the embedded core (no Electron imports, so this
|
||||||
|
// is unit-testable with plain node).
|
||||||
|
//
|
||||||
|
// Two problems this solves, both invisible from the gateway side:
|
||||||
|
// 1. The core deliberately does NOT default `plugin_dir` (an operator opts
|
||||||
|
// in). A desktop profile without it runs with ZERO plugins — the channels
|
||||||
|
// plugin (account login, credential vault, auto check-in) never appears.
|
||||||
|
// 2. The core seeds bundled plugins only when the file is ABSENT, so a
|
||||||
|
// desktop upgrade would keep the user's old channels.lua forever and
|
||||||
|
// silently skip every channel fix. On version change the shell must drop
|
||||||
|
// the built-in files so the new binary reseeds them.
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const BUILTIN_PLUGINS = ["billing.lua", "channels.lua", "quota.lua", "usage.lua"];
|
||||||
|
|
||||||
|
// ensurePluginDirConfigured back-fills `plugin_dir` into an existing config.
|
||||||
|
// Appending a top-level YAML key is safe for the shapes this file can have
|
||||||
|
// (flat mapping + a keys list). Returns "added" | "present" | "absent" | "error".
|
||||||
|
function ensurePluginDirConfigured(configFile, pluginsDir, log) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(configFile)) return "absent";
|
||||||
|
const cur = fs.readFileSync(configFile, "utf-8");
|
||||||
|
if (/^\s*plugin_dir\s*:/m.test(cur)) return "present";
|
||||||
|
fs.writeFileSync(
|
||||||
|
configFile,
|
||||||
|
cur.replace(/\s*$/, "") +
|
||||||
|
"\n# added by ModelRouter Desktop: enables the plugin tree (channels etc.)\n" +
|
||||||
|
"plugin_dir: " + JSON.stringify(pluginsDir) + "\n",
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
if (log) log("[gui] plugin_dir back-filled into " + configFile);
|
||||||
|
return "added";
|
||||||
|
} catch (e) {
|
||||||
|
if (log) log("[gui] ensurePluginDirConfigured failed: " + e.message);
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncBundledPluginsOnUpgrade drops built-in plugin files when the app version
|
||||||
|
// changed, so the core reseeds them from the binary it shipped with. Returns
|
||||||
|
// { changed, dropped:[...], kept: "prev-<ver>" }.
|
||||||
|
function syncBundledPluginsOnUpgrade(profileDir, pluginsDir, version, log) {
|
||||||
|
const stamp = path.join(profileDir, ".plugin-seed-version");
|
||||||
|
let have = "";
|
||||||
|
try {
|
||||||
|
have = fs.readFileSync(stamp, "utf-8").trim();
|
||||||
|
} catch (e) {
|
||||||
|
/* first run */
|
||||||
|
}
|
||||||
|
if (have === version) return { changed: false, dropped: [] };
|
||||||
|
const dropped = [];
|
||||||
|
for (const name of BUILTIN_PLUGINS) {
|
||||||
|
const f = path.join(pluginsDir, name);
|
||||||
|
if (!fs.existsSync(f)) continue;
|
||||||
|
try {
|
||||||
|
if (have) fs.renameSync(f, f + ".prev-" + have);
|
||||||
|
else fs.unlinkSync(f);
|
||||||
|
dropped.push(name);
|
||||||
|
} catch (e) {
|
||||||
|
if (log) log("[gui] drop builtin plugin " + name + " failed: " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(stamp, version + "\n", "utf-8");
|
||||||
|
} catch (e) {
|
||||||
|
if (log) log("[gui] write plugin seed stamp failed: " + e.message);
|
||||||
|
}
|
||||||
|
if (dropped.length && log) {
|
||||||
|
log(
|
||||||
|
"[gui] plugins refreshed for " + version + " (" + dropped.length +
|
||||||
|
" builtin file(s) reseeded by core)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { changed: true, dropped, kept: have ? "prev-" + have : "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
BUILTIN_PLUGINS,
|
||||||
|
ensurePluginDirConfigured,
|
||||||
|
syncBundledPluginsOnUpgrade,
|
||||||
|
};
|
||||||
80
cmd/gui/plugin-sync.test.js
Normal file
80
cmd/gui/plugin-sync.test.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Unit tests for the desktop plugin-tree maintenance (plain node, no Electron).
|
||||||
|
// Run: node --test cmd/gui/plugin-sync.test.js
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
|
const path = require("path");
|
||||||
|
const { BUILTIN_PLUGINS, ensurePluginDirConfigured, syncBundledPluginsOnUpgrade } = require("./plugin-sync");
|
||||||
|
|
||||||
|
const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), "plugin-sync-"));
|
||||||
|
|
||||||
|
test("back-fills plugin_dir into an existing profile without it", () => {
|
||||||
|
const dir = tmp();
|
||||||
|
const cfg = path.join(dir, "config.yaml");
|
||||||
|
fs.writeFileSync(cfg, 'listen: 127.0.0.1:8787\ndefault_model: AUTO\nkeys:\n - key: k\n role: admin\n');
|
||||||
|
const plugins = path.join(dir, "plugins");
|
||||||
|
assert.equal(ensurePluginDirConfigured(cfg, plugins, null), "added");
|
||||||
|
const out = fs.readFileSync(cfg, "utf-8");
|
||||||
|
assert.match(out, /^plugin_dir: "/m);
|
||||||
|
assert.ok(out.includes(plugins.replace(/\\/g, "\\\\")) || out.includes(plugins), "path written");
|
||||||
|
// the rest of the profile must survive untouched
|
||||||
|
assert.match(out, /^listen: 127\.0\.0\.1:8787$/m);
|
||||||
|
assert.match(out, /^ - key: k$/m);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("leaves a profile that already sets plugin_dir alone", () => {
|
||||||
|
const dir = tmp();
|
||||||
|
const cfg = path.join(dir, "config.yaml");
|
||||||
|
const orig = 'plugin_dir: "/custom/plugins"\nlisten: ":1"\n';
|
||||||
|
fs.writeFileSync(cfg, orig);
|
||||||
|
assert.equal(ensurePluginDirConfigured(cfg, "/whatever", null), "present");
|
||||||
|
assert.equal(fs.readFileSync(cfg, "utf-8"), orig);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("missing config is not created by the back-fill", () => {
|
||||||
|
const dir = tmp();
|
||||||
|
const cfg = path.join(dir, "config.yaml");
|
||||||
|
assert.equal(ensurePluginDirConfigured(cfg, path.join(dir, "p"), null), "absent");
|
||||||
|
assert.equal(fs.existsSync(cfg), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("upgrade drops builtin plugins (keeping a backup) but not user plugins", () => {
|
||||||
|
const dir = tmp();
|
||||||
|
const plugins = path.join(dir, "plugins");
|
||||||
|
fs.mkdirSync(plugins, { recursive: true });
|
||||||
|
for (const f of BUILTIN_PLUGINS) fs.writeFileSync(path.join(plugins, f), "OLD");
|
||||||
|
fs.writeFileSync(path.join(plugins, "my-own.lua"), "MINE");
|
||||||
|
fs.writeFileSync(path.join(dir, ".plugin-seed-version"), "1.0.0\n");
|
||||||
|
|
||||||
|
const r = syncBundledPluginsOnUpgrade(dir, plugins, "2.0.42", null);
|
||||||
|
assert.equal(r.changed, true);
|
||||||
|
assert.deepEqual(r.dropped.sort(), [...BUILTIN_PLUGINS].sort());
|
||||||
|
for (const f of BUILTIN_PLUGINS) {
|
||||||
|
assert.equal(fs.existsSync(path.join(plugins, f)), false, f + " dropped");
|
||||||
|
assert.equal(fs.readFileSync(path.join(plugins, f + ".prev-1.0.0"), "utf-8"), "OLD");
|
||||||
|
}
|
||||||
|
assert.equal(fs.readFileSync(path.join(plugins, "my-own.lua"), "utf-8"), "MINE");
|
||||||
|
assert.equal(fs.readFileSync(path.join(dir, ".plugin-seed-version"), "utf-8").trim(), "2.0.42");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("same version is a no-op (nothing dropped)", () => {
|
||||||
|
const dir = tmp();
|
||||||
|
const plugins = path.join(dir, "plugins");
|
||||||
|
fs.mkdirSync(plugins, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(plugins, "channels.lua"), "CURRENT");
|
||||||
|
fs.writeFileSync(path.join(dir, ".plugin-seed-version"), "2.0.42\n");
|
||||||
|
const r = syncBundledPluginsOnUpgrade(dir, plugins, "2.0.42", null);
|
||||||
|
assert.equal(r.changed, false);
|
||||||
|
assert.equal(fs.readFileSync(path.join(plugins, "channels.lua"), "utf-8"), "CURRENT");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("first run on a fresh profile just stamps the version", () => {
|
||||||
|
const dir = tmp();
|
||||||
|
const plugins = path.join(dir, "plugins");
|
||||||
|
fs.mkdirSync(plugins, { recursive: true });
|
||||||
|
const r = syncBundledPluginsOnUpgrade(dir, plugins, "2.0.42", null);
|
||||||
|
assert.equal(r.changed, true);
|
||||||
|
assert.deepEqual(r.dropped, []);
|
||||||
|
assert.equal(fs.existsSync(path.join(dir, ".plugin-seed-version")), true);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user