Conversation
…transport
The build script and the runner talked through five flat env vars, which leaves
no room for a second environment. Phase 1 of the multi-mode work rearranges the
plumbing without changing behaviour:
- gradle-plugin becomes a multi-project build: plugwright-api holds the contract
third-party modes compile against (PlugwrightMode, EnvironmentSpec, SecretRef,
ConfigNode, RunnerPackageRef, TaskRegistrationContext), plugwright-core holds
the plugin. api has no coordinates of its own yet, so its classes are merged
into the core jar; the published artifactId changes to plugwright-core, the
plugin id and its marker do not.
- plugwrightTest writes build/tmp/plugwright/local.json and passes it as
--config. The runner resolves config in the order --config file,
plugwright.config.json, then the old env vars, so an older plugin still drives
a newer runner. Host, port, jvm args and the tests dir come from the file
instead of being hardcoded in runner.ts.
- npm install and tsc move out of the test task into plugwrightCompileTests, so
several environments can share one install.
- Process and Node.js plumbing moves to AbstractNodeTask, shared by the test,
run-server and compile-tests tasks.
Secrets travel as references ({"from":"env","name":...}) and are read by the
runner, never resolved at configuration time.
…ment/ServerConsole
Phase 2 of the multi-mode runner redesign. local mode keeps its exact
behavior (spawn Paper, wait for "Done (", stdio console, process-tree
kill guards) but now lives behind the Environment/ServerConsole
contracts instead of being runner.ts's only code path.
- lib/session.ts: Session + MessageBuffer replace the module-level
activeBots/messageBuffer/serverConsoleBuffer singletons that made it
impossible to run two environments in one process.
- lib/environment.ts, lib/console.ts: Environment and ServerConsole
interfaces.
- lib/environments/local.ts: LocalEnvironment + StdioConsole, carrying
over spawn/waitForServerStart/killServerTree/teardown unchanged.
- PlayerWrapper and ServerWrapper now hold a session reference instead
of importing module state; matchers.ts reads buffers off that
reference instead of module imports.
- testRegistry/scopeStack stay module-level (documented why in
session.ts) — still correct for one environment per process.
Public API (test/opTest/describe/expect/PlayerWrapper/ServerWrapper/
wrappers) is unchanged. Verified: tsc --noEmit clean, full build clean,
all 46 example_plugin e2e tests pass under local mode.
Phase 3 of the multi-mode architecture:
- plugwright-api: PlugwrightMode gains applyLegacyDefaults() for seeding
an implicit environment from deprecated flat properties; TaskRegistrationContext
gains environmentConfig() so a mode can hand over its runner-config node
computed lazily at task execution time. New RunDirFile and
LegacyEnvironmentProperties types.
- plugwright-core: PlugwrightExtension gains registerMode()/environments{}
DSL and primaryEnvironment; new EnvironmentContainer (mode + spec registry),
TaskRegistrationContextImpl and ValidationContextImpl. PlugwrightPlugin is
renamed PlugwrightCorePlugin and made fully mode-agnostic: it creates the
implicit 'local' environment when no environments{} block is present, then
asks each environment's mode to validate and register its own tasks.
PlugwrightTestTask no longer hardcodes local-server specifics — it just
writes whatever ConfigNode its mode produced.
- plugwright-local (new module): LocalMode + LocalEnvironmentSpec, and the
local-only tasks split out of the old monolithic task base
(PaperProvisionTask, PlugwrightCleanTask, PlugwrightRunServerTask). Also
hosts the published io.github.drownek.plugwright plugin id for now — a
dedicated bundle module can take that over once a second built-in mode
exists to combine with it.
Task names are now generated per environment (plugwrightTestLocal,
plugwrightCleanLocal, ...), with bare aliases (plugwrightTest, ...) pointing
at whatever environment is primaryEnvironment (defaults to 'local'). Builds
with no environments{} block behave exactly as before, verified by the full
example_plugin e2e suite (46/46 passing).
…environments filters
- plugwrightTest is now the matrix task: runs every environment with
includeInMatrix=true through the same RunnerLauncher as
plugwrightTest<Env>, aggregates a summary, fails the build on any
non-allowFailure environment. -Pplugwright.env=a,b narrows it.
- matrix { parallel; maxParallel } runs environments concurrently
(off by default), each with its own build/reports/plugwright/<env>.log.
- Extracted RunnerLauncher (config write + cli.js resolution) out of
PlugwrightTestTask so both task types share it.
- Runner writes build/reports/plugwright/<env>.json and junit/<env>.xml
when the config carries report paths.
- test()/opTest() accept an optional {requires, environments} filter;
skips (plus the pre-existing tests.exclude and tests.names filters,
the latter no longer silently continue) land in results/reports with
a reason instead of vanishing.
…ed tests, and cleanup journal PlugwrightPlugin contract (setup/onPlayerCreate/beforeEach/afterEach/extendContext/matchers/tests/cleanup/teardown) loaded by PluginHost from config.json's new plugins[] list. Hook order matches modes-and-plugins §6.6: plugin.beforeEach -> spec beforeEach -> body -> cleanup finalizers -> spec afterEach -> plugin.afterEach. - lib/plugin.ts: PlugwrightPlugin, definePlugin, SessionContext, CleanupContext, PluginTestRef - lib/plugin-host.ts: loads/orders plugins, merges matchers into RunnerMatchers, runs hooks - lib/account.ts: Account type + syntheticAccount() placeholder until AccountPool (phase 6) - lib/journal.ts: CleanupJournal, typed-record crash journal for TestContext.cleanup - lib/test-runner.ts: runTestCase() extracted from runner.ts, sequences all the above - test-registry.ts: TestCase now exposes raw beforeHooks/afterHooks instead of a merged fn, so plugin hooks can be interleaved with spec hooks by the caller - player.ts: join()/rejoin() fire session.onPlayerCreate on every connection - runner.ts: wires PluginHost in, runs plugin preflight tests before user specs (abort on failure) and suite tests alongside them, tagged with plugin name in TestResult/reports
…g transport Extends the mode contract so a mode can declare runner plugins to load (TaskRegistrationContext.pluginConfigs) and reach the test project's tests directory (TaskRegistrationContext.testsDir), and wires both - plus a per-environment crash-recovery journal path - into the runner config alongside the existing environment/tests/reports sections. Also adds a small secret.env(...)/secret.file(...) DSL accessor on Project, so secret references read naturally in a build script instead of the fully-qualified Secrets.env(...).
… ping and cleanup tasks
New plugwright-external module, mirroring plugwright-local's shape for
a mode that attaches to an already-running server instead of spawning
one:
- ExternalEnvironmentSpec: host/port/minecraftVersion (mandatory),
joinThrottleMs, plus nested console { rcon { ... }; adminBot(...) {
... } }, accounts { pool { ... }; autoRegister { ... }; microsoft
{ ... } } and plugins { npm(...); local(...) } blocks.
- ExternalMode: validates the spec, serializes it into the runner
config (secrets stay references), and pulls in the RCON runner
package only when a rcon console block is actually declared.
- PlugwrightPingTask (plugwrightPing<Env>) and PlugwrightCleanupTask
(plugwrightClean<Env>) run the runner in a service mode instead of
the normal test mode - reachability/auth check, and compensating
cleanup + journal replay, respectively.
…ng local + external Moves PlugwrightPlugin (the io.github.drownek.plugwright entry point) out of plugwright-local into a new plugwright-bundle module that applies the core engine and registers both built-in modes. Mirrors plugwright-local's old jar-merging trick, now pulling in api, core, local and external classes since none of them publish standalone coordinates. plugwright-local goes back to being just a mode module - no publish plugin, no plugin-id registration - matching plugwright-external's shape. Published plugin id and artifact coordinates are unchanged, so existing consumer build scripts keep working.
…le, ping/cleanup entry points
- AccountPool merges pool/autoRegister/microsoft accounts, leased per
test and released in the test's finally block. local's account
generation is unchanged: it only degrades to a pool when
Environment.accounts() is implemented, which local still doesn't do.
- externalEnvironment: attaches to a running server, probes declared
console channels in order (rcon via a dynamic import of the optional
@plugwright/console-rcon package, else admin-bot), and honours
joinThrottleMs on every bot connect via a new Environment.beforeJoin
hook.
- AdminBotConsole: a second mineflayer bot with staff rights, console
commands sent through chat, responses read from its own buffer. Its
connection goes through PlayerWrapper.join(), so it authenticates
through the same onPlayerCreate hook a test bot does - runner.ts now
wires that hook before env.setup() runs so this actually applies
during environment setup, not just afterward.
- resolveEnvironment is now async and falls back to a dynamic
import(runtime.package) for any mode besides the two built-ins.
- runPingSession/runCleanupSession + cli.ts --ping/--cleanup: connect
and verify the console/auth without running tests, or replay the
crash-recovery journal via each plugin's cleanup({ scope: 'manual' })
handler.
A task a mode registers through TaskRegistrationContext never got nodeVersion, downloadNode or nodeInstallDir, so any of them extending AbstractNodeTask failed validation before running.
AccountPool resolved every password in its constructor, so a run that never connects a bot — a cleanup pass, a console-only ping — died on an unset variable it had no use for. The ping and cleanup entry points also relied on an unref'd timer to set the exit code, which never fires when nothing else holds the event loop open, so a failed check reported success.
…ket decode error mineflayer's default logErrors:true does an unconditional console.log(err) on every bot 'error' event. A backend sending a packet type outside the client's protocol data (e.g. an unrecognised particle) can emit that error hundreds of times a second; logging each one synchronously starves the event loop and the piped stdout, so timers that would otherwise fail a test fast stop firing in any useful time. Disable mineflayer's built-in logging and replace it with a throttled one (max once per second) that still reports total error count, keeping the connection usable against a server that outruns minecraft-data's coverage instead of hanging tests until their own timeout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Upstream runs `plugwrightNpmInstall` after an IDEA sync, so a fresh checkout has
its `node_modules` before anyone opens a spec file and finds every import
unresolved. Splitting the plugin across modules moved the code that did it and
merged that task into `plugwrightCompileTests`, which would have dropped the
feature without anyone deciding to.
It now hangs off `PlugwrightCorePlugin` and triggers the compile task, which
installs and compiles in one step — so a sync leaves the workspace in a better
state than it did before rather than the same one.
Still guarded by `plugins.withId("idea")`: `idea-ext` is what carries
`afterSync`, and applying it unconditionally would push a plugin onto builds that
never asked for one. The plugin marker it compiles against comes from the Gradle
Plugin Portal, which the root build now lists alongside Maven Central.
IntelliJ applies the `idea` plugin to an already-evaluated project during
sync, so the plugins.withId("idea") callback ran too late for
Project.afterEvaluate and the sync failed with "Failed to apply plugin
'org.gradle.idea': Cannot run Project.afterEvaluate(Action) when the
project is already evaluated".
Register the afterSync trigger straight away when the project is already
evaluated, and keep the deferred path for the normal case.
Applying `kotlin-dsl` from every subproject's plugins block loaded the Kotlin plugin several times, which Gradle warns is unsupported: "The Kotlin Gradle plugin was loaded multiple times in different subprojects ... ':plugwright-api', ':plugwright-bundle'". Declare it once in the root build with `apply false` and hand it to the subprojects from the shared subprojects block.
feat: modes, environments, and a plugin host
Implements ServerConsole over the Source RCON protocol directly on
top of net.Socket - no third-party dependency, and kept out of
@drownek/plugwright's own dependency list since it's only needed when
a build script declares console { rcon { ... } }.
executeAndWait resolves from the server's own response packet, so it
doesn't need the minecraft:say <syncId> round-trip the stdio and
admin-bot consoles rely on.
@plugwright/auth-authme: on every bot connection - initial join, rejoin, and the external mode's admin-bot console, all of which go through the same onPlayerCreate hook - waits for the login or register prompt and answers it: /register <pass> <pass> for a freshly generated account (account.justCreated), /login <pass> otherwise. Commands and prompt/success patterns are configurable options; Microsoft accounts are skipped since AuthMe never prompts them. Ships a preflight spec (auth.spec.js) so a broken login flow surfaces as a named failure at the top of the report instead of buried in the first user test that happens to create a bot.
runnerPackages() was declared by every mode and read by nobody, so an optional runner package such as the RCON console could never actually reach the test project. plugwrightCompileTests now installs the ones missing from node_modules, merged across environments, and only warns when an install fails: the runner already reports the missing package with the context to fix it.
A dynamic import of a package this one doesn't depend on — an optional console, a third-party mode, a plugin — resolved relative to the runner's own location, which finds nothing when the runner is a linked checkout instead of an entry in the test project's node_modules. Fall back to resolving from the test project, and include the underlying error in the missing-package message.
PluginsSpec was in plugwright-external, so `plugins { npm(...) }` only
existed on external environments. Nothing about a runner plugin is
mode-specific: a local server running an authentication plugin needs the
login hook exactly as much as a remote one does. Move the spec into
plugwright-api and give LocalEnvironmentSpec the same block.
An npm-named plugin now also joins the environment's runner packages, so
plugwrightCompileTests installs it instead of leaving the runner to fail
on a package nobody fetched. A plugin given as a path is left alone.
The runner resolves `local` and `external` by mode id, since both are compiled into it. Anything else has to be imported from a package, and the config file had no field saying which one — so a custom mode always died with "mode X, which this runner cannot run yet". The first RunnerPackageRef naming an export is that package, so write it into environment.runtime for every mode but the two built-in ones. Also fixes the misspelled "Environment sumarries" header.
join() waited for the spawn event and only then fired onPlayerCreate. A server with a login wall never spawns an unauthenticated player, so the hook that would have logged the bot in never ran and every test died on a 30s spawn timeout. Wait for the play state instead, run the hook there, then wait for the spawn. Message listeners now go up before the first await as well: the login prompt arrives immediately, and a prompt that lands before the buffer exists is one no authentication plugin can answer.
makeOp waited for "Made X a server operator" in the player's chat, and deOp waited for a `say` marker to come back. Both assume the whole server log reaches the bot, which is true for the stdio console and false for RCON: the answer goes back over the RCON socket, so every op-dependent test timed out against an external server. When the console returns responses, run the command through it and read the answer. Also expose executeAndWait on ServerWrapper, and report op: true for an external environment once a console channel answers — having a console is what being able to op means.
requires: ['console'] is satisfied by an RCON console, which answers its
own commands and nothing else. Reading the server log needs more than
that, so a log matcher on such an environment neither skipped nor worked
— it timed out after the full assertion timeout with no explanation.
requires now accepts 'key:value' ('consoleOutput:full'), and the server
log matcher fails immediately with the level it found and the requires
clause that would have skipped it.
The runner takes a --config file and needs no Gradle, but there was no bin entry, so `npx plugwright --config …` did not resolve to anything.
account.justCreated said whether to register or log in. It is a hint from the account pool and it is wrong the moment a pool account outlives the run that created it, which is the second run against any stand: the plugin sent /register for an account the server already knew. Wait for either prompt and answer the one that arrived, register first since AuthMe's register prompt mentions the password too. Match only messages newer than the step they belong to — a greeting with "welcome" in it was passing for a login confirmation, and tests started before the player could run a command. After registering, wait for the login AuthMe performs itself, or send it when it doesn't. Adds a `password` option for accounts an environment invents rather than leases, which is how the local mode names its throwaway bots.
Five pages for what the last phases added: how environments and modes relate and what tasks each produces, what changes when the server isn't yours, the runner plugin contract, the report formats, and a guide for writing a mode of your own. Configuration keeps its flat-property reference and gains the environments block that supersedes it; test filtering gains the capability and environment filters. READMEs updated to match.
The example described one implicit local server. It now declares two environments explicitly: `local`, which downloads Paper and installs AuthMe next to the plugin under test, and `stand`, which connects to a server started by hand from the same run directory and leaves it running. `local` writes an AuthMe config a bot can get through — the stock one asks for the password in a dialog, allows one registration per IP, and treats a test suite as a bot attack — and logs every bot in through @plugwright/auth-authme. `stand` leases four accounts from a pool, reaches the console over RCON, and resets op and inventory between tests with a local plugin, since a leased account carries over whatever the last test left on it. What it cannot reset is excluded by name; the one test that reads the server log now says requires: ['consoleOutput:full'] and skips there instead. 47 pass on local, 33 pass and 14 skip on the stand.
'publish' from maven-publish only reached the private repo; the portal side lived under plugin-publish's publishPlugins and never joined it. Wire both publishToPrivateRepository and publishToPublicRepository onto publish, so it is the one command a release runs, still respecting each side's enabled switch. Update release.yml to call ./gradlew publish instead of hardcoding publishToPublicRepository.
[v3] fix(gradle-plugin): make publish run both private and public repos
* feat(runner): cache Microsoft profile/certificates per account minecraft-protocol's built-in 'microsoft' auth refetches profile and certificates on every connect, no caching of their own. Every test gets its own bot connection, so a microsoft-auth account redoes that on every single test; enough tests in one run and one of those calls eventually hits a rate limit and fails a perfectly fine account. Add a custom auth function (microsoft-auth.ts) that mirrors minecraft-protocol's own microsoftAuth.authenticate but caches fetchProfile/fetchCertificates results per username for the life of the process. The access token itself is untouched — still fetched per connect via prismarine-auth's own disk cache, which is already cheap. Needs prismarine-auth as a direct dependency now (was only transitive through mineflayer) to call Authflow.getMinecraftJavaToken directly. * fix(runner): route microsoft accounts through cached auth Session.createBot passed auth straight through as a string. Swap it for the cached custom auth function (microsoft-auth.ts) whenever the account is microsoft-auth; offline/mojang accounts are unaffected. Fixes #69 * fix(runner): fix CJS named import of prismarine-auth under ESM 'import { Authflow, Titles } from prismarine-auth' compiled fine but crashed at runtime: 'SyntaxError: The requested module prismarine-auth does not provide an export named Titles'. prismarine-auth is CJS; Node's cjs-module-lexer interop for named ESM imports of a CJS module isn't reliable here — it missed Titles even though both are plain properties of module.exports. Import the default (the whole module.exports object) instead and destructure from that — the default import always works for CJS interop. Verified locally: typecheck, build, then imported the compiled dist/lib/microsoft-auth.js directly with node. Caught only in CI, not by tsc, since this is a runtime ESM/CJS interop quirk typecheck can't see; this package has no local e2e harness to catch it earlier. * chore: retrigger CI (check stand-suite RCON flake) * fix(rcon): queue RCON commands to prevent server disconnects from concurrent packets * fix(rcon): remove sentinel strategy to prevent server disconnects * fix(runner): set process.exitCode in runTestSession so test failures fail the build --------- Co-authored-by: Drownek <piotrgamingyt@gmail.com>
…e legacy abstractions (#73) * refactor(core): streamline v3 architecture (RCON runner, remove journal, admin-bot & abilities) * fix(gradle-plugin): remove dead AdminBot config & pass RCON config dynamically * fix(rcon): handle authentication failure cleanly & export RconConnection * fix(runner): properly close RCON socket on environment teardown * test(e2e): await floating server.execute promises * docs: remove outdated references to abilities, journal, admin bot and cleanup task * chore: update lockfiles * docs: fix remaining outdated references (await server.execute, AdminBot) * refactor(core): merge console-rcon package into runner * fix: remove remaining references to console-rcon * refactor(gradle-plugin): scope useExternalPluginsOnly to LocalMode Removes deprecated global extension.useExternalPluginsOnly check from PlugwrightCorePlugin and resolves pluginJar dynamically in LocalMode based on the environment spec, preserving backward compatibility while enabling per-environment configuration in v3. * refactor: migrate requires from string array to typed object map * refactor: remove redundant freshState, lifecycle, and arbitraryUsernames capabilities
…ute, and standardize item accessors
- Remove deprecated GUI methods (waitForGuiItem, clickGuiItem, etc.) - Fix nextId overflow in RCON connection - Stop swallowing RCON probe errors - Fix Minecraft version and remove dev branch in CI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Executive Summary
Plugwright 3.0.0 moves the framework from a single-threaded harness for ephemeral local Paper servers to a multi-environment matrix testing platform for the Minecraft server ecosystem.
This release adds support for testing existing external servers and QA staging stands (
ExternalMode), native bot concurrency for detecting race conditions and item duplication glitches (concurrency: N), stateful sequential test workflows (describe.serial), an extensible Runner Plugin architecture with an official@plugwright/auth-authmeimplementation, a complete modularization of the Gradle plugin into specialized subprojects (api,core,local,external,bundle), and the unification of the npm ecosystem under the official@plugwright/runnerorganization scope.Migrating from v2 requires minimal work. The Gradle plugin automatically migrates the workspace layout on first run, and the legacy flat
plugwright { ... }block inbuild.gradle.ktsremains fully backward compatible.Architectural Overview
flowchart LR subgraph BUILD["🔧 GRADLE / BUILD ORCHESTRATION (Kotlin)"] direction LR API["`**plugwright-api** PlugwrightMode<S> EnvironmentSpec SecretRef`"] CORE["`**plugwright-core** PlugwrightCompileTestsTask PlugwrightMatrixTask NodeManager (Locks & SHA256) NpmrcWriter`"] LOCAL["`**plugwright-local** LocalMode, Provision Clean, RunServer`"] EXTERNAL["`**plugwright-external** ExternalMode, Ping Accounts, RCON Spec`"] BUNDLE["`**plugwright-bundle** id 'io.github.drownek.plugwright' (Fat-JAR)`"] API --> CORE CORE --> LOCAL CORE --> EXTERNAL LOCAL --> BUNDLE EXTERNAL --> BUNDLE end subgraph RUNTIME["🧪 TEST RUNNER & RUNTIME (Node.js/TS)"] direction LR RUNNER(["@plugwright/runner"]) SESSION["`**Session** - Environment (Local / External / Custom) - Source RCON Engine (TCP Socket) - Isolated AccountPool & BotScope - Append-Only Console Buffer`"] HOST["`**PluginHost** - Lifecycle Hooks (onPlayerCreate, etc.) - Context Extensions & Custom Matchers`"] DSL["`**TestRunner Engine & DSL** - describe.serial (persistent session) - concurrency: N (race conditions) - JSON & JUnit XML CI Reporters`"] RUNNER --> SESSION --> HOST --> DSL end BUNDLE -. "JSON Config Transport" .-> DSL %% --- Styling --- classDef container fill:#0d1117,stroke:#58a6ff,stroke-width:1.5px,color:#c9d1d9 classDef node fill:#161b22,stroke:#30363d,stroke-width:1px,color:#c9d1d9,text-align:left classDef entry fill:#161b22,stroke:#8b949e,stroke-width:1.5px,color:#c9d1d9 classDef artifact fill:#1c1608,stroke:#f0883e,stroke-width:2px,color:#f0d9b5,font-weight:bold class RUNTIME,BUILD container class SESSION,HOST,CORE,LOCAL,EXTERNAL,API node class RUNNER entry class DSL,BUNDLE artifact linkStyle default stroke:#8b949e,stroke-width:1.5pxWhat's New in v3.0
1. Multi-Environment DSL & Mode Architecture (
PlugwrightMode)Tests are no longer tied to a single local Paper instance. In v3, test execution is abstracted around Environments backed by a pluggable Mode:
LocalMode: The classic developer workflow. It provisions PaperMC via the official API, configuresserver.properties, accepts the EULA, tunes anti-cheat thresholds inspigot.yml, provisions plugins, and launches the server using the configured Gradle Java Toolchains. Commands are routed via RCON with immediate synchronous output correlation, while server stdout is streamed to the console buffer (consoleOutput: 'full').ExternalMode: Target pre-existing, long-running servers (remote staging stands, QA clusters, Docker Compose stacks, or network proxies such as Velocity/BungeeCord):accounts { pool / autoRegister / microsoft }).joinThrottleMs) to bypass anti-bot heuristics../gradlew plugwrightPing<Env>(verifies game port reachability, RCON authentication, and bot login within seconds, without running the test suite).PlugwrightMode<S : EnvironmentSpec>fromplugwright-api(e.g. for Kubernetes runners or hybrid server environments).2. Race Condition & Concurrency Testing (
concurrency: N)Critical Minecraft server bugs, such as item duplication, race conditions when looting chests, or double-spending in economy shops, only show up under concurrent player traffic.
Plugwright v3 introduces first-class declarative concurrency:
BotScope) on independently leased bot accounts. Console logs are prefixed with instance tags ([1/5]..[5/5]).concurrency <= pool.capacity(), so a shortage of account slots fails fast instead of hanging the suite.player.messageBufferis strictly private per bot. Server console log assertions viaServerWrapperoperate on an immutable, cursor-based index (startIndex), which keeps instances from racing each other.PASSED [5/5], and the JSON report includes a full per-instance breakdown.3. Stateful Multi-Step Tests:
describe.serialStandard tests run in total isolation with fresh connections, but testing lifecycles, like kit cooldowns, multi-stage quest progression, or auction flows, needs persistent state. In v3,
describe.serialprovides this natively:requires,environments) applies atomically to the entire block.SKIPPED(serial block stopped at step X), instead of failing outward into a string of unrelated-looking errors.4. Runner Plugin Ecosystem & Plugin-Host
A modular Node.js/TypeScript plugin engine managed by
PluginHost:setup,onPlayerCreate,beforeEach,afterEach(executed in LIFO order),extendContext,cleanup, andteardown.onPlayerCreate), Not a Test: Login handshakes fire on every physical TCP connection, including helper bots spawned viacreatePlayer()and reconnects viaplayer.rejoin(), before the test receives the player handle.preflighttests (which halt the run if server infrastructure is broken) or normalsuitetests.@plugwright/auth-authmePackage:/registerand/loginprompts.sessionResumedPattern) to prevent hanging timeouts during reconnects.player.chat(cmd, { secrets: [pass] })automatically masks credentials as[REDACTED]in logs.5. Identity, Account Pools & Credential Safety
AccountPool:pool: Preconfigured list of static credentials.autoRegister: Dynamic username generation using templates (pw_%04dfor recyclable slots,pw_%sfor disposable accounts).microsoft: Online-mode accounts powered byprismarine-auth.microsoftAuthWithCachecaches player profiles and certificates in process memory, which stops HTTP 429 rate-limiting from Mojang/Xbox Live APIs when bots connect rapidly across multiple test specs.SecretRef):secret.env("KEY")orsecret.file("path").build/tmp/.NpmSpec/NpmrcWriter):npm { registry(...) { authToken(...) }; scope(...) }block generates an ephemeral.npmrcfile with strict0600permissions right before runningnpm install.6. In-House Source RCON Engine
Replaced unreliable third-party npm packages and fragile
/say <marker>stdout hacks with a dedicated TypeScript Source RCON engine (runner-package/lib/rcon/):_commandQueue) stops responses from interleaving during rapid asynchronous execution.const output = await server.execute('eco take ...').consoleOutput):'full': Paper stdout inLocalMode. Supports fullexpect(server).toHaveReceivedMessage(...)assertions.'responses': RCON inExternalMode. Supports command execution and reading command responses. Tests that need full server stdout are cleanly skipped (SKIPPED: requires capability [consoleOutput:full]).7. Gradle Plugin Modularization & System Reliability
The Gradle plugin monorepo is split into 5 subprojects:
plugwright-api: Pure API interfaces and contracts (PlugwrightMode,EnvironmentSpec,PlugwrightLayout,ConfigNode,SecretRef).plugwright-core: Core orchestration engine, IntelliJ IDEAafterSynchooks, TypeScript compilation (PlugwrightCompileTestsTask), and matrix orchestration (PlugwrightMatrixTask).plugwright-local:LocalModetasks (PaperProvisionTask,PlugwrightCleanTask,PlugwrightRunServerTask).plugwright-external:ExternalModeintegration, account specs, and thePlugwrightPingTask.plugwright-bundle: The unified distribution fat-jar published asid("io.github.drownek.plugwright").Platform & CI Hardening:
NodeManager: Concurrent node downloads are guarded by file locks (RandomAccessFile.channel.lock()), SHA-256 checksum verification against nodejs.org, and path traversal / Zip-Slip protection.^characters when invokingcmd.exe, which fixes version range parsing failures in npm commands on Windows.killProcessTree).Breaking Changes & Feature Matrix
@drownek/plugwright@plugwright/runnerimport { test } from '@drownek/plugwright'import { test, describe, expect } from '@plugwright/runner'*.spec.tsinsrc/test/e2e/roottests/, plugins inplugins/, build indist/(Auto migration on the first run)./run/generated/<env>/run/server.execute()Promise<string>(awaitrequired)item.getDisplayName()item.displayName(anditem.lore) (item.getDisplayName()still available)await player.clearInventory()with packet syncplugwright { minecraftVersion.set(...) }environments.create("local", LocalMode) { ... }(legacy block remains supported with deprecation notice)Step-by-Step Upgrade Guide (v2 to v3)
Step 1: Update Gradle Plugin Version in
build.gradle.ktsplugins { id("io.github.drownek.plugwright") version "3.0.0" }Step 2: Update Dependency in
src/test/e2e/package.jsonReplace
@drownek/plugwrightwith@plugwright/runner:Step 3: Update Test Spec Imports
Update the import paths across your TypeScript test files:
Step 4: Run Tests & Automatic Workspace Migration
Execute the test task:
Note
Zero-Config Workspace Migration: On first run, Plugwright detects the v2 layout, creates
tests/, moves existing*.spec.tsfiles into it (preserving subdirectory structures), and updates theincludeglob intsconfig.json.Step 5: Update
src/test/e2e/.gitignoreMake sure generated runtime paths are ignored:
Step 6: Add
awaittoserver.execute()CallsReview test files and prefix server command executions with
await:Release Tooling & CI Automation
scripts/bump-version.js: Synchronizes versions across all monorepo npm packages (runner-package,auth-authme-package), updatesversion.txt, and refreshes package lockfiles.scripts/publish.js: Publishes npm packages in dependency order with support for npmjs provenance (--provenance) and private registries (Nexus, Artifactory), using Base64 Basic_authand self-destructing temporary configuration files.build/reports/plugwright/<env>.json: Machine-readable report detailing test durations, skip reasons (skipReason), and per-instance concurrency metrics.build/reports/plugwright/junit/<env>.xml: Standard JUnit report with accurate<skipped>and<failure>nodes.