ExternalMode points bots at a server that is already running: a staging stand, a colleague’s box, the production copy someone keeps for QA. Plugwright starts nothing, patches nothing and shuts nothing down.
That changes what the suite can assume. A local server hands every test a fresh world and a brand new username. A stand hands you whatever the last test left behind, on an account you have to log in as, and the plugin under test is already installed there — deploying it is out of scope for this mode by design.
minecraftVersion is required here, unlike in LocalMode where the version is what Plugwright downloaded. A proxy in front of the stand (ViaVersion and friends) defeats protocol autodetection, so guessing would produce a confusing connection failure instead of a clear one.
joinThrottleMs is the minimum delay between two bot connections. Public servers treat a burst of logins as an attack; a few seconds of spacing is cheaper than getting the CI runner’s IP banned.
Console channels
Without a process of its own, the mode has no stdout to read and no stdin to write. A console channel is how tests reachserver.execute(...), player.makeOp() and everything else that needs the server side.
Channels are probed in declaration order, and the first one that answers becomes the session’s console. The chosen channel is printed in the run header.
The output level matters more than it looks.
full means the whole server log is readable, so expect(server).toHaveReceivedMessage(...) works. responses means you get back what the command printed and nothing else. A test that reads the server log should say so:
Accounts
A local server accepts any username; a stand usually does not.accounts { } builds a pool that tests lease from and return to, merged from three sources:
pool— accounts that already exist, with their passwords.autoRegister— generated names from a pattern, markedjustCreatedon their first lease so an authentication plugin registers them instead of logging in. The pattern must start withpw_, so test accounts stay recognizable on a server full of real players. The placeholder decides what happens to a name afterwards:pw_%04dnumbers a fixed set of accounts the run keeps coming back to, whilepw_%sputs a random suffix there and never hands the same name out twice. See below.microsoft— online-mode accounts. No password; mineflayer authenticates with a cached device-code token. PointcacheDirsomewhere outsidebuild/, and warm the cache before CI ever needs it, because the device-code flow is interactive.
finally, whatever the test did. When the pool is empty and autoRegister has hit max, lease() throws rather than hand the same identity to two connected bots.
An explicitly named bot bypasses the pool entirely:
createPlayer() with no arguments and let the pool answer — a name is worth asking for when the identity is, because somebody provisioned that account with a permission group or a balance, or because the name came from somewhere outside the test.
Numbered slots or fresh names
autoRegister answers a question the fixed pool can’t: where does a name come from when the server has never seen this test before? Which form you want depends on what the stand can clean up.
pw_%04d gives you pw_0001 … pw_000N, leased in turn and returned when a test ends. The set is finite and the accounts are provisioned once, which is what a stand with permission groups or a whitelist needs. The cost is that every test inherits whatever the last one left on that account, so anything you can’t reset with a command has to stay out of the suite (excludeTests) or be undone in a plugin’s beforeEach.
pw_%s generates a name per lease — pw_a8f2 — and never reuses it. Each test starts on an account with no history, which is the closest a stand gets to what local hands out for free. The cost is a registration the server keeps: after a few runs the login plugin’s database is full of test accounts, and pruning them is on you. max still caps how many bots are connected at once.
Naming an account from a test
Adescribe.serial block can ask for one specific pool account:
Checking the stand before you test
setup(), the environment reports what it actually supports. For ExternalMode that is: console plus op only if a console channel answered. Tests that declare requires are skipped against that list, with the reason in the report. See Test Filtering.