Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/api-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,33 @@ const player2 = await createPlayer();
player2.chat('/tpa ' + player.username);
```

<ParamField path="options.username" type="string">
Connect as this exact name instead of taking whatever the environment's account pool has free.
</ParamField>

<ParamField path="options.as" type="string">
Names the bot inside a `describe.serial` block, so a later test in that block gets the same bot
back instead of connecting another one.
</ParamField>

<ParamField path="options.password" type="string">
The password an authentication plugin logs `username` in with. Only for a named bot: a pool
account already carries its own, and passing both is an error.
</ParamField>

Ask for a name only when the test needs that specific identity — an account somebody provisioned
by hand, or a name that came out of an external API. A second bot that is only there to be a
second player should stay unnamed, so a stand can lease it from the pool.

```javascript
const friend = await createPlayer({
username: 'FriendBot',
password: process.env.FRIEND_BOT_PASSWORD,
});
```

Read the password from the environment. Spec files go to git.

### `sleep(ms)`

Pauses execution for a specified number of milliseconds.
Expand Down
6 changes: 4 additions & 2 deletions docs/external-servers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ One account is leased per bot and returned in a `finally`, whatever the test did
An explicitly named bot bypasses the pool entirely:

```ts
const friend = await createPlayer({ username: 'FriendBot' });
const friend = await createPlayer({ username: 'FriendBot', password: process.env.FRIEND_BOT_PASSWORD });
```

That is a request for a specific identity, not for whatever is free — so nothing knows its password. On a stand behind a login wall, either leave those tests to the local environment or give the account a password some other way.
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.

<Warning>
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`, exclude what you can't, and treat `capabilities.freshState = false` as the honest description it is.
Expand Down
6 changes: 2 additions & 4 deletions example_plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,9 @@ plugwright {
// Matched against test names. What is left out here is what no command puts back:
// an arena slot that is filled once and stays filled, and a first join, which only
// happens on an account the server has never seen. Op, inventory, balance and kit
// cooldowns are reset per test by the stand-reset plugin instead. multi-bot and
// Cross-bot are out for a different reason — they name their second bot, and a named
// bot is not a pool account, so nothing knows its password.
// cooldowns are reset per test by the stand-reset plugin instead.
excludeTests.set(listOf(
"arena", "first join", "multi-bot", "Cross-bot"
"arena", "first join"
))
}
}
Expand Down
2 changes: 1 addition & 1 deletion example_plugin/src/test/e2e/tests/message-buffer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test } from '@plugwright/runner';

test('Cross-bot message separation', async ({ player, createPlayer }) => {
const friend = await createPlayer({ username: 'FriendBot' });
const friend = await createPlayer();

player.chat('/help');

Expand Down
5 changes: 3 additions & 2 deletions example_plugin/src/test/e2e/tests/multi-bot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ test('multi-bot teleportation', async ({ player, createPlayer }) => {
// This can be also done with defining test as `opTest` instead of `test` or even within `beforeEach` block.
await player.makeOp();

// Spawn a second player
const friend = await createPlayer({ username: 'FriendBot' });
// Spawn a second player. No username: the test needs a second bot, not a specific one,
// so on a stand this leases the next free pool account instead of bypassing the pool.
const friend = await createPlayer();

// Teleport the friend to a specific location
// We wait for friend player to actually teleport.
Expand Down
8 changes: 6 additions & 2 deletions runner-package/lib/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ export interface Account {
/**
* Stand-in used when an environment has no [AccountPool] of its own — a `local` bot is a
* fresh offline-mode connection under a name the server has never seen.
*
* [password] is for the other case: a bot the test names itself. That bypasses the pool, so
* nothing else knows a password for it, and the test has to bring one for the authentication
* plugin to use.
*/
export function syntheticAccount(username: string): Account {
return { username, auth: 'offline', justCreated: true };
export function syntheticAccount(username: string, password?: string): Account {
return { username, password, auth: 'offline', justCreated: true };
}

/** A short random identity suffix. Four hex digits: long enough that two names in a run
Expand Down
22 changes: 16 additions & 6 deletions runner-package/lib/test-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ export interface RunSerialBlockParams {
/** Bots created while one test, or one `describe.serial` block, is running: who leased what,
* which player answers to which `as` name, and how to give it all back. */
interface BotScope {
connect(options?: { username?: string; account?: string }): Promise<PlayerWrapper>;
connect(options?: { username?: string; account?: string; password?: string }): Promise<PlayerWrapper>;
/** `ctx.createPlayer`. `as` names the player so a later call — a later test, inside a block —
* gets the same bot back instead of connecting a second one. */
createPlayer(options?: { username?: string; as?: string }): Promise<PlayerWrapper>;
* gets the same bot back instead of connecting a second one. `password` belongs with
* `username`: a named bot is not a pool account, so the test is the only thing that can
* say how it logs in. */
createPlayer(options?: { username?: string; as?: string; password?: string }): Promise<PlayerWrapper>;
/** Every player connected in this scope, in the order they joined. */
players(): PlayerWrapper[];
/** Disconnects every bot in the scope and returns the accounts they held. */
Expand All @@ -49,17 +51,25 @@ function createBotScope(session: Session, server: ServerWrapper, connOpts: BotCo
const named = new Map<string, PlayerWrapper>();
const connected: PlayerWrapper[] = [];

const connect = async (options?: { username?: string; account?: string }): Promise<PlayerWrapper> => {
const connect = async (options?: { username?: string; account?: string; password?: string }): Promise<PlayerWrapper> => {
const pool = options?.username ? null : session.env.accounts?.() ?? null;
if (options?.account && !pool) {
throw new Error(
`account "${options.account}" was requested, but environment "${session.env.id}" has no accounts pool ` +
'to take it from — a named account needs one the build script declares.'
);
}
// A pooled account brings its own password, so a password with no username would be
// read by nothing. Say so rather than connect as somebody else's account and ignore it.
if (options?.password && pool) {
throw new Error(
`a password was passed without a username, but environment "${session.env.id}" leases its ` +
'accounts from a pool and those carry their own. Name the bot too, or drop the password.'
);
}
const account: Account = pool
? await pool.lease(options?.account)
: syntheticAccount(options?.username || `pw_${randomSuffix()}`);
: syntheticAccount(options?.username || `pw_${randomSuffix()}`, options?.password);

try {
const botUsername = account.username;
Expand Down Expand Up @@ -97,7 +107,7 @@ function createBotScope(session: Session, server: ServerWrapper, connOpts: BotCo
const existing = named.get(handle);
if (existing) return existing;
}
const player = await connect({ username: options?.username });
const player = await connect({ username: options?.username, password: options?.password });
if (handle) named.set(handle, player);
return player;
},
Expand Down
8 changes: 6 additions & 2 deletions runner-package/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ export interface TestContext {
server: ServerWrapper;
/** Connects an extra bot. Inside a `describe.serial` block, `as` names it: the same name in
* a later test of that block returns the same bot instead of connecting another. Outside a
* block the name is scoped to the one test, which is as long as the bot lives anyway. */
createPlayer: (options?: { username?: string; as?: string }) => Promise<PlayerWrapper>;
* block the name is scoped to the one test, which is as long as the bot lives anyway.
*
* `username` asks for one specific identity instead of whatever the pool has free, and
* `password` is what an authentication plugin logs that identity in with. Read it from the
* environment rather than writing it in the spec — spec files go to git. */
createPlayer: (options?: { username?: string; as?: string; password?: string }) => Promise<PlayerWrapper>;
/** Says the player is in a state the tests after this one were not written for. Inside a
* `describe.serial` block that stops the block: the rest is reported skipped. Outside one
* it does nothing — the bot is disconnected at the end of the test either way. */
Expand Down
Loading