clusterio#1129 replaced DatastoreProvider.bootstrap with async factories
and Instance now takes its FactorioServer in the constructor, so the
instance tests pass their server stub there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
exp_groups and exp_roles only saved their datastores on shutdown, and
exp_scenario saved the player store but not the feature store on the
save hook. The controller runs the save hook on its autosave interval,
so a crash or kill lost every change since the controller started.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
exp_reports, exp_roles and exp_groups wrapped the json they send to Lua in
a fixed [=[ ]=] long bracket. A report reason, role name or group name
containing ]=] closed it early and the rest ran as Lua through /sc. Any
player can make a report, so any player could run code on the server.
Use the next level the json does not contain, like the other plugins do.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The instance plugins of exp_groups, exp_reports, exp_roles, exp_scenario
and exp_server_ups, and the controller plugins of exp_reports and
exp_scenario, were classes only to hold their handlers. They are now
init functions with closures, like clusterio's own plugins.
The exp_roles and exp_groups controller plugins stay classes because
exp_scenario reaches their datastores through get(controller).
AutoRestart stays a class because its tests step check() with a fake
clock.
Tests spy on handle() and the hooks to call the handlers. The reports
webhook tests stub fetch instead of replacing postWebhook, and the
feature reconcile test writes the stored records to disk before start.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Each plugin class now has a private constructor and a static async
fromContext(context) that loads its datastores, constructs the instance
and attaches its handlers and hooks. This replaces the public constructor
followed by init(), and AutoRestart's start().
Datastores are loaded before construction and passed to the constructor,
so the definite assignment assertions on them are gone. Tests build the
plugins through fromContext and pass a wrapped logger in the context
instead of overwriting plugin.logger before init().
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
clusterio logs "using deprecated class export" for every node entrypoint
that exports a Base*Plugin subclass, and the web UI logs "using deprecated
class hooks" for WebPlugin classes. Every entrypoint now has a default
export function, like exp_auto_restart and the exp_roles web plugin.
The classes stay where they hold state, but no longer extend the base
classes. The constructor takes the load context and init() attaches the
hooks the base class used to attach from the on* methods, so the tests can
still call those methods directly. exp_server_ups sends its RCON command
through instance.sendRcon with the plugin name, which is what the removed
BaseInstancePlugin.sendRcon did.
The instance entrypoints of exp_commands, exp_gui, exp_legacy, exp_util
and exp_scenario were empty classes, kept only so the host loads the
plugin and patches its module into the save. An empty function does the
same.
The exp_groups web components read the subscriptions through exported
useGroups, useAssignments and useRoleMappings hooks instead of looking
the plugin up in control.plugins, which only holds class plugins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The create-report, get-reports and clear-reports commands now live in the
plugin's own module next to the functions they call, with their locale.
The permissions keep their exp_scenario.command names because the command
authority derives them from the command name.
The Discord report alert in exp_scenario is gone since exp_reports posts to
its own webhook. The shared add_player stub takes an options table with
connected and admin defaulting to true and false, replacing the copy in
the exp_reports test environment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clusterio #984 converted its packages to ESM, so a CommonJS plugin can
no longer import @clusterio/lib and plugin entrypoints must name the
full file. Every package gets "type": "module", entrypoints and relative
imports carry the .js extension, and the webpack configs become .cjs as
webpack-cli loads them with require. tsconfig.node.json moves from
node16 to nodenext to match clusterio, which needs it for the JSON
import attributes in its sources.
The tap tests and the shared test helpers use import and
import.meta.dirname. The test scripts pass --type-strip-only so tap
leaves lib's .ts subpath imports to Node's own type stripping, as its
TypeScript loader compiled them without a default export. The lua
runner resolves fengari from the test file it is given, since a bare
import from the shared test directory does not reach the plugin's
devDependencies.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The seed roles were owned by exp_roles, but they describe the scenario, so
they now live in exp_scenario/seed.ts next to the permissions they grant.
Each seed role names the permission group its holders belong to, and the
five groups the legacy config defined (Admin, Trusted, Standard, Guest,
Restricted) are seeded through exp_groups along with one role mapping per
role. Mapping priorities follow how exp_roles ranks a player's highest
role, so Jail lands in Restricted regardless of other roles.
SeedRolesRequest becomes exp_scenario's SeedRequest behind a new
exp_scenario.seed permission, and the button moves to the scenario's web
plugin, which gets a web entrypoint for it. exp_roles no longer depends on
exp_scenario, and exp_scenario gains controller tests around the seed.
With groups seeded from the controller the legacy expcore.permission_groups
module and its config are removed, along with the Group rcon static.
Building on the class annotations from the smoke test changes:
- The suite is generic over its environment: Framework.suite is
@generic T : Stubs with the extension returning T, and Suite<T> passes T to
every test function. A test file declares
`local Suite = ... --- @type Suite<ExpRoles.TestEnv>` and env autocompletes
from there.
- The stubs declare their recorded fields, add_player returns Stubs.Player,
and the server is one.
- ExpRoles.TestEnv : Stubs declares the extension fields, so env.R returns
ExpRoles.Role and env.Roles is the module.
The typing immediately paid for itself by flagging real drift in the tests,
now fixed: _script_data is package private so the env exposes script_data()
instead, the join event carries its name and tick, the assignment record type
admits is_deleted, admin_state is a file local rather than an undeclared
field, and the event assertions which indexed [1] without a nil check compare
the whole event list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Suite.empty(value, name) checks a table has no entries and reports what it
held, and Suite.throws(fn, message, name) asserts a function errors with the
given message, so an unrelated error no longer passes as the expected one.
- Scalar comparisons and zero length checks go through eq and empty, and the
announced messages and print output are compared by content, so a failure
shows the actual data.
- get_roles() is asserted through sorted names rather than a count.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
- The controller tests run against a real Controller, which is side effect
free while not started, with real users through its UserManager. The only
fakes left are two spies which record broadcasts and permission pushes on
their way through. Since any user update also applies auto assignment, the
leave event is observed directly rather than through a role change.
- raise_event fills in the name and tick of the event the way factorio does,
rather than wrapping the payload, and game.players finds players by index
as well as by name through a metatable.
- names is generic over anything with a name property, so it lives on the
suite; the save and load helper no longer calls on_load, the test calls the
handler itself as it does for the other handlers.
- Sent messages and events are compared whole with deep_eq, which pins the
event shape, the tick, and that the unassign key is absent rather than
empty.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The registry is a Suite, created around the environment factory it runs its
tests with, so the plugin's env.lua reads as: build environments, hand the
test files a suite of them. The suite returned there becomes `...` in each
test file, which is now said where it happens.
- pass and fail are the primitives every other check goes through. eq and
deep_eq assert rather than compare, failing with both values in the detail.
- Stubs are extended through extend_requires, extend_script, extend_game and
extend_defines, a recursive merge which raises when a value already exists,
rather than by mutating the tables. The strict labels carry the factorio
class names.
- The stubs record registered metatables and can save and load the script
data the way factorio does: functions are refused and only registered
metatables survive. env.save_load() uses it, and a new on_load test shows
role methods and held roles surviving the round trip, which only passes
because the module registers its metatable.
- The names helper moved out of the shared framework, it is role specific.
- Tests are named after the function or method they cover, on both sides:
the lua tests read as "role:assign applies locally and is sent", and the
javascript files wrap their tests in the class they exercise with subtests
per method. module.test.js is control.test.js, after the file it covers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The generic parts move to test/ in the repository root so other plugins can
reuse them: the factorio and clusterio stubs, the test framework, the
fengari runner, and clusterio's testMatrix and round trip helpers. A plugin
composes them from its own env.lua, which adds its stubs and fixtures.
- Tests are declared with Test.test(name, fn) and every test function
receives a fresh environment, so nothing carries over between them. A test
which errors is reported as a failure rather than aborting the file.
- Every stub raises on properties it does not implement, which mirrors the
game api. game.player is the one property allowed to read as nil.
- Test.deep_eq compares tables recursively with keys checked from both sides.
- The message records round trip through the same testMatrix and
testRoundTripJsonSerialisable helpers the clusterio tests use, covering
every optional field combination of the records, events and requests.
- controller.test.js and instance.test.js cover the node side of the plugin
against faked controller and instance internals: property creation and
sweeping, record building, broadcasts, subscription replay, assignment
validation, auto assignment and its blocking role, seeding, the initialise
payload, sync mode gating, and the rejection rollback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>