Skip to main content
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 reach server.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:
Declaring no channel at all is valid. The environment runs without a console, and every test that requires one is skipped and reported as skipped.

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, marked justCreated on their first lease so an authentication plugin registers them instead of logging in. The pattern must start with pw_, so test accounts stay recognizable on a server full of real players. The placeholder decides what happens to a name afterwards: pw_%04d numbers a fixed set of accounts the run keeps coming back to, while pw_%s puts 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. Point cacheDir somewhere outside build/, and warm the cache before CI ever needs it, because the device-code flow is interactive.
One account is leased per bot and returned in a 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:
That is a request for a specific identity, not for whatever is free, so the pool knows nothing about it and neither does your authentication plugin. Pass the password with the name. Read it from the environment; the spec file goes to git. Most second bots don’t need this. A test that just wants another player should call 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.
A leased account comes back with the previous test’s inventory, balance and op status. Nothing resets it for you. Reset what you can in a plugin’s beforeEach, and exclude what you can’t.

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_0001pw_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

A describe.serial block can ask for one specific pool account:
That’s for a scenario tied to state somebody provisioned on that account — a permission group, a starting balance. The account has to be in the pool and free; anything else fails the block instead of quietly running as a different player. See Writing Tests.

Checking the stand before you test

Connects, probes the console channels, leases one account and authenticates with it, then disconnects. No tests run. When something is wrong with the stand — RCON password rotated, login plugin changed its messages, account pool exhausted — this fails in seconds with a specific message instead of failing test after test five minutes into a run. After 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.