Add a test suite for the roles plugin

Run with `pnpm --filter @expcluster/roles test`, using tap as the runner.

The lua module is exercised for real: each test file runs in its own lua
state, created on first use by the helper, with the factorio surface and the
clusterio modules stubbed. The environment is passed to the chunk as an
argument so no globals are involved. Covers lookups and comparisons, the
permission checks, assignment with confirmation, rejection and local only
roles, jail suppression, the sync entry points, and the holder listings,
including a player holding a role in both the synced and the local list.

The javascript side covers the message records round tripping through their
schemas, the indexed permission encoding, and the seed: every permission it
grants must be defined by exp_scenario, and every parent must exist and have
its permissions carried into its children.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bbassie
2026-08-20 22:02:51 +00:00
co-authored by Claude Fable 5
parent 8a15745cf8
commit e79822b26e
11 changed files with 674 additions and 1 deletions
+47
View File
@@ -0,0 +1,47 @@
--- Listing and printing to the players who hold a role
local Env = ...
local env = Env.new()
local check, eq, R = env.check, env.eq, env.R
local Roles = env.Roles
local alice = env.add_player("alice", 1)
env.add_player("bob", 2)
env.add_player("dave", 4, false) -- offline
env.initialise{
Env.assignment("alice", { 5 }),
Env.assignment("bob", { 6 }),
Env.assignment("dave", { 5 }),
Env.assignment("zed", { 5 }), -- has never joined this map
}
Roles.set_emit_events(true)
local mod = R("Moderator")
check(eq(env.sorted(mod:get_player_names()), { "alice", "dave", "zed" }),
"player names include players not on this map")
check(eq(env.sorted(env.names(mod:get_players())), { "alice", "dave" }),
"players are limited to this map")
check(eq(env.names(mod:get_players(true)), { "alice" }), "players can be filtered by connected state")
check(eq(env.names(mod:get_players(false)), { "dave" }), "players can be filtered to offline")
-- A player holding the role in both the synced and the local list counts once
R("Regular"):assign(alice, { silent = true, local_only = true })
Roles.receive_assignment_updates{ Env.assignment("alice", { 5, 6 }) }
local sd = Roles._script_data()
check(sd.local_players.alice ~= nil and sd.synced_players.alice ~= nil, "the role is held in both lists")
check(eq(env.sorted(R("Regular"):get_player_names()), { "alice", "bob" }),
"a player in both lists is counted once")
env.reset_log()
check(mod:print("hello") == 1, "print returns the number of players reached")
check(#env.printed == 1 and env.printed[1].to == "alice", "print reaches only online holders")
env.reset_log()
for _, role in ipairs(Roles.get_higher_roles(R("Regular"))) do
role:print("hello")
end
local got = {}
for _, entry in ipairs(env.printed) do got[#got + 1] = entry.to end
-- alice holds two of the roles, so like the legacy system the message can repeat
check(eq(env.sorted(got), { "alice", "alice", "bob" }), "printing to the higher roles reaches their online holders")
return Env.results_json(env)