From 1072e5f1dee09848c2554cd10ed0d961b54bd47e Mon Sep 17 00:00:00 2001 From: bbassie Date: Thu, 10 Sep 2026 00:36:35 +0000 Subject: [PATCH] Cover the create-scenario command handler and module loading Adds tests for handleCreateScenarioCommand using a fake Factorio install with the test scenario as freeplay, for loadModules skipping plugins without a module and rejecting a name clash with a bundled module, and an integration test running clusteriohost create-scenario. Co-Authored-By: Claude Fable 5.1 --- test/host/create_scenario.js | 87 ++++++++++++++++++++++++++++++++++- test/host/patch.js | 29 ++++++++++++ test/integration/clusterio.js | 13 ++++++ 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/test/host/create_scenario.js b/test/host/create_scenario.js index 4609d732..659ae9d0 100644 --- a/test/host/create_scenario.js +++ b/test/host/create_scenario.js @@ -5,7 +5,9 @@ const JSZip = require("jszip"); const path = require("path"); const lib = require("@clusterio/lib"); -const { createScenario } = require("@clusterio/host/dist/node/src/create_scenario"); +const { + createScenario, createScenarioCommand, handleCreateScenarioCommand, +} = require("@clusterio/host/dist/node/src/create_scenario"); const { SaveModule, PatchInfo } = require("@clusterio/host/dist/node/src/patch"); @@ -90,4 +92,87 @@ describe("host/create_scenario", function() { new Error("Unable to patch save, unknown scenario (3acc3be3861144e55604f5ac2f2555071885ebc4)") ); }); + + describe("createScenarioCommand()", function() { + it("should define the output positional and options", function() { + const positionals = []; + const options = []; + const yargs = { + positional(name) { positionals.push(name); return yargs; }, + option(name) { options.push(name); return yargs; }, + }; + createScenarioCommand(yargs); + assert.deepEqual(positionals, ["output"]); + assert.deepEqual(options, ["scenario", "factorio-version", "plugins"]); + }); + }); + + describe("handleCreateScenarioCommand()", function() { + const factorioDir = path.join(tempDir, "factorio"); + const scenario = path.join(tempDir, "scenario"); + let hostConfig; + let pluginInfos; + before(async function() { + // Fake install with the test scenario as freeplay + await fs.cp(path.join("test", "file", "factorio", "0.1.2"), factorioDir, { recursive: true }); + const freeplay = path.join(factorioDir, "data", "base", "scenarios", "freeplay"); + 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")], + ])); + lib.addPluginConfigFields(pluginInfos); + hostConfig = new lib.HostConfig("host", { "host.factorio_directory": factorioDir }); + }); + + it("should patch freeplay with the enabled plugins by default", async function() { + const output = path.join(tempDir, "default"); + await handleCreateScenarioCommand({ output, factorioVersion: "latest" }, hostConfig, pluginInfos); + const files = await readDir(output); + assert.equal(files.get("scenario.lua"), scenarioFiles.get("scenario.lua")); + const info = JSON.parse(files.get("clusterio.json")); + assert.deepEqual(info.modules.map(m => m.name), ["clusterio", "research_sync"]); + }); + + it("should skip plugins disabled in the host config", async function() { + const output = path.join(tempDir, "disabled"); + hostConfig.set("research_sync.load_plugin", false); + try { + await handleCreateScenarioCommand({ output, factorioVersion: "latest" }, hostConfig, pluginInfos); + } finally { + hostConfig.set("research_sync.load_plugin", true); + } + const info = JSON.parse((await readDir(output)).get("clusterio.json")); + assert.deepEqual(info.modules.map(m => m.name), ["clusterio"]); + }); + + it("should use the given scenario and plugins", async function() { + const output = path.join(tempDir, "selected"); + await handleCreateScenarioCommand( + { output, scenario, plugins: ["research_sync"] }, hostConfig, pluginInfos + ); + const info = JSON.parse((await readDir(output)).get("clusterio.json")); + assert.deepEqual(info.modules.map(m => m.name), ["clusterio", "research_sync"]); + }); + + it("should throw on unknown plugin", async function() { + await assert.rejects( + handleCreateScenarioCommand( + { output: path.join(tempDir, "unknown_plugin"), scenario, plugins: ["nope"] }, + hostConfig, + pluginInfos, + ), + new Error("Plugin nope is not in the plugin list") + ); + }); + + it("should throw on invalid Factorio version", async function() { + await assert.rejects( + handleCreateScenarioCommand( + { output: path.join(tempDir, "bad_version"), factorioVersion: "1..2" }, hostConfig, pluginInfos + ), + new Error("Invalid Factorio version 1..2") + ); + }); + }); }); diff --git a/test/host/patch.js b/test/host/patch.js index 53c4aa43..41a07bd3 100644 --- a/test/host/patch.js +++ b/test/host/patch.js @@ -240,6 +240,35 @@ describe("host/patch", function() { assert.deepEqual([...modules.keys()], ["research_sync", "clusterio"]); assert(modules.get("research_sync").files.has("modules/research_sync/sync.lua")); }); + it("should skip plugins without a module", async function() { + const pluginInfos = await lib.loadPluginInfos( + new Map([["test_plugin", path.resolve("test/file/test_plugin")]]) + ); + const modules = await patch.loadModules(pluginInfos); + assert.deepEqual([...modules.keys()], ["clusterio"]); + }); + it("should throw on a plugin module named like a bundled module", async function() { + const pluginDir = path.join("temp", "test", "clusterio_plugin"); + await fs.mkdir(path.join(pluginDir, "module"), { recursive: true }); + await fs.writeFile(path.join(pluginDir, "package.json"), "{}"); + await fs.writeFile(path.join(pluginDir, "module", "module.json"), "{}"); + await assert.rejects( + patch.loadModules([{ name: "clusterio", version: "1.0.0", requirePath: path.resolve(pluginDir) }]), + new Error("Module with name clusterio already exists in a plugin") + ); + }); + }); + + describe("class SaveModule", function() { + describe("static fromPlugin()", function() { + it("should load the module of the plugin", async function() { + const [info] = await lib.loadPluginInfos( + new Map([["research_sync", path.resolve("plugins/research_sync")]]) + ); + const pluginModule = await patch.SaveModule.fromPlugin({ info }); + assert.equal(pluginModule.info.name, "research_sync"); + }); + }); }); describe("patch()", function() { diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 26d8dfa9..af7a1462 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -257,6 +257,19 @@ describe("Integration of Clusterio", function() { }); describe("clusteriohost", function() { + describe("create-scenario", function() { + it("should create a patched scenario", async function() { + slowTest(this); + requiresFactorio(this); + const output = path.join("temp", "test", "patched-scenario"); + await fs.rm(output, { recursive: true, force: true }); + await execHost("create-scenario patched-scenario --plugins research_sync"); + const control = await fs.readFile(path.join(output, "control.lua"), "utf8"); + assert(control.includes('require("modules/research_sync/sync")')); + await fs.access(path.join(output, "scenario.lua")); + }); + }); + describe("hostUpdateEventHandler()", function() { it("should trigger when a new host is added", async function() { slowTest(this);