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
+62 -50
View File
@@ -1,36 +1,37 @@
--- 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 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 bob a regular, with changes sent to the controller
local function setup(env)
local players = {
alice = env.add_player("alice", 1),
bob = env.add_player("bob", 2),
alice = env.add_player("alice"),
bob = env.add_player("bob"),
}
env.initialise{
env.assignment("alice", { 5 }),
env.assignment("bob", { 6 }),
env.assignment("alice", { "Moderator" }),
env.assignment("bob", { "Regular" }),
}
env.Roles.set_emit_events(true)
env.reset_log()
return players
end
test("role:assign applies locally and is sent to the controller", function(env)
test(".assign() applies locally and is sent to the controller", function(env)
local players = setup(env)
env.R("Moderator"):assign(players.bob, { by_player_name = "alice" })
deep_eq(env.sent, {
{ channel = "exp_roles:assignment_update", data = { name = "bob", assign = { 5 } } },
local moderator = env.R("Moderator")
moderator:assign(players.bob, { by_player_name = "alice" })
eq(env.sent, {
{ channel = "exp_roles:assignment_update", data = { name = "bob", assign = { moderator.id } } },
}, "the assignment is sent to the controller")
check(env.R("Moderator"):has_player(players.bob), "the role applies before confirmation")
deep_eq(env.events, {
check(moderator:has_player(players.bob), "the role applies before confirmation")
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = 2,
by_player_index = 1,
assigned = { 5 },
player_index = players.bob.index,
by_player_index = players.alice.index,
assigned = { moderator.id },
unassigned = {},
},
}, "the event carries the change")
@@ -38,19 +39,26 @@ test("role:assign applies locally and is sent to the controller", function(env)
eq(env.sounds, { "bob:utility/achievement_unlocked" }, "the assign sound plays")
local sd = env.Roles._script_data()
eq(sd.local_players.bob, { 5 }, "the role is held locally")
eq(sd.pending.bob, { 5 }, "the role is pending")
eq(sd.local_players.bob, { moderator.id }, "the role is held locally")
eq(sd.pending.bob, { moderator.id }, "the role is pending")
env.reset_log()
env.R("Moderator"):assign(players.bob)
moderator:assign(players.bob)
check(#env.sent == 0 and #env.events == 0, "assigning a held role is a no-op")
end)
test("receive_assignment_updates releases the local hold once confirmed", function(env)
test(".assign() defaults the by player to game.player", function(env)
local players = setup(env)
game.player = players.alice
env.R("Jail"):assign(players.bob, { silent = true })
check(env.events[1].by_player_index == players.alice.index, "the acting player is game.player")
end)
test("receive_assignment_updates() releases the local hold once confirmed", function(env)
local players = setup(env)
env.R("Moderator"):assign(players.bob)
env.reset_log()
env.Roles.receive_assignment_updates{ env.assignment("bob", { 6, 5 }) }
env.Roles.receive_assignment_updates{ env.assignment("bob", { "Regular", "Moderator" }) }
check(#env.events == 0 and #env.printed == 0, "a confirmation raises nothing")
local sd = env.Roles._script_data()
@@ -58,44 +66,46 @@ test("receive_assignment_updates releases the local hold once confirmed", functi
check(env.R("Moderator"):has_player(players.bob), "the role is still held after confirmation")
end)
test("role:unassign removes a synced role", function(env)
test(".unassign() removes a synced role", function(env)
local players = setup(env)
env.R("Regular"):unassign(players.bob, { by_player_name = "alice", silent = true })
deep_eq(env.sent, {
{ channel = "exp_roles:assignment_update", data = { name = "bob", unassign = { 6 } } },
local regular = env.R("Regular")
regular:unassign(players.bob, { by_player_name = "alice", silent = true })
eq(env.sent, {
{ channel = "exp_roles:assignment_update", data = { name = "bob", unassign = { regular.id } } },
}, "the unassignment is sent to the controller")
check(not env.R("Regular"):has_player(players.bob), "the role is removed locally")
deep_eq(env.events, {
check(not regular:has_player(players.bob), "the role is removed locally")
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = 2,
by_player_index = 1,
player_index = players.bob.index,
by_player_index = players.alice.index,
assigned = {},
unassigned = { 6 },
unassigned = { regular.id },
},
}, "the event carries the change")
check(#env.printed == 0, "silent suppresses the announcement")
eq(env.sounds, { "bob:utility/game_lost" }, "the unassign sound plays")
end)
test("reject_assignment rolls the assignment back", function(env)
test("reject_assignment() rolls the assignment back", function(env)
local players = setup(env)
env.R("Jail"):assign(players.alice)
check(env.R("Jail"):has_player(players.alice), "the role applies before rejection")
local jail = env.R("Jail")
jail:assign(players.alice)
check(jail:has_player(players.alice), "the role applies before rejection")
env.reset_log()
env.Roles.reject_assignment{ name = "alice", role_ids = { 7 } }
check(not env.R("Jail"):has_player(players.alice), "the rejected role is rolled back")
env.Roles.reject_assignment{ name = "alice", role_ids = { jail.id } }
check(not jail:has_player(players.alice), "the rejected role is rolled back")
check(#env.sent == 0, "the rollback is not sent to the controller")
deep_eq(env.events, {
eq(env.events, {
{
name = env.Roles.events.on_player_roles_changed,
tick = 1,
player_index = 1,
player_index = players.alice.index,
by_player_index = 0,
assigned = {},
unassigned = { 7 },
unassigned = { jail.id },
},
}, "the rollback raises the event")
@@ -103,44 +113,46 @@ test("reject_assignment rolls the assignment back", function(env)
check(sd.local_players.alice == nil and sd.pending.alice == nil, "the rollback clears the local state")
end)
test("role:assign with local_only never reaches the controller", function(env)
test(".assign() with local_only never reaches the controller", function(env)
local players = setup(env)
env.R("Regular"):assign(players.alice, { silent = true, local_only = true })
local regular = env.R("Regular")
regular:assign(players.alice, { silent = true, local_only = true })
check(#env.sent == 0, "the assignment is not sent")
check(env.R("Regular"):has_player(players.alice), "the role applies")
check(regular:has_player(players.alice), "the role applies")
local sd = env.Roles._script_data()
check(sd.pending.alice == nil, "the role is not pending")
eq(sd.local_players.alice, { 6 }, "the role is held locally")
eq(sd.local_players.alice, { regular.id }, "the role is held locally")
env.Roles.receive_assignment_updates{ env.assignment("alice", { 5 }) }
check(env.R("Regular"):has_player(players.alice), "the role survives controller updates")
env.Roles.receive_assignment_updates{ env.assignment("alice", { "Moderator" }) }
check(regular:has_player(players.alice), "the role survives controller updates")
env.reset_log()
env.R("Regular"):unassign(players.alice, { local_only = true })
check(not env.R("Regular"):has_player(players.alice) and #env.sent == 0, "removed without sync")
regular:unassign(players.alice, { local_only = true })
check(not regular:has_player(players.alice) and #env.sent == 0, "removed without sync")
end)
test("role:assign of a higher priority role suppresses the rest", function(env)
test(".assign() of a higher priority role suppresses the rest", function(env)
local players = setup(env)
env.R("Jail"):assign(players.alice, { by_player_name = "<server>", silent = true })
local jail = env.R("Jail")
jail:assign(players.alice, { by_player_name = "<server>", silent = true })
eq(Suite.names(env.Roles.get_player_roles(players.alice)), { "Jail" }, "only the jail role applies")
check(env.R("Moderator"):has_player(players.alice), "a suppressed role is still held")
check(not env.Roles.player_has_permission(players.alice, "exp_scenario.command.kill"), "permissions are lost")
check(not env.Roles.player_has_permission(players.alice, "exp_scenario.gui.readme"), "the default role is lost")
check(not env.Roles.player_outranks(players.alice, players.bob), "a jailed player no longer outranks")
eq(env.events[1].assigned, { 7 }, "the event lists the jail role")
eq(env.events[1].assigned, { jail.id }, "the event lists the jail role")
check(#env.events[1].unassigned == 0, "the suppressed roles are not listed as unassigned")
env.reset_log()
env.R("Jail"):unassign(players.alice, { by_player_name = "<server>", silent = true })
jail:unassign(players.alice, { by_player_name = "<server>", silent = true })
eq(Suite.sorted(Suite.names(env.Roles.get_player_roles(players.alice))), { "Moderator", "Player" },
"unjail restores the roles")
check(#env.events[1].assigned == 0, "the restored roles are not listed as assigned")
eq(env.events[1].unassigned, { 7 }, "the unjail event lists the jail role")
eq(env.events[1].unassigned, { jail.id }, "the unjail event lists the jail role")
end)
test("role:assign ignores the server", function(env)
test(".assign() ignores the server", function(env)
setup(env)
env.R("Moderator"):assign(env.server)
check(#env.sent == 0 and #env.events == 0, "assigning to the server is a no-op")