Skip to content

[Tests] Add unit tests for external plugin hooks - #83

Open
DiamondDagger590 wants to merge 1 commit into
developfrom
claude/eager-thompson-1h24ci
Open

[Tests] Add unit tests for external plugin hooks#83
DiamondDagger590 wants to merge 1 commit into
developfrom
claude/eager-thompson-1h24ci

Conversation

@DiamondDagger590

@DiamondDagger590 DiamondDagger590 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add comprehensive unit test suites for all four external plugin hook implementations: CoreItemsAdderHook, CoreNexoHook, CoreModelEngineHook, and CoreMythicMobsHook
  • Add testImplementation dependencies for ItemsAdder, Nexo, MythicMobs, and ModelEngine APIs, plus testRuntimeOnly for Kotlin stdlib (required by Nexo's Kotlin classes)
  • Total: ~60 new tests across 4 test classes covering item/block/entity detection, model resolution, drops, block placement/removal, naming fallbacks, and block drop effects

Details

Test approach

All four hook classes extend PluginHook<CorePlugin>, whose constructor requires a running CorePlugin. Tests use Unsafe.allocateInstance() to bypass the constructor and create hook instances without a server.

ItemsAdder (37 tests, ~83% line coverage): Uses MockedStatic<CustomStack> and MockedStatic<CustomBlock> to mock the ItemsAdder static API. Covers items, blocks, drops, removal, naming (Adventure Component → legacy → formatted ID fallback chain), and playBlockDropEffects.

Nexo (23 tests, ~50% line coverage): Uses MockedStatic<NexoBlocks> only. NexoItems cannot be statically mocked because its method signatures reference ItemBuilder, whose static initializer requires NexoPlugin. Similarly, CustomBlockMechanic (Kotlin) cannot be Mockito-mocked, so instances are created via Unsafe.allocateInstance on NoteBlockMechanic with fields set via reflection. BlockSounds (Kotlin final class) is also Unsafe-allocated for the playBlockDropEffects sound path test.

MythicMobs (12 tests, 100% line coverage): Uses MockedStatic<MythicBukkit> with mock MobExecutor. All entity detection, model resolution, and display name resolution paths are covered.

ModelEngine (11 tests, 100% line coverage): Uses MockedStatic<ModelEngineAPI>. All entity detection, blueprint lookup, model key resolution, and entity naming paths are covered.

Known limitations

  • Nexo item-related methods (item, isItem, isItemOfType, itemModels, itemName) are untested due to the NexoItemsItemBuilderNexoPlugin static initialization chain being fundamentally unmockable
  • Nexo drops happy path (Player entity breaking a custom block with a Breakable mechanic) is untested because Breakable likely has the same ItemBuilder dependency chain

Test plan

  • All 4 new test files compile and pass
  • Full test suite passes with no regressions (./gradlew test → BUILD SUCCESSFUL)
  • JaCoCo coverage report generates successfully

Generated by Claude Code

Summary by CodeRabbit

  • Tests
    • Added comprehensive automated coverage for integrations with ItemsAdder, ModelEngine, MythicMobs, and Nexo.
    • Verified custom item, block, entity, naming, placement, removal, drops, and effect behaviors.
    • Added coverage for vanilla fallbacks and error-handling scenarios.

Add comprehensive test suites for CoreItemsAdderHook, CoreNexoHook,
CoreModelEngineHook, and CoreMythicMobsHook. Tests cover item/block/entity
detection, model resolution, drops, block placement/removal, naming
fallbacks, and playBlockDropEffects. Uses Unsafe.allocateInstance to bypass
PluginHook constructors and reflection to work around unmockable Kotlin
classes in Nexo and MythicMobs. Adds testImplementation dependencies for
all four hook libraries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9jjQTGj22zgVWetvZMtNn
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request adds test-scoped external API dependencies and introduces JUnit 5 coverage for ItemsAdder, Nexo, MythicMobs, and ModelEngine hook behavior.

Changes

External hook test coverage

Layer / File(s) Summary
External API test dependencies
build.gradle.kts
Adds test dependencies for ItemsAdder, Nexo, MythicMobs, and ModelEngine, plus Kotlin standard library runtime support.
ItemsAdder hook behavior tests
src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java
Tests item and block lookup, classification, drops, removal, effects, and custom/vanilla name resolution.
Nexo hook behavior tests
src/test/java/com/diamonddagger590/mccore/external/nexo/CoreNexoHookTest.java
Tests custom block detection, placement, drops, removal, effects, model identifiers, and block names.
MythicMobs entity hook tests
src/test/java/com/diamonddagger590/mccore/external/mythicmobs/CoreMythicMobsHookTest.java
Tests active mob detection, type matching, model identifiers, and entity name resolution.
ModelEngine entity hook tests
src/test/java/com/diamonddagger590/mccore/external/modelengine/CoreModelEngineHookTest.java
Tests modeled entity detection, blueprint matching, model key extraction, and wrapper name resolution.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding unit tests for external plugin hooks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/eager-thompson-1h24ci

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

Claude finished @DiamondDagger590's task in 4m 30s —— View job


Claude PR Review — 0 important, 2 nits

Summary: This PR is test-only — four new JUnit test classes (~1557 lines) covering CoreItemsAdderHook, CoreNexoHook, CoreMythicMobsHook, and CoreModelEngineHook, plus build.gradle.kts additions of matching testImplementation/testRuntimeOnly dependencies. No src/main files changed, so only the testing lens applies (security/architecture/error-handling/performance/extensibility lenses skipped — no production code touched). Tests consistently use @DisplayName, follow the given/when/then naming convention, close MockedStatic mocks in @AfterEach, and don't need RegistryResetExtension/ManagedExecutorExtension (no singleton state or cross-thread work exercised). The documented Nexo limitation (item-related methods and the drops happy path untestable due to the NexoItemsItemBuilderNexoPlugin static-init chain) is reasonable and well-justified.

No blocking issues found.

Nits (2)
  • src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java:55 (also CoreModelEngineHookTest.java:44, CoreMythicMobsHookTest.java:46) — these three hooks use Unsafe.allocateInstance() to construct the hook, but PluginHook's constructor (src/main/java/com/diamonddagger590/mccore/registry/plugin/PluginHook.java:14) only assigns a field with no side effects. The existing pattern in PluginHookTest.java (new TestPluginHook(mock(CorePlugin.class))) works fine without JDK-internal reflection. Recommend new CoreItemsAdderHook(mock(CorePlugin.class)) etc. instead — keep Unsafe only where Nexo's CustomBlockMechanic/BlockSounds genuinely require it.
  • src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java:352 (and CoreNexoHookTest.java:249) — delegatesToBlockGetDrops_whenBlockIsVanilla only asserts assertEquals(1, result.size()) rather than checking contents, so it would still pass if the wrong list were returned. Suggest assertEquals(blockDrops, result) to confirm the drops list passes through unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (6)
src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java (2)

341-353: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Vanilla-drops tests assert only collection size, not delegation. Both tests stub block.getDrops(tool, entity) and then assert result.size() == 1, which passes for any single-element list and does not prove the hook forwarded the vanilla drops.

  • src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java#L341-L353: replace assertEquals(1, result.size()) with assertEquals(List.copyOf(blockDrops), result).
  • src/test/java/com/diamonddagger590/mccore/external/nexo/CoreNexoHookTest.java#L238-L250: replace assertEquals(1, result.size()) with assertEquals(List.copyOf(blockDrops), result).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java`
around lines 341 - 353, The vanilla-drops tests verify only list size instead of
delegation. In CoreItemsAdderHookTest.java lines 341-353 and
CoreNexoHookTest.java lines 238-250, update the assertions in the corresponding
vanilla block drop test methods to compare the returned result with
List.copyOf(blockDrops), preserving the existing stubbing and setup.

50-65: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Close the static mocks defensively in teardown.

customStackMock.close() is still an AutoCloseable.close() path, so if it throws before customBlockMock.close() runs, the block mock remains registered and can poison later tests with “static mocking already registered”. Use a null-safe/clean fallback close sequence or switch to try-with-resources per static mock.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java`
around lines 50 - 65, Update CoreItemsAdderHookTest.tearDown to close
customStackMock and customBlockMock defensively so one close failure cannot
prevent the other mock from being released; use a null-safe cleanup sequence
that preserves cleanup of both static mocks.
src/test/java/com/diamonddagger590/mccore/external/nexo/CoreNexoHookTest.java (1)

69-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fragile fixed-depth superclass hop for itemID.

getSuperclass().getSuperclass() hard-codes the Nexo class-hierarchy depth; any change in NoteBlockMechanic's ancestry (or in Mechanic's field placement) turns this into a NoSuchFieldException that is unrelated to the behavior under test. Walk the hierarchy instead.

♻️ Proposed hierarchy walk
-        Field itemIdField = mechanic.getClass().getSuperclass().getSuperclass().getDeclaredField("itemID");
-        itemIdField.setAccessible(true);
-        itemIdField.set(mechanic, itemId);
+        Field itemIdField = null;
+        for (Class<?> current = mechanic.getClass(); current != null && itemIdField == null; current = current.getSuperclass()) {
+            try {
+                itemIdField = current.getDeclaredField("itemID");
+            } catch (NoSuchFieldException ignored) {
+                // keep walking up the hierarchy
+            }
+        }
+        if (itemIdField == null) {
+            throw new NoSuchFieldException("itemID not found on " + mechanic.getClass());
+        }
+        itemIdField.setAccessible(true);
+        itemIdField.set(mechanic, itemId);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/diamonddagger590/mccore/external/nexo/CoreNexoHookTest.java`
around lines 69 - 80, Update createMechanicWithItemId to locate the itemID field
by walking the superclass hierarchy from mechanic.getClass(), rather than using
the fixed getSuperclass().getSuperclass() chain. Continue searching until the
field is found, then make it accessible and assign itemId while preserving the
existing mechanic allocation and return behavior.
src/test/java/com/diamonddagger590/mccore/external/mythicmobs/CoreMythicMobsHookTest.java (1)

175-199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the null-getDisplayName() case.

MythicMob.getDisplayName() can return null for mobs registered without a display name, which is a distinct branch from "mob type not registered". Only the present and absent-mob cases are covered here. As per coding guidelines, "Cover edge cases in tests: null inputs, empty collections, zero/negative numeric inputs, and max/limit values".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/diamonddagger590/mccore/external/mythicmobs/CoreMythicMobsHookTest.java`
around lines 175 - 199, Add a test alongside
returnsDisplayName_whenMobTypeHasDisplayName and
returnsRawId_whenMobTypeNotRegistered that mocks a registered MythicMob whose
getDisplayName() returns null, then verifies hook.entityName returns the raw
entity ID. Keep this distinct from the Optional.empty() unregistered-mob case.

Source: Coding guidelines

src/test/java/com/diamonddagger590/mccore/external/modelengine/CoreModelEngineHookTest.java (2)

151-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing empty-models edge case.

A ModeledEntity present with an empty getModels() map is a distinct branch from a null modeled entity, and it decides whether the hook returns Optional.of(Set.of()) or Optional.empty(). As per coding guidelines, "Cover edge cases in tests: null inputs, empty collections, zero/negative numeric inputs, and max/limit values".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/diamonddagger590/mccore/external/modelengine/CoreModelEngineHookTest.java`
around lines 151 - 167, Add a test in CoreModelEngineHookTest covering a
non-null ModeledEntity whose getModels() returns an empty map, and assert the
expected Optional result for hook.entityModels. Keep the existing modeled-entity
test unchanged and use the same KNOWN_UUID and ModelEngineAPI mocking setup.

Source: Coding guidelines


100-136: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Map key and blueprint name are identical, so the assertion cannot pin down which one the hook matches on.

Both tests stub getModels() with a key equal to blueprint.getName(). Use a distinct key (e.g. key "main", blueprint name "dragon") so the test actually documents the matching contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/diamonddagger590/mccore/external/modelengine/CoreModelEngineHookTest.java`
around lines 100 - 136, Update the modeled-entity fixtures in
returnsTrue_whenEntityHasMatchingModel and
returnsFalse_whenEntityHasNonMatchingModel so the getModels() map key differs
from blueprint.getName(), using a distinct key such as “main” while preserving
the dragon/golem blueprint names and assertions. This ensures the tests verify
matching against the blueprint name rather than accidentally validating
identical map keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java`:
- Around line 341-353: The vanilla-drops tests verify only list size instead of
delegation. In CoreItemsAdderHookTest.java lines 341-353 and
CoreNexoHookTest.java lines 238-250, update the assertions in the corresponding
vanilla block drop test methods to compare the returned result with
List.copyOf(blockDrops), preserving the existing stubbing and setup.
- Around line 50-65: Update CoreItemsAdderHookTest.tearDown to close
customStackMock and customBlockMock defensively so one close failure cannot
prevent the other mock from being released; use a null-safe cleanup sequence
that preserves cleanup of both static mocks.

In
`@src/test/java/com/diamonddagger590/mccore/external/modelengine/CoreModelEngineHookTest.java`:
- Around line 151-167: Add a test in CoreModelEngineHookTest covering a non-null
ModeledEntity whose getModels() returns an empty map, and assert the expected
Optional result for hook.entityModels. Keep the existing modeled-entity test
unchanged and use the same KNOWN_UUID and ModelEngineAPI mocking setup.
- Around line 100-136: Update the modeled-entity fixtures in
returnsTrue_whenEntityHasMatchingModel and
returnsFalse_whenEntityHasNonMatchingModel so the getModels() map key differs
from blueprint.getName(), using a distinct key such as “main” while preserving
the dragon/golem blueprint names and assertions. This ensures the tests verify
matching against the blueprint name rather than accidentally validating
identical map keys.

In
`@src/test/java/com/diamonddagger590/mccore/external/mythicmobs/CoreMythicMobsHookTest.java`:
- Around line 175-199: Add a test alongside
returnsDisplayName_whenMobTypeHasDisplayName and
returnsRawId_whenMobTypeNotRegistered that mocks a registered MythicMob whose
getDisplayName() returns null, then verifies hook.entityName returns the raw
entity ID. Keep this distinct from the Optional.empty() unregistered-mob case.

In
`@src/test/java/com/diamonddagger590/mccore/external/nexo/CoreNexoHookTest.java`:
- Around line 69-80: Update createMechanicWithItemId to locate the itemID field
by walking the superclass hierarchy from mechanic.getClass(), rather than using
the fixed getSuperclass().getSuperclass() chain. Continue searching until the
field is found, then make it accessible and assign itemId while preserving the
existing mechanic allocation and return behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 92b8f099-0fda-4a85-94d1-ce437aa6f353

📥 Commits

Reviewing files that changed from the base of the PR and between 0fbf220 and 552e574.

📒 Files selected for processing (5)
  • build.gradle.kts
  • src/test/java/com/diamonddagger590/mccore/external/itemsadder/CoreItemsAdderHookTest.java
  • src/test/java/com/diamonddagger590/mccore/external/modelengine/CoreModelEngineHookTest.java
  • src/test/java/com/diamonddagger590/mccore/external/mythicmobs/CoreMythicMobsHookTest.java
  • src/test/java/com/diamonddagger590/mccore/external/nexo/CoreNexoHookTest.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants