Merge pull request #1157 from bbassie/refactor/plugin-state

Replace the PluginDetails booleans with a single state
This commit is contained in:
Daniel authored and GitHub committed 2026-10-30 17:52:51 +01:00
commit 8a3aa56c11
12 files changed
+261 -171

No files matched your search

+1 -1
View File
@@ -271,7 +271,7 @@ hostPluginCommands.add(new lib.Command({
let hostId = await lib.resolveHost(ctl, args.host);
const plugins = await ctl.sendTo({ hostId }, new lib.PluginListRequest());
print(asTable(plugins.map(p => ({
title: p.title, version: p.version, loaded: p.loaded, enabled: p.enabled, npmPackage: p.npmPackage,
title: p.title, version: p.version, state: p.state, npmPackage: p.npmPackage,
}))));
},
}));
+73 -21
View File
@@ -4,57 +4,94 @@ import type { PluginNodeEnvInfo } from "../plugin.js";
import { Address, MessageRequest } from "./messages_core.js";
import type { IUser } from "./UserDetails.js";
export const pluginStateSchema = Type.Union([
Type.Literal("loaded"),
Type.Literal("disabled"),
Type.Literal("unloadOnRestart"),
Type.Literal("loadOnRestart"),
Type.Literal("noEntrypoint"),
Type.Literal("pendingInstall"),
Type.Literal("removed"),
Type.Literal("removedLoaded"),
Type.Literal("nameTaken"),
]);
/**
* State of a plugin on a machine
*
* - loaded: Running and loaded again on the next restart
* - disabled: Not loaded as `<plugin>.load_plugin` is false
* - unloadOnRestart: Running, but disabled since startup
* - loadOnRestart: Not running, enabled since startup or skipped by recovery mode
* - noEntrypoint: Has no entrypoint for this machine, so it is never loaded here
* - pendingInstall: Installed since startup, loaded on the next restart
* - removed: Uninstalled since startup while not running
* - removedLoaded: Uninstalled since startup, running until the next restart
* - nameTaken: Not loaded as another package provides a plugin with the same name
*/
export type PluginState = Static<typeof pluginStateSchema>;
/* This is similar to other plugin definitions but with lots removed */
export class PluginDetails {
constructor(
public name: string,
public title: string,
public version: string,
public loaded: boolean,
public enabled: boolean,
public state: PluginState,
public description?: string,
public npmPackage?: string,
/** Version installed on disk, differs from version when an update is waiting for a restart */
public installedVersion?: string,
/** Installed after the machine started, will be loaded on the next restart */
public pendingInstall?: boolean,
/** Uninstalled after the machine started, will not be loaded on the next restart */
public removed?: boolean,
/** Name in the root package.json dependencies, differs from npmPackage for aliases */
public dependencyName?: string,
/** Has no entrypoint for this machine, so it is never loaded here */
public noEntrypoint?: boolean,
/** Not loaded as another package provides a plugin with the same name */
public nameTaken?: boolean,
) {}
static jsonSchema = Type.Object({
name: Type.String(),
title: Type.String(),
version: Type.String(),
// Missing from 2.0.3 and older, which require loaded and enabled instead
state: Type.Optional(pluginStateSchema),
loaded: Type.Boolean(),
enabled: Type.Boolean(),
description: Type.Optional(Type.String()),
npmPackage: Type.Optional(Type.String()),
installedVersion: Type.Optional(Type.String()),
pendingInstall: Type.Optional(Type.Boolean()),
removed: Type.Optional(Type.Boolean()),
dependencyName: Type.Optional(Type.String()),
noEntrypoint: Type.Optional(Type.Boolean()),
nameTaken: Type.Optional(Type.Boolean()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(
json.name, json.title, json.version, json.loaded, json.enabled,
json.description, json.npmPackage, json.installedVersion, json.pendingInstall, json.removed,
json.dependencyName, json.noEntrypoint, json.nameTaken,
json.name, json.title, json.version, json.state ?? this.loadState(json.loaded, json.enabled),
json.description, json.npmPackage, json.installedVersion, json.dependencyName,
);
}
static fromNodeEnvInfo(pluginInfo: PluginNodeEnvInfo, loaded: boolean, enabled: boolean) {
toJSON(): Static<typeof PluginDetails.jsonSchema> {
return {
name: this.name,
title: this.title,
version: this.version,
state: this.state,
loaded: this.loaded,
enabled: this.loadsOnRestart,
description: this.description,
npmPackage: this.npmPackage,
installedVersion: this.installedVersion,
dependencyName: this.dependencyName,
};
}
/** State of a plugin known since startup */
static loadState(loaded: boolean, enabled: boolean): PluginState {
if (loaded) {
return enabled ? "loaded" : "unloadOnRestart";
}
return enabled ? "loadOnRestart" : "disabled";
}
static fromNodeEnvInfo(pluginInfo: PluginNodeEnvInfo, state: PluginState) {
return new this(
pluginInfo.name, pluginInfo.title, pluginInfo.version, loaded, enabled,
pluginInfo.name, pluginInfo.title, pluginInfo.version, state,
pluginInfo.description, pluginInfo.npmPackage,
);
}
@@ -63,10 +100,25 @@ export class PluginDetails {
static fromPackageJson(packageName: string, packageJson: any, npmPackage?: string, dependencyName?: string) {
const description = typeof packageJson.description === "string" ? packageJson.description : undefined;
return new this(
packageName, packageName, packageJson.version, false, false,
description, npmPackage, packageJson.version, true, undefined, dependencyName,
packageName, packageName, packageJson.version, "pendingInstall",
description, npmPackage, packageJson.version, dependencyName,
);
}
/** Running on the machine now */
get loaded() {
return ["loaded", "unloadOnRestart", "removedLoaded"].includes(this.state);
}
/** Running on the machine after the next restart */
get loadsOnRestart() {
return ["loaded", "loadOnRestart", "pendingInstall"].includes(this.state);
}
/** Uninstalled since startup */
get removed() {
return this.state === "removed" || this.state === "removedLoaded";
}
}
export class PluginListRequest {
+5 -8
View File
@@ -128,14 +128,12 @@ export async function listPluginDetails(
for (const pluginInfo of pluginInfos) {
const dir = await realDir(path.dirname(pluginInfo.packagePath));
knownDirs.add(dir);
const loaded = isLoaded(pluginInfo);
// Without an entrypoint for this machine the plugin has no load_plugin field here
const loadable = hasEntrypoint(pluginInfo);
const plugin = PluginDetails.fromNodeEnvInfo(
pluginInfo, isLoaded(pluginInfo), loadable && isEnabled(pluginInfo),
pluginInfo,
hasEntrypoint(pluginInfo) ? PluginDetails.loadState(loaded, isEnabled(pluginInfo)) : "noEntrypoint",
);
if (!loadable) {
plugin.noEntrypoint = true;
}
if (Object.hasOwn(dependencies, pluginInfo.requirePath)) {
plugin.dependencyName = pluginInfo.requirePath;
}
@@ -147,7 +145,7 @@ export async function listPluginDetails(
packageJson = await readPackageJson(path.dirname(pluginInfo.packagePath), false);
if (packageJson === undefined) {
// Uninstalled since startup, it won't be found on the next restart
plugin.removed = true;
plugin.state = loaded ? "removedLoaded" : "removed";
}
} catch {
// Ignored: npm is still writing it
@@ -185,8 +183,7 @@ export async function listPluginDetails(
const plugin = PluginDetails.fromPackageJson(packageName, packageJson, npmPackage, dependencyName);
if (takenDirs.has(dir)) {
// Skipped on startup as its plugin name is taken, will be again
plugin.pendingInstall = undefined;
plugin.nameTaken = true;
plugin.state = "nameTaken";
}
plugins.push(plugin);
}
@@ -160,7 +160,7 @@ export function PluginVersions(props: { status: PluginStatus, latest?: string })
.sort(compareVersions)
.reverse();
const count = (filter: (machine: MachinePlugin) => boolean) => machines.filter(filter).length;
const missing = count(machine => (!machine.plugin || Boolean(machine.plugin.removed)) && !machine.nameTaken);
const missing = count(machine => machine.state === "missing" || machine.state === "dependencyTaken");
return <>
{versions.map(version => {
@@ -175,14 +175,14 @@ export function PluginVersions(props: { status: PluginStatus, latest?: string })
</div>;
})}
{count(isPending) > 0 && <CountLine label="Restart pending on" count={count(isPending)} />}
{count(machine => machine.disabled) > 0 && <CountLine
label="Disabled on" count={count(machine => machine.disabled)} type="secondary"
{count(machine => machine.state === "disabled") > 0 && <CountLine
label="Disabled on" count={count(machine => machine.state === "disabled")} type="secondary"
/>}
{count(machine => machine.noEntrypoint) > 0 && <CountLine
label="No entrypoint on" count={count(machine => machine.noEntrypoint)} type="secondary"
{count(machine => machine.state === "noEntrypoint") > 0 && <CountLine
label="No entrypoint on" count={count(machine => machine.state === "noEntrypoint")} type="secondary"
/>}
{count(machine => machine.nameTaken) > 0 && <CountLine
label="From another package on" count={count(machine => machine.nameTaken)} type="secondary"
{count(machine => machine.state === "nameTaken") > 0 && <CountLine
label="From another package on" count={count(machine => machine.state === "nameTaken")} type="secondary"
/>}
{missing > 0 && <CountLine label="Not installed on" count={missing} type="warning" />}
{unknown.length > 0 && <CountLine label="Plugins not reported by" count={unknown.length} type="warning" />}
+62 -46
View File
@@ -16,18 +16,26 @@ export type Machine = {
};
export type MachinePlugins = Machine & { plugins: lib.PluginDetails[] };
/**
* What happens with a plugin on one machine, in order of precedence
*
* - nameTaken: Another package here provides the plugin by the same name, startup skips this one
* - dependencyTaken: Not installed, and the group's npm package name is a dependency here for
* another plugin, such as an alias
* - missing: Not installed
* - noEntrypoint: Installed but never loaded on this machine as it has no entrypoint for it
* - disabled: Installed but not loaded after the next restart
* - enabled: Installed and loaded after the next restart
*/
export type MachinePluginState =
"nameTaken" | "dependencyTaken" | "missing" | "noEntrypoint" | "disabled" | "enabled";
/** A plugin on one machine, with the version it runs now and the version it will run after a restart */
export type MachinePlugin = Machine & {
plugin?: lib.PluginDetails,
state: MachinePluginState,
running?: string,
afterRestart?: string,
disabled: boolean,
/** Installed but never loaded on this machine as it has no entrypoint for it */
noEntrypoint: boolean,
/** The group's npm package name is a dependency here for another plugin, such as an alias */
dependencyTaken: boolean,
/** Another package here provides the plugin by the same name, startup skips this one */
nameTaken: boolean,
/** A host from before installedVersion was reported, it can't install a specific version */
oldHost: boolean,
};
@@ -53,45 +61,51 @@ export function compareVersions(a: string, b: string) {
return strcmp(a, b);
}
/** Not uninstalled since startup and not skipped by name, so its plugin name is in use */
function holdsName(plugin: lib.PluginDetails) {
return !plugin.removed && plugin.state !== "pendingInstall" && plugin.state !== "nameTaken";
}
function machinePluginState(
plugins: lib.PluginDetails[], plugin: lib.PluginDetails | undefined, key: string, names: Set<string>,
): MachinePluginState {
if (plugin?.state === "nameTaken") {
return "nameTaken";
}
const absent = !plugin || plugin.removed;
if ((absent || plugin.state === "pendingInstall") && plugins.some(
candidate => pluginStatusKey(candidate) !== key && names.has(candidate.name) && holdsName(candidate)
)) {
return "nameTaken";
}
if (absent) {
return plugins.some(candidate => candidate.dependencyName === key && !candidate.removed)
? "dependencyTaken" : "missing";
}
if (plugin.state === "noEntrypoint") {
return "noEntrypoint";
}
return plugin.loadsOnRestart ? "enabled" : "disabled";
}
/** The plugin with the given key on one machine, names are the plugin names known for that key */
function machinePlugin(machine: MachinePlugins, key: string, names: Set<string>): MachinePlugin {
const { plugins, ...rest } = machine;
// A package removed and installed again is listed twice, the removed one may still be running
const candidates = plugins.filter(candidate => pluginStatusKey(candidate) === key);
const plugin = candidates.find(candidate => !candidate.removed) ?? candidates[0];
const absent = !plugin || Boolean(plugin.removed);
const dependencyTaken = absent && plugins.some(
candidate => candidate.dependencyName === key && !candidate.removed
);
const nameTaken = Boolean(plugin?.nameTaken) || (absent || Boolean(plugin.pendingInstall)) && plugins.some(
candidate => (
pluginStatusKey(candidate) !== key && names.has(candidate.name)
&& !candidate.removed && !candidate.pendingInstall && !candidate.nameTaken
)
);
const noEntrypoint = Boolean(plugin?.noEntrypoint);
const state = machinePluginState(plugins, plugin, key, names);
const oldHost = !machine.isController && plugins.length > 0 && plugins.every(
candidate => candidate.installedVersion === undefined && !candidate.removed
);
const entry: MachinePlugin = {
...rest, plugin, disabled: false, noEntrypoint, dependencyTaken, nameTaken, oldHost,
return {
...rest,
plugin,
state,
oldHost,
running: candidates.find(candidate => candidate.loaded)?.version,
afterRestart: state === "enabled" && plugin ? plugin.installedVersion ?? plugin.version : undefined,
};
// What runs now follows loaded, what runs after a restart follows enabled and what is on disk
entry.running = candidates.find(candidate => candidate.loaded)?.version;
if (nameTaken) {
return entry;
}
if (plugin?.pendingInstall) {
// Plugins load by default, so a new install runs after the next restart
entry.afterRestart = plugin.installedVersion ?? plugin.version;
} else if (plugin && !plugin.removed && !noEntrypoint) {
if (plugin.enabled) {
entry.afterRestart = plugin.installedVersion ?? plugin.version;
} else {
entry.disabled = true;
}
}
return entry;
}
/** Group the plugin lists of every machine by plugin */
@@ -107,7 +121,7 @@ export function computePluginStatuses(
if (!titles.has(key) || plugin.loaded) {
titles.set(key, plugin.title);
}
if (!plugin.pendingInstall && !plugin.nameTaken) {
if (plugin.state !== "pendingInstall" && plugin.state !== "nameTaken") {
pluginNames.set(key, (pluginNames.get(key) ?? new Set()).add(plugin.name));
}
}
@@ -154,15 +168,15 @@ export function planUpdate(
if (installed && compareVersions(plugin.installedVersion ?? plugin.version, version) >= 0) {
continue;
}
const kind = !installed || plugin.pendingInstall ? "install" : "update";
const kind = !installed || plugin.state === "pendingInstall" ? "install" : "update";
const skip = (reason: string, skipKind: PackageOperation["kind"] = kind) => {
plan.skipped.push({ kind: skipKind, target, reason });
};
if (machine.offline) {
skip("offline");
} else if (machine.nameTaken) {
} else if (machine.state === "nameTaken") {
skip("another package provides this plugin", "install");
} else if (machine.dependencyTaken) {
} else if (machine.state === "dependencyTaken") {
skip(`another package is installed as ${status.npmPackage}`, "install");
} else if (installed && !plugin.npmPackage) {
// An alias, private or local package, installing the group's npm package would replace it
@@ -211,18 +225,20 @@ export function machineStatusLabels(machine: MachinePlugin) {
if (machine.offline) {
labels.push("Offline");
}
if (machine.nameTaken) {
if (machine.state === "nameTaken") {
labels.push("Another package provides this plugin");
} else if (!machine.plugin || machine.plugin.removed) {
labels.push(machine.dependencyTaken ? "Another package is installed by this name" : "Not installed");
} else if (machine.state === "dependencyTaken") {
labels.push("Another package is installed by this name");
} else if (machine.state === "missing") {
labels.push("Not installed");
} else {
if (!machine.plugin.npmPackage) {
if (!machine.plugin?.npmPackage) {
labels.push("Not installed from npm");
}
if (machine.noEntrypoint) {
if (machine.state === "noEntrypoint") {
labels.push("No entrypoint for this machine");
}
if (machine.disabled) {
if (machine.state === "disabled") {
labels.push("Disabled");
}
}
+1 -2
View File
@@ -302,8 +302,7 @@ describe("host/src/Host", function() {
await host.loadPlugins();
const plugins = await host.handlePluginListRequest(new lib.PluginListRequest());
const plugin = plugins.find(candidate => candidate.name === "controller_only");
assert.equal(plugin.loaded, false);
assert.equal(plugin.noEntrypoint, true);
assert.equal(plugin.state, "noEntrypoint");
const [entry] = computePluginStatuses([
{ name: "host", isController: false, address: { hostId: 1 }, plugins: [plugin] },
]).get("controller_only").machines;
+2 -4
View File
@@ -443,14 +443,12 @@ describe("Integration of Clusterio", function() {
it("should load the plugin on the controller", async function() {
const details = await pluginDetails("controller");
assert(details.enabled, "test_plugin is not enabled on the controller");
assert(details.loaded, "test_plugin is not loaded on the controller");
assert.equal(details.state, "loaded", "test_plugin is not loaded on the controller");
});
it("should load the plugin on the host", async function() {
const details = await pluginDetails({ hostId: 4 });
assert(details.enabled, "test_plugin is not enabled on the host");
assert(details.loaded, "test_plugin is not loaded on the host");
assert.equal(details.state, "loaded", "test_plugin is not loaded on the host");
});
it("should report the plugin's title and version", async function() {
+1 -1
View File
@@ -41,7 +41,7 @@ describe("lib/data/SystemInfo", function() {
testRoundTripJsonSerialisable(SystemInfo, [
base,
[...base, []],
[...base, [new PluginDetails("name", "Title", "1.0.0", true, true, undefined, "name", "1.1.0")]],
[...base, [new PluginDetails("name", "Title", "1.0.0", "loaded", undefined, "name", "1.1.0")]],
]);
});
});
+14 -12
View File
@@ -414,12 +414,12 @@ describe("lib/plugin_loader", function() {
const loaded = byPackage.get("plugin-npm");
assert.equal(loaded.version, "0.0.0");
assert.equal(loaded.installedVersion, "0.0.1");
assert.equal(loaded.pendingInstall, undefined);
assert.equal(loaded.state, "loaded");
// Dependencies of plugin packages are found the same way as on startup
for (const packageName of ["plugin-aliased", "plugin-deep", "plugin-transitive"]) {
const pending = byPackage.get(packageName);
assert.equal(pending.pendingInstall, true, packageName);
assert.equal(pending.state, "pendingInstall", packageName);
assert.equal(pending.installedVersion, "0.0.1", packageName);
assert.equal(pending.loaded, false, packageName);
}
@@ -513,13 +513,12 @@ describe("lib/plugin_loader", function() {
};
let plugins = await lib.listPluginDetails([local], () => true, () => true);
const taken = plugins.find(plugin => plugin.name === "plugin-npm");
assert.equal(taken.nameTaken, true);
assert.equal(taken.pendingInstall, undefined);
assert.equal(taken.state, "nameTaken");
// Loaded on the next restart once the plugin with that name is gone
const gone = { ...local, packagePath: path.resolve("plugins", "gone", "package.json") };
plugins = await lib.listPluginDetails([gone], () => true, () => true);
assert.equal(plugins.find(plugin => plugin.name === "plugin-npm").pendingInstall, true);
assert.equal(plugins.find(plugin => plugin.name === "plugin-npm").state, "pendingInstall");
});
it("should list a dependency made direct since startup as pending", async function() {
@@ -531,7 +530,7 @@ describe("lib/plugin_loader", function() {
await fs.writeFile("package.json", JSON.stringify(root));
const plugins = await lib.listPluginDetails([], () => true, () => true);
const hidden = plugins.find(plugin => plugin.name === "plugin-hidden");
assert.equal(hidden.pendingInstall, true);
assert.equal(hidden.state, "pendingInstall");
assert.equal(hidden.npmPackage, "plugin-hidden");
} finally {
await fs.writeFile("package.json", rootPackageJson);
@@ -573,7 +572,7 @@ describe("lib/plugin_loader", function() {
assert.equal(npm.npmPackage, undefined);
assert.equal(npm.dependencyName, "plugin-npm");
const deep = plugins.find(plugin => plugin.name === "plugin-deep");
assert.equal(deep.pendingInstall, true);
assert.equal(deep.state, "pendingInstall");
assert.equal(deep.npmPackage, undefined);
} finally {
await fs.writeFile("package.json", rootPackageJson);
@@ -637,7 +636,7 @@ describe("lib/plugin_loader", function() {
npmPackage: "plugin-npm",
}], () => true, () => true);
assert.equal(plugin.npmPackage, undefined);
assert.equal(plugin.removed, undefined);
assert.equal(plugin.state, "loaded");
} finally {
await fs.writeFile(packageJsonPath, original);
}
@@ -650,7 +649,7 @@ describe("lib/plugin_loader", function() {
await fs.writeFile(packageJsonPath, JSON.stringify({ ...JSON.parse(original), private: true }));
const plugins = await lib.listPluginDetails([], () => true, () => true);
const deep = plugins.find(plugin => plugin.name === "plugin-deep");
assert.equal(deep.pendingInstall, true);
assert.equal(deep.state, "pendingInstall");
assert.equal(deep.npmPackage, undefined);
} finally {
await fs.writeFile(packageJsonPath, original);
@@ -705,7 +704,7 @@ describe("lib/plugin_loader", function() {
assert.equal(alias.npmPackage, undefined);
const published = new lib.PluginDetails(
"published", "Published", "1.0.0", true, true, undefined, "plugin-aliased", "1.0.0",
"published", "Published", "1.0.0", "loaded", undefined, "plugin-aliased", "1.0.0",
);
const statuses = computePluginStatuses([
{ name: "A", isController: false, address: { hostId: 1 }, plugins: [published] },
@@ -740,9 +739,12 @@ describe("lib/plugin_loader", function() {
}];
const plugins = await lib.listPluginDetails(pluginInfos, () => true, () => true);
const byPackage = new Map(plugins.map(plugin => [plugin.npmPackage ?? plugin.name, plugin]));
assert.equal(byPackage.get("plugin-gone").removed, true);
assert.equal(byPackage.get("plugin-gone").state, "removedLoaded");
assert.equal(byPackage.get("plugin-gone").installedVersion, undefined);
assert.equal(byPackage.get("plugin-npm").removed, undefined);
assert.equal(byPackage.get("plugin-npm").state, "loaded");
const [disabled] = await lib.listPluginDetails([pluginInfos[0]], () => false, () => false);
assert.equal(disabled.state, "removed");
});
it("should load existing plugin list", async function() {
+37 -11
View File
@@ -38,20 +38,46 @@ describe("messages/plugin", function() {
describe("PluginDetails", function() {
it("has round trip json serialisation", function() {
testRoundTripJsonSerialisable(lib.PluginDetails, [
["Name", "Title", "Version", true, true],
[
"Name", "Title", "Version", false, false, "Description", "Npm Package",
"Installed Version", true, true, "Dependency Name", true, true,
],
["Name", "Title", "Version", "loaded"],
["Name", "Title", "Version", "removedLoaded", "Description", "Npm Package", "Installed Version", "Dep"],
]);
});
it("sends loaded and enabled for 2.0.3 and older", function() {
const cases = [
["loaded", true, true],
["disabled", false, false],
["unloadOnRestart", true, false],
["loadOnRestart", false, true],
["noEntrypoint", false, false],
["pendingInstall", false, true],
["removed", false, false],
["removedLoaded", true, false],
["nameTaken", false, false],
];
for (const [state, loaded, enabled] of cases) {
const json = JSON.parse(JSON.stringify(new lib.PluginDetails("Name", "Title", "Version", state)));
assert.deepEqual([json.loaded, json.enabled], [loaded, enabled], state);
}
});
it("reads the state of plugins listed by 2.0.3 and older", function() {
const cases = [
[true, true, "loaded"],
[true, false, "unloadOnRestart"],
[false, true, "loadOnRestart"],
[false, false, "disabled"],
];
for (const [loaded, enabled, state] of cases) {
const json = { name: "Name", title: "Title", version: "1.0.0", loaded, enabled };
assert.equal(lib.PluginDetails.fromJSON(json).state, state);
}
});
it("can be created from NodeEnvInfo interface", function() {
const pluginDetails = lib.PluginDetails.fromNodeEnvInfo({
name: "Name", title: "Title", version: "Version",
description: "Description", npmPackage: "Npm Package",
}, true, true);
}, "loaded");
assert.deepEqual(pluginDetails, new lib.PluginDetails(
"Name", "Title", "Version", true, true,
"Name", "Title", "Version", "loaded",
"Description", "Npm Package",
));
});
@@ -60,8 +86,8 @@ describe("messages/plugin", function() {
"package", { version: "1.0.0", description: "Description" }, "package", "package",
);
assert.deepEqual(pluginDetails, new lib.PluginDetails(
"package", "package", "1.0.0", false, false,
"Description", "package", "1.0.0", true, undefined, "package",
"package", "package", "1.0.0", "pendingInstall",
"Description", "package", "1.0.0", "package",
));
const invalid = lib.PluginDetails.fromPackageJson("package", { version: "1.0.0", description: 1 });
assert.equal(invalid.description, undefined);
@@ -99,7 +125,7 @@ describe("messages/plugin", function() {
const response = await controlConnection.handlePluginListRequest(new lib.PluginListRequest());
assert.deepEqual(response, [new lib.PluginDetails(
"Name", "Title", "Version", true, true,
"Name", "Title", "Version", "loaded",
"Description", "Npm Package", "Version",
)]);
});
@@ -114,7 +140,7 @@ describe("messages/plugin", function() {
const response = await host.handlePluginListRequest(new lib.PluginListRequest());
assert.deepEqual(response, [new lib.PluginDetails(
"Name", "Title", "Version", true, true,
"Name", "Title", "Version", "loaded",
"Description", "Npm Package", "Version",
)]);
});
+11 -13
View File
@@ -13,11 +13,11 @@ describe("web_ui/util/plugin_status", function() {
return { name, isController: false, address: { hostId: name.length }, plugins };
}
function loaded(npmPackage, version) {
return new lib.PluginDetails("plugin", "Plugin", version, true, true, undefined, 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, false, false, undefined, npmPackage, version, true,
packageName, packageName, version, "pendingInstall", undefined, npmPackage, version,
);
}
function skippedText(result) {
@@ -50,7 +50,7 @@ describe("web_ui/util/plugin_status", function() {
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", true, true)]),
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)" }]);
@@ -58,7 +58,7 @@ describe("web_ui/util/plugin_status", function() {
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", true, true, undefined, "plugin-aliased");
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]),
@@ -70,7 +70,7 @@ describe("web_ui/util/plugin_status", function() {
});
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", true, true);
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")]),
@@ -110,7 +110,7 @@ describe("web_ui/util/plugin_status", function() {
describe("planMachineUpdate()", function() {
it("should plan for the given machine only", function() {
const loaded = new lib.PluginDetails(
"plugin", "Plugin", "1.0.0", true, true, undefined, "plugin-aliased", "1.0.0",
"plugin", "Plugin", "1.0.0", "loaded", undefined, "plugin-aliased", "1.0.0",
);
const statuses = computePluginStatuses([
{ name: "a", isController: false, address: { hostId: 1 }, plugins: [loaded] },
@@ -136,7 +136,7 @@ describe("web_ui/util/plugin_status", function() {
}
function loadedPlugin(version = "1.0.0", installedVersion = version) {
return new lib.PluginDetails(
"plugin", "Plugin", version, true, true, undefined, "plugin", installedVersion,
"plugin", "Plugin", version, "loaded", undefined, "plugin", installedVersion,
);
}
@@ -146,24 +146,22 @@ describe("web_ui/util/plugin_status", function() {
assert.deepEqual(labels([]), ["Not installed"]);
assert.deepEqual(labels([loadedPlugin()], { offline: true }), ["Offline"]);
const disabled = new lib.PluginDetails(
"plugin", "Plugin", "1.0.0", false, false, undefined, "plugin", "1.0.0",
"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", true, true);
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", false, false);
plugin.noEntrypoint = true;
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.noEntrypoint, true);
assert.equal(entry.disabled, false);
assert.equal(entry.state, "noEntrypoint");
assert.equal(entry.running, undefined);
assert.equal(entry.afterRestart, undefined);
});
+47 -45
View File
@@ -9,62 +9,64 @@ const target = "2.0.0";
const versions = ["1.0.0", "2.0.0", "3.0.0"];
function details(fields) {
const plugin = new lib.PluginDetails(
fields.name, fields.name, fields.version, fields.loaded, fields.enabled, undefined, fields.npmPackage,
fields.installedVersion, fields.pendingInstall, fields.removed, fields.dependencyName, fields.noEntrypoint,
fields.nameTaken,
return new lib.PluginDetails(
fields.name, fields.name, fields.version, fields.state, undefined, fields.npmPackage,
fields.installedVersion, fields.dependencyName,
);
return plugin;
}
function* states() {
yield { label: "absent", plugins: [] };
for (const version of versions) {
for (const installed of [version, ...versions.filter(other => other !== version)]) {
for (const enabled of [true, false]) {
for (const loaded of enabled ? [true] : [true, false]) {
const state = `${enabled ? "enabled" : "disabled"}${loaded ? " loaded" : ""}`;
yield {
label: `npm ${version} on disk ${installed} ${state}`,
entry: { source: "npm", version, installed, enabled, loaded },
plugins: [details({
name: "plugin", version, loaded, enabled, npmPackage: "pkg",
installedVersion: installed, dependencyName: "pkg",
})],
};
}
for (const [state, loaded, enabled] of [
["loaded", true, true],
["unloadOnRestart", true, false],
["loadOnRestart", false, true],
["disabled", false, false],
]) {
yield {
label: `npm ${version} on disk ${installed} ${state}`,
entry: { source: "npm", version, installed, enabled, loaded },
plugins: [details({
name: "plugin", version, state, npmPackage: "pkg",
installedVersion: installed, dependencyName: "pkg",
})],
};
}
}
yield {
label: `npm ${version} removed`,
entry: { source: "npm", version, removed: true },
plugins: [details({
name: "plugin", version, loaded: true, enabled: true, npmPackage: "pkg",
removed: true, dependencyName: "pkg",
})],
};
for (const loaded of [true, false]) {
yield {
label: `npm ${version} removed${loaded ? " while loaded" : ""}`,
entry: { source: "npm", version, removed: true, loaded },
plugins: [details({
name: "plugin", version, state: loaded ? "removedLoaded" : "removed", npmPackage: "pkg",
dependencyName: "pkg",
})],
};
}
yield {
label: `npm ${version} no entrypoint`,
entry: { source: "npm", version, installed: version, noEntrypoint: true },
plugins: [details({
name: "plugin", version, loaded: false, enabled: false, npmPackage: "pkg",
installedVersion: version, dependencyName: "pkg", noEntrypoint: true,
name: "plugin", version, state: "noEntrypoint", npmPackage: "pkg",
installedVersion: version, dependencyName: "pkg",
})],
};
yield {
label: `npm ${version} pending`,
entry: { source: "npm", version, installed: version, pending: true },
plugins: [details({
name: "pkg", version, loaded: false, enabled: false, npmPackage: "pkg",
installedVersion: version, pendingInstall: true, dependencyName: "pkg",
name: "pkg", version, state: "pendingInstall", npmPackage: "pkg",
installedVersion: version, dependencyName: "pkg",
})],
};
yield {
label: `alias ${version} pending`,
entry: { source: "alias", version, installed: version, pending: true },
plugins: [details({
name: "pkg", version, loaded: false, enabled: false,
installedVersion: version, pendingInstall: true, dependencyName: "pkg",
name: "pkg", version, state: "pendingInstall",
installedVersion: version, dependencyName: "pkg",
})],
};
yield {
@@ -72,12 +74,12 @@ function* states() {
entry: { source: "npm", version, installed: target, replaced: true },
plugins: [
details({
name: "plugin", version, loaded: true, enabled: true, npmPackage: "pkg",
removed: true, dependencyName: "pkg",
name: "plugin", version, state: "removedLoaded", npmPackage: "pkg",
dependencyName: "pkg",
}),
details({
name: "pkg", version: target, loaded: false, enabled: false, npmPackage: "pkg",
installedVersion: target, pendingInstall: true, dependencyName: "pkg",
name: "pkg", version: target, state: "pendingInstall", npmPackage: "pkg",
installedVersion: target, dependencyName: "pkg",
}),
],
};
@@ -86,7 +88,7 @@ function* states() {
label: "alias loaded as pkg",
taken: "dependency",
plugins: [details({
name: "real_plugin", version: "1.0.0", loaded: true, enabled: true,
name: "real_plugin", version: "1.0.0", state: "loaded",
installedVersion: "1.0.0", dependencyName: "pkg",
})],
};
@@ -95,10 +97,10 @@ function* states() {
label: `npm ${version} skipped as its name is taken`,
taken: "name",
plugins: [
details({ name: "plugin", version: "1.0.0", loaded: true, enabled: true, installedVersion: "1.0.0" }),
details({ name: "plugin", version: "1.0.0", state: "loaded", installedVersion: "1.0.0" }),
details({
name: "pkg", version, loaded: false, enabled: false, npmPackage: "pkg",
installedVersion: version, dependencyName: "pkg", nameTaken: true,
name: "pkg", version, state: "nameTaken", npmPackage: "pkg",
installedVersion: version, dependencyName: "pkg",
}),
],
entry: { source: "npm", version, installed: version },
@@ -108,17 +110,17 @@ function* states() {
label: "local plugin with the same name",
taken: "name",
plugins: [details({
name: "plugin", version: "1.0.0", loaded: true, enabled: true, installedVersion: "1.0.0",
name: "plugin", version: "1.0.0", state: "loaded", installedVersion: "1.0.0",
})],
};
yield {
label: "local plugin with the same name and pkg pending",
taken: "name",
plugins: [
details({ name: "plugin", version: "1.0.0", loaded: true, enabled: true, installedVersion: "1.0.0" }),
details({ name: "plugin", version: "1.0.0", state: "loaded", installedVersion: "1.0.0" }),
details({
name: "pkg", version: "1.0.0", loaded: false, enabled: false, npmPackage: "pkg",
installedVersion: "1.0.0", pendingInstall: true, dependencyName: "pkg",
name: "pkg", version: "1.0.0", state: "pendingInstall", npmPackage: "pkg",
installedVersion: "1.0.0", dependencyName: "pkg",
}),
],
};
@@ -145,7 +147,7 @@ function expected(state) {
}
let running;
let afterRestart;
if (entry?.loaded || entry?.removed || entry?.replaced) {
if (entry?.loaded || entry?.replaced) {
running = entry.version;
}
if (entry && !entry.removed && !entry.noEntrypoint && (entry.pending || entry.replaced || entry.enabled)) {
@@ -159,7 +161,7 @@ function semverGte(a, b) {
}
const reference = { name: "ref", isController: true, address: "controller", plugins: [details({
name: "plugin", version: "1.0.0", loaded: true, enabled: true, npmPackage: "pkg",
name: "plugin", version: "1.0.0", state: "loaded", npmPackage: "pkg",
installedVersion: "1.0.0", dependencyName: "pkg",
})] };