mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Fetch the cluster research queue again after a new controller session
Test the research_sync instance plugin. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c70be25dc9
commit
ed1e4ee3cb
3 files changed
+145
-7
No files matched your search
@@ -54,6 +54,13 @@ export default async function(context: InstancePluginContext) {
|
||||
await applyQueue({ queue: result });
|
||||
}
|
||||
|
||||
async function syncQueue(seed: boolean) {
|
||||
let state = JSON.parse(await sendOrderedRcon("/sc research_sync.queue_state()"));
|
||||
if (state.follow) {
|
||||
await followQueue(seed && state.first ? asArray(state.queue) : null, null);
|
||||
}
|
||||
}
|
||||
|
||||
async function researchContribution(tech: IpcContribution) {
|
||||
instance.sendTo("controller", new ContributionEvent(tech.name, tech.level, tech.contribution));
|
||||
}
|
||||
@@ -147,9 +154,19 @@ export default async function(context: InstancePluginContext) {
|
||||
}
|
||||
|
||||
// A first start counts as turning following on, which seeds an empty cluster queue
|
||||
let state = JSON.parse(await sendOrderedRcon("/sc research_sync.queue_state()"));
|
||||
if (state.follow) {
|
||||
await followQueue(state.first ? asArray(state.queue) : null, null);
|
||||
await syncQueue(true);
|
||||
});
|
||||
|
||||
// QueueEvents sent while the controller session was down are lost, so take the cluster queue again.
|
||||
// An edit made on this instance during the outage was never sent and is replaced: the cluster queue wins.
|
||||
instance.hooks.controllerConnectionEvent.attach(plugin.name, async (event) => {
|
||||
if (event !== "connect" || !syncStarted || !["starting", "running"].includes(instance.status)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await syncQueue(false);
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as mock from "../../../test/mock.js";
|
||||
import loadInstancePlugin from "../dist/node/instance.js";
|
||||
import { plugin as info } from "../dist/node/index.js";
|
||||
import {
|
||||
QueueEvent, QueueSyncRequest, QueueUpdateEvent, SyncTechnologiesRequest,
|
||||
} from "../dist/node/messages.js";
|
||||
|
||||
describe("research_sync instance", function() {
|
||||
let instance, sent;
|
||||
const clusterQueue = ["a", "b"];
|
||||
|
||||
before(function() {
|
||||
try {
|
||||
lib.registerPluginMessages([info]);
|
||||
} catch (err) {
|
||||
// Already registered by the full test suite
|
||||
}
|
||||
});
|
||||
beforeEach(async function() {
|
||||
({ instance } = await mock.loadInstancePlugin(loadInstancePlugin, info));
|
||||
sent = [];
|
||||
instance.sendTo = async (dst, message) => {
|
||||
sent.push(message);
|
||||
if (message instanceof SyncTechnologiesRequest) {
|
||||
return [];
|
||||
}
|
||||
if (message instanceof QueueSyncRequest) {
|
||||
return clusterQueue;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
instance.server.rconCommandResults.set("/sc research_sync.dump_technologies()", "[]");
|
||||
});
|
||||
|
||||
function queueState(state) {
|
||||
instance.server.rconCommandResults.set("/sc research_sync.queue_state()", JSON.stringify(state));
|
||||
}
|
||||
const applyCommand = data => `/sc research_sync.apply_queue("${lib.escapeString(JSON.stringify(data))}")`;
|
||||
const lastCommand = () => instance.server.rconCommands[instance.server.rconCommands.length - 1];
|
||||
const queueSyncs = () => sent.filter(m => m instanceof QueueSyncRequest);
|
||||
|
||||
describe("start", function() {
|
||||
it("should seed the cluster queue on a first start", async function() {
|
||||
queueState({ follow: true, first: true, queue: {} });
|
||||
await instance.hooks.start.invoke();
|
||||
assert.equal(queueSyncs().length, 1);
|
||||
assert.deepEqual(queueSyncs()[0].queue, []);
|
||||
assert.equal(lastCommand(), applyCommand({ queue: clusterQueue }));
|
||||
});
|
||||
it("should only fetch the cluster queue after the first start", async function() {
|
||||
queueState({ follow: true, first: false });
|
||||
await instance.hooks.start.invoke();
|
||||
assert.equal(queueSyncs()[0].queue, null);
|
||||
});
|
||||
it("should not sync the queue when not following", async function() {
|
||||
queueState({ follow: false });
|
||||
await instance.hooks.start.invoke();
|
||||
assert.equal(queueSyncs().length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("QueueEvent", function() {
|
||||
beforeEach(async function() {
|
||||
queueState({ follow: false });
|
||||
await instance.hooks.start.invoke();
|
||||
instance.server.reset();
|
||||
});
|
||||
const handler = () => mock.getHandler(instance, QueueEvent);
|
||||
|
||||
it("should apply the queue with the player", async function() {
|
||||
await handler()(new QueueEvent(["a"], "Bob", "rs-2"));
|
||||
assert.equal(lastCommand(), applyCommand({ queue: ["a"], instance: "rs-2", player: "Bob" }));
|
||||
});
|
||||
it("should leave out a null player", async function() {
|
||||
await handler()(new QueueEvent(["a"], null, "rs-2"));
|
||||
assert.equal(lastCommand(), applyCommand({ queue: ["a"], instance: "rs-2" }));
|
||||
});
|
||||
it("should ignore the event while stopped", async function() {
|
||||
instance.status = "stopped";
|
||||
await handler()(new QueueEvent(["a"], null, "rs-2"));
|
||||
assert.equal(instance.server.rconCommands.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("controller connection", function() {
|
||||
beforeEach(async function() {
|
||||
queueState({ follow: true, first: false });
|
||||
await instance.hooks.start.invoke();
|
||||
sent.length = 0;
|
||||
instance.server.reset();
|
||||
queueState({ follow: true, first: false });
|
||||
});
|
||||
|
||||
it("should fetch the cluster queue again on a new session", async function() {
|
||||
await instance.hooks.controllerConnectionEvent.invoke("connect");
|
||||
assert.equal(queueSyncs().length, 1);
|
||||
assert.equal(queueSyncs()[0].queue, null);
|
||||
assert.equal(lastCommand(), applyCommand({ queue: clusterQueue }));
|
||||
});
|
||||
it("should do nothing on resume", async function() {
|
||||
await instance.hooks.controllerConnectionEvent.invoke("resume");
|
||||
assert.equal(sent.length, 0);
|
||||
});
|
||||
it("should do nothing while stopped", async function() {
|
||||
instance.status = "stopped";
|
||||
await instance.hooks.controllerConnectionEvent.invoke("connect");
|
||||
assert.equal(sent.length, 0);
|
||||
});
|
||||
it("should not throw when the fetch fails", async function() {
|
||||
instance.sendTo = async () => { throw new Error("gone"); };
|
||||
await instance.hooks.controllerConnectionEvent.invoke("connect");
|
||||
});
|
||||
});
|
||||
|
||||
it("should forward queue changes from the game", async function() {
|
||||
instance.server.emit("ipc-research_sync:queue", { queue: {}, player: "Bob" });
|
||||
assert.equal(sent.length, 1);
|
||||
assert.ok(sent[0] instanceof QueueUpdateEvent);
|
||||
assert.deepEqual(sent[0].queue, []);
|
||||
assert.equal(sent[0].player, "Bob");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
// These are libraries that does not have any units test, they are
|
||||
// required here in order to track coverage for them.
|
||||
|
||||
import "../dist/node/instance.js";
|
||||
Reference in new issue
Block a user