@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 +1170 -10
- package/lib/collectors/DockerLogCollector.js +334 -0
- package/lib/collectors/index.js +9 -0
- package/lib/core/CommandRouter.js +154 -0
- package/lib/core/CorrelationStrategy.js +172 -0
- package/lib/core/LogCollector.js +194 -0
- package/lib/core/LogParser.js +125 -0
- package/lib/core/index.js +26 -0
- package/lib/errors/index.js +363 -0
- package/lib/helpers/EventObserver.js +267 -0
- package/lib/helpers/QueryHelper.js +279 -0
- package/lib/helpers/index.js +13 -0
- package/lib/index.js +72 -1
- package/lib/mineflayer-backend.js +266 -1
- package/lib/monitoring/CircularBuffer.js +202 -0
- package/lib/monitoring/LogMonitor.js +303 -0
- package/lib/monitoring/correlations/TagCorrelationStrategy.js +214 -0
- package/lib/monitoring/correlations/UsernameCorrelationStrategy.js +233 -0
- package/lib/monitoring/correlations/index.js +13 -0
- package/lib/monitoring/index.js +16 -0
- package/lib/parsers/MinecraftLogParser.js +512 -0
- package/lib/parsers/PatternRegistry.js +366 -0
- package/lib/parsers/fixtures/minecraft-logs.js +188 -0
- package/lib/parsers/index.js +13 -0
- package/lib/rcon-backend.js +42 -26
- package/package.json +3 -2
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
|
|
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
|
-
|
|
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()
|
|
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
|
|
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:
|
|
133
|
+
host:localhost',
|
|
73
134
|
port: 25565,
|
|
74
135
|
username: 'TestBot',
|
|
75
136
|
auth: 'offline'
|
|
76
137
|
});
|
|
77
138
|
```
|
|
78
139
|
|
|
79
|
-
|
|
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
|
-
|
|
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
|