From acf462906dbd37ff2c17750555181583718959d3 Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 28 May 2026 08:31:46 -0700 Subject: [PATCH 1/6] Surface import errors to the user instead of silently logging Both `importDatabaseFile` and `loadDownloadedChallenges` caught all exceptions, wrote a stacktrace to the console, and returned with no chat-visible feedback. The Poseidon zh-CN library payload incident showed why this hurts: admins saw the conversation's "successfully imported" message, then "no challenges available" with no clue where to look. Send a `challenges.errors.import-failed` message to the triggering player and include the exception's brief message, so they know to check the console instead of assuming the addon is broken. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../challenges/managers/ChallengesImportManager.java | 10 ++++++++++ src/main/resources/locales/en-US.yml | 1 + 2 files changed, 11 insertions(+) diff --git a/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java b/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java index 7b461c0..c12f82f 100644 --- a/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java +++ b/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java @@ -737,6 +737,11 @@ public void importDatabaseFile(User user, World world, String fileName) catch (Exception e) { this.addon.getPlugin().logStacktrace(e); + if (user != null && user.isPlayer()) + { + Utils.sendMessage(user, world, Constants.ERRORS + "import-failed", + "[message]", e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()); + } return; } @@ -814,6 +819,11 @@ public void loadDownloadedChallenges(User user, World world, String downloadStri catch (Exception e) { this.addon.getPlugin().logStacktrace(e); + if (user != null && user.isPlayer()) + { + Utils.sendMessage(user, world, Constants.ERRORS + "import-failed", + "[message]", e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()); + } return; } diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 581b345..e6e662a 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -1232,6 +1232,7 @@ challenges: island-level: "Your island must be at least level [number] to complete this challenge!" no-load: "Error: Could not load [file]. Error: [message]." load-error: "Error: Cannot load [value]." + import-failed: "Could not import challenges: [message]. Check the server console for details." no-rank: "You do not have a high enough rank to do that." cannot-remove-items: "Some items cannot be removed from your inventory!" exist-challenges-or-levels: "Challenges already exist in your world. Cannot proceed!" From 6d8dfbdd18d53ab54754994bb252d0a8aaf7b47d Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 28 May 2026 17:48:12 -0700 Subject: [PATCH 2/6] Bump to 1.6.2 Co-Authored-By: Claude Opus 4.7 (1M context) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 471fcd1..73ba700 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ ${build.version}-SNAPSHOT - 1.6.1 + 1.6.2 -LOCAL BentoBoxWorld_Challenges From e459a31e42a8b835683b2fe530c3db25151fc3dd Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 28 May 2026 18:02:49 -0700 Subject: [PATCH 3/6] Prime Bukkit material registry in JUnit 5 panel tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI build for PR #397 erupted with 286 errors: `NoSuchElementException: No value for minecraft:acacia_hanging_sign` thrown from `org.bukkit.inventory.ItemType.` inside `ItemStackMock.`, which then poisoned `ItemStackMock` for the rest of the JVM. Root cause: ItemType's static initializer iterates Material and looks each entry up in the registry. If it runs before MockBukkit has populated the registry, the static init errors and every subsequent test that touches ItemStack fails with NoClassDefFoundError. AbstractChallengesTest already worked around this by touching `Tag.LEAVES` right after `MockBukkit.mock()`, but seven JUnit 5 panel tests didn't extend it and didn't carry the workaround. The flake didn't surface locally because filesystem ordering on macOS put a primed test first; ubuntu-latest's ordering didn't. Extract `PanelTestHelper.primeBukkitRegistry()` and call it right after `MockBukkit.mock()` in CommonPanelTest, CommonPagedPanelTest, ConversationUtilsTest, AdminPanelTest, ListChallengesPanelTest, GameModePanelTest, and MultiplePanelTest. The fix is order-independent — each affected test class now primes the registry itself. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../challenges/panel/CommonPagedPanelTest.java | 1 + .../challenges/panel/CommonPanelTest.java | 1 + .../challenges/panel/ConversationUtilsTest.java | 1 + .../challenges/panel/PanelTestHelper.java | 17 +++++++++++++++++ .../challenges/panel/admin/AdminPanelTest.java | 1 + .../panel/admin/ListChallengesPanelTest.java | 1 + .../panel/user/GameModePanelTest.java | 1 + .../panel/user/MultiplePanelTest.java | 1 + 8 files changed, 24 insertions(+) diff --git a/src/test/java/world/bentobox/challenges/panel/CommonPagedPanelTest.java b/src/test/java/world/bentobox/challenges/panel/CommonPagedPanelTest.java index ece17b3..6015aeb 100644 --- a/src/test/java/world/bentobox/challenges/panel/CommonPagedPanelTest.java +++ b/src/test/java/world/bentobox/challenges/panel/CommonPagedPanelTest.java @@ -116,6 +116,7 @@ public int getPageIndex() throws Exception { void setUp() { closeable = MockitoAnnotations.openMocks(this); ServerMock mbServer = MockBukkit.mock(); + PanelTestHelper.primeBukkitRegistry(); when(addon.getChallengesManager()).thenReturn(manager); PanelTestHelper.setupUserTranslations(user); diff --git a/src/test/java/world/bentobox/challenges/panel/CommonPanelTest.java b/src/test/java/world/bentobox/challenges/panel/CommonPanelTest.java index 337ab89..654be42 100644 --- a/src/test/java/world/bentobox/challenges/panel/CommonPanelTest.java +++ b/src/test/java/world/bentobox/challenges/panel/CommonPanelTest.java @@ -78,6 +78,7 @@ public PanelItem getReturnButton() { void setUp() { closeable = MockitoAnnotations.openMocks(this); ServerMock mbServer = MockBukkit.mock(); + PanelTestHelper.primeBukkitRegistry(); when(addon.getChallengesManager()).thenReturn(manager); PanelTestHelper.setupUserTranslations(user); diff --git a/src/test/java/world/bentobox/challenges/panel/ConversationUtilsTest.java b/src/test/java/world/bentobox/challenges/panel/ConversationUtilsTest.java index b8f9ff6..4125ea1 100644 --- a/src/test/java/world/bentobox/challenges/panel/ConversationUtilsTest.java +++ b/src/test/java/world/bentobox/challenges/panel/ConversationUtilsTest.java @@ -45,6 +45,7 @@ class ConversationUtilsTest { void setUp() { closeable = MockitoAnnotations.openMocks(this); ServerMock mbServer = MockBukkit.mock(); + PanelTestHelper.primeBukkitRegistry(); when(user.getTranslation(anyString())).thenAnswer( (Answer) inv -> inv.getArgument(0, String.class)); diff --git a/src/test/java/world/bentobox/challenges/panel/PanelTestHelper.java b/src/test/java/world/bentobox/challenges/panel/PanelTestHelper.java index 39552a3..d496ae8 100644 --- a/src/test/java/world/bentobox/challenges/panel/PanelTestHelper.java +++ b/src/test/java/world/bentobox/challenges/panel/PanelTestHelper.java @@ -22,6 +22,23 @@ */ public class PanelTestHelper { + /** + * Force Bukkit's material registry to be populated while MockBukkit's mock + * server is active. Call this immediately after `MockBukkit.mock()`. + * + *

Why: `org.bukkit.inventory.ItemType.<clinit>` iterates over Material and + * looks each entry up in the registry. If it runs before MockBukkit has populated + * the registry (e.g., when a panel test that doesn't extend AbstractChallengesTest + * runs first on CI's filesystem ordering), it throws NoSuchElementException, and + * ItemStackMock stays in an errored state for the rest of the JVM — poisoning every + * subsequent test. Touching `Tag.LEAVES` forces the registry to populate before + * ItemType loads. + */ + public static void primeBukkitRegistry() { + @SuppressWarnings("unused") + var unused = org.bukkit.Tag.LEAVES; + } + /** * Set up user translation mocks to return the key for any invocation. * Uses a lenient default answer that returns the first String argument diff --git a/src/test/java/world/bentobox/challenges/panel/admin/AdminPanelTest.java b/src/test/java/world/bentobox/challenges/panel/admin/AdminPanelTest.java index 08d910e..bdefd47 100644 --- a/src/test/java/world/bentobox/challenges/panel/admin/AdminPanelTest.java +++ b/src/test/java/world/bentobox/challenges/panel/admin/AdminPanelTest.java @@ -54,6 +54,7 @@ class AdminPanelTest { void setUp() throws Exception { closeable = MockitoAnnotations.openMocks(this); ServerMock mbServer = MockBukkit.mock(); + PanelTestHelper.primeBukkitRegistry(); when(addon.getChallengesManager()).thenReturn(manager); PanelTestHelper.setupUserTranslations(user); diff --git a/src/test/java/world/bentobox/challenges/panel/admin/ListChallengesPanelTest.java b/src/test/java/world/bentobox/challenges/panel/admin/ListChallengesPanelTest.java index 36c2a55..2a184d8 100644 --- a/src/test/java/world/bentobox/challenges/panel/admin/ListChallengesPanelTest.java +++ b/src/test/java/world/bentobox/challenges/panel/admin/ListChallengesPanelTest.java @@ -47,6 +47,7 @@ class ListChallengesPanelTest { void setUp() { closeable = MockitoAnnotations.openMocks(this); ServerMock mbServer = MockBukkit.mock(); + PanelTestHelper.primeBukkitRegistry(); when(addon.getChallengesManager()).thenReturn(manager); PanelTestHelper.setupUserTranslations(user); diff --git a/src/test/java/world/bentobox/challenges/panel/user/GameModePanelTest.java b/src/test/java/world/bentobox/challenges/panel/user/GameModePanelTest.java index 038f09e..bffd0f0 100644 --- a/src/test/java/world/bentobox/challenges/panel/user/GameModePanelTest.java +++ b/src/test/java/world/bentobox/challenges/panel/user/GameModePanelTest.java @@ -58,6 +58,7 @@ class GameModePanelTest { void setUp() { closeable = MockitoAnnotations.openMocks(this); ServerMock mbServer = MockBukkit.mock(); + PanelTestHelper.primeBukkitRegistry(); when(addon.getChallengesManager()).thenReturn(manager); PanelTestHelper.setupUserTranslations(user); diff --git a/src/test/java/world/bentobox/challenges/panel/user/MultiplePanelTest.java b/src/test/java/world/bentobox/challenges/panel/user/MultiplePanelTest.java index 322b609..19ca940 100644 --- a/src/test/java/world/bentobox/challenges/panel/user/MultiplePanelTest.java +++ b/src/test/java/world/bentobox/challenges/panel/user/MultiplePanelTest.java @@ -53,6 +53,7 @@ class MultiplePanelTest { void setUp() { closeable = MockitoAnnotations.openMocks(this); ServerMock mbServer = MockBukkit.mock(); + PanelTestHelper.primeBukkitRegistry(); PanelTestHelper.setupUserTranslations(user); when(user.getWorld()).thenReturn(world); From abcef2962aa48df7511fa6160c44ed20b37bf458 Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 28 May 2026 20:41:04 -0700 Subject: [PATCH 4/6] Stop ChallengesAddonTest from poisoning Tag for the rest of the JVM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot was right that my earlier `Tag.LEAVES`-in-PanelTestHelper patch didn't actually fix anything — it just touched a Tag constant that was already broken. The real flake is upstream: ChallengesAddonTest installs `Mockito.mockStatic(Bukkit.class)` *without* MockBukkit, then runs code during `addon.onLoad()` that triggers `org.bukkit.Tag.`. With Bukkit's static methods returning null stubs at that moment, every Tag constant is permanently set to null for the JVM — there is no way to re-initialize an interface's static fields once has run. Any test later in the suite that creates an ItemStack then walks: ItemType. -> Registry.ITEM.getOrThrow("acacia_hanging_sign") -> RegistryMock.loadIfEmpty -> ItemTypeMock.from(...) -> Class.forName("BlockStateMetaMock") -> BlockStateMetaMock. -> MaterialTags. -> Objects.requireNonNull(Tag.ALL_SIGNS) // null -> NPE The NPE wraps as ExceptionInInitializerError, RegistryMock catches and swallows part of it, and `acacia_hanging_sign` is missing from the registry forever after — hence the cascade of 286 errors on CI but a clean local run, where the test order happens to put a properly- MockBukkit-bracketed test first. Reproduced locally with `mvn test -Dtest=ChallengesAddonTest, CommonPagedPanelTest -Dsurefire.runOrder=alphabetical`. Fix: install MockBukkit briefly at the very top of ChallengesAddonTest.setUp() and prime Tag while it's active, then unmock immediately. Tag's static constants are now real MaterialTagMock instances and stay valid for the rest of the JVM, regardless of what subsequent tests do with Bukkit. As a side effect, this also clears up ChallengesAddonTest's own pre-existing "Settings is null" failures when it runs early in the suite. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bentobox/challenges/ChallengesAddonTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/java/world/bentobox/challenges/ChallengesAddonTest.java b/src/test/java/world/bentobox/challenges/ChallengesAddonTest.java index 1e2bd48..02ac704 100644 --- a/src/test/java/world/bentobox/challenges/ChallengesAddonTest.java +++ b/src/test/java/world/bentobox/challenges/ChallengesAddonTest.java @@ -42,12 +42,15 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockbukkit.mockbukkit.MockBukkit; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; +import world.bentobox.challenges.panel.PanelTestHelper; + import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.api.addons.Addon; @@ -101,6 +104,15 @@ public class ChallengesAddonTest { @BeforeEach public void setUp() throws Exception { closeable = MockitoAnnotations.openMocks(this); + // Force Bukkit's Tag. to run against a real MockBukkit ServerMock before + // we install the Mockito static mock below. Without this, Tag. can later + // fire while Bukkit is statically mocked, permanently null-ing every Tag constant + // for the JVM and corrupting any subsequent test that creates an ItemStack + // (e.g. CommonPagedPanelTest -> ItemType. -> MaterialTags. -> + // Objects.requireNonNull(Tag.ALL_SIGNS) -> NPE). + MockBukkit.mock(); + PanelTestHelper.primeBukkitRegistry(); + MockBukkit.unmock(); // Set up plugin WhiteBox.setInternalState(BentoBox.class, "instance", plugin); when(plugin.getLogger()).thenReturn(Logger.getAnonymousLogger()); From 1e020a654591bf9f0032058a6cfeff27464f413b Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 28 May 2026 20:48:03 -0700 Subject: [PATCH 5/6] Extract duplicated import-failure handler to satisfy SonarCloud The two catch blocks added in acf4629 were byte-identical and tripped Sonar's `new_duplicated_lines_density` on PR #397. Lift them into a single `reportImportFailure(user, world, e)` helper. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../managers/ChallengesImportManager.java | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java b/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java index c12f82f..ccb6d46 100644 --- a/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java +++ b/src/main/java/world/bentobox/challenges/managers/ChallengesImportManager.java @@ -736,12 +736,7 @@ public void importDatabaseFile(User user, World world, String fileName) } catch (Exception e) { - this.addon.getPlugin().logStacktrace(e); - if (user != null && user.isPlayer()) - { - Utils.sendMessage(user, world, Constants.ERRORS + "import-failed", - "[message]", e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()); - } + reportImportFailure(user, world, e); return; } @@ -750,6 +745,21 @@ public void importDatabaseFile(User user, World world, String fileName) } + /** + * Logs the stacktrace and, if a player triggered the import, surfaces a chat error + * so they know to look at the server console. + */ + private void reportImportFailure(User user, World world, Exception e) + { + this.addon.getPlugin().logStacktrace(e); + if (user != null && user.isPlayer()) + { + Utils.sendMessage(user, world, Constants.ERRORS + "import-failed", + "[message]", e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()); + } + } + + /** * This method loads downloaded challenges into memory. * @param user User who calls downloaded challenge loading @@ -818,12 +828,7 @@ public void loadDownloadedChallenges(User user, World world, String downloadStri } catch (Exception e) { - this.addon.getPlugin().logStacktrace(e); - if (user != null && user.isPlayer()) - { - Utils.sendMessage(user, world, Constants.ERRORS + "import-failed", - "[message]", e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()); - } + reportImportFailure(user, world, e); return; } From 419815ac4df738aad79ab5a2838d7529a1a76026 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 29 May 2026 17:25:47 -0700 Subject: [PATCH 6/6] Pin MockBukkit to Maven Central 4.110.0 instead of jitpack snapshot The floating jitpack snapshot v1.21-SNAPSHOT resolves to an ephemeral git-described build whose jar/POM get evicted, breaking CI even on unchanged commits. Switch to the equivalent stable Maven Central coordinates (org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.110.0). Co-Authored-By: Claude Opus 4.8 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 73ba700..ffe6007 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ 5.10.2 5.11.0 - v1.21-SNAPSHOT + 4.110.0 1.21.11-R0.1-SNAPSHOT 3.14.0 @@ -205,8 +205,8 @@ - com.github.MockBukkit - MockBukkit + org.mockbukkit.mockbukkit + mockbukkit-v1.21 ${mock-bukkit.version} test