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
89 changes: 89 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: E2E (plugwright)

# In-game end-to-end tests via plugwright (boots a real Paper server + Mineflayer bot) —
# the layer the Maven/MockBukkit unit tests can't reach. Run ON DEMAND as a regression check.
#
# Trigger it from the GitHub UI (Actions tab -> "E2E (plugwright)" -> "Run workflow", pick the
# branch/tag) or from the CLI: gh workflow run e2e.yml --ref <branch-or-tag>
#
# It is deliberately NOT wired to every push/PR: a run boots a server and generates worlds
# (~1-2 min) and is heavier/flakier than the unit tests. For a nightly regression, add:
# schedule:
# - cron: "0 3 * * *"
on:
workflow_dispatch:
inputs:
test_names:
description: "Only run tests whose name contains this (comma-separated substrings). Blank = all."
required: false
default: ""

jobs:
e2e:
name: E2E tests
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Set up Gradle (with caching)
uses: gradle/actions/setup-gradle@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # v4.4.4

# Reuse the Paper jar + downloaded libraries and the BentoBox/BSkyBlock jars across runs.
# plugwright's clean step preserves server.jar / cache / libraries, and build.gradle.kts
# caches the dependency jars in .deps, so this avoids re-downloading ~60 MB every run.
- name: Cache Paper + dependency jars
uses: actions/cache@v4
with:
path: |
e2e/.deps
e2e/run/server.jar
e2e/run/cache
e2e/run/libraries
key: plugwright-${{ hashFiles('e2e/build.gradle.kts') }}

- name: Cache npm modules
uses: actions/cache@v4
with:
path: e2e/src/test/e2e/node_modules
key: e2e-npm-${{ hashFiles('e2e/src/test/e2e/package.json') }}

- name: Build Challenges jar (Maven)
run: mvn -B -DskipTests package

- name: Run E2E tests
working-directory: e2e
env:
PLUGWRIGHT_DEBUG: "1"
# Bind the user-controlled input to an env var and reference it as a shell variable
# below — never interpolate ${{ inputs.* }} directly into a run block (script injection).
TEST_NAMES: ${{ inputs.test_names }}
run: |
if [ -n "$TEST_NAMES" ]; then
./gradlew plugwrightTest "-PtestNames=$TEST_NAMES" --no-daemon
else
./gradlew plugwrightTest --no-daemon
fi

# Always upload the server log + any plugwright report so a regression is debuggable.
- name: Upload server log & report
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-logs
path: |
e2e/run/logs/**
e2e/run/plugwright-report/**
if-no-files-found: ignore
retention-days: 14
11 changes: 11 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Plugwright server run directory (Paper jar, worlds, downloaded plugins, logs)
run/
# Cached dependency jars (BentoBox, BSkyBlock) downloaded by build.gradle.kts
.deps/
# Gradle
.gradle/
build/
# Node / TypeScript test artifacts
src/test/e2e/node_modules/
src/test/e2e/dist/
src/test/e2e/package-lock.json
48 changes: 48 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# End-to-end (in-game) tests — plugwright spike

This is a spike using [plugwright](https://github.com/Drownek/plugwright) to verify Challenges
**in a real running server** — something the JUnit/MockBukkit unit tests can't do. Plugwright boots
a real Paper server, loads the plugins, and drives a headless [Mineflayer](https://github.com/PrismarineJS/mineflayer)
bot that runs commands and clicks GUIs, then asserts on what the bot sees.

It is a **separate Gradle sidecar** — it does not touch the Maven build. It deploys *prebuilt* jars:

- **BentoBox 3.14.0** and **BSkyBlock 1.20.0** are downloaded (cached in `.deps/`).
- The **Challenges** jar is taken from `../target/` (build it first with `mvn -DskipTests package`).

> **Gotcha that cost most of the spike:** BentoBox *addons* (BSkyBlock, Challenges) must be staged
> into `plugins/BentoBox/addons/`, **not** `plugins/`. In `plugins/` they load as bare Paper plugins
> and never register as gamemodes, so no `bskyblock_world` is created and every `/island`,
> `/bsbadmin`, `/challenges` command comes back "Unknown". See `build.gradle.kts`.

## Running locally

```bash
# 1. Build the Challenges jar (Maven)
mvn -DskipTests package

# 2. Run the E2E tests (needs Java 21, Node 18+)
cd e2e
JAVA_HOME=/path/to/jdk-21 ./gradlew plugwrightTest

# add PLUGWRIGHT_DEBUG=1 to see every message the bot receives
```

First run downloads Paper 1.21.11 (~50 MB) and the plugin jars; later runs reuse them
(`run/server.jar`, `.deps/`). A full run is ~30–40s (server boot + world gen dominate).

## What the tests cover (`src/test/e2e/challenges.spec.ts`)

- **`bot can interact with the server`** — smoke test: bot joins Paper 1.21.11, ops, runs a command,
receives the reply. Proves the whole stack (BentoBox 3.14.0 + BSkyBlock + Challenges 1.8.0) loads
and is drivable.
- **`confirmation prompts tell the player how to answer (#329)`** — opens the Challenges admin GUI,
clicks "Challenge Wipe", and asserts the bot receives the confirmation instruction line
("Type 'confirm' ... or 'cancel' ...") added in #415. A real in-game validation of a 1.8.0 feature.

## Notes / next steps

- Tests requiring an **island** (block/biome/completion flows) still need island bootstrap — either
a bot-driven `/island create` (blueprint GUI) or a pre-staged world via `writeFiles`.
- In CI this runs via `.github/workflows/e2e.yml` (manual `workflow_dispatch` for now — advisory,
not a required check).
57 changes: 57 additions & 0 deletions e2e/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import java.net.URI

plugins {
// Plugwright: boots a real Paper server, deploys plugins, drives Mineflayer bots.
id("io.github.drownek.plugwright") version "2.0.2"
}

// --- Dependency jars -------------------------------------------------------------------------
// BentoBox is a Paper plugin (goes in plugins/). BSkyBlock and Challenges are BentoBox *addons*
// and MUST live in plugins/BentoBox/addons/ so BentoBox loads them as gamemode/addon (creating
// the gamemode world and registering commands) — dropping them straight into plugins/ makes them
// load as bare Paper plugins that never register with BentoBox.

val depsDir = layout.projectDirectory.dir(".deps").asFile.apply { mkdirs() }

fun cachedJar(name: String, url: String): File {
val f = File(depsDir, name)
if (!f.exists()) {
logger.lifecycle("plugwright: downloading $name ...")
URI(url).toURL().openStream().use { input -> f.outputStream().use { input.copyTo(it) } }
}
return f
}

val bentoboxJar = cachedJar(
"BentoBox-3.14.0.jar",
"https://github.com/BentoBoxWorld/BentoBox/releases/download/3.14.0/BentoBox-3.14.0.jar"
)
val bskyblockJar = cachedJar(
"BSkyBlock-1.20.0.jar",
"https://github.com/BentoBoxWorld/BSkyBlock/releases/download/1.20.0/BSkyBlock-1.20.0.jar"
)

// Maven-built Challenges jar (version/profile suffix varies: -SNAPSHOT-LOCAL locally, plain on CI).
val challengesJar: File = (file("../target").listFiles()?.toList() ?: emptyList())
.firstOrNull {
it.name.startsWith("Challenges-") && it.name.endsWith(".jar") &&
!it.name.contains("sources") && !it.name.contains("javadoc")
}
?: throw GradleException(
"Challenges jar not found in ../target — run 'mvn -q -DskipTests package' first."
)

plugwright {
minecraftVersion.set("1.21.11")
acceptEula.set(true)
// We deploy prebuilt jars (this is a Maven project, not a Gradle plugin build).
useExternalPluginsOnly.set(true)
testsDir.set(file("src/test/e2e"))
jvmArgs.set(listOf("-Xmx3G"))

writeFiles {
file("plugins/BentoBox.jar", bentoboxJar)
file("plugins/BentoBox/addons/BSkyBlock.jar", bskyblockJar)
file("plugins/BentoBox/addons/Challenges.jar", challengesJar)
}
}
Binary file added e2e/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
7 changes: 7 additions & 0 deletions e2e/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading