In the Java rewrite of Lilies, right clicking a tulip into the air was supposed to bank it. It did nothing. The module had five unit tests and all of them passed.
What the handler looked like
@EventHandler(ignoreCancelled = true)
public void onUse(PlayerInteractEvent e) {
if (e.getAction() != Action.RIGHT_CLICK_AIR) return;
// bank the tulip ...
}
ignoreCancelled = true is a habit in Bukkit plugins: if another plugin cancelled the event, skip
it. It reads as harmless. Here it made the whole handler dead code.
Why it never ran
For PlayerInteractEvent, isCancelled() is not a separate flag. It returns true when
useInteractedBlock() is DENY. When you click air there is no block, and Paper builds
the event with DENY already set. So every air click arrives already cancelled, and a handler that
ignores cancelled events never sees one.
The unit tests could not catch it. They built the event by hand and called the handler method directly, so
the event was never cancelled and the ignoreCancelled check never ran at all.
How it was found
A boot test. It starts a real server with every plugin jar loaded and fires real events through the plugin manager, the same path a player's click takes. It failed on the first run. Searching the rest of the suite found the same flag on seven handlers, including the one that picks up money drops.
The fix
@EventHandler
public void onUse(PlayerInteractEvent e) {
if (e.getAction() == Action.RIGHT_CLICK_BLOCK && e.isCancelled()) return;
// ...
}
Drop the flag and say what you mean. A denied block click still does nothing, and air clicks get through.
The lesson
A test that calls a handler directly skips everything the framework does before the handler runs. Keep at least one test that goes in through the real front door.