mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
loaded, enabled, pendingInstall, removed, noEntrypoint and nameTaken allowed far more combinations than can happen, and every consumer had to know which took priority. PluginDetails now has one state, with loaded, loadsOnRestart and removed as getters over it. 2.0.3 web UIs and ctl send PluginListRequest straight to hosts and require loaded and enabled, so those are still sent next to the optional state. Plugins listed by 2.0.3 hosts get their state from loaded and enabled. The web UI planner gets the same treatment: MachinePlugin has one state in place of disabled, noEntrypoint, dependencyTaken and nameTaken. ctl host plugin list shows the state. Closes #1134 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
7.4 KiB
JavaScript
170 lines
7.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
|
|
import * as lib from "@clusterio/lib";
|
|
import {
|
|
computePluginStatuses, machineStatusLabels, planMachineUpdate, planUpdate,
|
|
} from "../../packages/web_ui/src/util/plugin_status.ts";
|
|
|
|
describe("web_ui/util/plugin_status", function() {
|
|
describe("planUpdate()", function() {
|
|
const access = { canInstall: () => true, canUpdate: () => true };
|
|
const account = { hasPermission: () => true };
|
|
function machine(name, plugins) {
|
|
return { name, isController: false, address: { hostId: name.length }, plugins };
|
|
}
|
|
function loaded(npmPackage, version) {
|
|
return new lib.PluginDetails("plugin", "Plugin", version, "loaded", undefined, npmPackage, version);
|
|
}
|
|
function pending(packageName, npmPackage, version) {
|
|
return new lib.PluginDetails(
|
|
packageName, packageName, version, "pendingInstall", undefined, npmPackage, version,
|
|
);
|
|
}
|
|
function skippedText(result) {
|
|
return result.skipped.map(skip => ({ kind: skip.kind, text: `${skip.target.name} (${skip.reason})` }));
|
|
}
|
|
function plan(machines) {
|
|
const statuses = computePluginStatuses(machines);
|
|
assert.equal(statuses.size, 1);
|
|
const status = [...statuses.values()][0];
|
|
return planUpdate(status, "2.0.0", access, account);
|
|
}
|
|
|
|
it("should install on machines where the plugin is missing", function() {
|
|
const result = plan([machine("a", [loaded("plugin-aliased", "1.0.0")]), machine("b", [])]);
|
|
assert.deepEqual(result.operations.map(op => [op.kind, op.target.name, op.packageSpec]), [
|
|
["update", "a", "plugin-aliased@2.0.0"],
|
|
["install", "b", "plugin-aliased@2.0.0"],
|
|
]);
|
|
});
|
|
|
|
it("should not install over a pending alias with another machine's package", function() {
|
|
const result = plan([
|
|
machine("a", [loaded("plugin-aliased", "2.0.0")]),
|
|
machine("b", [pending("plugin-aliased", undefined, "1.0.0")]),
|
|
]);
|
|
assert.deepEqual(result.operations, []);
|
|
assert.deepEqual(skippedText(result), [{ kind: "install", text: "b (not installed from npm)" }]);
|
|
});
|
|
|
|
it("should not update a loaded package that isn't from npm", function() {
|
|
const result = plan([
|
|
machine("a", [loaded("plugin-aliased", "1.0.0")]),
|
|
machine("b", [new lib.PluginDetails("plugin-aliased", "Plugin", "1.0.0", "loaded")]),
|
|
]);
|
|
assert.deepEqual(result.operations.map(op => op.target.name), ["a"]);
|
|
assert.deepEqual(skippedText(result), [{ kind: "update", text: "b (not installed from npm)" }]);
|
|
});
|
|
|
|
it("should not send a specific version to hosts that can't parse it", function() {
|
|
// Hosts from before installedVersion was reported take name@version as a package name
|
|
const old = new lib.PluginDetails("plugin", "Plugin", "1.0.0", "loaded", undefined, "plugin-aliased");
|
|
const result = plan([
|
|
machine("a", [loaded("plugin-aliased", "1.0.0")]),
|
|
machine("old", [old]),
|
|
]);
|
|
assert.deepEqual(result.operations.map(op => op.target.name), ["a"]);
|
|
assert.deepEqual(skippedText(result), [
|
|
{ kind: "update", text: "old (host too old to install a specific version)" },
|
|
]);
|
|
});
|
|
|
|
it("should not install over a loaded alias that has the package name", function() {
|
|
const alias = new lib.PluginDetails("real_plugin", "Real", "1.0.0", "loaded");
|
|
alias.dependencyName = "plugin-aliased";
|
|
const statuses = computePluginStatuses([
|
|
machine("a", [loaded("plugin-aliased", "1.0.0")]),
|
|
machine("b", [alias]),
|
|
]);
|
|
const result = planUpdate(statuses.get("plugin-aliased"), "2.0.0", access, account);
|
|
assert.deepEqual(result.operations.map(op => op.target.name), ["a"]);
|
|
assert.deepEqual(skippedText(result), [
|
|
{ kind: "install", text: "b (another package is installed as plugin-aliased)" },
|
|
]);
|
|
});
|
|
|
|
it("should install over a pending npm package", function() {
|
|
const result = plan([
|
|
machine("a", [loaded("plugin-aliased", "2.0.0")]),
|
|
machine("b", [pending("plugin-aliased", "plugin-aliased", "1.0.0")]),
|
|
]);
|
|
assert.deepEqual(result.operations.map(op => [op.kind, op.target.name]), [["install", "b"]]);
|
|
});
|
|
|
|
it("should skip offline machines and machines that haven't reported their plugins", function() {
|
|
const offline = { ...machine("off", [loaded("plugin-aliased", "1.0.0")]), offline: true };
|
|
const statuses = computePluginStatuses(
|
|
[machine("a", [loaded("plugin-aliased", "1.0.0")]), offline, { ...machine("gone", []), offline: true }],
|
|
[{ name: "old", isController: false, address: { hostId: 9 } }],
|
|
);
|
|
const result = planUpdate(statuses.get("plugin-aliased"), "2.0.0", access, account);
|
|
assert.deepEqual(result.operations.map(op => op.target.name), ["a"]);
|
|
assert.deepEqual(skippedText(result), [
|
|
{ kind: "update", text: "off (offline)" },
|
|
{ kind: "install", text: "gone (offline)" },
|
|
{ kind: "update", text: "old (plugins not reported)" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("planMachineUpdate()", function() {
|
|
it("should plan for the given machine only", function() {
|
|
const loaded = new lib.PluginDetails(
|
|
"plugin", "Plugin", "1.0.0", "loaded", undefined, "plugin-aliased", "1.0.0",
|
|
);
|
|
const statuses = computePluginStatuses([
|
|
{ name: "a", isController: false, address: { hostId: 1 }, plugins: [loaded] },
|
|
{ name: "b", isController: false, address: { hostId: 2 }, plugins: [] },
|
|
], [{ name: "old", isController: false, address: { hostId: 9 } }]);
|
|
const status = statuses.get("plugin-aliased");
|
|
const plan = planMachineUpdate(
|
|
status, status.machines[1], "2.0.0",
|
|
{ canInstall: () => true, canUpdate: () => true }, { hasPermission: () => true },
|
|
);
|
|
assert.deepEqual(plan.operations.map(op => [op.kind, op.target.name]), [["install", "b"]]);
|
|
assert.deepEqual(plan.skipped, []);
|
|
});
|
|
});
|
|
|
|
describe("machineStatusLabels()", function() {
|
|
function labels(plugins, extra = {}) {
|
|
const [status] = computePluginStatuses([
|
|
{ name: "a", isController: false, address: { hostId: 1 }, plugins: [loadedPlugin()] },
|
|
{ name: "b", isController: false, address: { hostId: 2 }, plugins, ...extra },
|
|
]).values();
|
|
return machineStatusLabels(status.machines[1]);
|
|
}
|
|
function loadedPlugin(version = "1.0.0", installedVersion = version) {
|
|
return new lib.PluginDetails(
|
|
"plugin", "Plugin", version, "loaded", undefined, "plugin", installedVersion,
|
|
);
|
|
}
|
|
|
|
it("should describe the state of a plugin on a machine", function() {
|
|
assert.deepEqual(labels([loadedPlugin()]), []);
|
|
assert.deepEqual(labels([loadedPlugin("1.0.0", "2.0.0")]), ["Restart pending"]);
|
|
assert.deepEqual(labels([]), ["Not installed"]);
|
|
assert.deepEqual(labels([loadedPlugin()], { offline: true }), ["Offline"]);
|
|
const disabled = new lib.PluginDetails(
|
|
"plugin", "Plugin", "1.0.0", "disabled", undefined, "plugin", "1.0.0",
|
|
);
|
|
assert.deepEqual(labels([disabled]), ["Disabled"]);
|
|
const local = new lib.PluginDetails("plugin", "Plugin", "1.0.0", "loaded");
|
|
assert.deepEqual(labels([local]), ["Not installed from npm"]);
|
|
});
|
|
});
|
|
|
|
describe("computePluginStatuses()", function() {
|
|
it("should not expect a plugin without an entrypoint for the machine to run after restart", function() {
|
|
const plugin = new lib.PluginDetails("controller_only", "Plugin", "1.0.0", "noEntrypoint");
|
|
const [status] = computePluginStatuses([
|
|
{ name: "host", isController: false, address: { hostId: 1 }, plugins: [plugin] },
|
|
]).values();
|
|
const [entry] = status.machines;
|
|
assert.equal(entry.state, "noEntrypoint");
|
|
assert.equal(entry.running, undefined);
|
|
assert.equal(entry.afterRestart, undefined);
|
|
});
|
|
});
|
|
});
|