@braulio-dev/hopper
v0.1.1
Published
End to end verification for Minecraft server plugins
Maintainers
Readme
hopper
End to end testing for Minecraft server plugins. One command builds your plugin, deploys it to a real server, restarts the server, checks the startup log, runs scenarios with real bots, and exits nonzero on any failure.
What a run does
- Build. Runs your build commands in order and stops at the first failure.
- Collect. Finds the built files with your globs.
- Deploy. Copies them into the server's plugins folder. Any file it replaces is backed up first.
- Restart. Stops and starts the server and waits until it is ready.
- Scan startup. Reads the startup log for plugin load failures, enable errors, missing dependencies and stack traces. Known issues you list are hidden and counted.
- Scenarios. Connects bots and runs each scenario file.
- Report. Prints a short summary, writes a JSON report and saves full logs.
A rerun with no source changes since the last pass exits 0 at once. --force runs anyway.
Install
npm install --save-dev --save-exact @braulio-dev/hopperIt needs Node 22 or newer. Pin the version so every machine and CI runs the same one.
Configure
Create hopper.config.ts in your project:
import { defineConfig } from '@braulio-dev/hopper'
export default defineConfig({
version: 1,
build: {
steps: [
{ name: 'compile', command: './gradlew compileJava', kind: 'compile' },
{ name: 'unit tests', command: './gradlew test', kind: 'test' },
{ name: 'package', command: './gradlew shadowJar', kind: 'package' },
],
},
artifacts: { include: ['build/libs/*.jar'] },
target: {
// Where the server runs. See the setup guides below.
},
startup: {
knownIssues: [
{ id: 'noisy-plugin', match: /SomePlugin\] Could not find config/, reason: 'harmless on test servers' },
],
},
})Build commands are plain shell commands, so any build tool works.
| Key | What it sets |
| --- | --- |
| build.steps | Commands to run in order. kind: 'test' steps are skipped by --skip-tests. |
| artifacts | Globs for the files to deploy. |
| target | Where the server runs and how to control it. |
| topology | direct, or proxied to join through a proxy and switch to a backend. |
| auth | offline bot names, or microsoft accounts. |
| scenarios | Scenario folder, globs and the bot connect timeout. |
| startup | Extra scan rules, known issues, failOn and settleMs. |
| fingerprint | Files that decide whether anything changed. |
| deploy | How many backups to keep. |
| secrets | The env file to read secrets from. |
| selector | Optional. Skip scenarios a change cannot affect. |
Choose a setup
| Setup | Guide | | --- | --- | | A throwaway server the tool downloads and runs, for local runs and CI | docs/setup/local-managed.md | | A server folder you already run on your machine | docs/setup/local-existing.md | | A server on a Pterodactyl panel, direct or behind a proxy | docs/setup/pterodactyl.md |
Write scenarios
A scenario is a file in scenarios/ that exports a name, the bots it needs and a default
async function.
import type { ScenarioContext } from '@braulio-dev/hopper/scenario'
export const name = 'greeting'
export const description = 'The greet command replies to the player'
export const bots = ['alice']
export default async function ({ bot, console: server, assert }: ScenarioContext) {
const marker = await bot.command('/greet')
await bot.expectChat(/Hello, alice/, { after: marker, timeoutMs: 5000 })
const lines = await server.run('minecraft:list')
assert.ok(lines.length > 0, 'the server console answered')
}bot.commandandbot.chatreturn a marker. Pass it asafterso only newer chat counts.bot.expectChatand everyassert.*record a failure and keep going.bot.waitForChatthrows, for setup that has to succeed.bot.inventory(),bot.sidebar(),bot.openWindow()andbot.position()read state.console.run(command)runs a server command and returns what it printed.- Scenarios share one server, so each one cleans up what it changed.
The full API is in @braulio-dev/hopper/scenario. It is versioned, and a scenario written
for another version is refused with a clear message.
Run
npx hopper run| Flag | Effect |
| --- | --- |
| --only <scenario> | Run one scenario. |
| --skip-tests | Skip build steps with kind: 'test'. |
| --no-build | Skip the build. |
| --no-deploy | Skip the deploy. |
| --no-restart | Skip the restart and the startup scan. |
| --force | Run even if nothing changed since the last pass. |
| --all | Run every scenario even when a selector is configured. |
| --config <path> | Use a config file that is not found automatically. |
Other commands:
npx hopper statussays whether the current source has passed a full run.npx hopper auth loginsigns in Microsoft accounts once.npx hopper auth statusshows which accounts have a usable sign in.
| Exit code | Meaning | | --- | --- | | 0 | Passed, or skipped because nothing changed. | | 1 | A build, startup scan or scenario failed. | | 2 | The config or setup is wrong, such as a missing secret. | | 3 | The tool could not reach or control the server. |
The report is written to .plugin-e2e/report.json and the full logs to .plugin-e2e/logs/.
Add .plugin-e2e/ to your .gitignore.
Secrets
Secrets are never written in the config. Reference them instead:
apiKey: { env: 'PTERODACTYL_API_KEY' }The value is read from the environment, then from a .env file next to the config, then
asked for when a person is at the terminal. Unattended runs fail with the name of the
missing variable. Every secret is masked in the console, the logs and the report.
More
- GitHub Actions
- Claude Code plugin
- Scenario selector, to skip scenarios a change cannot affect
Safety
- It never edits or deletes world data, player data or plugins it did not deploy.
- It backs up every file it replaces and keeps the last few backups.
- It never kills a server it did not start, and never force kills a remote one.
- It never restarts a proxy unless the config allows it.
- It never retries a failed assertion.
License
MIT
