npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pilaf/backends

v1.3.1

Published

Backend implementations for Pilaf testing framework.

Readme

@pilaf/backends

Backend implementations for Pilaf testing framework.

This package provides RCON and Mineflayer backends, log monitoring, event correlation, and pattern-based parsing for comprehensive Minecraft PaperMC server testing.

Installation

pnpm add @pilaf/backends

🏗️ Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         Pilaf Test Suite                          │
│              (Jest Tests with Pilaf Reporter)                      │
└─────────────────────────────────────────────────────────────────┘
                                  │
                ┌─────────────────┼─────────────────┐
                ▼                 ▼                 ▼
    ┌──────────────┐   ┌──────────────┐   ┌──────────────┐
    │ Mineflayer   │   │     RCON     │   │    Docker     │
    │   Backend    │   │   Backend    │   │   Collector   │
    │              │   │              │   │              │
    │ - Chat       │   │ - Commands  │   │ - Stream      │
    │ - Movement   │   │ - Queries   │   │ - Reconnect  │
    │ - Actions    │   │ - Health     │   │ - Parsing    │
    └──────────────┘   └──────────────┘   └──────────────┘
            │                  │                  │
            └──────────────────┼──────────────────┘
                               ▼
                    ┌──────────────────────┐
                    │   Pilaf Core Layer    │
                    │                      │
                    │ ┌──────────────────┐ │
                    │ │  QueryHelper      │ │
                    │ │  - listPlayers()  │ │
                    │ │  - getTPS()       │ │
                    │ │  - getWorldTime() │ │
                    │ └──────────────────┘ │
                    │                      │
                    │ ┌──────────────────┐ │
                    │ │ EventObserver   │ │
                    │ │  - onPlayerJoin() │ │
                    │ │  - onPlayerDeath()│ │
                    │ │  - onEvent()      │ │
                    │ └──────────────────┘ │
                    │                      │
                    │ ┌──────────────────┐ │
                    │ │   LogMonitor     │ │
                    │ │  - Correlation    │ │
                    │ │  - Buffers       │ │
                    │ └──────────────────┘ │
                    │                      │
                    │ ┌──────────────────┐ │
                    │ │ PatternRegistry  │ │
                    │ │  - Add/remove    │ │
                    │ │  - Priority      │ │
                    │ └──────────────────┘ │
                    └──────────────────────┘

📦 Backends

RCONBackend

Connects via RCON protocol to execute server commands and retrieve responses.

const { RconBackend } = require('@pilaf/backends');

const backend = new RconBackend().connect({
  host: 'localhost',
  port: 25575,
  password: 'your_password'
});

// Execute command
const response = await backend.send('/op player1');
// { raw: '...', parsed: null }

MineflayerBackend

Creates a real Minecraft player using Mineflayer for realistic player simulation.

const { MineflayerBackend } = require('@pilaf/backends');

const backend = new MineflayerBackend();
await backend.connect({
  host: 'localhost',
  port: 25565,
  username: 'TestBot',
  auth: 'offline'  // or 'microsoft'
});

// Spawn bot player
await backend.spawn();

// Get player position
const pos = await backend.getPlayerPosition();

// Chat as player
await backend.chat('Hello world!');

// Disconnect
await backend.disconnect();

PilafBackendFactory

Factory for creating backend instances.

const { PilafBackendFactory } = require('@pilaf/backends');

// Create RCON backend
const rcon = PilafBackendFactory.create('rcon', {
  host: 'localhost',
  port: 25575,
  password: 'password'
});

// Create Mineflayer backend
const bot = PilafBackendFactory.create('mineflayer', {
  host:localhost',
  port: 25565,
  username: 'TestBot',
  auth: 'offline'
});

🔍 QueryHelper

Convenience methods for common RCON queries with structured response parsing.

Overview

QueryHelper eliminates the need for manual string parsing of RCON responses, providing type-safe, structured data.

API Reference

const { QueryHelper } = require('@pilaf/backends');

const helper = new QueryHelper(rconBackend);

// List online players
const players = await helper.listPlayers();
// { online: 2, players: ['Steve', 'Alex'], raw: '...' }

// Get detailed player info
const info = await helper.getPlayerInfo('Steve');
// { player: 'Steve', dimension: 'minecraft:overworld',
//   position: { x: 100.5, y: 64.0, z: -200.3 },
//   health: 20, food: 20, saturation: 5, etc. }

// Get world time
const time = await helper.getWorldTime();
// { time: 1500, daytime: true, raw: '...' }

// Get weather
const weather = await helper.getWeather();
// { weather: 'clear', duration: -1, raw: '...' }

// Get difficulty
const difficulty = await helper.getDifficulty();
//// { difficulty: 'hard', raw: '...' }

// Get game mode
const gameMode = await helper.getGameMode();
// { gameMode: 'survival', mode: 'Survival', raw: '...' }

// Get server TPS
const tps = await helper.getTPS();
// { tps: 19.8, raw: '...' }

// Get world seed
const seed = await helper.getSeed();
// { seed: 1234567890, raw: '...' }

Usage Example

const { MineflayerBackend } = require('@pilaf/backends');

describe('Server State Tests', () => {
  let backend;

  beforeEach(async () => {
    backend = new MineflayerBackend();
    await backend.connect({
      host: 'localhost',
      port: 25565,
      auth: 'offline',
      rconPort: 25575,
      rconPassword: 'test'
    });
  });

  it('should retrieve player information', async () => {
    const info = await backend.getPlayerInfo('TestPlayer');

    expect(info.player).toBe('TestPlayer');
    expect(info.health).toBeGreaterThan(0);
    expect(info.position).toBeDefined();
  });

  it('should get current server TPS', async () => {
    const { tps } = await backend.getTPS();

    expect(tps).toBeGreaterThan(15); // Server should be healthy
  });

  afterEach(async () => {
    await backend.disconnect();
  });
});

📡 EventObserver

Clean API for subscribing to Minecraft server events with pattern matching and wildcard support.

Overview

EventObserver provides a declarative interface for subscribing to log events without manual pattern matching. Events are parsed from logs in real-time and emitted as structured objects.

API Reference

const { EventObserver } = require('@pilaf/backends');

const observer = new EventObserver({
  logMonitor: monitor,  // LogMonitor instance
  parser: parser        // MinecraftLogParser instance
});

// Subscribe to specific events
const unsubscribe = observer.onEvent('entity.join', (event) => {
  console.log('Join:', event.type, event.data);
});

// Convenience methods
observer.onPlayerJoin((event) => { /* ... */ });
observer.onPlayerLeave((event) => { /* ... */ });
observer.onPlayerDeath((event) => { /* ... */ });
observer.onPlayerChat((event) => { /* ... */ });
observer.onCommand((event) => { /* ... */ });

// Wildcard patterns
observer.onEvent('entity.death.*', (event) => {
  // Matches: entity.death.player, entity.death.mob, etc.
});

// Start observing
await observer.start();

// Stop observing
observer.stop();

// Unsubscribe from specific pattern
unsubscribe();

Event Types

| Event Type | Description | Event Data Structure | |------------|-------------|---------------------| | entity.join | Player joined | { player, timestamp, location } | | entity.leave | Player left | { player, timestamp, reason } | | entity.death.player | Player died | { victim, killer, cause } | | entity.death.mob | Mob died | { victim, killer, cause } | | entity.chat | Player chat | { player, message, timestamp } | | command.success | Command succeeded | { command, executor, result } | | command.failed | Command failed | { command, error } |

Usage Example

const { MineflayerBackend } = require('@pilaf/backends');

describe('Event Monitoring Tests', () => {
  let backend;

  beforeEach(async () => {
    backend = new MineflayerBackend();
    await backend.connect({ /* ... */ });
    await backend.observe();
  });

  it('should track player join events', async () => {
    const joins = [];
    backend.onPlayerJoin((event) => {
      joins.push(event.data.player);
    });

    // Simulate player joining
    await backend.chat('/test join Player123');

    await new Promise(resolve => setTimeout(resolve, 1000));

    expect(joins).toContain('Player123');
  });

  afterEach(async () => {
    backend.unobserve();
    await backend.disconnect();
  });
});

📊 LogMonitor

Orchestrates log collection, parsing, correlation, and real-time event emission.

Overview

LogMonitor is the central orchestrator that:

  1. Collects logs from Docker containers or files
  2. Parses logs using MinecraftLogParser
  3. Correlates related events using strategies
  4. Emits structured events for consumption
  5. Uses a circular buffer for memory efficiency

Architecture

DockerLogCollector → Log Lines → MinecraftLogParser → Structured Events
                                                              ↓
                                                   CircularBuffer (1000 lines)
                                                              ↓
                                                   Correlation Strategy
                                                              ↓
                                                   EventEmitter → Events
                                                              ↓
                                                   Correlation Sessions

API Reference

const { LogMonitor } = require('@pilaf/backends');

const monitor = new LogMonitor({
  collector: new DockerLogCollector({ container: 'mc-server', follow: true }),
  parser: new MinecraftLogParser(),
  correlation: new UsernameCorrelationStrategy(),
  bufferSize: 1000  // Max events in circular buffer
});

// Subscribe to all events
monitor.on('event', (event) => {
  console.log('Event:', event.type, event.data);
});

// Subscribe to correlation sessions
monitor.on('correlation', (session) => {
  console.log('Session:', session.username, session.events.length);
});

// Start monitoring
await monitor.start();

// Stop monitoring
monitor.stop();

Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | collector | LogCollector | required | Log collector instance | | parser | LogParser | required | Log parser instance | | correlation | CorrelationStrategy | optional | Event correlation strategy | | bufferSize | number | 1000 | Circular buffer size |


🔄 Correlation Strategies

UsernameCorrelationStrategy

Tracks player sessions by grouping events by username.

const { UsernameCorrelationStrategy } = require('@pilaf/backends');

const strategy = new UsernameCorrelationStrategy();

// Returns session object with all events for that player
const session = strategy.correlate({
  type: 'entity.join',
  data: { player: 'Steve' }
});

console.log(session.username);    // 'Steve'
console.log(session.isActive);   // true
console.log(session.events);     // Array of all events

// Session ends when player leaves
strategy.correlate({
  type: 'entity.leave',
  data: { player: 'Steve' }
});

TagCorrelationStrategy

Groups events by custom tag/ID with automatic expiration.

const { TagCorrelationStrategy } = require('@pilaf/backends');

const strategy = new TagCorrelationStrategy({
  tagExtractor: (event) => event.data.questId,  // Extract tag from event
  timeout: 300000  // 5 minutes auto-expire
});

// All events with same questId are grouped
strategy.correlate({
  type: 'quest.started',
  data: { questId: 'dragon-quest-123', player: 'Steve' }
});

strategy.correlate({
  type: 'quest.objective',
  data: { questId: 'dragon-quest-123', objective: 'kill_dragon' }
});

strategy.correlate({
  type: 'quest.completed',
  data: { questId: 'dragon-quest-123', player: 'Steve' }
});

// All three events are grouped in one session

📝 MinecraftLogParser

Pattern-based log parser that extracts structured events from raw Minecraft log lines.

Supported Log Patterns

| Pattern | Event Type | Description | |---------|-----------|-------------| | /Teleported (\w+) to (.+)/ | teleport | Player teleportation | | /(\w+) was slain by (\w+)/ | entity.death.player | Player killed by entity | | /(\w+) was slain by (.+)/ | entity.death.mob | Mob killed | | /(\w+) joined the game/ | entity.join | Player joined | | /(\w+) left the game/ | entity.leave | Player left | | /\<(\w+)\> (?:ordered|said)\:/ | entity.chat | Player chat | | /Issued server command: (\S+) .+ by (\w+)/ | command.success | Command executed |

Usage Example

const { MinecraftLogParser } = require('@pilaf/backends');

const parser = new MinecraftLogParser();

// Add custom pattern
parser.addPattern('custom', '/Custom event: (.+)/', (match) => ({
  message: match[1]
}));

// Parse a log line
const event = parser.parse('[12:34:56] [Server thread/INFO]: Teleported Steve to 100 70 200');

if (event) {
  console.log(event.type);     // 'teleport'
  console.log(event.data);     // { player: 'Steve', destination: '100 70 200' }
  console.log(event.raw);       // Original log line
}

🗃️ PatternRegistry

Centralized pattern management with priority-based ordering for complex log parsing scenarios.

Overview

PatternRegistry manages multiple parsing patterns with:

  • Priority ordering: More specific patterns tested first (lower priority number = higher priority)
  • Pattern types: RegExp or string patterns
  • Handler functions: Custom parsing logic
  • Dynamic management: Add/remove patterns at runtime

API Reference

const { PatternRegistry } = require('@pilaf/backends');

const registry = new PatternRegistry({
  caseInsensitive: false  // Enable case-insensitive matching
});

// Add pattern with priority (0-10, where 0 is highest priority)
registry.addPattern('high-priority', /Specific pattern (.+)/, (match) => ({
  captured: match[1]
}), 1);  // High priority (tested first)

registry.addPattern('low-priority', /.+/, (match) => ({
  everything: match[0]
}), 10);  // Low priority (tested last)

// Match first matching pattern in priority order
const result = registry.match('[Server] Specific pattern matched');

// Remove pattern
registry.removePattern('high-priority');

// Get pattern
const pattern = registry.getPattern('low-priority');

// Get all pattern names
const names = registry.getPatterns();

// Clone registry
const cloned = registry.clone();

// Clear all patterns
registry.clear();

Priority System

Patterns are tested in ascending priority order:

// Priority 1: High priority (tested first)
registry.addPattern('exact-match', /^Teleported Steve to (.+)/), handler, 1);

// Priority 10: Low priority (fallback)
registry.addPattern('fallback', /.+/, handler, 10);

// When parsing "Teleported Steve to 100 64 100":
// - Tests 'exact-match' first → matches!
// - Returns immediately, 'fallback' never tested

Usage Example

const { PatternRegistry, MinecraftLogParser } = require('@pilaf/backends');

const parser = new MinecraftLogParser();

// Add custom high-priority pattern
parser.addPattern('dragon-spawn', /Dragon spawned at (.+)/, (match) => ({
  location: match[1]
}), 1);

// Add low-priority catch-all
parser.addPattern('default', /.+/, (match) => ({
  raw: match[0]
}), 10);

// Parse log line - dragon pattern tested first
const event = parser.parse('[12:34:56] Dragon spawned at 100 70 200');
// { type: 'dragon-spawn', data: { location: '100 70 200' }, raw: '...' }

🐳 DockerLogCollector

Streams logs from Docker containers with automatic reconnection and error handling.

Overview

DockerLogCollector connects to Docker containers and follows log output in real-time, with:

  • Automatic reconnection: Exponential backoff on disconnection
  • ANSI code stripping: Clean log output without color codes
  • Pause/resume: Control data flow during tests
  • Error handling: Graceful error recovery

API Reference

const { DockerLogCollector } = require('@pilaf/backends');

const collector = new DockerLogCollector({
  dockerodeOptions: {
    socketPath: '/var/run/docker.sock'
  },
  reconnectDelay: 1000,        // Initial reconnection delay (ms)
  maxReconnectDelay: 30000,   // Maximum reconnection delay (ms)
  reconnectAttempts: 5        // Maximum reconnection attempts
});

// Connect to container
await collector.connect({
  containerName: 'minecraft-server',
  follow: true,             // Follow log stream
  tail: 100,               // Last N lines from history
  stdout: true,
  stderr: true,
  disableAutoReconnect: false  // Disable automatic reconnection
});

// Subscribe to events
collector.on('data', (line) => {
  console.log('Log:', line);
});

collector.on('connected', () => {
  console.log('Connected to container');
});

collector.on('reconnecting', (info) => {
  console.log(`Reconnecting attempt ${info.attempt}/${info.maxAttempts}`);
});

collector.on('end', () => {
  console.log('Stream ended');
});

collector.on('error', (error) => {
  console.error('Collector error:', error);
});

// Pause/resume
collector.pause();  // Stop emitting data events
collector.resume(); // Resume emitting data events

// Get reconnection status
const status = collector.getReconnectStatus();
// { attempt: 0, maxAttempts: 5, reconnecting: false }

// Disconnect
await collector.disconnect();

Usage Example with LogMonitor

const { LogMonitor, DockerLogCollector, MinecraftLogParser, UsernameCorrelationStrategy } = require('@pilaf/backends');

const monitor = new LogMonitor({
  collector: new DockerLogCollector({
    container: 'minecraft-server',
    follow: true
  }),
  parser: new MinecraftLogParser(),
  correlation: new UsernameCorrelationStrategy(),
  bufferSize: 1000
});

// Subscribe to events
monitor.on('event', (event) => {
  console.log('Event:', event.type, event.data);
});

// Start monitoring
await monitor.start();

// Stop monitoring
monitor.stop();

⭕ CommandRouter

Abstract base class for routing commands to appropriate execution channels (bot chat, RCON, or log monitoring).

Overview

CommandRouter implements intelligent command routing:

  • /data get commands → RCON (structured NBT responses)
  • /execute with run data → RCON (structured queries)
  • useRcon option → RCON (forced routing)
  • expectLogResponse option → Log monitoring (event correlation)
  • Default → Bot chat (player commands)

Channels

const { CommandRouter } = require('@pilaf/backends');

// Available channels
CommandRouter.CHANNELS.BOT     // Send via bot.chat()
CommandRouter.CHANNELS.RCON    // Send via RCON
CommandRouter.CHANNELS.LOG     // Send via bot and wait for log response

Example Implementation

const { CommandRouter } = require('@pilaf/backends');

class SmartCommandRouter extends CommandRouter {
  route(command, context) {
    const { options } = context;

    // Check forced options first
    if (options?.useRcon) {
      return { channel: CommandRouter.CHANNELS.RCON, options };
    }
    if (options?.expectLogResponse) {
      return { channel: CommandRouter.CHANNELS.LOG, options };
    }

    // Check custom rules
    const rules = this.getRules();
    for (const { pattern, channel } of rules) {
      if (this._matchesPattern(command, pattern)) {
        return { channel, options };
      }
    }

    // Default: bot chat
    return { channel: CommandRouter.CHANNELS.BOT, options };
  }
}

Usage Example

const router = new SmartCommandRouter();

// Add custom routing rule
router.addRule(/^\/data get/, CommandRouter.CHANNELS.RCON);

// Route command
const result = router.route('/data get entity TestPlayer', { options: {} });
console.log(result.channel);  // 'rcon'

🔁 CircularBuffer

Fixed-size circular buffer for memory-efficient event storage with automatic overflow handling.

Overview

CircularBuffer provides O(1) operations and prevents memory leaks in long-running tests by limiting stored events.

API Reference

const { CircularBuffer } = require('@pilaf/backends');

const buffer = new CircularBuffer({
  size: 1000,           // Maximum number of events
  onOverflow: 'discard'  // 'discard' or 'error'
});

// Add event
buffer.push(event);

// Get all events
const events = buffer.getAll();

// Get buffer size
buffer.size;        // Current event count
buffer.maxSize;     // Maximum capacity

// Check if full
buffer.isFull();

// Clear buffer
buffer.clear();

// Iterate over events
buffer.forEach((event, index) => {
  console.log(`Event ${index}:`, event.type);
});

Usage Example with LogMonitor

const { LogMonitor } = require('@pilaf/backends');

const monitor = new LogMonitor({
  bufferSize: 500  // Store last 500 events
});

monitor.on('event', (event) => {
  // Events are automatically buffered
});

// Retrieve recent events
const recentEvents = monitor.getEvents();

🚀 Enhanced MineflayerBackend

The MineflayerBackend now includes integrated QueryHelper and EventObserver with lazy initialization.

New Features

  1. Query Methods (delegates to QueryHelper)

    • listPlayers()
    • getPlayerInfo(username)
    • getWorldTime()
    • getWeather()
    • getDifficulty()
    • getGameMode()
    • getTPS()
    • getSeed()
  2. Event Methods (delegates to EventObserver, lazy-loaded)

    • observe() - Starts log monitoring
    • unobserve() - Stops log monitoring
    • isObserving() - Check if observing
    • onPlayerJoin(callback)
    • onPlayerLeave(callback)
    • onPlayerDeath(callback)
    • onPlayerChat(callback)
    • onCommand(callback)
    • onEvent(pattern, callback)
  3. Lazy Initialization

    • EventObserver is only created when observe() is called
    • LogMonitor and parser are created on-demand
    • Zero overhead if events are not used

Usage Example

const { MineflayerBackend } = require('@pilaf/backends');

const backend = new MineflayerBackend();

// Connect with RCON integration
await backend.connect({
  host: 'localhost',
  port: 25565,
  auth: 'offline',
  rconPort: 25575,
  rconPassword: 'password'
});

// Query server state
const players = await backend.listPlayers();
console.log('Online players:', players.players);

const tps = await backend.getTPS();
console.log('Server TPS:', tps.tps);

// Start event observation (lazy initialization)
await backend.observe();

// Subscribe to events
backend.onPlayerJoin((event) => {
  console.log('Player joined:', event.data.player);
});

backend.onPlayerDeath((event) => {
  console.log('Player died:', event.data.cause);
});

// Trigger command
await backend.chat('/test command');

// Clean up
backend.unobserve();
await backend.disconnect();

Complete Test Example

describe('Elemental Dragon Tests', () => {
  let backend;

  beforeEach(async () => {
    backend = new MineflayerBackend();
    await backend.connect({
      host: 'localhost',
      port: 25565,
      username: `TestBot_${Date.now()}`,
      auth: 'offline',
      rconPort: 25575,
      rconPassword: 'test'
    });
    await backend.observe();
  });

  afterEach(async () => {
    backend.unobserve();
    await backend.disconnect();
  });

  it('should spawn dragon and verify TPS', async () => {
    // Spawn dragon
    await backend.chat('/summon elemental_dragon 100 70 200');

    // Wait for spawn
    await new Promise(resolve => setTimeout(resolve, 1000));

    // Verify server health
    const { tps } = await backend.getTPS();
    expect(tps.tps).toBeGreaterThan(15);
  });

  it('should track player join events', async () => {
    const joins = [];
    backend.onPlayerJoin((event) => {
      joins.push(event.data.player);
    });

    // Trigger join
    await backend.chat('/test join Player123');

    await new Promise(resolve => setTimeout(resolve, 500));

    expect(joins).toContain('Player123');
  });
});

🎯 Using QueryHelper with createTestContext

When using @pilaf/framework's createTestContext() helper, you can access QueryHelper through either the separate RCON backend or via the MineflayerBackend:

const { createTestContext, cleanupTestContext } = require('@pilaf/framework');

describe('Plugin Tests with QueryHelper', () => {
  let context;

  beforeAll(async () => {
    context = await createTestContext({
      username: 'TestPlayer',
      rconPassword: 'dragon123'
    });
  });

  afterAll(async () => {
    await cleanupTestContext(context);
  });

  it('should verify player position using QueryHelper', async () => {
    // Get player info BEFORE using RCON backend directly
    const beforeInfo = await context.rcon.send('data get entity TestPlayer Pos');

    // Execute ability
    context.bot.chat('/myplugin teleport 100 64 100');
    await new Promise(resolve => setTimeout(resolve, 1000));

    // Get player info AFTER
    const afterInfo = await context.rcon.send('data get entity TestPlayer Pos');

    // Or use MineflayerBackend's QueryHelper (if available)
    // const info = await context.backend.getPlayerInfo('TestPlayer');
    // expect(info.position.x).toBeCloseTo(100);
  });

  it('should verify server TPS', async () => {
    // Use QueryHelper methods via backend
    const { tps } = await context.backend.getTPS();
    expect(tps.tps).toBeGreaterThan(15);
  });

  it('should list online players', async () => {
    const players = await context.backend.listPlayers();
    expect(players.players).toContain('TestPlayer');
  });
});

Why Both Backends?

  • context.rcon - For raw RCON commands that return responses (e.g., /data get)
  • context.backend - For QueryHelper methods and bot control
  • context.bot - For bot player actions (chat, movement)

This dual-backend approach is necessary because MineflayerBackend.sendCommand() sends via bot chat and returns empty responses.


⚙️ Jest Configuration Recommendations

maxWorkers: 1 for Bot Player Tests

When testing with multiple bot players, set maxWorkers: 1 to avoid connection throttling:

// jest.config.js
module.exports = {
  testMatch: ['**/*.pilaf.test.js'],
  testTimeout: 300000,
  maxWorkers: 1,  // ← Important: Prevents connection throttling
  reporters: ['default']
};

Why? Each Jest worker creates separate bot connections. Multiple workers connecting simultaneously can trigger server connection throttling or race conditions.

testTimeout: 300000

Server operations (bot spawning, command execution) are slower than unit tests. Set a higher timeout:

testTimeout: 300000,  // 5 minutes

Module Resolution for ES Modules

If using ES modules ("type": "module"), configure Jest accordingly:

module.exports = {
  extension: ['.js', '.cjs'],
  transform: {},
  testTimeout: 300000,
  maxWorkers: 1
};

📊 QueryHelper vs Raw RCON Comparison

Before: Raw RCON with Manual Parsing

// ❌ Verbose and error-prone
const result = await context.rcon.send('data get entity TestPlayer Pos');

// Manual regex parsing
const match = result.raw.match(/Pos.*?\[.*?d/);
if (!match) {
  throw new Error('Could not parse position');
}

const coords = match[0].split(', ').map(s => {
  const num = s.match(/-?\d+\.\d+/);
  return num ? parseFloat(num[0]) : 0;
});

const x = coords[0];
const y = coords[1];
const z = coords[2];

expect(x).toBeCloseTo(100);

After: QueryHelper (Clean & Type-Safe)

// ✅ Clean and structured
const info = await context.backend.getPlayerInfo('TestPlayer');

expect(info.position.x).toBeCloseTo(100);
expect(info.position.y).toBeDefined();
expect(info.health).toBeGreaterThan(0);

Benefits Summary

| Feature | Raw RCON | QueryHelper | |---------|----------|-------------| | Code length | 20+ lines | 1-2 lines | | Error handling | Manual regex | Built-in parsing | | Type safety | Strings | Structured objects | | Maintainability | Brittle (breaks on format changes) | Resilient |


🤖 Bot Player Limitations

Velocity-Based Abilities Don't Work on Bot Players

Bot players (Mineflayer) do not respond to server velocity the same way as real players:

// ❌ This won't work as expected with bot players
context.bot.chat('/agile 1');  // Dash ability that applies velocity

await new Promise(resolve => setTimeout(resolve, 1000));

// Bot's client-side position won't reflect the server-side movement
const botPos = context.bot.entity.position;
console.log(botPos);  // Position unchanged (bot doesn't move)

Workaround: Test ability activation instead of actual movement:

// ✅ Test that ability activates (not actual movement)
context.bot.chat('/agile 1');
await new Promise(resolve => setTimeout(resolve, 500));

// Verify cooldown was applied (ability was used)
const quickResult = await context.rcon.send('execute as TestPlayer run agile 1');
expect(quickResult.raw).toContain('cooldown');  // Ability is on cooldown

Entity Position Tracking

Use server-side queries instead of bot position:

// ❌ Don't use bot position for velocity-based abilities
const botPos = context.bot.entity.position;  // Not updated by server velocity

// ✅ Use RCON to get server-side entity position
const result = await context.rcon.send('data get entity Pig Pos');
// Parse the result to verify entity moved

Chat and Commands Work Fine

Bot players correctly handle:

  • ✅ Chat messages
  • ✅ Command execution (/command)
  • ✅ Inventory manipulation
  • ✅ Block interaction
  • ✅ Entity detection (bot.entities)

🔧 Core Components

ConnectionState

Connection states for backend lifecycle management:

const { ConnectionState } = require('@pilaf/backends');

ConnectionState.DISCONNECTED     // Initial state
ConnectionState.CONNECTING      // Connecting to server
ConnectionState.CONNECTED       // Connection established
ConnectionState.SPAWNING        // Player spawning
ConnectionState.SPAWNED         // Player ready
ConnectionState.ERROR           // Error occurred
ConnectionState.DISCONNECTING   // Disconnecting

BotPool

Manages a pool of Mineflayer bot instances for parallel testing.

const { BotPool } = require('@pilaf/backends');

const pool = new BotPool({
  maxBots: 5,
  defaultConfig: {
    host: 'localhost',
    port: 25565,
    auth: 'offline'
  }
});

// Acquire a bot
const bot = await pool.acquire('Bot1');

// Use bot
await bot.chat('Hello!');

// Release when done
await pool.release('Bot1');

// Shutdown all
await pool.shutdownAll();

ServerHealthChecker

Monitors server health and availability.

const { ServerHealthChecker } = require('@pilaf/backends');

const checker = new ServerHealthChecker({
  rcon: { host: 'localhost', port: 25575, password: 'pass' }
});

// Check health
const isHealthy = await checker.check();
console.log(isHealthy.status); // 'healthy' | 'unhealthy'

🧪 Testing Patterns

Pattern 1: Server State Verification

it('should verify server state after action', async () => {
  await backend.chat('/time set 12000');

  const { time } = await backend.getWorldTime();
  expect(time.time).toBe(12000);
});

Pattern 2: Event Correlation

it('should track multi-step scenario', async () => {
  const questEvents = [];

  monitor.on('correlation', (session) => {
    if (session.questId === 'test-quest') {
      questEvents.push(...session.events);
    }
  });

  await backend.chat('/quest start test-quest');
  await backend.chat('/quest complete test-quest');

  await new Promise(resolve => setTimeout(resolve, 1000));

  expect(questEvents).toHaveLength(2);
});

Pattern 3: Docker Integration

describe('Docker Tests', () => {
  let monitor;

  beforeEach(async () => {
    monitor = new LogMonitor({
      collector: new DockerLogCollector({
        container: 'mc-server',
        follow: true
      }),
      parser: new MinecraftLogParser()
    });
    await monitor.start();
  });

  afterEach(async () => {
    await monitor.stop();
  });
});

Pattern 4: Async Event Handling

it('should handle async events', async () => {
  let eventReceived = false;

  backend.onPlayerJoin(() => {
    eventReceived = true;
  });

  await backend.chat('/test trigger');

  await waitFor(() => eventReceived, 5000);
  expect(eventReceived).toBe(true);
});

function waitFor(condition, timeout) {
  return new Promise((resolve) => {
    const startTime = Date.now();
    const check = () => {
      if (condition() || Date.now() - startTime > timeout) {
        resolve(condition());
      } else {
        setTimeout(check, 100);
      }
    };
    check();
  });
}

📚 API Reference

Exports

const {
  // Backends
  RconBackend,
  MineflayerBackend,
  PilafBackendFactory,

  // Core
  ConnectionState,
  CommandRouter,

  // Helpers
  QueryHelper,
  EventObserver,

  // Monitoring
  LogMonitor,
  DockerLogCollector,

  // Parsing
  LogParser,
  MinecraftLogParser,
  PatternRegistry,

  // Correlation
  CorrelationStrategy,
  UsernameCorrelationStrategy,
  TagCorrelationStrategy,

  // Utilities
  BotPool,
  ServerHealthChecker,
  CircularBuffer
} = require('@pilaf/backends');

🐛 Troubleshooting

Issue: Cannot find module './lib/features'

Cause: prismarine-physics package has a bug where it imports ./lib/features but the file is actually ./lib/features.json.

Solution: This is automatically fixed by the postinstall script in @pilaf/backends. If you're not using the postinstall script, you can create a symlink manually:

# Navigate to node_modules/prismarine-physics/lib
cd node_modules/prismarine-physics/lib
# Create symlink
ln -s features.json features

Note: This fix runs automatically when @pilaf/backends is installed via npm/pnpm.

Issue: Events not being captured

Cause: Not observing before triggering events

Solution:

// WRONG
await backend.chat('/test');
await backend.observe();

// CORRECT
await backend.observe();
backend.onPlayerJoin(handler);
await backend.chat('/test');

Issue: Tests timing out

Cause: Not waiting for async operations

Solution:

it('test with timeout', async () => {
  await backend.chat('/command');
  await new Promise(resolve => setTimeout(resolve, 1000));
  // Then verify
}, 10000);

Issue: Docker connection fails

Cause: Docker socket not accessible

Solution:

const collector = new DockerLogCollector({
  dockerodeOptions: {
    socketPath: '/var/run/docker.sock'  // Default path
  }
});

Issue: Pattern not matching

Cause: Case sensitivity or priority ordering

Solution:

const registry = new PatternRegistry({ caseInsensitive: true });
registry.addPattern('test', /test/i, handler);  // Case insensitive
registry.addPattern('test', /^test/, handler, 1);  // High priority

📋 Migration Checklist

From Manual Testing to Pilaf

  • [ ] Install @pilaf/backends package
  • [ ] Configure test environment (Jest, Pilaf reporter)
  • [ ] Set up test server (local or Docker)
  • [ ] Write first test (start with simple query)
  • [ ] Add event observation
  • [ ] Implement correlation for complex scenarios
  • [ ] Set up CI/CD integration
  • [ ] Train team on Pilaf usage

From Raw RCON to QueryHelper

  • [ ] Replace manual RCON string parsing with QueryHelper
  • [ ] Use structured responses in assertions
  • [ ] Remove regex parsing code
  • [ ] Test with actual server responses

From Manual Log Parsing to EventObserver

  • [ ] Replace log monitoring code with EventObserver
  • [ ] Use convenience methods (onPlayerJoin, etc.)
  • [ ] Add wildcard patterns for custom events
  • [ ] Implement correlation strategies
  • [ ] Remove manual parsing code

🎯 Best Practices

1. Test Isolation

beforeEach(async () => {
  backend = new MineflayerBackend();
  await backend.connect({
    username: `TestBot_${Date.now()}`,  // Unique username
    auth: 'offline'
  });
});

afterEach(async () => {
  await backend.disconnect();
});

2. Event Observation Lifecycle

it('test events', async () => {
  await backend.observe();     // Start FIRST
  backend.onPlayerJoin(() => { });
  await backend.chat('/test');
  await new Promise(resolve => setTimeout(resolve, 500));
  backend.unobserve();   // Stop LAST
});

3. Use Correlation for Multi-Step Tests

monitor.on('correlation', (session) => {
  // All events for this player/session
  expect(session.events).toHaveLength(expectedCount);
});

4. Docker for CI/CD

# docker-compose.test.yml
version: '3'
services:
  minecraft-server:
    image: pilaf/minecraft-test-server
    ports:
      - "25565:25565"
      - "25575:25575"
    environment:
      - RCON_PASSWORD=test
      - OPS=TestBot

5. Handle Async Timing

// Always wait for server response
await new Promise(resolve => setTimeout(resolve, 500));

// For longer operations
await waitFor(() => condition(), 5000);

📖 Additional Documentation

  • Architecture: See docs/plans/2025-01-16-pilaf-js-design.md for detailed architecture
  • Examples: Check lib/helpers/*.pilaf.test.js for reference implementations
  • Integration Guide: See docs/proposals/elementaldragon-pilaf-integration-guide.md for ElementalDragon-specific usage

📄 License

MIT