Skip to content

Plugwright v3.0.0 - #76

Merged
Drownek merged 139 commits into
masterfrom
v3-dev
Sep 12, 2026
Merged

Drownek merged 139 commits into
masterfrom
v3-dev

Conversation

@Drownek

@Drownek Drownek commented Sep 12, 2026

Copy link
Copy Markdown
Owner

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-authme implementation, 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/runner organization scope.

Migrating from v2 requires minimal work. The Gradle plugin automatically migrates the workspace layout on first run, and the legacy flat plugwright { ... } block in build.gradle.kts remains 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.5px
Loading

What'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, configures server.properties, accepts the EULA, tunes anti-cheat thresholds in spigot.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):
    • Operates non-invasively without touching server disk files.
    • Manages player credentials via an account pool (accounts { pool / autoRegister / microsoft }).
    • Configurable join rate limiting (joinThrottleMs) to bypass anti-bot heuristics.
    • New instant diagnostic task: ./gradlew plugwrightPing<Env> (verifies game port reachability, RCON authentication, and bot login within seconds, without running the test suite).
  • Custom Modes: Community-extensible by implementing PlugwrightMode<S : EnvironmentSpec> from plugwright-api (e.g. for Kubernetes runners or hybrid server environments).
// build.gradle.kts
import me.drownek.plugwright.api.secret
import me.drownek.plugwright.local.LocalMode
import me.drownek.plugwright.external.ExternalMode

plugwright {
    matrix {
        parallel.set(true)
        maxParallel.set(2)
    }

    environments {
        // 1. Managed local Paper instance
        create("local", LocalMode) {
            minecraftVersion.set("1.21.4")
            acceptEula.set(true)
        }

        // 2. Remote staging stand
        create("staging", ExternalMode) {
            host.set("staging.server.net")
            port.set(25565)
            minecraftVersion.set("1.21.4")
            joinThrottleMs.set(1000)
            includeInMatrix.set(false) // Run explicitly in dedicated CI jobs

            console {
                rcon {
                    port.set(25575)
                    password.set(secret.env("STAND_RCON_PASSWORD"))
                }
            }
            accounts {
                autoRegister {
                    usernamePattern.set("pw_%04d")
                    password.set(secret.env("STAND_BOT_PASSWORD"))
                    max.set(4)
                }
            }
            plugins {
                npm("@plugwright/auth-authme")
                local("stand-reset")
            }
        }
    }
}

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:

test('only one player can loot the treasure chest', { concurrency: 5 }, async ({ player }) => {
    player.chat('/lootchest claim');
    await expect(player).toHaveReceivedMessage(/Claimed reward|Chest already looted/);
});
  • Fanning-Out & Aggregation: The runner fans out $N$ concurrent instances (BotScope) on independently leased bot accounts. Console logs are prefixed with instance tags ([1/5]..[5/5]).
  • Fail-Fast Capacity Validation: Before the test suite runs, the runner checks that concurrency <= pool.capacity(), so a shortage of account slots fails fast instead of hanging the suite.
  • Isolated Message & Log Buffers: player.messageBuffer is strictly private per bot. Server console log assertions via ServerWrapper operate on an immutable, cursor-based index (startIndex), which keeps instances from racing each other.
  • Aggregated Reporting: A concurrent test appears as a single row in summaries. Its duration is the slowest instance's duration, its status is PASSED [5/5], and the JSON report includes a full per-instance breakdown.

3. Stateful Multi-Step Tests: describe.serial

Standard 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.serial provides this natively:

describe.serial('starter kit cooldown lifecycle', () => {
    test('player claims starter kit', async ({ player }) => {
        player.chat('/kit starter');
        await expect(player).toHaveReceivedMessage('Received starter kit');
    });

    test('immediate subsequent claim triggers cooldown warning', async ({ player }) => {
        player.chat('/kit starter');
        await expect(player).toHaveReceivedMessage(/Please wait \d+s/);
    });
});
  • Single Persistent Connection: The same bot and account keep their server connection across the entire sequence.
  • Step Cleanliness: Between steps, the runner clears the player message buffer and advances the server console cursor, so each step's assertions only see events that step triggered.
  • The Block is the Unit:
    • Filtering (requires, environments) applies atomically to the entire block.
    • Cascading Skip: If a step fails or times out, every remaining step in the block is immediately marked 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:

  • Complete Lifecycle: Hooks for setup, onPlayerCreate, beforeEach, afterEach (executed in LIFO order), extendContext, cleanup, and teardown.
  • Authentication is a Hook (onPlayerCreate), Not a Test: Login handshakes fire on every physical TCP connection, including helper bots spawned via createPlayer() and reconnects via player.rejoin(), before the test receives the player handle.
  • Plugin-Supplied Tests: Plugins can declare preflight tests (which halt the run if server infrastructure is broken) or normal suite tests.
  • Official @plugwright/auth-authme Package:
    • Regex detection of /register and /login prompts.
    • Session resumption detection (sessionResumedPattern) to prevent hanging timeouts during reconnects.
    • Handles login wall enforcement for Microsoft online-mode accounts (Issue [v3] Microsoft account pool bug #65).
    • Native password redaction: player.chat(cmd, { secrets: [pass] }) automatically masks credentials as [REDACTED] in logs.
// plugins/stand-reset.ts
import { definePlugin, expect } from '@plugwright/runner';

export default definePlugin({
    name: 'stand-reset',
    async beforeEach({ player, server }) {
        await player.deOp();
        await player.clearInventory();
        await server.execute(`eco set ${player.username} 1000`);
    },
});

5. Identity, Account Pools & Credential Safety

  • Flexible AccountPool:
    • pool: Preconfigured list of static credentials.
    • autoRegister: Dynamic username generation using templates (pw_%04d for recyclable slots, pw_%s for disposable accounts).
    • microsoft: Online-mode accounts powered by prismarine-auth.
  • In-Memory Microsoft Auth Cache (Fixes Issue [v3] Repeated Microsoft profile/certificate fetch causes intermittent auth failures in long runs #69): microsoftAuthWithCache caches 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.
  • Zero-Leakage Secrets (SecretRef):
    • Passwords and tokens are specified as references: secret.env("KEY") or secret.file("path").
    • Values are resolved lazily at execution time inside the Node.js process. Secrets never leak into Gradle Configuration Cache entries or files under build/tmp/.
  • Private npm Registries (NpmSpec / NpmrcWriter):
    • The npm { registry(...) { authToken(...) }; scope(...) } block generates an ephemeral .npmrc file with strict 0600 permissions right before running npm 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/):

  • Pure TCP binary encoding and decoding adhering to the Source RCON specification.
  • A request queue (_commandQueue) stops responses from interleaving during rapid asynchronous execution.
  • Direct output return: const output = await server.execute('eco take ...').
  • Transparent reconnects upon socket timeouts.
  • Console Capability Granularity (consoleOutput):
    • 'full': Paper stdout in LocalMode. Supports full expect(server).toHaveReceivedMessage(...) assertions.
    • 'responses': RCON in ExternalMode. 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:

  1. plugwright-api: Pure API interfaces and contracts (PlugwrightMode, EnvironmentSpec, PlugwrightLayout, ConfigNode, SecretRef).
  2. plugwright-core: Core orchestration engine, IntelliJ IDEA afterSync hooks, TypeScript compilation (PlugwrightCompileTestsTask), and matrix orchestration (PlugwrightMatrixTask).
  3. plugwright-local: LocalMode tasks (PaperProvisionTask, PlugwrightCleanTask, PlugwrightRunServerTask).
  4. plugwright-external: ExternalMode integration, account specs, and the PlugwrightPingTask.
  5. plugwright-bundle: The unified distribution fat-jar published as id("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.
  • Windows CLI Fixes: The plugin escapes ^ characters when invoking cmd.exe, which fixes version range parsing failures in npm commands on Windows.
  • Graceful Shutdown: The runner recursively terminates child process trees via the Java 9+ ProcessHandle API (killProcessTree).
  • Mineflayer Error Throttling: Network packet error spam is throttled to 1 message/sec, which stops the Node.js event loop from starving in CI.

Breaking Changes & Feature Matrix

Area Before (v2.x) After (v3.0.0)
NPM Package @drownek/plugwright @plugwright/runner
Spec Imports import { test } from '@drownek/plugwright' import { test, describe, expect } from '@plugwright/runner'
Directory Layout *.spec.ts in src/test/e2e/ root Specs in tests/, plugins in plugins/, build in dist/ (Auto migration on the first run)
Server Run Directory Root ./run/ Per-environment: generated/<env>/run/
server.execute() Synchronous fire-and-forget Asynchronous returning Promise<string> (await required)
GUI Item API item.getDisplayName() Property getter item.displayName (and item.lore) (item.getDisplayName() still available)
Inventory Cleaning Manual chat commands Built-in await player.clearInventory() with packet sync
Gradle Configuration Flat plugwright { minecraftVersion.set(...) } DSL 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.kts

plugins {
    id("io.github.drownek.plugwright") version "3.0.0"
}

Step 2: Update Dependency in src/test/e2e/package.json

Replace @drownek/plugwright with @plugwright/runner:

"@plugwright/runner": "^3.0.0"

Step 3: Update Test Spec Imports

Update the import paths across your TypeScript test files:

// Before:
import { test, expect } from '@drownek/plugwright';

// After:
import { test, expect, describe } from '@plugwright/runner';

Step 4: Run Tests & Automatic Workspace Migration

Execute the test task:

./gradlew plugwrightTest

Note

Zero-Config Workspace Migration: On first run, Plugwright detects the v2 layout, creates tests/, moves existing *.spec.ts files into it (preserving subdirectory structures), and updates the include glob in tsconfig.json.

Step 5: Update src/test/e2e/.gitignore

Make sure generated runtime paths are ignored:

node_modules
dist
generated
.npmrc

Step 6: Add await to server.execute() Calls

Review test files and prefix server command executions with await:

// v3 syntax:
const output = await server.execute(`op ${player.username}`);

Release Tooling & CI Automation

  • scripts/bump-version.js: Synchronizes versions across all monorepo npm packages (runner-package, auth-authme-package), updates version.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 _auth and self-destructing temporary configuration files.
  • CI Test Reporting:
    • 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.

monikon22 and others added 30 commits August 22, 2026 16:51
…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.
Drownek and others added 28 commits September 3, 2026 18:48
'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
- 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
@Drownek
Drownek merged commit 124e075 into master Sep 12, 2026
3 checks passed
@Drownek
Drownek deleted the v3-dev branch September 12, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants