From 9ab36151bd8699d16e8fb1f6e26bdeefeecf2e3f Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:01:27 +0000 Subject: [PATCH] Finish the plugin system integration tests Adds the last three checks from #342 to test_plugin: an InstanceEcho request handled on a running instance, a Lua module with a remote interface that the request calls over RCON, and log tagging checks for the plugin field and host/instance ids on the controller, host and instance log lines. test_plugin now has a module, so the create_scenario and patch unit tests that relied on it having none use global_chat instead. Closes #342 Co-Authored-By: Claude Opus 5.5 --- test/file/test_plugin/index.js | 3 +- test/file/test_plugin/instance.js | 11 ++++ test/file/test_plugin/messages.js | 19 +++++++ test/file/test_plugin/module/module.json | 4 ++ test/file/test_plugin/module/test_plugin.lua | 5 ++ test/host/create_scenario.js | 2 +- test/host/patch.js | 2 +- test/integration/clusterio.js | 58 +++++++++++++++++++- 8 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 test/file/test_plugin/module/module.json create mode 100644 test/file/test_plugin/module/test_plugin.lua diff --git a/test/file/test_plugin/index.js b/test/file/test_plugin/index.js index 437d6172..559226f8 100644 --- a/test/file/test_plugin/index.js +++ b/test/file/test_plugin/index.js @@ -1,7 +1,7 @@ // Plugin exercising the plugin system in the integration tests, see #342. // Written as plain JavaScript so that it does not need to be built. -import { ControllerEcho, HostEcho, HostEchoReceived } from "./messages.js"; +import { ControllerEcho, HostEcho, HostEchoReceived, InstanceEcho } from "./messages.js"; export const plugin = { name: "test_plugin", @@ -17,5 +17,6 @@ export const plugin = { ControllerEcho, HostEcho, HostEchoReceived, + InstanceEcho, ], }; diff --git a/test/file/test_plugin/instance.js b/test/file/test_plugin/instance.js index 3f467777..31f901da 100644 --- a/test/file/test_plugin/instance.js +++ b/test/file/test_plugin/instance.js @@ -1,3 +1,7 @@ +import * as lib from "@clusterio/lib"; + +import { InstanceEcho } from "./messages.js"; + export default async function(context) { const { instance, plugin, logger } = context; logger.info("test_plugin instance loaded"); @@ -5,4 +9,11 @@ export default async function(context) { instance.hooks.start.attach(plugin.name, async () => { logger.info("test_plugin instance started"); }); + + instance.handle(InstanceEcho, async (request) => { + logger.info(`test_plugin instance echo ${request.text}`); + const text = lib.escapeString(request.text); + const response = await instance.sendRcon(`/sc rcon.print(remote.call("test_plugin", "echo", "${text}"))`); + return response.replace(/\n$/, ""); + }); } diff --git a/test/file/test_plugin/messages.js b/test/file/test_plugin/messages.js index 59baf5af..6ed33346 100644 --- a/test/file/test_plugin/messages.js +++ b/test/file/test_plugin/messages.js @@ -54,3 +54,22 @@ export class HostEchoReceived { toJSON() { return this.text; } static fromJSON(json) { return new this(json); } } + +// Sent from ctl to an instance, which answers with the text echoed back by +// the remote interface of the plugin's Lua module. +export class InstanceEcho { + static type = "request"; + static src = "control"; + static dst = "instance"; + static plugin = "test_plugin"; + static permission = null; + static Response = JsonString; + + constructor(text) { + this.text = text; + } + + static jsonSchema = { type: "string" }; + toJSON() { return this.text; } + static fromJSON(json) { return new this(json); } +} diff --git a/test/file/test_plugin/module/module.json b/test/file/test_plugin/module/module.json new file mode 100644 index 00000000..76033218 --- /dev/null +++ b/test/file/test_plugin/module/module.json @@ -0,0 +1,4 @@ +{ + "name": "test_plugin", + "require": ["test_plugin.lua"] +} diff --git a/test/file/test_plugin/module/test_plugin.lua b/test/file/test_plugin/module/test_plugin.lua new file mode 100644 index 00000000..df310fb2 --- /dev/null +++ b/test/file/test_plugin/module/test_plugin.lua @@ -0,0 +1,5 @@ +remote.add_interface("test_plugin", { + echo = function(text) + return text + end, +}) diff --git a/test/host/create_scenario.js b/test/host/create_scenario.js index de032da7..c3e176dc 100644 --- a/test/host/create_scenario.js +++ b/test/host/create_scenario.js @@ -154,7 +154,7 @@ describe("host/create_scenario", function() { await fs.cp(scenario, freeplay, { recursive: true }); pluginInfos = await lib.loadPluginInfos(new Map([ ["research_sync", path.resolve("plugins/research_sync")], - ["test_plugin", path.resolve("test/file/test_plugin")], + ["global_chat", path.resolve("plugins/global_chat")], ])); lib.addPluginConfigFields(pluginInfos); hostConfig = new lib.HostConfig("host", { "host.factorio_directory": factorioDir }); diff --git a/test/host/patch.js b/test/host/patch.js index 1bebefca..838f46ef 100644 --- a/test/host/patch.js +++ b/test/host/patch.js @@ -241,7 +241,7 @@ describe("host/patch", function() { }); it("should skip plugins without a module", async function() { const pluginInfos = await lib.loadPluginInfos( - new Map([["test_plugin", path.resolve("test/file/test_plugin")]]) + new Map([["global_chat", path.resolve("plugins/global_chat")]]) ); const modules = await patch.loadModules(pluginInfos); assert.deepEqual([...modules.keys()], ["clusterio"]); diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index e2c74fec..4fe8a7a9 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -17,7 +17,7 @@ import { } from "./index.js"; import { externalTest, slowTest } from "../common.js"; import { getTestRconPort } from "./rcon_port.js"; -import { ControllerEcho, HostEchoReceived } from "../file/test_plugin/messages.js"; +import { ControllerEcho, HostEchoReceived, InstanceEcho } from "../file/test_plugin/messages.js"; /** @returns {Promise>} */ @@ -1002,6 +1002,62 @@ describe("Integration of Clusterio", function() { }); }); + describe("test_plugin on a running instance", function() { + requiresFactorio(this); + before(async function() { + this.timeout(20000); + if ((await getInstances()).get(44).status !== "running") { + await execCtl("instance start test"); + } + }); + + async function findLog(filter, message) { + // Host and instance logs are forwarded to the controller asynchronously. + for (let attempt = 0; attempt < 100; attempt++) { + const { log } = await getControl().send( + new lib.LogQueryRequest(false, filter.controller ?? false, filter.hostIds ?? [], + filter.instanceIds ?? [], undefined, 100, "desc") + ); + const entry = log.find(info => info.message === message); + if (entry) { + return entry; + } + await wait(20); + } + throw new Error(`log entry "${message}" not found`); + } + + it("should route a plugin request from control to an instance", async function() { + const response = await getControl().sendTo({ instanceId: 44 }, new InstanceEcho("to-instance")); + assert.equal(response, "to-instance"); + }); + + it("should patch the plugin's Lua module into the save", async function() { + const result = await sendRcon(44, "/sc rcon.print(remote.interfaces.test_plugin.echo)"); + assert.equal(result, "true\n"); + }); + + it("should tag plugin log entries with the plugin and where they came from", async function() { + await getControl().sendTo("controller", new ControllerEcho("log-tagging")); + await getControl().sendTo({ instanceId: 44 }, new InstanceEcho("log-tagging")); + + const controllerEntry = await findLog({ controller: true }, "test_plugin controller echo log-tagging"); + assert.equal(controllerEntry.plugin, "test_plugin"); + assert.equal(controllerEntry.host_id, undefined); + assert.equal(controllerEntry.instance_id, undefined); + + const hostEntry = await findLog({ hostIds: [4] }, "test_plugin host echo log-tagging"); + assert.equal(hostEntry.plugin, "test_plugin"); + assert.equal(hostEntry.host_id, 4); + assert.equal(hostEntry.instance_id, undefined); + + const instanceEntry = await findLog({ instanceIds: [44] }, "test_plugin instance echo log-tagging"); + assert.equal(instanceEntry.plugin, "test_plugin"); + assert.equal(instanceEntry.host_id, 4); + assert.equal(instanceEntry.instance_id, 44); + }); + }); + describe("instance config set-prop", function() { requiresFactorio(this); it("applies factorio settings while running", async function() {