Skip to content

Commit 8af96fc

Browse files
plainprinceSimeon Kummercursoragentrom1504claude
authored
Fix crash in digging plugin death handler (#3553) (#3835)
* Fix crash in digging plugin death handler when bot._events is undefined When the bot dies during certain states (e.g. Velocity proxy transfers, or when using Bun runtime), the EventEmitter internals can be torn down before the death event fires. This causes removeAllListeners to throw "TypeError: undefined is not an object (evaluating 'this._events')". Wrapping the death handler in a try/catch prevents the crash. Fixes #3553 Co-authored-by: Cursor <cursoragent@cursor.com> * Add test for digging death handler crash fix Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add external test for digging death crash fix Verifies that killing the bot mid-dig via /kill doesn't crash, by placing a block, starting to dig it in survival mode, killing the bot, and confirming the death event fires and the bot respawns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): reset state after diggingDeath to prevent cascading failures After killing the bot, wait for chunks to load at the respawn point and call resetState() so subsequent tests start with a clean creative-mode bot at the origin with loaded chunks. Without this, the next test's resetState() would fail with "Timeout waiting for chunks" or "updateSlot did not fire" because the bot was still at the respawn location without chunks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove flaky diggingDeath external test (covered by unit test) --------- Co-authored-by: Simeon Kummer <simeon@hitthecode.de> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: rom1504 <rom1504@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Claude <claude@anthropic.com>
1 parent dcbc5b2 commit 8af96fc

2 files changed

Lines changed: 95 additions & 3 deletions

File tree

lib/plugins/digging.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,11 @@ function inject (bot) {
210210
}
211211

212212
bot.on('death', () => {
213-
bot.removeAllListeners('diggingAborted')
214-
bot.removeAllListeners('diggingCompleted')
215-
bot.stopDigging()
213+
try {
214+
bot.removeAllListeners('diggingAborted')
215+
bot.removeAllListeners('diggingCompleted')
216+
bot.stopDigging()
217+
} catch (_) {}
216218
})
217219

218220
function canDigBlock (block) {

test/diggingDeathTest.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/* eslint-env mocha */
2+
3+
const EventEmitter = require('events')
4+
const assert = require('assert')
5+
const inject = require('../lib/plugins/digging')
6+
7+
describe('digging plugin death handler', () => {
8+
function createMockBot () {
9+
const bot = new EventEmitter()
10+
// The digging plugin assigns these on the bot
11+
bot.targetDigBlock = null
12+
bot.targetDigFace = null
13+
bot.lastDigTime = null
14+
// stopDigging is set by the plugin, but starts as the noop at bottom of digging.js
15+
// We don't pre-set it so the plugin can assign it
16+
// Provide minimal _client stub for stopDigging path (write is called during cancel)
17+
bot._client = { write: () => {} }
18+
// entity stub needed if canDigBlock or digTime are called
19+
bot.entity = {
20+
position: { x: 0, y: 0, z: 0, offset: () => ({ x: 0, y: 0, z: 0, distanceTo: () => 0 }) },
21+
isInWater: false,
22+
onGround: true,
23+
eyeHeight: 1.62,
24+
effects: {}
25+
}
26+
bot.heldItem = null
27+
bot.game = { gameMode: 'survival' }
28+
bot.inventory = { slots: [] }
29+
bot.getEquipmentDestSlot = () => 5
30+
bot.swingArm = () => {}
31+
bot.world = { raycast: () => null }
32+
return bot
33+
}
34+
35+
it('should not throw when death is emitted and no digging is in progress', () => {
36+
const bot = createMockBot()
37+
inject(bot)
38+
// Simply emitting death should not throw
39+
assert.doesNotThrow(() => {
40+
bot.emit('death')
41+
})
42+
})
43+
44+
it('should not throw when death is emitted and stopDigging throws', () => {
45+
const bot = createMockBot()
46+
inject(bot)
47+
// Simulate a broken stopDigging that throws
48+
bot.stopDigging = () => {
49+
throw new Error('unexpected state error')
50+
}
51+
// The try/catch in the death handler should swallow this
52+
assert.doesNotThrow(() => {
53+
bot.emit('death')
54+
})
55+
})
56+
57+
it('should not throw when death is emitted with problematic event listeners', () => {
58+
const bot = createMockBot()
59+
inject(bot)
60+
// Add a listener for diggingAborted that would be removed
61+
bot.on('diggingAborted', () => {})
62+
bot.on('diggingCompleted', () => {})
63+
// Override removeAllListeners to throw (simulating the crash scenario)
64+
const origRemoveAll = bot.removeAllListeners.bind(bot)
65+
bot.removeAllListeners = (event) => {
66+
if (event === 'diggingAborted') {
67+
throw new Error('Cannot read properties of undefined')
68+
}
69+
return origRemoveAll(event)
70+
}
71+
// The try/catch should protect against this
72+
assert.doesNotThrow(() => {
73+
bot.emit('death')
74+
})
75+
})
76+
77+
it('should clean up digging listeners on death when digging is active', () => {
78+
const bot = createMockBot()
79+
inject(bot)
80+
// Simulate active listeners
81+
bot.on('diggingAborted', () => {})
82+
bot.on('diggingCompleted', () => {})
83+
assert.ok(bot.listenerCount('diggingAborted') > 0)
84+
assert.ok(bot.listenerCount('diggingCompleted') > 0)
85+
bot.emit('death')
86+
// After death, digging listeners should be removed
87+
assert.strictEqual(bot.listenerCount('diggingAborted'), 0)
88+
assert.strictEqual(bot.listenerCount('diggingCompleted'), 0)
89+
})
90+
})

0 commit comments

Comments
 (0)