From 9cc5c64852d891165a8bfaa59a82140ca7ce699c Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:58:48 +0000 Subject: [PATCH] Cover the player-sync ctl commands in tests Codecov flagged ctl.ts at 21% patch coverage. Run each command through a fake control link with canned responses and check what it prints and sends. Co-Authored-By: Claude Fable 5.1 --- plugins/player_sync/test/plugin.js | 135 +++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/plugins/player_sync/test/plugin.js b/plugins/player_sync/test/plugin.js index bfa82933..56968025 100644 --- a/plugins/player_sync/test/plugin.js +++ b/plugins/player_sync/test/plugin.js @@ -1,8 +1,17 @@ import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import * as lib from "@clusterio/lib"; +import { CtlHooks } from "@clusterio/ctl"; + +import * as mock from "../../../test/mock.js"; import { testRoundTripJsonSerialisable, testMatrix } from "../../../test/common.js"; import * as msg from "../dist/node/messages.js"; import { summarizeInventory, summarizePlayerInventories } from "../dist/node/player_data.js"; +import ctlEntrypoint from "../dist/node/ctl.js"; +import { plugin as info } from "../dist/node/index.js"; function playerData(name, generation, extra = {}) { @@ -56,6 +65,10 @@ describe("player_sync plugin", function() { }); it("should handle empty inventories", function() { assert.deepEqual(summarizeInventory({}), []); + assert.deepEqual( + summarizeInventory({ i: [{ n: "wood" }] }), + [{ name: "wood", quality: undefined, count: 0 }], + ); }); }); @@ -77,6 +90,128 @@ describe("player_sync plugin", function() { }); it("should handle missing inventories", function() { assert.deepEqual(summarizePlayerInventories(playerData("player", 1)), []); + assert.deepEqual(summarizePlayerInventories(playerData("player", 1, { character: {} })), []); + }); + }); + + describe("ctl commands", function() { + let commands; + let sent; + let printed; + let responses; + const ctl = { + async send(message) { + sent.push(message); + const handler = responses.get(message.constructor); + if (!handler) { + throw new Error(`Unexpected ${message.constructor.name}`); + } + return handler(message); + }, + }; + const storedData = playerData("alice", 2, { + character: { inventories: { main: { i: [{ n: "wood", c: 5, q: "rare" }, { e: "0eNq" }] } } }, + }); + let tempDir; + + before(async function() { + const root = new lib.CommandTree({ name: "clusterioctl", description: "root" }); + const hooks = new CtlHooks(new mock.MockLogger()); + await ctlEntrypoint({ hooks, logger: new mock.MockLogger(), plugin: info }); + await hooks.addCommands.invoke(root); + commands = root.subCommands.get("player-sync").subCommands; + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "player_sync-ctl-")); + }); + after(async function() { + await fs.rm(tempDir, { recursive: true, force: true }); + }); + beforeEach(function() { + sent = []; + printed = []; + responses = new Map([ + [lib.InstanceDetailsGetRequest, request => ({ id: request.instanceId, name: "Test" })], + [msg.ListPlayersRequest, () => [ + new msg.PlayerEntry("bob", undefined, undefined, 1), + new msg.PlayerEntry("alice", 2, 100), + ]], + [msg.GetPlayerDataRequest, request => new msg.GetPlayerDataResponse( + request.playerName === "alice" ? storedData : null, request.playerName === "alice" ? 1 : undefined + )], + [msg.SetPlayerDataRequest, () => new msg.SetPlayerDataResponse(3)], + [msg.DeletePlayerDataRequest, () => undefined], + [msg.ForceReleaseRequest, () => undefined], + ]); + }); + + async function run(name, args = {}) { + /* eslint-disable no-console */ + const original = console.log; + console.log = (...values) => printed.push(values.join(" ")); + try { + await commands.get(name).run(args, ctl); + } finally { + console.log = original; + } + /* eslint-enable no-console */ + } + + it("list should print sorted entries", async function() { + await run("list"); + assert.deepEqual(printed, [ + "name | generation | size | acquired by", + `alice | 2 | ${lib.formatBytes(100)} | -`, + "bob | - | - | Test (1)", + ]); + }); + it("show should print a summary", async function() { + await run("show", { player: "alice" }); + assert.deepEqual(printed, [ + "acquired by: Test (1)", + "generation: 2", + `size: ${lib.formatBytes(JSON.stringify(storedData).length)}`, + "controller: character", + "force: player", + "main:", + " 5 wood (rare)", + ` 1 exported item ${lib.formatBytes(4)}`, + ]); + }); + it("show should handle players without data", async function() { + await run("show", { player: "carol" }); + assert.deepEqual(printed, ["acquired by: -", "No player data stored for carol"]); + }); + it("export should print or write JSON", async function() { + await run("export", { player: "alice" }); + assert.deepEqual(JSON.parse(printed.join("\n")), storedData); + const file = path.join(tempDir, "alice.json"); + await run("export", { player: "alice", file }); + assert.deepEqual(JSON.parse(await fs.readFile(file, "utf8")), storedData); + await assert.rejects( + run("export", { player: "carol" }), + new lib.CommandError("No player data stored for carol"), + ); + }); + it("import should send the file contents", async function() { + const file = path.join(tempDir, "import.json"); + await fs.writeFile(file, JSON.stringify({ name: "other", controller: "god" })); + await run("import", { player: "alice", file }); + assert.deepEqual(sent, [ + new msg.SetPlayerDataRequest("alice", { name: "alice", controller: "god", generation: 0 }), + ]); + assert.deepEqual(printed, ["Stored player data for alice as generation 3"]); + }); + it("import should reject non-object JSON", async function() { + const file = path.join(tempDir, "bad.json"); + await fs.writeFile(file, "[1]"); + await assert.rejects( + run("import", { player: "alice", file }), + new lib.CommandError("Player data must be a JSON object"), + ); + }); + it("delete and release should send requests", async function() { + await run("delete", { player: "alice" }); + await run("release", { player: "alice" }); + assert.deepEqual(sent, [new msg.DeletePlayerDataRequest("alice"), new msg.ForceReleaseRequest("alice")]); }); }); });