Manual changes following smoke test

This commit is contained in:
Cooldude2606
2026-09-01 01:04:09 +01:00
parent 574a0a4b52
commit 59df8bdff3
18 changed files with 191 additions and 98 deletions
+2 -1
View File
@@ -8,7 +8,8 @@
"main": "dist/node/index.js",
"scripts": {
"prepare": "tsc --build && webpack-cli --env production",
"test": "tap --disable-coverage --allow-empty-coverage test/*.test.js"
"test": "tap --disable-coverage --allow-empty-coverage test/*.test.js",
"coverage": "tap --coverage-report=text test/*.test.js"
},
"engines": {
"node": ">=18"
+31
View File
@@ -20,6 +20,12 @@ lib.Link.register(messages.AssignmentUpdateRequest);
const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} };
// Silence the singleton logger to avoid polluting the test output
lib.logger.silent = true;
t.after(() => {
lib.logger.silent = false;
});
/** Build a plugin around a real controller, which is side effect free while not started. */
async function startPlugin(t2, { roles = [] } = {}) {
const controllerConfig = new lib.ControllerConfig("controller", {
@@ -173,6 +179,18 @@ t.test("class ControllerPlugin", t2 => {
t3.strictSame(applied.length, 1, "other player events do not");
});
t2.test(".handleRoleListRequest() returns the current roles", async t3 => {
const { plugin } = await startPlugin(t3, {
roles: [role(1, "Player"), role(5, "Moderator")],
});
plugin.roleMeta.set(new messages.RoleMetaRecord(5, 2, 0, "Mod"));
const response = await plugin.handleRoleListRequest(new messages.RoleListRequest());
t3.strictSame(response.length, 2, "the current roles are returned");
t3.ok(response.some(record => record.name === "Moderator"));
t3.ok(response.some(record => record.name === "Player"));
});
t2.test(".handleRoleMetaUpdateRequest() validates the role", async t3 => {
const { plugin } = await startPlugin(t3, { roles: [role(1, "Player"), role(5, "Moderator")] });
@@ -187,6 +205,19 @@ t.test("class ControllerPlugin", t2 => {
t3.strictSame(plugin.roleMeta.get(5).shortHand, "Mod", "the properties are stored");
});
t2.test(".handleAssignmentListRequest() returns the current assignments", async t3 => {
const { plugin, controller } = await startPlugin(t3, {
roles: [role(1, "Player"), role(5, "Moderator")],
});
controller.users.createUser("alice").set("roleIds", new Set([5]));
controller.users.createUser("bob");
const response = await plugin.handleAssignmentListRequest(new messages.AssignmentListRequest());
t3.strictSame(response.length, 1, "the current assignments are returned");
t3.strictSame(response[0].name, "alice");
t3.strictSame(response[0].roleIds, new Set([5]), "the default role is not listed");
});
t2.test(".handleAssignmentSubscription() replays only newer records", async t3 => {
const { plugin, controller } = await startPlugin(t3, { roles: [role(1, "Player"), role(5, "Moderator")] });
controller.users.createUser("alice").set("roleIds", new Set([5]));
+43 -37
View File
@@ -7,18 +7,18 @@ const fullMeta = new messages.RoleMetaRecord(
7, 3, 1, "Mod", "[Mod]", new messages.RoleColor(1, 2, 3), 3600000, true, 12345, false,
);
t.test("class RoleColor", subtest => {
testRoundTripJsonSerialisable(messages.RoleColor, testMatrix(
t.test("class RoleColor", t2 => {
testRoundTripJsonSerialisable(t2, messages.RoleColor, testMatrix(
[0, 255], // r
[0, 128], // g
[0, 1], // b
));
subtest.pass("round trips");
subtest.end();
t2.end();
});
t.test("class RoleMetaRecord", subtest => {
testRoundTripJsonSerialisable(messages.RoleMetaRecord, testMatrix(
t.test("class RoleMetaRecord", t2 => {
testRoundTripJsonSerialisable(t2, messages.RoleMetaRecord, testMatrix(
[7], // id
[3], // order
[0, 1], // priority
@@ -30,12 +30,12 @@ t.test("class RoleMetaRecord", subtest => {
[0, 12345], // updatedAtMs
[false, true], // isDeleted
));
subtest.pass("round trips");
subtest.end();
t2.end();
});
t.test("class RoleRecord", subtest => {
testRoundTripJsonSerialisable(messages.RoleRecord, testMatrix(
t.test("class RoleRecord", t2 => {
testRoundTripJsonSerialisable(t2, messages.RoleRecord, testMatrix(
[7], // id
["Moderator"], // name
[[], ["a.b", "c.d"]], // permissions
@@ -44,67 +44,73 @@ t.test("class RoleRecord", subtest => {
[0, 12345], // updatedAtMs
[false, true], // isDeleted
));
subtest.pass("round trips");
subtest.end();
t2.end();
});
t.test("class AssignmentRecord", subtest => {
testRoundTripJsonSerialisable(messages.AssignmentRecord, testMatrix(
t.test("class AssignmentRecord", t2 => {
testRoundTripJsonSerialisable(t2, messages.AssignmentRecord, testMatrix(
["alice"], // name
[new Set(), new Set([5, 6])], // roleIds
[0, 12345], // updatedAtMs
[false, true], // isDeleted
));
subtest.pass("round trips");
subtest.end();
t2.test("get id()", t3 => {
const record = new messages.AssignmentRecord("alice", new Set([5]), 12345);
t3.equal(record.id, "alice");
t3.end();
});
t2.end();
});
const sampleRecord = new messages.RoleRecord(7, "Moderator", ["a.b"], fullMeta, true, 12345);
const sampleAssignment = new messages.AssignmentRecord("alice", new Set([5]), 12345);
t.test("class RoleUpdatedEvent", subtest => {
testRoundTripJsonSerialisable(messages.RoleUpdatedEvent, testMatrix(
t.test("class RoleUpdatedEvent", t2 => {
testRoundTripJsonSerialisable(t2, messages.RoleUpdatedEvent, testMatrix(
[[], [sampleRecord]], // updates
));
subtest.pass("round trips");
subtest.end();
t2.end();
});
t.test("class AssignmentUpdatedEvent", subtest => {
testRoundTripJsonSerialisable(messages.AssignmentUpdatedEvent, testMatrix(
t.test("class AssignmentUpdatedEvent", t2 => {
testRoundTripJsonSerialisable(t2, messages.AssignmentUpdatedEvent, testMatrix(
[[], [sampleAssignment]], // updates
));
subtest.pass("round trips");
subtest.end();
t2.end();
});
t.test("class RoleMetaUpdateRequest", subtest => {
testRoundTripJsonSerialisable(messages.RoleMetaUpdateRequest, testMatrix(
t.test("class RoleMetaUpdateRequest", t2 => {
testRoundTripJsonSerialisable(t2, messages.RoleMetaUpdateRequest, testMatrix(
[new messages.RoleMetaRecord(7, 3), fullMeta], // meta
));
subtest.pass("round trips");
subtest.end();
t2.end();
});
t.test("class AssignmentUpdateRequest", subtest => {
testRoundTripJsonSerialisable(messages.AssignmentUpdateRequest, testMatrix(
t.test("class AssignmentUpdateRequest", t2 => {
testRoundTripJsonSerialisable(t2, messages.AssignmentUpdateRequest, testMatrix(
["alice"], // name
[[], [5]], // assign
[[], [6]], // unassign
));
subtest.pass("round trips");
subtest.end();
t2.end();
});
t.test("encodeRolesForLua() sends each permission name once", subtest => {
t.test("encodeRolesForLua() sends each permission name once", t2 => {
const meta = id => new messages.RoleMetaRecord(id, id);
const encoded = messages.encodeRolesForLua([
new messages.RoleRecord(1, "A", ["p.one", "p.two"], meta(1)),
new messages.RoleRecord(2, "B", ["p.two", "p.three"], meta(2)),
]);
subtest.strictSame(encoded.permission_names, ["p.one", "p.two", "p.three"], "names are deduplicated");
subtest.strictSame(encoded.roles[0].permissions, [0, 1], "indexes are zero based");
subtest.strictSame(encoded.roles[1].permissions, [1, 2], "shared names reuse their index");
subtest.end();
t2.strictSame(encoded.permission_names, ["p.one", "p.two", "p.three"], "names are deduplicated");
t2.strictSame(encoded.roles[0].permissions, [0, 1], "indexes are zero based");
t2.strictSame(encoded.roles[1].permissions, [1, 2], "shared names reuse their index");
t2.end();
});
@@ -3,12 +3,12 @@ const path = require("node:path");
const t = require("tap");
const { reportLuaTests } = require("../../test/lua/runner");
const envFile = path.join(__dirname, "lua", "env.lua");
const envFile = path.join(__dirname, "module", "env.lua");
// Each file runs in its own lua state, and each test in a fresh environment
t.test("module/control.lua", t2 => {
t.test("control.lua", t2 => {
for (const file of ["lookup.lua", "players.lua", "assignment.lua", "sync.lua", "holders.lua"]) {
t2.test(`lua/${file}`, subtest => reportLuaTests(subtest, envFile, path.join(__dirname, "lua", file)));
t2.test(file, t3 => reportLuaTests(t3, envFile, path.join(__dirname, "module", file)));
}
t2.end();
});
@@ -1,5 +1,5 @@
--- Tests for the assignment methods of module/control.lua
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
local Suite = ... --- @type Suite The suite this file adds its tests to, see env.lua
local test, check, eq, empty = Suite.test, Suite.check, Suite.eq, Suite.empty
--- alice is a moderator and bob a regular, with changes sent to the controller
@@ -8,10 +8,10 @@ returned here becomes `...` in each test file.
]]
local shared_root = ... --- @type string
local source = assert(debug.getinfo(1, "S")).source
local plugin_root = assert(source:match("^@(.*)/test/lua/env%.lua$"))
local source = assert(debug.getinfo(1, "S")).source:gsub("\\", "/")
local plugin_root = assert(source:match("^@(.*)/test/module/env%.lua$"))
local Framework = assert(loadfile(shared_root .. "/framework.lua"))()
local Framework = assert(loadfile(shared_root .. "/framework.lua"))() --- @type Framework
--- Standard fixture: permission names sent once and referenced by zero based index
local permission_names = {
@@ -48,7 +48,7 @@ for _, record in ipairs(role_records()) do
end
return Framework.suite(function(env)
env.Roles = assert(loadfile(plugin_root .. "/module/control.lua"))()
env.Roles = assert(loadfile(plugin_root .. "/module/control.lua"))() --- @type ExpRoles
env.Roles.on_server_startup()
--- Get a fixture role by name, failing the test when it does not exist
@@ -1,5 +1,5 @@
--- Tests for listing and printing to the players who hold a role
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua
local test, check, eq = Suite.test, Suite.check, Suite.eq
--- alice and dave are moderators with dave offline, zed has never joined
@@ -1,5 +1,5 @@
--- Tests for the role lookup functions of module/control.lua
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua
local test, check, eq = Suite.test, Suite.check, Suite.eq
test("get_role() returns the role with the given clusterio id", function(env)
@@ -1,5 +1,5 @@
--- Tests for the player checks of module/control.lua
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua
local test, check, eq = Suite.test, Suite.check, Suite.eq
--- alice is a moderator, bob is a regular, and carol has only the default role
@@ -1,5 +1,5 @@
--- Tests for the sync entry points of module/control.lua
local Suite = ... --- The suite this file adds its tests to, see test/lua/framework.lua
local Suite = ... --- @type Suite The suite this file adds its tests to, see test/lua/framework.lua
local test, check, eq, empty = Suite.test, Suite.check, Suite.eq, Suite.empty
--- alice is a moderator and carol has only the default role, both connected
+37 -15
View File
@@ -6,35 +6,57 @@ const { seedRoles, flattenSeedPermissions } = require("../dist/node/seed");
// Importing this defines the exp_scenario permissions the seed grants
require("@expcluster/scenario/dist/node/permissions");
t.test("seedRoles grant only defined permissions", subtest => {
t.test("seedRoles[] grant only defined permissions", t2 => {
for (const role of seedRoles) {
for (const permission of flattenSeedPermissions(role)) {
subtest.ok(lib.permissions.has(permission), `${role.name} grants defined permission ${permission}`);
for (const permission of role.permissions) {
t2.ok(lib.permissions.has(permission), `${role.name} grants defined permission ${permission}`);
}
}
subtest.end();
t2.end();
});
t.test("flattenSeedPermissions() carries parents into their children", subtest => {
t.test("seedRoles[] are unique with one default and one admin", t2 => {
const names = seedRoles.map(role => role.name);
t2.strictSame(names.length, new Set(names).size, "names are unique");
t2.strictSame(seedRoles.filter(role => role.isDefault).length, 1, "one default role");
t2.strictSame(seedRoles.filter(role => role.isAdmin).length, 1, "one admin role");
t2.end();
});
t.test("flattenSeedPermissions() inherits permissions from parent roles", t2 => {
const byName = new Map(seedRoles.map(role => [role.name, role]));
for (const role of seedRoles) {
if (role.parent === undefined) {
continue;
}
const parent = byName.get(role.parent);
subtest.ok(parent, `${role.name} has parent ${role.parent}`);
t2.ok(parent, `${role.name} has parent ${role.parent}`);
const flattened = flattenSeedPermissions(role);
for (const permission of flattenSeedPermissions(parent)) {
subtest.ok(flattened.has(permission), `${role.name} inherits ${permission}`);
t2.ok(flattened.has(permission), `${role.name} inherits ${permission}`);
}
}
subtest.end();
t2.end();
});
t.test("seedRoles are unique with one default and one admin", subtest => {
const names = seedRoles.map(role => role.name);
subtest.strictSame(names.length, new Set(names).size, "names are unique");
subtest.strictSame(seedRoles.filter(role => role.isDefault).length, 1, "one default role");
subtest.strictSame(seedRoles.filter(role => role.isAdmin).length, 1, "one admin role");
subtest.end();
});
t.test("flattenSeedPermissions() does not inherit permissions when parent is undefined", t2 => {
for (const role of seedRoles) {
if (role.parent !== undefined) {
continue;
}
const flattened = flattenSeedPermissions(role);
for (const permission of role.permissions) {
t2.ok(flattened.has(permission), `${role.name} contains ${permission}`);
}
t2.equal(
flattened.size,
role.permissions.length,
`${role.name} has no inherited permissions`,
);
}
t2.end();
});
@@ -19,7 +19,7 @@ local function check_items(player, type, banned_items)
local inventory = assert(player.get_inventory(type))
-- Check what items the player has
for i = 1, #inventory do
local item = inventory[i]
local item = inventory[i --[[@as uint]]]
if item.valid_for_read and banned_items[item.name] then
player.print{ "exp_nuke-protection.chat-found", item.prototype.localised_name }
items[#items + 1] = item
+35 -18
View File
@@ -5,8 +5,8 @@ Adds a custom spawn area with chests and afk turrets
local config = require("modules.exp_legacy.config.spawn_area")
--- Apply an offset to a LuaPosition
--- @param position MapPosition
--- @param offset MapPosition
--- @param position MapPosition.struct
--- @param offset MapPosition.struct
--- @return MapPosition.struct
local function apply_offset(position, offset)
return {
@@ -17,7 +17,7 @@ end
--- Apply offset to an array of positions
--- @param positions table
--- @param offset MapPosition
--- @param offset MapPosition.struct
--- @param x_index number
--- @param y_index number
local function apply_offset_to_array(positions, offset, x_index, y_index)
@@ -97,14 +97,19 @@ local belt_details = {
--- Makes a 2x2 afk belt at the locations in the config
--- @param surface LuaSurface
--- @param offset MapPosition
--- @param offset MapPosition.struct
local function create_belts(surface, offset)
local belt_type = config.afk_belts.belt_type
local entity_pos = { x = 0, y = 0 }
local group_offset = { x = 0, y = 0 }
for _, position in pairs(config.afk_belts.locations) do
position = apply_offset(position, offset)
group_offset.x = position[1] + offset.x
group_offset.y = position[2] + offset.y
for _, belt in pairs(belt_details) do
local pos = apply_offset(position, belt)
entity_pos.x = belt[1]
entity_pos.y = belt[2]
local pos = apply_offset(entity_pos, group_offset)
local entity = surface.create_entity{ name = belt_type, position = pos, force = "neutral", direction = belt[3] }
if entity and config.afk_belts.protected then
protect_entity(entity)
@@ -115,13 +120,16 @@ end
-- Generates extra tiles in a set pattern as defined in the config
--- @param surface LuaSurface
--- @param offset MapPosition
--- @param offset MapPosition.struct
local function create_pattern_tiles(surface, offset)
local tiles_to_make = {}
local pattern_tile = config.pattern.pattern_tile
local tile_pos = { x = 0, y = 0 }
for index, position in pairs(config.pattern.locations) do
tiles_to_make[index] = { name = pattern_tile, position = apply_offset(position, offset) }
tile_pos.x = position[1]
tile_pos.y = position[2]
tiles_to_make[index] = { name = pattern_tile, position = apply_offset(tile_pos, offset) }
end
surface.set_tiles(tiles_to_make)
@@ -129,13 +137,16 @@ end
-- Generates extra water as defined in the config
--- @param surface LuaSurface
--- @param offset MapPosition
--- @param offset MapPosition.struct
local function create_water_tiles(surface, offset)
local tiles_to_make = {}
local water_tile = config.water.water_tile
local tile_pos = { x = 0, y = 0 }
for _, position in pairs(config.water.locations) do
table.insert(tiles_to_make, { name = water_tile, position = apply_offset(position, offset) })
tile_pos.x = position[1]
tile_pos.y = position[2]
table.insert(tiles_to_make, { name = water_tile, position = apply_offset(tile_pos, offset) })
end
surface.set_tiles(tiles_to_make)
@@ -143,11 +154,17 @@ end
--- Generates the entities that are in the config
--- @param surface LuaSurface
--- @param offset MapPosition
--- @param offset MapPosition.struct
local function create_entities(surface, offset)
local entity_pos = { x = 0, y = 0 }
for _, entity_details in pairs(config.entities.locations) do
local pos = apply_offset({ entity_details[2], entity_details[3] }, offset)
local entity = surface.create_entity{ name = entity_details[1], position = pos, force = "neutral" }
entity_pos.x = entity_details[2]
entity_pos.y = entity_details[3]
local entity = surface.create_entity{
name = entity_details[1],
position = apply_offset(entity_pos, offset),
force = "neutral"
}
if entity then
if config.entities.protected then
@@ -161,7 +178,7 @@ end
--- Generates an area with no water or entities, no water area is larger
--- @param surface LuaSurface
--- @param offset MapPosition
--- @param offset MapPosition.struct
local function clear_spawn_area(surface, offset)
local get_tile = surface.get_tile
@@ -182,7 +199,7 @@ local function clear_spawn_area(surface, offset)
for y = -fill_radius, fill_radius do -- loop over y
local y_sqr = (y + 0.5) ^ 2
local dst = x_sqr + y_sqr
local pos = apply_offset({ x, y }, offset)
local pos = apply_offset({ x = x, y = y }, offset)
if dst < tile_radius_sqr then
-- If it is inside the decon radius always set the tile
tiles_to_make[#tiles_to_make + 1] = { name = decon_tile, position = pos }
@@ -204,14 +221,14 @@ end
--- Spawn the resource tiles
--- @param surface LuaSurface
--- @param offset MapPosition
--- @param offset MapPosition.struct
local function create_resources_tiles(surface, offset)
for _, resource in ipairs(config.resource_tiles.resources) do
if resource.enabled then
local pos = apply_offset(resource.offset, offset)
for x = pos.x, pos.x + resource.size[1] do
for y = pos.y, pos.y + resource.size[2] do
surface.create_entity{ name = resource.name, amount = resource.amount, position = { x, y } }
surface.create_entity{ name = resource.name, amount = resource.amount, position = { x = x, y = y } }
end
end
end
@@ -220,7 +237,7 @@ end
--- Spawn the resource entities
--- @param surface LuaSurface
--- @param offset MapPosition
--- @param offset MapPosition.struct
local function create_resource_patches(surface, offset)
for _, resource in ipairs(config.resource_patches.resources) do
if resource.enabled then
+1 -1
View File
@@ -459,7 +459,7 @@ local function on_entity_settings_pasted(event)
-- Get the modules and add them to the planner
local all_modules = {}
for i = 1, #module_inventory do
local slot = module_inventory[i]
local slot = module_inventory[i --[[@as uint]]]
if slot.valid_for_read and slot.count > 0 then
all_modules[i] = { name = slot.name, quality = slot.quality.name }
else
@@ -294,9 +294,9 @@ end
--- Get the achieved time for a force
--- @param force LuaForce
--- @param research_index number
--- @return number
--- @return number?
function Elements.container.get_achieved_time(force, research_index)
return assert(Elements.container.data[force][research_index])
return assert(Elements.container.data[force])[research_index]
end
--- Calculate the starting research index for a force
+25 -11
View File
@@ -1,5 +1,4 @@
"use strict";
const assert = require("assert").strict;
const { compile } = require("@clusterio/lib");
/**
@@ -15,22 +14,37 @@ function testMatrix(...arrays) {
}
/**
* Test that a class is round trip json serialisable across multiple test cases.
* Test that a class is round trip JSON serialisable across multiple test cases.
*
* @template {any[]} T - Constructor arguments
* @param {import("tap").Test} t - Parent test.
* @param {{new(...args: T): object}} Class - The class which has toJSON and fromJSON methods.
* @param {T[]} tests - The tests inputs to pass to the class constructor.
* @param {T[]} tests - The test inputs to pass to the class constructor.
*/
function testRoundTripJsonSerialisable(Class, tests) {
function testRoundTripJsonSerialisable(t, Class, tests) {
const validate = compile(Class.jsonSchema);
for (const test of tests) {
const original = new Class(...test);
const serialised = JSON.stringify(original);
const jsonObject = JSON.parse(serialised);
const reconstructed = Class.fromJSON(jsonObject);
assert.deepEqual(reconstructed, original, JSON.stringify(test));
if (!validate(jsonObject)) {
throw validate.errors;
const name = JSON.stringify(test);
try {
const original = new Class(...test);
const jsonObject = JSON.parse(JSON.stringify(original));
const reconstructed = Class.fromJSON(jsonObject);
if (!validate(jsonObject)) {
t.fail(`schema validation: ${name}`, {
diagnostic: {
json: jsonObject,
errors: validate.errors,
},
});
continue;
}
t.strictSame(reconstructed, original, `round trip: ${name}`);
} catch (error) {
t.fail(`round trip: ${name}`, { error });
}
}
}
+1
View File
@@ -10,6 +10,7 @@ local source = assert(debug.getinfo(1, "S")).source
local shared_root = assert(source:match("^@(.*)/framework%.lua$"))
local Stubs = assert(loadfile(shared_root .. "/stubs.lua"))()
--- @class Framework
local Framework = {}
--- Turn a value into a string for failure messages
+1
View File
@@ -50,6 +50,7 @@ end
--- Create an independent set of stubs, installed as the lua globals
function Stubs.new()
--- @class Stubs
local stubs = {
events = {}, -- events raised through script.raise_event
printed = {}, -- game.print and player.print messages