Files
bbassieandClaude Opus 5.5 e455d17bb7 Write plugin versions while loading configs
The staging copy made by Config.set() went through the constructor, so
plugin migrations ran a second time on the live fields. A version-gated
migration was applied twice when setPluginVersions() changed the version.
Staging now copies the fields with update() instead.

A config saved without <plugin>.version got the running version as its
default, so setPluginVersions() saw no change and nothing saved the stamp.
The constructor now writes the registered plugin versions after the
migrations run and marks the config dirty when that changes a loaded value.
Controller.bootstrap() saves the instances that came out dirty, and
setPluginVersions() is gone.

Restructure the SyncHook and plugin config tests by method, and suggest the
controller config version for role migrations in the docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:37:03 +00:00

152 lines
5.9 KiB
JavaScript

import * as lib from "@clusterio/lib";
import assert from "node:assert/strict";
// Each migration test should indicate which version introduced the migration
describe("lib/config/definitions/migrations", function() {
describe("Controller Config", function() {
it("should migrate 'external_address' to 'public_url'", function() { // Alpha 19
const config = lib.ControllerConfig.fromJSON({
"controller.external_address": "https://example.com:4000",
}, "controller");
assert.equal(config.get("controller.public_url"), "https://example.com:4000",
"public_url contains the wrong value");
assert.equal(config.fields["controller.external_address"], undefined,
"external_address was not removed");
});
});
describe("Host Config", function() {
});
describe("Instance Config", function() {
it("should migrate boolean sync to string enum", function() { // Alpha 19
const configTrue = lib.InstanceConfig.fromJSON({
"factorio.sync_adminlist": true,
"factorio.sync_whitelist": true,
"factorio.sync_banlist": true,
}, "controller");
assert.equal(configTrue.get("factorio.sync_adminlist"), "enabled", "sync_adminlist contains wrong value");
assert.equal(configTrue.get("factorio.sync_whitelist"), "enabled", "sync_adminlist contains wrong value");
assert.equal(configTrue.get("factorio.sync_banlist"), "enabled", "sync_adminlist contains wrong value");
const configFalse = lib.InstanceConfig.fromJSON({
"factorio.sync_adminlist": false,
"factorio.sync_whitelist": false,
"factorio.sync_banlist": false,
}, "controller");
assert.equal(configFalse.get("factorio.sync_adminlist"), "disabled", "sync_adminlist contains wrong value");
assert.equal(configFalse.get("factorio.sync_whitelist"), "disabled", "sync_adminlist contains wrong value");
assert.equal(configFalse.get("factorio.sync_banlist"), "disabled", "sync_adminlist contains wrong value");
});
});
describe("Ctl Config", function() {
});
describe("Plugin Config", function() {
const pluginInfo = {
name: "test_plugin",
version: "1.2.0",
instanceEntrypoint: "instance.js",
instanceConfigFields: {
"test_plugin.delay": { type: "number", initialValue: 1 },
},
};
before(function() {
lib.addPluginConfigFields([pluginInfo]);
});
afterEach(function() {
lib.InstanceConfig.pluginMigrations.clear();
lib.Config.pluginVersions.clear();
});
after(function() {
for (const Config of [lib.ControllerConfig, lib.HostConfig, lib.InstanceConfig]) {
for (const name of Object.keys(Config.fieldDefinitions)) {
if (name.startsWith("test_plugin.")) {
delete Config.fieldDefinitions[name];
}
}
}
});
describe("static .pluginMigrations", function() {
it("should run on load", function() {
lib.InstanceConfig.pluginMigrations.attach("test_plugin", config => (
lib.renameConfigPrefix(config, "old_plugin.", "test_plugin.")
));
const config = lib.InstanceConfig.fromJSON({
"old_plugin.load_plugin": false,
"old_plugin.delay": 5,
}, "controller");
assert.equal(config.get("test_plugin.load_plugin"), false);
assert.equal(config.get("test_plugin.delay"), 5);
assert.equal(config.toJSON()["old_plugin.delay"], undefined);
});
it("should run after core migrations", function() {
lib.InstanceConfig.pluginMigrations.attach("test_plugin", config => {
assert.equal(config["factorio.sync_adminlist"], "enabled");
return config;
});
lib.InstanceConfig.fromJSON({ "factorio.sync_adminlist": true }, "controller");
});
it("should see the version the config was saved with", function() {
let seen;
lib.InstanceConfig.pluginMigrations.attach("test_plugin", config => {
seen = config["test_plugin.version"];
return config;
});
lib.InstanceConfig.fromJSON({ "test_plugin.version": "1.1.0" }, "controller");
assert.equal(seen, "1.1.0");
});
it("should not run again when the config is changed", function() {
lib.Config.pluginVersions.set("test_plugin", "1.2.0");
let calls = 0;
lib.InstanceConfig.pluginMigrations.attach("test_plugin", config => {
calls += 1;
if (config["test_plugin.version"] === "1.1.0") {
config["test_plugin.delay"] *= 60;
}
return config;
});
const config = lib.InstanceConfig.fromJSON({
"test_plugin.version": "1.1.0",
"test_plugin.delay": 2,
}, "controller");
config.set("instance.name", "changed");
assert.equal(config.get("test_plugin.delay"), 120);
assert.equal(calls, 1);
});
});
describe("static .pluginVersions", function() {
it("should write the version after migrations run", function() {
lib.Config.pluginVersions.set("test_plugin", "1.2.0");
const config = lib.InstanceConfig.fromJSON({ "test_plugin.version": "1.1.0" }, "controller");
assert.equal(config.get("test_plugin.version"), "1.2.0");
assert.equal(config.dirty, true);
});
it("should mark configs saved without a version as dirty", function() {
lib.Config.pluginVersions.set("test_plugin", "1.2.0");
const config = lib.InstanceConfig.fromJSON({}, "controller");
assert.equal(config.get("test_plugin.version"), "1.2.0");
assert.equal(config.dirty, true);
});
it("should not mark configs already at the version as dirty", function() {
lib.Config.pluginVersions.set("test_plugin", "1.2.0");
const config = lib.InstanceConfig.fromJSON({ "test_plugin.version": "1.2.0" }, "controller");
assert.equal(config.dirty, false);
});
it("should write the version to new configs", function() {
lib.Config.pluginVersions.set("test_plugin", "1.2.0");
const config = new lib.InstanceConfig("controller");
assert.equal(config.get("test_plugin.version"), "1.2.0");
});
it("should leave the version empty when no versions are registered", function() {
const config = new lib.InstanceConfig("controller");
assert.equal(config.get("test_plugin.version"), "");
});
});
});
});