@pilaf/backends 1.0.2 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Backend implementations for Pilaf testing framework.
4
4
 
5
- This package provides RCON and Mineflayer backends for connecting to Minecraft PaperMC servers during testing.
5
+ This package provides RCON and Mineflayer backends, log monitoring, event correlation, and pattern-based parsing for comprehensive Minecraft PaperMC server testing.
6
6
 
7
7
  ## Installation
8
8
 
@@ -10,11 +10,67 @@ This package provides RCON and Mineflayer backends for connecting to Minecraft P
10
10
  pnpm add @pilaf/backends
11
11
  ```
12
12
 
13
- ## Backends
13
+ ---
14
+
15
+ ## 🏗️ Architecture
16
+
17
+ ```
18
+ ┌─────────────────────────────────────────────────────────────────┐
19
+ │ Pilaf Test Suite │
20
+ │ (Jest Tests with Pilaf Reporter) │
21
+ └─────────────────────────────────────────────────────────────────┘
22
+
23
+ ┌─────────────────┼─────────────────┐
24
+ ▼ ▼ ▼
25
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
26
+ │ Mineflayer │ │ RCON │ │ Docker │
27
+ │ Backend │ │ Backend │ │ Collector │
28
+ │ │ │ │ │ │
29
+ │ - Chat │ │ - Commands │ │ - Stream │
30
+ │ - Movement │ │ - Queries │ │ - Reconnect │
31
+ │ - Actions │ │ - Health │ │ - Parsing │
32
+ └──────────────┘ └──────────────┘ └──────────────┘
33
+ │ │ │
34
+ └──────────────────┼──────────────────┘
35
+
36
+ ┌──────────────────────┐
37
+ │ Pilaf Core Layer │
38
+ │ │
39
+ │ ┌──────────────────┐ │
40
+ │ │ QueryHelper │ │
41
+ │ │ - listPlayers() │ │
42
+ │ │ - getTPS() │ │
43
+ │ │ - getWorldTime() │ │
44
+ │ └──────────────────┘ │
45
+ │ │
46
+ │ ┌──────────────────┐ │
47
+ │ │ EventObserver │ │
48
+ │ │ - onPlayerJoin() │ │
49
+ │ │ - onPlayerDeath()│ │
50
+ │ │ - onEvent() │ │
51
+ │ └──────────────────┘ │
52
+ │ │
53
+ │ ┌──────────────────┐ │
54
+ │ │ LogMonitor │ │
55
+ │ │ - Correlation │ │
56
+ │ │ - Buffers │ │
57
+ │ └──────────────────┘ │
58
+ │ │
59
+ │ ┌──────────────────┐ │
60
+ │ │ PatternRegistry │ │
61
+ │ │ - Add/remove │ │
62
+ │ │ - Priority │ │
63
+ │ └──────────────────┘ │
64
+ └──────────────────────┘
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 📦 Backends
14
70
 
15
71
  ### RCONBackend
16
72
 
17
- Connects via RCON protocol to execute server commands.
73
+ Connects via RCON protocol to execute server commands and retrieve responses.
18
74
 
19
75
  ```javascript
20
76
  const { RconBackend } = require('@pilaf/backends');
@@ -26,7 +82,8 @@ const backend = new RconBackend().connect({
26
82
  });
27
83
 
28
84
  // Execute command
29
- const response = await backend.send('op player1');
85
+ const response = await backend.send('/op player1');
86
+ // { raw: '...', parsed: null }
30
87
  ```
31
88
 
32
89
  ### MineflayerBackend
@@ -36,7 +93,8 @@ Creates a real Minecraft player using Mineflayer for realistic player simulation
36
93
  ```javascript
37
94
  const { MineflayerBackend } = require('@pilaf/backends');
38
95
 
39
- const backend = new MineflayerBackend().connect({
96
+ const backend = new MineflayerBackend();
97
+ await backend.connect({
40
98
  host: 'localhost',
41
99
  port: 25565,
42
100
  username: 'TestBot',
@@ -50,7 +108,10 @@ await backend.spawn();
50
108
  const pos = await backend.getPlayerPosition();
51
109
 
52
110
  // Chat as player
53
- await bot.chat('Hello world!');
111
+ await backend.chat('Hello world!');
112
+
113
+ // Disconnect
114
+ await backend.disconnect();
54
115
  ```
55
116
 
56
117
  ### PilafBackendFactory
@@ -69,14 +130,814 @@ const rcon = PilafBackendFactory.create('rcon', {
69
130
 
70
131
  // Create Mineflayer backend
71
132
  const bot = PilafBackendFactory.create('mineflayer', {
72
- host: 'localhost',
133
+ host:localhost',
73
134
  port: 25565,
74
135
  username: 'TestBot',
75
136
  auth: 'offline'
76
137
  });
77
138
  ```
78
139
 
79
- ## API
140
+ ---
141
+
142
+ ## 🔍 QueryHelper
143
+
144
+ Convenience methods for common RCON queries with structured response parsing.
145
+
146
+ ### Overview
147
+
148
+ QueryHelper eliminates the need for manual string parsing of RCON responses, providing type-safe, structured data.
149
+
150
+ ### API Reference
151
+
152
+ ```javascript
153
+ const { QueryHelper } = require('@pilaf/backends');
154
+
155
+ const helper = new QueryHelper(rconBackend);
156
+
157
+ // List online players
158
+ const players = await helper.listPlayers();
159
+ // { online: 2, players: ['Steve', 'Alex'], raw: '...' }
160
+
161
+ // Get detailed player info
162
+ const info = await helper.getPlayerInfo('Steve');
163
+ // { player: 'Steve', dimension: 'minecraft:overworld',
164
+ // position: { x: 100.5, y: 64.0, z: -200.3 },
165
+ // health: 20, food: 20, saturation: 5, etc. }
166
+
167
+ // Get world time
168
+ const time = await helper.getWorldTime();
169
+ // { time: 1500, daytime: true, raw: '...' }
170
+
171
+ // Get weather
172
+ const weather = await helper.getWeather();
173
+ // { weather: 'clear', duration: -1, raw: '...' }
174
+
175
+ // Get difficulty
176
+ const difficulty = await helper.getDifficulty();
177
+ //// { difficulty: 'hard', raw: '...' }
178
+
179
+ // Get game mode
180
+ const gameMode = await helper.getGameMode();
181
+ // { gameMode: 'survival', mode: 'Survival', raw: '...' }
182
+
183
+ // Get server TPS
184
+ const tps = await helper.getTPS();
185
+ // { tps: 19.8, raw: '...' }
186
+
187
+ // Get world seed
188
+ const seed = await helper.getSeed();
189
+ // { seed: 1234567890, raw: '...' }
190
+ ```
191
+
192
+ ### Usage Example
193
+
194
+ ```javascript
195
+ const { MineflayerBackend } = require('@pilaf/backends');
196
+
197
+ describe('Server State Tests', () => {
198
+ let backend;
199
+
200
+ beforeEach(async () => {
201
+ backend = new MineflayerBackend();
202
+ await backend.connect({
203
+ host: 'localhost',
204
+ port: 25565,
205
+ auth: 'offline',
206
+ rconPort: 25575,
207
+ rconPassword: 'test'
208
+ });
209
+ });
210
+
211
+ it('should retrieve player information', async () => {
212
+ const info = await backend.getPlayerInfo('TestPlayer');
213
+
214
+ expect(info.player).toBe('TestPlayer');
215
+ expect(info.health).toBeGreaterThan(0);
216
+ expect(info.position).toBeDefined();
217
+ });
218
+
219
+ it('should get current server TPS', async () => {
220
+ const { tps } = await backend.getTPS();
221
+
222
+ expect(tps).toBeGreaterThan(15); // Server should be healthy
223
+ });
224
+
225
+ afterEach(async () => {
226
+ await backend.disconnect();
227
+ });
228
+ });
229
+ ```
230
+
231
+ ---
232
+
233
+ ## 📡 EventObserver
234
+
235
+ Clean API for subscribing to Minecraft server events with pattern matching and wildcard support.
236
+
237
+ ### Overview
238
+
239
+ 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.
240
+
241
+ ### API Reference
242
+
243
+ ```javascript
244
+ const { EventObserver } = require('@pilaf/backends');
245
+
246
+ const observer = new EventObserver({
247
+ logMonitor: monitor, // LogMonitor instance
248
+ parser: parser // MinecraftLogParser instance
249
+ });
250
+
251
+ // Subscribe to specific events
252
+ const unsubscribe = observer.onEvent('entity.join', (event) => {
253
+ console.log('Join:', event.type, event.data);
254
+ });
255
+
256
+ // Convenience methods
257
+ observer.onPlayerJoin((event) => { /* ... */ });
258
+ observer.onPlayerLeave((event) => { /* ... */ });
259
+ observer.onPlayerDeath((event) => { /* ... */ });
260
+ observer.onPlayerChat((event) => { /* ... */ });
261
+ observer.onCommand((event) => { /* ... */ });
262
+
263
+ // Wildcard patterns
264
+ observer.onEvent('entity.death.*', (event) => {
265
+ // Matches: entity.death.player, entity.death.mob, etc.
266
+ });
267
+
268
+ // Start observing
269
+ await observer.start();
270
+
271
+ // Stop observing
272
+ observer.stop();
273
+
274
+ // Unsubscribe from specific pattern
275
+ unsubscribe();
276
+ ```
277
+
278
+ ### Event Types
279
+
280
+ | Event Type | Description | Event Data Structure |
281
+ |------------|-------------|---------------------|
282
+ | `entity.join` | Player joined | `{ player, timestamp, location }` |
283
+ | `entity.leave` | Player left | `{ player, timestamp, reason }` |
284
+ | `entity.death.player` | Player died | `{ victim, killer, cause }` |
285
+ | `entity.death.mob` | Mob died | `{ victim, killer, cause }` |
286
+ | `entity.chat` | Player chat | `{ player, message, timestamp }` |
287
+ | `command.success` | Command succeeded | `{ command, executor, result }` |
288
+ | `command.failed` | Command failed | `{ command, error }` |
289
+
290
+ ### Usage Example
291
+
292
+ ```javascript
293
+ const { MineflayerBackend } = require('@pilaf/backends');
294
+
295
+ describe('Event Monitoring Tests', () => {
296
+ let backend;
297
+
298
+ beforeEach(async () => {
299
+ backend = new MineflayerBackend();
300
+ await backend.connect({ /* ... */ });
301
+ await backend.observe();
302
+ });
303
+
304
+ it('should track player join events', async () => {
305
+ const joins = [];
306
+ backend.onPlayerJoin((event) => {
307
+ joins.push(event.data.player);
308
+ });
309
+
310
+ // Simulate player joining
311
+ await backend.chat('/test join Player123');
312
+
313
+ await new Promise(resolve => setTimeout(resolve, 1000));
314
+
315
+ expect(joins).toContain('Player123');
316
+ });
317
+
318
+ afterEach(async () => {
319
+ backend.unobserve();
320
+ await backend.disconnect();
321
+ });
322
+ });
323
+ ```
324
+
325
+ ---
326
+
327
+ ## 📊 LogMonitor
328
+
329
+ Orchestrates log collection, parsing, correlation, and real-time event emission.
330
+
331
+ ### Overview
332
+
333
+ LogMonitor is the central orchestrator that:
334
+ 1. Collects logs from Docker containers or files
335
+ 2. Parses logs using MinecraftLogParser
336
+ 3. Correlates related events using strategies
337
+ 4. Emits structured events for consumption
338
+ 5. Uses a circular buffer for memory efficiency
339
+
340
+ ### Architecture
341
+
342
+ ```
343
+ DockerLogCollector → Log Lines → MinecraftLogParser → Structured Events
344
+
345
+ CircularBuffer (1000 lines)
346
+
347
+ Correlation Strategy
348
+
349
+ EventEmitter → Events
350
+
351
+ Correlation Sessions
352
+ ```
353
+
354
+ ### API Reference
355
+
356
+ ```javascript
357
+ const { LogMonitor } = require('@pilaf/backends');
358
+
359
+ const monitor = new LogMonitor({
360
+ collector: new DockerLogCollector({ container: 'mc-server', follow: true }),
361
+ parser: new MinecraftLogParser(),
362
+ correlation: new UsernameCorrelationStrategy(),
363
+ bufferSize: 1000 // Max events in circular buffer
364
+ });
365
+
366
+ // Subscribe to all events
367
+ monitor.on('event', (event) => {
368
+ console.log('Event:', event.type, event.data);
369
+ });
370
+
371
+ // Subscribe to correlation sessions
372
+ monitor.on('correlation', (session) => {
373
+ console.log('Session:', session.username, session.events.length);
374
+ });
375
+
376
+ // Start monitoring
377
+ await monitor.start();
378
+
379
+ // Stop monitoring
380
+ monitor.stop();
381
+ ```
382
+
383
+ ### Configuration Options
384
+
385
+ | Option | Type | Default | Description |
386
+ |--------|------|---------|-------------|
387
+ | `collector` | LogCollector | required | Log collector instance |
388
+ | `parser` | LogParser | required | Log parser instance |
389
+ | `correlation` | CorrelationStrategy | optional | Event correlation strategy |
390
+ | `bufferSize` | number | 1000 | Circular buffer size |
391
+
392
+ ---
393
+
394
+ ## 🔄 Correlation Strategies
395
+
396
+ ### UsernameCorrelationStrategy
397
+
398
+ Tracks player sessions by grouping events by username.
399
+
400
+ ```javascript
401
+ const { UsernameCorrelationStrategy } = require('@pilaf/backends');
402
+
403
+ const strategy = new UsernameCorrelationStrategy();
404
+
405
+ // Returns session object with all events for that player
406
+ const session = strategy.correlate({
407
+ type: 'entity.join',
408
+ data: { player: 'Steve' }
409
+ });
410
+
411
+ console.log(session.username); // 'Steve'
412
+ console.log(session.isActive); // true
413
+ console.log(session.events); // Array of all events
414
+
415
+ // Session ends when player leaves
416
+ strategy.correlate({
417
+ type: 'entity.leave',
418
+ data: { player: 'Steve' }
419
+ });
420
+ ```
421
+
422
+ ### TagCorrelationStrategy
423
+
424
+ Groups events by custom tag/ID with automatic expiration.
425
+
426
+ ```javascript
427
+ const { TagCorrelationStrategy } = require('@pilaf/backends');
428
+
429
+ const strategy = new TagCorrelationStrategy({
430
+ tagExtractor: (event) => event.data.questId, // Extract tag from event
431
+ timeout: 300000 // 5 minutes auto-expire
432
+ });
433
+
434
+ // All events with same questId are grouped
435
+ strategy.correlate({
436
+ type: 'quest.started',
437
+ data: { questId: 'dragon-quest-123', player: 'Steve' }
438
+ });
439
+
440
+ strategy.correlate({
441
+ type: 'quest.objective',
442
+ data: { questId: 'dragon-quest-123', objective: 'kill_dragon' }
443
+ });
444
+
445
+ strategy.correlate({
446
+ type: 'quest.completed',
447
+ data: { questId: 'dragon-quest-123', player: 'Steve' }
448
+ });
449
+
450
+ // All three events are grouped in one session
451
+ ```
452
+
453
+ ---
454
+
455
+ ## 📝 MinecraftLogParser
456
+
457
+ Pattern-based log parser that extracts structured events from raw Minecraft log lines.
458
+
459
+ ### Supported Log Patterns
460
+
461
+ | Pattern | Event Type | Description |
462
+ |---------|-----------|-------------|
463
+ | `/Teleported (\w+) to (.+)/` | `teleport` | Player teleportation |
464
+ | `/(\w+) was slain by (\w+)/` | `entity.death.player` | Player killed by entity |
465
+ | `/(\w+) was slain by (.+)/` | `entity.death.mob` | Mob killed |
466
+ | `/(\w+) joined the game/` | `entity.join` | Player joined |
467
+ | `/(\w+) left the game/` | `entity.leave` | Player left |
468
+ | `/\<(\w+)\> (?:ordered|said)\:/` | `entity.chat` | Player chat |
469
+ | `/Issued server command: (\S+) .+ by (\w+)/` | `command.success` | Command executed |
470
+
471
+ ### Usage Example
472
+
473
+ ```javascript
474
+ const { MinecraftLogParser } = require('@pilaf/backends');
475
+
476
+ const parser = new MinecraftLogParser();
477
+
478
+ // Add custom pattern
479
+ parser.addPattern('custom', '/Custom event: (.+)/', (match) => ({
480
+ message: match[1]
481
+ }));
482
+
483
+ // Parse a log line
484
+ const event = parser.parse('[12:34:56] [Server thread/INFO]: Teleported Steve to 100 70 200');
485
+
486
+ if (event) {
487
+ console.log(event.type); // 'teleport'
488
+ console.log(event.data); // { player: 'Steve', destination: '100 70 200' }
489
+ console.log(event.raw); // Original log line
490
+ }
491
+ ```
492
+
493
+ ---
494
+
495
+ ## 🗃️ PatternRegistry
496
+
497
+ Centralized pattern management with priority-based ordering for complex log parsing scenarios.
498
+
499
+ ### Overview
500
+
501
+ PatternRegistry manages multiple parsing patterns with:
502
+ - **Priority ordering**: More specific patterns tested first (lower priority number = higher priority)
503
+ - **Pattern types**: RegExp or string patterns
504
+ - **Handler functions**: Custom parsing logic
505
+ - **Dynamic management**: Add/remove patterns at runtime
506
+
507
+ ### API Reference
508
+
509
+ ```javascript
510
+ const { PatternRegistry } = require('@pilaf/backends');
511
+
512
+ const registry = new PatternRegistry({
513
+ caseInsensitive: false // Enable case-insensitive matching
514
+ });
515
+
516
+ // Add pattern with priority (0-10, where 0 is highest priority)
517
+ registry.addPattern('high-priority', /Specific pattern (.+)/, (match) => ({
518
+ captured: match[1]
519
+ }), 1); // High priority (tested first)
520
+
521
+ registry.addPattern('low-priority', /.+/, (match) => ({
522
+ everything: match[0]
523
+ }), 10); // Low priority (tested last)
524
+
525
+ // Match first matching pattern in priority order
526
+ const result = registry.match('[Server] Specific pattern matched');
527
+
528
+ // Remove pattern
529
+ registry.removePattern('high-priority');
530
+
531
+ // Get pattern
532
+ const pattern = registry.getPattern('low-priority');
533
+
534
+ // Get all pattern names
535
+ const names = registry.getPatterns();
536
+
537
+ // Clone registry
538
+ const cloned = registry.clone();
539
+
540
+ // Clear all patterns
541
+ registry.clear();
542
+ ```
543
+
544
+ ### Priority System
545
+
546
+ Patterns are tested in ascending priority order:
547
+
548
+ ```javascript
549
+ // Priority 1: High priority (tested first)
550
+ registry.addPattern('exact-match', /^Teleported Steve to (.+)/), handler, 1);
551
+
552
+ // Priority 10: Low priority (fallback)
553
+ registry.addPattern('fallback', /.+/, handler, 10);
554
+
555
+ // When parsing "Teleported Steve to 100 64 100":
556
+ // - Tests 'exact-match' first → matches!
557
+ // - Returns immediately, 'fallback' never tested
558
+ ```
559
+
560
+ ### Usage Example
561
+
562
+ ```javascript
563
+ const { PatternRegistry, MinecraftLogParser } = require('@pilaf/backends');
564
+
565
+ const parser = new MinecraftLogParser();
566
+
567
+ // Add custom high-priority pattern
568
+ parser.addPattern('dragon-spawn', /Dragon spawned at (.+)/, (match) => ({
569
+ location: match[1]
570
+ }), 1);
571
+
572
+ // Add low-priority catch-all
573
+ parser.addPattern('default', /.+/, (match) => ({
574
+ raw: match[0]
575
+ }), 10);
576
+
577
+ // Parse log line - dragon pattern tested first
578
+ const event = parser.parse('[12:34:56] Dragon spawned at 100 70 200');
579
+ // { type: 'dragon-spawn', data: { location: '100 70 200' }, raw: '...' }
580
+ ```
581
+
582
+ ---
583
+
584
+ ## 🐳 DockerLogCollector
585
+
586
+ Streams logs from Docker containers with automatic reconnection and error handling.
587
+
588
+ ### Overview
589
+
590
+ DockerLogCollector connects to Docker containers and follows log output in real-time, with:
591
+ - **Automatic reconnection**: Exponential backoff on disconnection
592
+ - **ANSI code stripping**: Clean log output without color codes
593
+ - **Pause/resume**: Control data flow during tests
594
+ - **Error handling**: Graceful error recovery
595
+
596
+ ### API Reference
597
+
598
+ ```javascript
599
+ const { DockerLogCollector } = require('@pilaf/backends');
600
+
601
+ const collector = new DockerLogCollector({
602
+ dockerodeOptions: {
603
+ socketPath: '/var/run/docker.sock'
604
+ },
605
+ reconnectDelay: 1000, // Initial reconnection delay (ms)
606
+ maxReconnectDelay: 30000, // Maximum reconnection delay (ms)
607
+ reconnectAttempts: 5 // Maximum reconnection attempts
608
+ });
609
+
610
+ // Connect to container
611
+ await collector.connect({
612
+ containerName: 'minecraft-server',
613
+ follow: true, // Follow log stream
614
+ tail: 100, // Last N lines from history
615
+ stdout: true,
616
+ stderr: true,
617
+ disableAutoReconnect: false // Disable automatic reconnection
618
+ });
619
+
620
+ // Subscribe to events
621
+ collector.on('data', (line) => {
622
+ console.log('Log:', line);
623
+ });
624
+
625
+ collector.on('connected', () => {
626
+ console.log('Connected to container');
627
+ });
628
+
629
+ collector.on('reconnecting', (info) => {
630
+ console.log(`Reconnecting attempt ${info.attempt}/${info.maxAttempts}`);
631
+ });
632
+
633
+ collector.on('end', () => {
634
+ console.log('Stream ended');
635
+ });
636
+
637
+ collector.on('error', (error) => {
638
+ console.error('Collector error:', error);
639
+ });
640
+
641
+ // Pause/resume
642
+ collector.pause(); // Stop emitting data events
643
+ collector.resume(); // Resume emitting data events
644
+
645
+ // Get reconnection status
646
+ const status = collector.getReconnectStatus();
647
+ // { attempt: 0, maxAttempts: 5, reconnecting: false }
648
+
649
+ // Disconnect
650
+ await collector.disconnect();
651
+ ```
652
+
653
+ ### Usage Example with LogMonitor
654
+
655
+ ```javascript
656
+ const { LogMonitor, DockerLogCollector, MinecraftLogParser, UsernameCorrelationStrategy } = require('@pilaf/backends');
657
+
658
+ const monitor = new LogMonitor({
659
+ collector: new DockerLogCollector({
660
+ container: 'minecraft-server',
661
+ follow: true
662
+ }),
663
+ parser: new MinecraftLogParser(),
664
+ correlation: new UsernameCorrelationStrategy(),
665
+ bufferSize: 1000
666
+ });
667
+
668
+ // Subscribe to events
669
+ monitor.on('event', (event) => {
670
+ console.log('Event:', event.type, event.data);
671
+ });
672
+
673
+ // Start monitoring
674
+ await monitor.start();
675
+
676
+ // Stop monitoring
677
+ monitor.stop();
678
+ ```
679
+
680
+ ---
681
+
682
+ ## ⭕ CommandRouter
683
+
684
+ Abstract base class for routing commands to appropriate execution channels (bot chat, RCON, or log monitoring).
685
+
686
+ ### Overview
687
+
688
+ CommandRouter implements intelligent command routing:
689
+ - `/data get` commands → RCON (structured NBT responses)
690
+ - `/execute` with run data → RCON (structured queries)
691
+ - `useRcon` option → RCON (forced routing)
692
+ - `expectLogResponse` option → Log monitoring (event correlation)
693
+ - Default → Bot chat (player commands)
694
+
695
+ ### Channels
696
+
697
+ ```javascript
698
+ const { CommandRouter } = require('@pilaf/backends');
699
+
700
+ // Available channels
701
+ CommandRouter.CHANNELS.BOT // Send via bot.chat()
702
+ CommandRouter.CHANNELS.RCON // Send via RCON
703
+ CommandRouter.CHANNELS.LOG // Send via bot and wait for log response
704
+ ```
705
+
706
+ ### Example Implementation
707
+
708
+ ```javascript
709
+ const { CommandRouter } = require('@pilaf/backends');
710
+
711
+ class SmartCommandRouter extends CommandRouter {
712
+ route(command, context) {
713
+ const { options } = context;
714
+
715
+ // Check forced options first
716
+ if (options?.useRcon) {
717
+ return { channel: CommandRouter.CHANNELS.RCON, options };
718
+ }
719
+ if (options?.expectLogResponse) {
720
+ return { channel: CommandRouter.CHANNELS.LOG, options };
721
+ }
722
+
723
+ // Check custom rules
724
+ const rules = this.getRules();
725
+ for (const { pattern, channel } of rules) {
726
+ if (this._matchesPattern(command, pattern)) {
727
+ return { channel, options };
728
+ }
729
+ }
730
+
731
+ // Default: bot chat
732
+ return { channel: CommandRouter.CHANNELS.BOT, options };
733
+ }
734
+ }
735
+ ```
736
+
737
+ ### Usage Example
738
+
739
+ ```javascript
740
+ const router = new SmartCommandRouter();
741
+
742
+ // Add custom routing rule
743
+ router.addRule(/^\/data get/, CommandRouter.CHANNELS.RCON);
744
+
745
+ // Route command
746
+ const result = router.route('/data get entity TestPlayer', { options: {} });
747
+ console.log(result.channel); // 'rcon'
748
+ ```
749
+
750
+ ---
751
+
752
+ ## 🔁 CircularBuffer
753
+
754
+ Fixed-size circular buffer for memory-efficient event storage with automatic overflow handling.
755
+
756
+ ### Overview
757
+
758
+ CircularBuffer provides O(1) operations and prevents memory leaks in long-running tests by limiting stored events.
759
+
760
+ ### API Reference
761
+
762
+ ```javascript
763
+ const { CircularBuffer } = require('@pilaf/backends');
764
+
765
+ const buffer = new CircularBuffer({
766
+ size: 1000, // Maximum number of events
767
+ onOverflow: 'discard' // 'discard' or 'error'
768
+ });
769
+
770
+ // Add event
771
+ buffer.push(event);
772
+
773
+ // Get all events
774
+ const events = buffer.getAll();
775
+
776
+ // Get buffer size
777
+ buffer.size; // Current event count
778
+ buffer.maxSize; // Maximum capacity
779
+
780
+ // Check if full
781
+ buffer.isFull();
782
+
783
+ // Clear buffer
784
+ buffer.clear();
785
+
786
+ // Iterate over events
787
+ buffer.forEach((event, index) => {
788
+ console.log(`Event ${index}:`, event.type);
789
+ });
790
+ ```
791
+
792
+ ### Usage Example with LogMonitor
793
+
794
+ ```javascript
795
+ const { LogMonitor } = require('@pilaf/backends');
796
+
797
+ const monitor = new LogMonitor({
798
+ bufferSize: 500 // Store last 500 events
799
+ });
800
+
801
+ monitor.on('event', (event) => {
802
+ // Events are automatically buffered
803
+ });
804
+
805
+ // Retrieve recent events
806
+ const recentEvents = monitor.getEvents();
807
+ ```
808
+
809
+ ---
810
+
811
+ ## 🚀 Enhanced MineflayerBackend
812
+
813
+ The MineflayerBackend now includes integrated QueryHelper and EventObserver with **lazy initialization**.
814
+
815
+ ### New Features
816
+
817
+ 1. **Query Methods** (delegates to QueryHelper)
818
+ - `listPlayers()`
819
+ - `getPlayerInfo(username)`
820
+ - `getWorldTime()`
821
+ - `getWeather()`
822
+ - `getDifficulty()`
823
+ - `getGameMode()`
824
+ - `getTPS()`
825
+ - `getSeed()`
826
+
827
+ 2. **Event Methods** (delegates to EventObserver, lazy-loaded)
828
+ - `observe()` - Starts log monitoring
829
+ - `unobserve()` - Stops log monitoring
830
+ - `isObserving()` - Check if observing
831
+ - `onPlayerJoin(callback)`
832
+ - `onPlayerLeave(callback)`
833
+ - `onPlayerDeath(callback)`
834
+ - `onPlayerChat(callback)`
835
+ - `onCommand(callback)`
836
+ - `onEvent(pattern, callback)`
837
+
838
+ 3. **Lazy Initialization**
839
+ - EventObserver is only created when `observe()` is called
840
+ - LogMonitor and parser are created on-demand
841
+ - Zero overhead if events are not used
842
+
843
+ ### Usage Example
844
+
845
+ ```javascript
846
+ const { MineflayerBackend } = require('@pilaf/backends');
847
+
848
+ const backend = new MineflayerBackend();
849
+
850
+ // Connect with RCON integration
851
+ await backend.connect({
852
+ host: 'localhost',
853
+ port: 25565,
854
+ auth: 'offline',
855
+ rconPort: 25575,
856
+ rconPassword: 'password'
857
+ });
858
+
859
+ // Query server state
860
+ const players = await backend.listPlayers();
861
+ console.log('Online players:', players.players);
862
+
863
+ const tps = await backend.getTPS();
864
+ console.log('Server TPS:', tps.tps);
865
+
866
+ // Start event observation (lazy initialization)
867
+ await backend.observe();
868
+
869
+ // Subscribe to events
870
+ backend.onPlayerJoin((event) => {
871
+ console.log('Player joined:', event.data.player);
872
+ });
873
+
874
+ backend.onPlayerDeath((event) => {
875
+ console.log('Player died:', event.data.cause);
876
+ });
877
+
878
+ // Trigger command
879
+ await backend.chat('/test command');
880
+
881
+ // Clean up
882
+ backend.unobserve();
883
+ await backend.disconnect();
884
+ ```
885
+
886
+ ### Complete Test Example
887
+
888
+ ```javascript
889
+ describe('Elemental Dragon Tests', () => {
890
+ let backend;
891
+
892
+ beforeEach(async () => {
893
+ backend = new MineflayerBackend();
894
+ await backend.connect({
895
+ host: 'localhost',
896
+ port: 25565,
897
+ username: `TestBot_${Date.now()}`,
898
+ auth: 'offline',
899
+ rconPort: 25575,
900
+ rconPassword: 'test'
901
+ });
902
+ await backend.observe();
903
+ });
904
+
905
+ afterEach(async () => {
906
+ backend.unobserve();
907
+ await backend.disconnect();
908
+ });
909
+
910
+ it('should spawn dragon and verify TPS', async () => {
911
+ // Spawn dragon
912
+ await backend.chat('/summon elemental_dragon 100 70 200');
913
+
914
+ // Wait for spawn
915
+ await new Promise(resolve => setTimeout(resolve, 1000));
916
+
917
+ // Verify server health
918
+ const { tps } = await backend.getTPS();
919
+ expect(tps.tps).toBeGreaterThan(15);
920
+ });
921
+
922
+ it('should track player join events', async () => {
923
+ const joins = [];
924
+ backend.onPlayerJoin((event) => {
925
+ joins.push(event.data.player);
926
+ });
927
+
928
+ // Trigger join
929
+ await backend.chat('/test join Player123');
930
+
931
+ await new Promise(resolve => setTimeout(resolve, 500));
932
+
933
+ expect(joins).toContain('Player123');
934
+ });
935
+ });
936
+ ```
937
+
938
+ ---
939
+
940
+ ## 🔧 Core Components
80
941
 
81
942
  ### ConnectionState
82
943
 
@@ -96,7 +957,7 @@ ConnectionState.DISCONNECTING // Disconnecting
96
957
 
97
958
  ### BotPool
98
959
 
99
- Manages a pool of Mineflayer bot instances.
960
+ Manages a pool of Mineflayer bot instances for parallel testing.
100
961
 
101
962
  ```javascript
102
963
  const { BotPool } = require('@pilaf/backends');
@@ -113,6 +974,9 @@ const pool = new BotPool({
113
974
  // Acquire a bot
114
975
  const bot = await pool.acquire('Bot1');
115
976
 
977
+ // Use bot
978
+ await bot.chat('Hello!');
979
+
116
980
  // Release when done
117
981
  await pool.release('Bot1');
118
982
 
@@ -136,6 +1000,302 @@ const isHealthy = await checker.check();
136
1000
  console.log(isHealthy.status); // 'healthy' | 'unhealthy'
137
1001
  ```
138
1002
 
139
- ## License
1003
+ ---
1004
+
1005
+ ## 🧪 Testing Patterns
1006
+
1007
+ ### Pattern 1: Server State Verification
1008
+
1009
+ ```javascript
1010
+ it('should verify server state after action', async () => {
1011
+ await backend.chat('/time set 12000');
1012
+
1013
+ const { time } = await backend.getWorldTime();
1014
+ expect(time.time).toBe(12000);
1015
+ });
1016
+ ```
1017
+
1018
+ ### Pattern 2: Event Correlation
1019
+
1020
+ ```javascript
1021
+ it('should track multi-step scenario', async () => {
1022
+ const questEvents = [];
1023
+
1024
+ monitor.on('correlation', (session) => {
1025
+ if (session.questId === 'test-quest') {
1026
+ questEvents.push(...session.events);
1027
+ }
1028
+ });
1029
+
1030
+ await backend.chat('/quest start test-quest');
1031
+ await backend.chat('/quest complete test-quest');
1032
+
1033
+ await new Promise(resolve => setTimeout(resolve, 1000));
1034
+
1035
+ expect(questEvents).toHaveLength(2);
1036
+ });
1037
+ ```
1038
+
1039
+ ### Pattern 3: Docker Integration
1040
+
1041
+ ```javascript
1042
+ describe('Docker Tests', () => {
1043
+ let monitor;
1044
+
1045
+ beforeEach(async () => {
1046
+ monitor = new LogMonitor({
1047
+ collector: new DockerLogCollector({
1048
+ container: 'mc-server',
1049
+ follow: true
1050
+ }),
1051
+ parser: new MinecraftLogParser()
1052
+ });
1053
+ await monitor.start();
1054
+ });
1055
+
1056
+ afterEach(async () => {
1057
+ await monitor.stop();
1058
+ });
1059
+ });
1060
+ ```
1061
+
1062
+ ### Pattern 4: Async Event Handling
1063
+
1064
+ ```javascript
1065
+ it('should handle async events', async () => {
1066
+ let eventReceived = false;
1067
+
1068
+ backend.onPlayerJoin(() => {
1069
+ eventReceived = true;
1070
+ });
1071
+
1072
+ await backend.chat('/test trigger');
1073
+
1074
+ await waitFor(() => eventReceived, 5000);
1075
+ expect(eventReceived).toBe(true);
1076
+ });
1077
+
1078
+ function waitFor(condition, timeout) {
1079
+ return new Promise((resolve) => {
1080
+ const startTime = Date.now();
1081
+ const check = () => {
1082
+ if (condition() || Date.now() - startTime > timeout) {
1083
+ resolve(condition());
1084
+ } else {
1085
+ setTimeout(check, 100);
1086
+ }
1087
+ };
1088
+ check();
1089
+ });
1090
+ }
1091
+ ```
1092
+
1093
+ ---
1094
+
1095
+ ## 📚 API Reference
1096
+
1097
+ ### Exports
1098
+
1099
+ ```javascript
1100
+ const {
1101
+ // Backends
1102
+ RconBackend,
1103
+ MineflayerBackend,
1104
+ PilafBackendFactory,
1105
+
1106
+ // Core
1107
+ ConnectionState,
1108
+ CommandRouter,
1109
+
1110
+ // Helpers
1111
+ QueryHelper,
1112
+ EventObserver,
1113
+
1114
+ // Monitoring
1115
+ LogMonitor,
1116
+ DockerLogCollector,
1117
+
1118
+ // Parsing
1119
+ LogParser,
1120
+ MinecraftLogParser,
1121
+ PatternRegistry,
1122
+
1123
+ // Correlation
1124
+ CorrelationStrategy,
1125
+ UsernameCorrelationStrategy,
1126
+ TagCorrelationStrategy,
1127
+
1128
+ // Utilities
1129
+ BotPool,
1130
+ ServerHealthChecker,
1131
+ CircularBuffer
1132
+ } = require('@pilaf/backends');
1133
+ ```
1134
+
1135
+ ---
1136
+
1137
+ ## 🐛 Troubleshooting
1138
+
1139
+ ### Issue: Events not being captured
1140
+
1141
+ **Cause**: Not observing before triggering events
1142
+
1143
+ **Solution**:
1144
+ ```javascript
1145
+ // WRONG
1146
+ await backend.chat('/test');
1147
+ await backend.observe();
1148
+
1149
+ // CORRECT
1150
+ await backend.observe();
1151
+ backend.onPlayerJoin(handler);
1152
+ await backend.chat('/test');
1153
+ ```
1154
+
1155
+ ### Issue: Tests timing out
1156
+
1157
+ **Cause**: Not waiting for async operations
1158
+
1159
+ **Solution**:
1160
+ ```javascript
1161
+ it('test with timeout', async () => {
1162
+ await backend.chat('/command');
1163
+ await new Promise(resolve => setTimeout(resolve, 1000));
1164
+ // Then verify
1165
+ }, 10000);
1166
+ ```
1167
+
1168
+ ### Issue: Docker connection fails
1169
+
1170
+ **Cause**: Docker socket not accessible
1171
+
1172
+ **Solution**:
1173
+ ```javascript
1174
+ const collector = new DockerLogCollector({
1175
+ dockerodeOptions: {
1176
+ socketPath: '/var/run/docker.sock' // Default path
1177
+ }
1178
+ });
1179
+ ```
1180
+
1181
+ ### Issue: Pattern not matching
1182
+
1183
+ **Cause**: Case sensitivity or priority ordering
1184
+
1185
+ **Solution**:
1186
+ ```javascript
1187
+ const registry = new PatternRegistry({ caseInsensitive: true });
1188
+ registry.addPattern('test', /test/i, handler); // Case insensitive
1189
+ registry.addPattern('test', /^test/, handler, 1); // High priority
1190
+ ```
1191
+
1192
+ ---
1193
+
1194
+ ## 📋 Migration Checklist
1195
+
1196
+ ### From Manual Testing to Pilaf
1197
+
1198
+ - [ ] Install `@pilaf/backends` package
1199
+ - [ ] Configure test environment (Jest, Pilaf reporter)
1200
+ - [ ] Set up test server (local or Docker)
1201
+ - [ ] Write first test (start with simple query)
1202
+ - [ ] Add event observation
1203
+ - [ ] Implement correlation for complex scenarios
1204
+ - [ ] Set up CI/CD integration
1205
+ - [ ] Train team on Pilaf usage
1206
+
1207
+ ### From Raw RCON to QueryHelper
1208
+
1209
+ - [ ] Replace manual RCON string parsing with QueryHelper
1210
+ - [ ] Use structured responses in assertions
1211
+ - [ ] Remove regex parsing code
1212
+ - [ ] Test with actual server responses
1213
+
1214
+ ### From Manual Log Parsing to EventObserver
1215
+
1216
+ - [ ] Replace log monitoring code with EventObserver
1217
+ - [ ] Use convenience methods (onPlayerJoin, etc.)
1218
+ - [ ] Add wildcard patterns for custom events
1219
+ - [ ] Implement correlation strategies
1220
+ - [ ] Remove manual parsing code
1221
+
1222
+ ---
1223
+
1224
+ ## 🎯 Best Practices
1225
+
1226
+ ### 1. Test Isolation
1227
+
1228
+ ```javascript
1229
+ beforeEach(async () => {
1230
+ backend = new MineflayerBackend();
1231
+ await backend.connect({
1232
+ username: `TestBot_${Date.now()}`, // Unique username
1233
+ auth: 'offline'
1234
+ });
1235
+ });
1236
+
1237
+ afterEach(async () => {
1238
+ await backend.disconnect();
1239
+ });
1240
+ ```
1241
+
1242
+ ### 2. Event Observation Lifecycle
1243
+
1244
+ ```javascript
1245
+ it('test events', async () => {
1246
+ await backend.observe(); // Start FIRST
1247
+ backend.onPlayerJoin(() => { });
1248
+ await backend.chat('/test');
1249
+ await new Promise(resolve => setTimeout(resolve, 500));
1250
+ backend.unobserve(); // Stop LAST
1251
+ });
1252
+ ```
1253
+
1254
+ ### 3. Use Correlation for Multi-Step Tests
1255
+
1256
+ ```javascript
1257
+ monitor.on('correlation', (session) => {
1258
+ // All events for this player/session
1259
+ expect(session.events).toHaveLength(expectedCount);
1260
+ });
1261
+ ```
1262
+
1263
+ ### 4. Docker for CI/CD
1264
+
1265
+ ```yaml
1266
+ # docker-compose.test.yml
1267
+ version: '3'
1268
+ services:
1269
+ minecraft-server:
1270
+ image: pilaf/minecraft-test-server
1271
+ ports:
1272
+ - "25565:25565"
1273
+ - "25575:25575"
1274
+ environment:
1275
+ - RCON_PASSWORD=test
1276
+ - OPS=TestBot
1277
+ ```
1278
+
1279
+ ### 5. Handle Async Timing
1280
+
1281
+ ```javascript
1282
+ // Always wait for server response
1283
+ await new Promise(resolve => setTimeout(resolve, 500));
1284
+
1285
+ // For longer operations
1286
+ await waitFor(() => condition(), 5000);
1287
+ ```
1288
+
1289
+ ---
1290
+
1291
+ ## 📖 Additional Documentation
1292
+
1293
+ - **Architecture**: See `docs/plans/2025-01-16-pilaf-js-design.md` for detailed architecture
1294
+ - **Examples**: Check `lib/helpers/*.pilaf.test.js` for reference implementations
1295
+ - **Integration Guide**: See `docs/proposals/elementaldragon-pilaf-integration-guide.md` for ElementalDragon-specific usage
1296
+
1297
+ ---
1298
+
1299
+ ## 📄 License
140
1300
 
141
1301
  MIT