diff --git a/test/ctl/instance.js b/test/ctl/instance.js deleted file mode 100644 index b18ad353..00000000 --- a/test/ctl/instance.js +++ /dev/null @@ -1,120 +0,0 @@ -"use strict"; - -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); - -// Import the command registration helper from the compiled Ctl package. This -// gives us access to the full command tree without spinning up a controller or -// host process. -const { registerCommands } = require("@clusterio/ctl/dist/node/src/commands"); - -// yargs instance used only for command registration (it will not parse any -// command-line input in these tests). -function createYargsStub() { - const stub = {}; - for (const method of ["command", "option", "options", "positional"]) { - stub[method] = () => stub; - } - return stub; -} -const yargs = createYargsStub(); - -/** - * Fetches the requested sub-command from the command tree under the - * "instance" command. - * - * @param {string} name – The name of the sub-command (e.g. "start-all"). - * @returns {Promise} The command object. - */ -async function getInstanceSubCommand(name) { - const rootCommands = await registerCommands(new Map(), yargs); - const instanceTree = rootCommands.subCommands.get("instance"); - const command = instanceTree.subCommands.get(name); - - if (!command) { - throw new Error(`${name} command was not found in command tree`); - } - return command; -} - -/** - * Constructs a mock control link implementing the minimal API surface - * required by the *-all commands (send and sendTo). - * - * @param {lib.InstanceDetails[]} instances – List returned by the mocked - * InstanceDetailsListRequest. - * @param {string} action – The property name ("started" | "stopped") that - * should collect the instance ids passed to sendTo. - * @returns {object} The mock control link. - */ -function createMockControl(instances, action) { - return { - // Called by the command to fetch the instance list. - async send(_request) { - return instances; - }, - - // Populated by sendTo calls performed by the command. - [action]: [], - async sendTo(target, _request) { - this[action].push(target.instanceId); - }, - }; -} - -describe("ctl instance start-all command", function () { - let startAllCommand; - - before(async function () { - startAllCommand = await getInstanceSubCommand("start-all"); - }); - - it("starts only stopped instances that are not excluded by default", async function () { - const instances = [ - new lib.InstanceDetails("one", 1, undefined, undefined, "stopped", "1.1.0", 0, false), - new lib.InstanceDetails("two", 2, undefined, undefined, "running", "1.1.0", 0, false), - new lib.InstanceDetails("three", 3, undefined, undefined, "stopped", "1.1.0", 0, true), - ]; - const control = createMockControl(instances, "started"); - - await startAllCommand.run({ force: false }, control); - - assert.deepEqual(control.started, [1], "Should only start non-excluded stopped instances"); - }); - - it("starts excluded instances when --force flag is provided", async function () { - const instances = [ - new lib.InstanceDetails("one", 1, undefined, undefined, "stopped", "1.1.0", 0, false), - new lib.InstanceDetails("three", 3, undefined, undefined, "stopped", "1.1.0", 0, true), - ]; - const control = createMockControl(instances, "started"); - - await startAllCommand.run({ force: true }, control); - - // Order is not guaranteed – sort before comparison. - assert.deepEqual(control.started.sort(), [1, 3], "--force should include excluded instances"); - }); -}); - -describe("ctl instance stop-all command", function () { - let stopAllCommand; - - before(async function () { - stopAllCommand = await getInstanceSubCommand("stop-all"); - }); - - it("stops only running or starting instances", async function () { - const instances = [ - new lib.InstanceDetails("one", 1, undefined, undefined, "stopped", "1.1.0", 0, false), - // Does not care about excludeFromStartAll - new lib.InstanceDetails("two", 2, undefined, undefined, "running", "1.1.0", 0, true), - new lib.InstanceDetails("three", 3, undefined, undefined, "starting", "1.1.0", 0, false), - ]; - const control = createMockControl(instances, "stopped"); - - await stopAllCommand.run({}, control); - - // Order is not guaranteed – sort before comparison. - assert.deepEqual(control.stopped.sort(), [2, 3], "Should only stop running or starting instances"); - }); -}); diff --git a/test/integration/instance.js b/test/integration/instance.js index 77a654b8..acba6761 100644 --- a/test/integration/instance.js +++ b/test/integration/instance.js @@ -34,6 +34,8 @@ describe("Clusterio Instance", function() { await execCtlProcess(`instance config set-prop ${instName} factorio.settings visibility "${visibility}"`); await execCtl(`instance config set-prop ${instName} factorio.settings require_user_verification false`); await execCtl(`instance assign ${instName} 4`); + // Exclude main test instance from start-all to prevent interference + await execCtl(`instance config set ${instName} instance.exclude_from_start_all true`); for (const plugin of [ "global_chat", "research_sync", "statistics_exporter", "subspace_storage", "player_auth", "inventory_sync", @@ -47,6 +49,8 @@ describe("Clusterio Instance", function() { await execCtlProcess(`instance config set-prop ${instAltName} factorio.settings visibility "${visibility}"`); await execCtl(`instance config set-prop ${instAltName} factorio.settings require_user_verification false`); await execCtl(`instance assign ${instAltName} 4`); + // Exclude alt test instance from start-all to prevent interference + await execCtl(`instance config set ${instAltName} instance.exclude_from_start_all true`); await execCtl(`instance start ${instAltName}`); await sendRcon(instAltId, "/sc disable achievements"); }); @@ -162,3 +166,148 @@ describe("Clusterio Instance", function() { }); } }); +describe("start-all and stop-all commands", function() { + // Test instance IDs for start-all/stop-all tests + const testInstId1 = 50; + const testInstId2 = 51; + const testInstId3 = 52; + const testInstName1 = "StartAllTest1"; + const testInstName2 = "StartAllTest2"; + const testInstName3 = "StartAllTest3"; + + before(async function() { + this.timeout(30000); + + // Create test instances for start-all/stop-all testing + await execCtl(`instance create ${testInstName1} --id ${testInstId1}`); + await execCtl(`instance create ${testInstName2} --id ${testInstId2}`); + await execCtl(`instance create ${testInstName3} --id ${testInstId3}`); + + // Configure instances + await execCtl(`instance assign ${testInstName1} 4`); + await execCtl(`instance assign ${testInstName2} 4`); + await execCtl(`instance assign ${testInstName3} 4`); + + // Exclude testInstName3 from start-all by default + await execCtl(`instance config set ${testInstName3} instance.exclude_from_start_all true`); + + // Disable plugins to speed up testing + for (const instanceName of [testInstName1, testInstName2, testInstName3]) { + for (const plugin of [ + "global_chat", "research_sync", "statistics_exporter", + "subspace_storage", "player_auth", "inventory_sync", + ]) { + await execCtl(`instance config set ${instanceName} ${plugin}.load_plugin false`); + } + } + }); + + after(async function() { + this.timeout(30000); + // Clean up test instances - ensure they are stopped first + await execCtl(`instance stop ${testInstName1}`).catch(() => {}); + await execCtl(`instance stop ${testInstName2}`).catch(() => {}); + await execCtl(`instance stop ${testInstName3}`).catch(() => {}); + + await execCtl(`instance delete ${testInstName1}`).catch(() => {}); + await execCtl(`instance delete ${testInstName2}`).catch(() => {}); + await execCtl(`instance delete ${testInstName3}`).catch(() => {}); + }); + + async function getInstanceStatus(id) { + const instances = await getControl().send(new lib.InstanceDetailsListRequest()); + const instanceMap = new Map(instances.map(instance => [instance.id, instance])); + return instanceMap.get(id)?.status; + } + + describe("start-all command", function() { + beforeEach(async function() { + this.timeout(15000); + // Ensure all test instances are stopped before each test + await execCtl(`instance stop ${testInstName1}`).catch(() => {}); + await execCtl(`instance stop ${testInstName2}`).catch(() => {}); + await execCtl(`instance stop ${testInstName3}`).catch(() => {}); + }); + + afterEach(async function() { + this.timeout(15000); + // Clean up: stop any instances that were started during tests + await execCtl(`instance stop ${testInstName1}`).catch(() => {}); + await execCtl(`instance stop ${testInstName2}`).catch(() => {}); + await execCtl(`instance stop ${testInstName3}`).catch(() => {}); + }); + + it("starts only stopped instances that are not excluded by default", async function() { + this.timeout(20000); + // Start one instance to verify it's not started again + await execCtl(`instance start ${testInstName2}`); + + // Verify initial state + assert.equal(await getInstanceStatus(testInstId1), "stopped"); + assert.equal(await getInstanceStatus(testInstId2), "running"); + assert.equal(await getInstanceStatus(testInstId3), "stopped"); + + // Run start-all without force + await execCtl("instance start-all"); + + // Verify results - only testInstName1 should be started + // testInstName2 was already running, testInstName3 is excluded + assert.equal(await getInstanceStatus(testInstId1), "running"); + assert.equal(await getInstanceStatus(testInstId2), "running"); + assert.equal(await getInstanceStatus(testInstId3), "stopped"); + }); + + it("starts excluded instances when --force flag is provided", async function() { + this.timeout(20000); + // Verify initial state - all stopped + assert.equal(await getInstanceStatus(testInstId1), "stopped"); + assert.equal(await getInstanceStatus(testInstId2), "stopped"); + assert.equal(await getInstanceStatus(testInstId3), "stopped"); + + // Run start-all with force + await execCtl("instance start-all --force"); + + // Verify results - all instances should be started including excluded one + assert.equal(await getInstanceStatus(testInstId1), "running"); + assert.equal(await getInstanceStatus(testInstId2), "running"); + assert.equal(await getInstanceStatus(testInstId3), "running"); + }); + }); + + describe("stop-all command", function() { + beforeEach(async function() { + this.timeout(15000); + // Ensure a known state before each test + await execCtl(`instance stop ${testInstName1}`).catch(() => {}); + await execCtl(`instance stop ${testInstName2}`).catch(() => {}); + await execCtl(`instance stop ${testInstName3}`).catch(() => {}); + }); + + afterEach(async function() { + this.timeout(15000); + // Clean up: stop any instances that were started during tests + await execCtl(`instance stop ${testInstName1}`).catch(() => {}); + await execCtl(`instance stop ${testInstName2}`).catch(() => {}); + await execCtl(`instance stop ${testInstName3}`).catch(() => {}); + }); + + it("stops only running or starting instances", async function() { + this.timeout(20000); + // Start some instances to test stop-all + await execCtl(`instance start ${testInstName1}`); + await execCtl(`instance start ${testInstName3}`); + + // Verify initial state + assert.equal(await getInstanceStatus(testInstId1), "running"); + assert.equal(await getInstanceStatus(testInstId2), "stopped"); + assert.equal(await getInstanceStatus(testInstId3), "running"); + + await execCtl("instance stop-all"); + + // Verify results - running instances should be stopped, stopped ones unchanged + assert.equal(await getInstanceStatus(testInstId1), "stopped"); + assert.equal(await getInstanceStatus(testInstId2), "stopped"); + assert.equal(await getInstanceStatus(testInstId3), "stopped"); + }); + }); +});