Address review on the suite and instance tests

- The instance tests run against a real Instance with a real InstanceConfig,
  matching the controller tests: the spies are sendTo and the server, which
  record what leaves the instance, so sendRcon passes through the real script
  command gate. The plugin's config fields are registered so the sync mode is
  a real field.
- Shallow eq is gone and eq compares recursively; the suite is created with
  Framework.suite(extend_env), which hands the extension a fresh set of stubs
  per test, so the plugin's env.lua is only the extension.
- add_player assigns continuous indexes itself.
- The lua fixtures are referenced by role name: assignments take names, and
  ids in expectations come from the role object. The name lookup in env.R
  uses a fixture index rather than searching the roles.
- Test names format functions as name() and methods as .name().
- Branch coverage filled in: sort tie breaking, the highest role assertion
  when a player has no roles, has all for the server, the by player default
  from game.player, deleted assignment records on initialise, set_emit_events
  with no argument, printing to a role with no holders, role property update
  validation, the assignment subscription replay, unassignment over the ipc,
  and a refused removal having nothing to roll back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bbassie
2026-08-25 21:11:58 +00:00
co-authored by Claude Fable 5
parent e6552a39be
commit 3218351db6
12 changed files with 340 additions and 234 deletions
+44 -31
View File
@@ -1,61 +1,69 @@
--- 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 test, check, eq, deep_eq = Suite.test, Suite.check, Suite.eq, Suite.deep_eq
local test, check, eq = Suite.test, Suite.check, Suite.eq
--- alice is a moderator and carol has only the default role, both connected
local function setup(env)
local players = {
alice = env.add_player("alice", 1),
carol = env.add_player("carol", 3),
alice = env.add_player("alice"),
carol = env.add_player("carol"),
}
env.admin_state = {}
env.Roles.define_permission_trigger("exp_scenario.player.admin", function(player, state)
env.admin_state[player.name] = state
end)
env.initialise{
env.assignment("alice", { 5 }),
env.assignment("zed", { 5, 6 }), -- has never joined this map
env.assignment("alice", { "Moderator" }),
env.assignment("zed", { "Moderator", "Regular" }), -- has never joined this map
}
env.Roles.set_emit_events(true)
return players
end
test("initialise applies triggers and raises empty events", function(env)
test("initialise() applies triggers and raises empty events", function(env)
setup(env)
deep_eq(env.admin_state, { alice = true, carol = false }, "triggers run for connected players")
eq(env.admin_state, { alice = true, carol = false }, "triggers run for connected players")
check(#env.events == 2, "the event is raised once per connected player")
check(#env.events[1].assigned == 0 and #env.events[1].unassigned == 0, "the events are empty")
check(#env.printed == 0 and #env.sent == 0 and #env.sounds == 0, "initialise is silent and sends nothing")
end)
test("initialise is authoritative for pending roles", function(env)
test("initialise() skips deleted assignments", function(env)
local players = setup(env)
local record = env.assignment("carol", { "Moderator" })
record.is_deleted = true
env.initialise{ record }
check(not env.R("Moderator"):has_player(players.carol), "a deleted assignment is ignored")
end)
test("initialise() is authoritative for pending roles", function(env)
local players = setup(env)
env.R("Moderator"):assign(players.carol)
env.R("Jail"):assign(players.carol, { silent = true, local_only = true })
local sd = env.Roles._script_data()
eq(sd.pending.carol, { 5 }, "the role is pending before initialise")
eq(sd.pending.carol, { env.R("Moderator").id }, "the role is pending before initialise")
env.reset_log()
env.initialise{ env.assignment("alice", { 5 }) }
env.initialise{ env.assignment("alice", { "Moderator" }) }
check(sd.pending.carol == nil, "pending roles are cleared")
check(not env.R("Moderator"):has_player(players.carol), "an unconfirmed role is given up")
eq(sd.local_players.carol, { 7 }, "local only roles are kept")
eq(sd.local_players.carol, { env.R("Jail").id }, "local only roles are kept")
check(#env.sent == 0, "initialise sends nothing")
end)
test("receive_assignment_updates applies controller changes", function(env)
test("receive_assignment_updates() applies controller changes", function(env)
local players = setup(env)
env.reset_log()
env.Roles.receive_assignment_updates{ env.assignment("carol", { 5 }) }
env.Roles.receive_assignment_updates{ env.assignment("carol", { "Moderator" }) }
check(env.R("Moderator"):has_player(players.carol), "the assignment applies")
deep_eq(env.events, {
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = 3,
player_index = players.carol.index,
by_player_index = 0,
assigned = { 5 },
assigned = { env.R("Moderator").id },
unassigned = {},
},
}, "the event carries the change with no by player")
@@ -65,53 +73,58 @@ test("receive_assignment_updates applies controller changes", function(env)
env.reset_log()
env.Roles.receive_assignment_updates{ { name = "carol", role_ids = {}, is_deleted = true } }
check(not env.R("Moderator"):has_player(players.carol), "a removal applies")
eq(env.events[1].unassigned, { 5 }, "the removal raises the event")
eq(env.events[1].unassigned, { env.R("Moderator").id }, "the removal raises the event")
check(env.admin_state.carol == false, "triggers follow the removal")
end)
test("receive_assignment_updates for players not on this map raises nothing", function(env)
test("receive_assignment_updates() for players not on this map raises nothing", function(env)
setup(env)
env.reset_log()
env.Roles.receive_assignment_updates{ env.assignment("zed", { 5 }) }
env.Roles.receive_assignment_updates{ env.assignment("zed", { "Moderator" }) }
check(#env.events == 0 and #env.printed == 0, "no event and no announcement")
end)
test("receive_role_updates applies to the holders", function(env)
test("receive_role_updates() applies to the holders", function(env)
setup(env)
env.reset_log()
local regular_id = env.R("Regular").id
env.Roles.receive_role_updates{
{ id = 6, name = "Regular", permissions = { "exp_scenario.command.jail" }, meta = { id = 0, order = 3 } },
{ id = regular_id, name = "Regular", permissions = { "exp_scenario.command.jail" }, meta = { id = 0, order = 3 } },
}
check(env.R("Regular"):has_permission("exp_scenario.command.jail"), "the permission change applies")
check(#env.events == 2, "a role change raises for every connected player")
check(#env.events[1].assigned == 0, "role change events are empty")
env.Roles.receive_role_updates{
{ id = 6, name = "Regular", permissions = {}, meta = { id = 0, order = 3 }, is_deleted = true },
{ id = regular_id, name = "Regular", permissions = {}, meta = { id = 0, order = 3 }, is_deleted = true },
}
check(env.Roles.get_role(6) == nil, "a deleted role is removed")
check(env.Roles.get_role(regular_id) == nil, "a deleted role is removed")
end)
test("receive_role_updates moves the default role", function(env)
test("receive_role_updates() moves the default role", function(env)
local players = setup(env)
env.Roles.receive_role_updates{
{ id = 1, name = "Player", permissions = {}, meta = { id = 0, order = 5 } },
{ id = env.R("Player").id, name = "Player", permissions = {}, meta = { id = 0, order = 5 } },
{ id = 8, name = "Guest", permissions = {}, meta = { id = 0, order = 7 }, is_default = true },
}
check(env.Roles.get_default_role() == env.R("Guest"), "the new default role is known")
eq(Suite.names(env.Roles.get_player_roles(players.carol)), { "Guest" }, "players pick up the new default")
end)
test("set_emit_events gates what is sent", function(env)
test("set_emit_events() gates what is sent and defaults to enabled", function(env)
local players = setup(env)
env.Roles.set_emit_events(false)
env.reset_log()
env.R("Regular"):assign(players.alice)
check(#env.sent == 0, "the assignment is not sent")
check(#env.sent == 0, "nothing is sent while disabled")
check(env.R("Regular"):has_player(players.alice), "the assignment still applies locally")
env.Roles.set_emit_events()
env.R("Jail"):assign(players.alice, { silent = true })
check(#env.sent == 1, "no argument enables sending")
end)
test("on_load restores the state after a save and load", function(env)
test("on_load() restores the state after a save and load", function(env)
local players = setup(env)
env.R("Jail"):assign(players.carol, { silent = true, local_only = true })
env.save_load()
@@ -123,10 +136,10 @@ test("on_load restores the state after a save and load", function(env)
check(env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "permission checks still answer")
end)
test("on_player_joined_game runs the triggers", function(env)
setup(env)
test("on_player_joined_game() runs the triggers", function(env)
local players = setup(env)
env.admin_state.alice = nil
env.Roles.on_player_joined_game{ player_index = 1 }
env.Roles.on_player_joined_game{ player_index = players.alice.index }
check(env.admin_state.alice ~= nil, "triggers run when a player joins")
end)