Skip to content

[Unit Tests] Add coverage for quest reward and objective types - #326

Open
DiamondDagger590 wants to merge 1 commit into
recodefrom
claude/magical-cray-15d4tu
Open

[Unit Tests] Add coverage for quest reward and objective types#326
DiamondDagger590 wants to merge 1 commit into
recodefrom
claude/magical-cray-15d4tu

Conversation

@DiamondDagger590

@DiamondDagger590 DiamondDagger590 commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add coverage tests for RedeemableExperienceRewardType, RedeemableLevelsRewardType, and BoostedExperienceRewardType — three structurally similar quest reward types that were at ~44-45% coverage
  • Add coverage tests for AdvancementCompleteObjectiveType — a quest objective type at ~34% coverage
  • All four test classes follow the project's @Nested/@DisplayName grouping conventions and extend McRPGBaseTest

What's covered

Each reward type test covers: parseConfig, serializeConfig/fromSerializedConfig round-trips, withExactAmount, withAmountMultiplier, withLocalizationRoute, withInlineDisplayLabel, describeForDisplay, isScalable, getNumericAmount, and edge cases (missing keys, string/Long/Double type coercion, non-numeric amounts).

The advancement objective test covers: checkInitialProgress (unregistered advancements, invalid keys, mixed valid/invalid keys), processProgress with multi-advancement filters (matching and non-matching), parseConfig edge cases (empty list, single advancement), and AdvancementCompleteQuestContext construction.

What's not covered

grant() and describeForDisplay(McRPGPlayer) on the reward types require deep McRPG runtime state (registry access, player manager) and are excluded from these pure-logic coverage tests. checkInitialProgress with actually-granted advancements is excluded because MockBukkit does not support registering custom advancements.

Test plan

  • All new tests compile and pass (./gradlew test — zero failures)
  • Full test suite passes (no regressions)
  • Testing audit persona reviewed — no critical findings

Generated by Claude Code

Summary by CodeRabbit

  • Tests
    • Expanded automated coverage for advancement completion objectives.
    • Added comprehensive validation for boosted experience, redeemable experience, and redeemable level rewards.
    • Covered progress handling, configuration edge cases, formatting, localization, serialization, parsing, scaling, and amount calculations.

Add unit tests for RedeemableExperienceRewardType,
RedeemableLevelsRewardType, BoostedExperienceRewardType, and
AdvancementCompleteObjectiveType. Tests cover parseConfig,
serializeConfig/fromSerializedConfig round-trips, withExactAmount,
withAmountMultiplier, withLocalizationRoute, withInlineDisplayLabel,
describeForDisplay, isScalable, getNumericAmount, and edge cases
(invalid keys, empty filters, non-numeric amounts, type coercion).

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

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added JUnit coverage for AdvancementCompleteObjectiveType, BoostedExperienceRewardType, RedeemableExperienceRewardType, and RedeemableLevelsRewardType. Tests cover configuration, transformations, formatting, serialization, parsing, metadata, and numeric edge cases.

Changes

Quest and reward coverage

Layer / File(s) Summary
Advancement objective coverage
src/test/.../objective/type/builtin/AdvancementCompleteObjectiveTypeCoverageTest.java
Tests advancement filters, initial progress, matching, invalid keys, empty and singleton configurations, and quest-context accessors.
Boosted experience reward coverage
src/test/.../reward/builtin/BoostedExperienceRewardTypeCoverageTest.java
Tests construction, transformations, formatting, serialization, parsing, metadata preservation, numeric conversion, and amount edge cases.
Redeemable experience reward coverage
src/test/.../reward/builtin/RedeemableExperienceRewardTypeCoverageTest.java
Tests amounts, immutable metadata updates, formatting, serialization, deserialization, section parsing, and amount scaling.
Redeemable levels reward coverage
src/test/.../reward/builtin/RedeemableLevelsRewardTypeCoverageTest.java
Tests level amounts, metadata updates, formatting, serialization, deserialization, section parsing, and amount scaling.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 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 clearly summarizes the addition of unit test coverage for quest reward and objective types.
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/magical-cray-15d4tu

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 31, 2026

Copy link
Copy Markdown

Claude finished @DiamondDagger590's task in 7m 48s —— View job


Claude finished the review

Todo list

  • Gather context (PR diff, changed files)
  • Route to applicable review lenses
  • Fan out to persona subagents (review-testing)
  • Consolidate findings
  • Post final review

Verdict: 4 important, 3 nits

This PR adds four new test classes (1237 lines, zero production code touched) covering RedeemableExperienceRewardType, RedeemableLevelsRewardType, BoostedExperienceRewardType, and AdvancementCompleteObjectiveType. Since the diff is test-only, the review-testing lens was applied (no other lens's file patterns matched). The new tests are well-organized (@Nested/@DisplayName, correct naming convention) and their assertions were spot-checked against the production source — all verified correct. The main gaps are methods the PR claims are "covered" at the class level but whose most important branches are actually left untested.

Important findings

  • AdvancementCompleteObjectiveTypeCoverageTest.java:39 — all four checkInitialProgress tests only exercise the "not yet completed" path (always assert progress == 0). The "already granted" retroactive-progress branch is never tested. The PR notes this is skipped because MockBukkit can't register real advancements, but Player/Advancement/AdvancementProgress are mockable interfaces — no real registration is needed (mock them directly + mockStatic(Bukkit.class) for advancementIterator()/getAdvancement(...)). This leaves the entire reason the method exists unverified.
  • AdvancementCompleteObjectiveTypeCoverageTest.java:29describeObjective(McRPGPlayer, long) and its extractDisplayName helper (production AdvancementCompleteObjectiveType.java:130-168) have zero coverage anywhere in the repo — the only non-trivial formatting method on the class (3 branches + tag-escaping), completely unverified.
  • BoostedExperienceRewardTypeCoverageTest.java:245 — none of the three new reward-type test files cover grant(Player) (no-op branch or success branch). Sibling coverage tests in the same package (ItemRewardTypeCoverageTest, CommandRewardTypeCoverageTest, ExperienceRewardTypeCoverageTest, ScalableCommandRewardTypeCoverageTest) all test grant() — this PR breaks that established convention for the reward's core side-effecting behavior.
  • RedeemableExperienceRewardTypeCoverageTest.java:21 — only the no-arg describeForDisplay() is tested; the describeForDisplay(McRPGPlayer) overload (localization-route branch + exception fallback, inline-label branch, default-lookup branch + exception fallback) is untested across all three reward types, despite AbilityUpgradeRewardTypeDisplayTest already establishing this exact test pattern in the same package.
Nits
  • AdvancementCompleteObjectiveTypeCoverageTest.java:204-210context_isNotNull() asserts a constructor result is non-null, which can never fail (a constructor either returns non-null or throws). No real verification beyond what getAdvancementEvent_returnsOriginalEvent already covers.
  • AdvancementCompleteObjectiveTypeCoverageTest.java:97-190 — the processProgress/parseConfig scenarios here substantially duplicate the pre-existing AdvancementCompleteObjectiveTypeTest.java, while the genuinely uncovered methods (checkInitialProgress true branch, describeObjective) remain untested. Consider consolidating net-new scenarios into the existing file instead of a second parallel file.
  • The three reward-type test files (BoostedExperienceRewardTypeCoverageTest, RedeemableExperienceRewardTypeCoverageTest, RedeemableLevelsRewardTypeCoverageTest) are near line-for-line duplicates, mirroring that the three production reward types are themselves independent duplicated implementations with no shared abstract base. Not a defect in this PR, but any future edge case found in one must be manually copied to the other two.

@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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/test/java/us/eunoians/mcrpg/quest/reward/builtin/RedeemableLevelsRewardTypeCoverageTest.java (1)

311-338: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add an assertion for the scaled amount in WithAmountMultiplier.

Both tests in this nested class check only that localizationRoute and displayLabel survive withAmountMultiplier. Neither test checks the resulting amount. withAmountMultiplier computes Math.max(1, (int) (amount * multiplier)), which truncates and floors the result to a minimum of 1. Add a test that verifies the scaled amount for a normal multiplier, and a boundary test that verifies the floor-clamp to 1.

As per path instructions, "Test every new public method with non-trivial logic" and "Cover relevant edge cases, including... off-by-one boundaries."

🤖 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/us/eunoians/mcrpg/quest/reward/builtin/RedeemableLevelsRewardTypeCoverageTest.java`
around lines 311 - 338, Extend the WithAmountMultiplier tests for
RedeemableLevelsRewardType.withAmountMultiplier to assert the serialized amount
after scaling, including a normal multiplier result and a low-multiplier
boundary that clamps the amount to 1. Preserve the existing localization-route
and display-label assertions.

Source: Path instructions

src/test/java/us/eunoians/mcrpg/quest/reward/builtin/BoostedExperienceRewardTypeCoverageTest.java (1)

286-313: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add an assertion for the scaled amount in WithAmountMultiplier.

Both tests in this nested class check only that localizationRoute and displayLabel survive withAmountMultiplier. Neither test checks the resulting amount. withAmountMultiplier computes Math.max(1, (int) (amount * multiplier)), which truncates and floors the result to a minimum of 1. Add a test that verifies the scaled amount for a normal multiplier, and a boundary test that verifies the floor-clamp to 1 when a small multiplier would otherwise produce 0 or a negative value.

✅ Suggested additional tests
     `@Nested`
     `@DisplayName`("withAmountMultiplier")
     class WithAmountMultiplier {

+        `@Test`
+        `@DisplayName`("scales amount by multiplier")
+        void withAmountMultiplier_scalesAmount() {
+            BoostedExperienceRewardType configured = new BoostedExperienceRewardType()
+                    .fromSerializedConfig(Map.of("amount", 400));
+            BoostedExperienceRewardType scaled = configured.withAmountMultiplier(0.5);
+            assertEquals(200, scaled.getNumericAmount().orElse(0));
+        }
+
+        `@Test`
+        `@DisplayName`("clamps scaled amount to minimum of 1")
+        void withAmountMultiplier_clampsToMinimumOne() {
+            BoostedExperienceRewardType configured = new BoostedExperienceRewardType()
+                    .fromSerializedConfig(Map.of("amount", 1));
+            BoostedExperienceRewardType scaled = configured.withAmountMultiplier(0.1);
+            assertEquals(1, scaled.getNumericAmount().orElse(0));
+        }
+
         `@Test`
         `@DisplayName`("preserves localization route")
         void withAmountMultiplier_preservesRoute() {

As per path instructions, "Test every new public method with non-trivial logic" and "Cover relevant edge cases, including... off-by-one boundaries."

🤖 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/us/eunoians/mcrpg/quest/reward/builtin/BoostedExperienceRewardTypeCoverageTest.java`
around lines 286 - 313, Add amount assertions to the WithAmountMultiplier tests,
verifying normal scaling through withAmountMultiplier and the Math.max
floor-clamp to 1 for a multiplier producing zero or a negative result. Keep the
existing localization-route and display-label preservation assertions intact,
and cover both the ordinary and boundary cases.

Source: Path instructions

src/test/java/us/eunoians/mcrpg/quest/reward/builtin/RedeemableExperienceRewardTypeCoverageTest.java (1)

327-354: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add an assertion for the scaled amount in WithAmountMultiplier.

Both tests in this nested class check only that localizationRoute and displayLabel survive withAmountMultiplier. Neither test checks the resulting amount. withAmountMultiplier computes Math.max(1, (int) (amount * multiplier)), which truncates and floors the result to a minimum of 1. Add a test that verifies the scaled amount for a normal multiplier, and a boundary test that verifies the floor-clamp to 1.

As per path instructions, "Test every new public method with non-trivial logic" and "Cover relevant edge cases, including... off-by-one boundaries."

🤖 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/us/eunoians/mcrpg/quest/reward/builtin/RedeemableExperienceRewardTypeCoverageTest.java`
around lines 327 - 354, The WithAmountMultiplier tests currently verify only
metadata preservation; add assertions covering its calculated amount. Extend the
existing normal-multiplier case to assert the expected scaled amount, and add a
boundary test using a multiplier whose truncated result is below 1 to verify the
amount is clamped to 1, using serializeConfig() and the existing configured
reward setup.

Source: Path instructions

🤖 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.

Inline comments:
In
`@src/test/java/us/eunoians/mcrpg/quest/objective/type/builtin/AdvancementCompleteObjectiveTypeCoverageTest.java`:
- Around line 62-94: Update AdvancementCompleteObjectiveType.parseConfig to
validate every advancement entry, warn with the configuration key and invalid
value, and enforce a match-nothing result or throw a descriptive configuration
exception instead of silently retaining malformed keys. Revise
checkInitialProgress_specificFilter_skipsInvalidKey and
checkInitialProgress_specificFilter_handlesMixedKeys to assert the selected
invalid-configuration behavior rather than accepting zero progress.

---

Nitpick comments:
In
`@src/test/java/us/eunoians/mcrpg/quest/reward/builtin/BoostedExperienceRewardTypeCoverageTest.java`:
- Around line 286-313: Add amount assertions to the WithAmountMultiplier tests,
verifying normal scaling through withAmountMultiplier and the Math.max
floor-clamp to 1 for a multiplier producing zero or a negative result. Keep the
existing localization-route and display-label preservation assertions intact,
and cover both the ordinary and boundary cases.

In
`@src/test/java/us/eunoians/mcrpg/quest/reward/builtin/RedeemableExperienceRewardTypeCoverageTest.java`:
- Around line 327-354: The WithAmountMultiplier tests currently verify only
metadata preservation; add assertions covering its calculated amount. Extend the
existing normal-multiplier case to assert the expected scaled amount, and add a
boundary test using a multiplier whose truncated result is below 1 to verify the
amount is clamped to 1, using serializeConfig() and the existing configured
reward setup.

In
`@src/test/java/us/eunoians/mcrpg/quest/reward/builtin/RedeemableLevelsRewardTypeCoverageTest.java`:
- Around line 311-338: Extend the WithAmountMultiplier tests for
RedeemableLevelsRewardType.withAmountMultiplier to assert the serialized amount
after scaling, including a normal multiplier result and a low-multiplier
boundary that clamps the amount to 1. Preserve the existing localization-route
and display-label assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: fd2b710d-2b5c-401a-ab5a-816d457abeb5

📥 Commits

Reviewing files that changed from the base of the PR and between 71d4ebb and 2661fae.

📒 Files selected for processing (4)
  • src/test/java/us/eunoians/mcrpg/quest/objective/type/builtin/AdvancementCompleteObjectiveTypeCoverageTest.java
  • src/test/java/us/eunoians/mcrpg/quest/reward/builtin/BoostedExperienceRewardTypeCoverageTest.java
  • src/test/java/us/eunoians/mcrpg/quest/reward/builtin/RedeemableExperienceRewardTypeCoverageTest.java
  • src/test/java/us/eunoians/mcrpg/quest/reward/builtin/RedeemableLevelsRewardTypeCoverageTest.java

Comment on lines +62 to +94
@Test
@DisplayName("skips null NamespacedKey from invalid advancement string")
void checkInitialProgress_specificFilter_skipsInvalidKey() {
Section section = mock(Section.class);
when(section.contains("advancements")).thenReturn(true);
when(section.getStringList("advancements")).thenReturn(List.of(
":::invalid:::key:::"
));
AdvancementCompleteObjectiveType configured = type.parseConfig(section);

PlayerMock player = server.addPlayer();
long progress = configured.checkInitialProgress(player);

assertEquals(0, progress);
}

@Test
@DisplayName("handles mix of valid and invalid keys in filter")
void checkInitialProgress_specificFilter_handlesMixedKeys() {
Section section = mock(Section.class);
when(section.contains("advancements")).thenReturn(true);
when(section.getStringList("advancements")).thenReturn(List.of(
":::bad:::",
"minecraft:story/iron_tools",
"also:::bad"
));
AdvancementCompleteObjectiveType configured = type.parseConfig(section);

PlayerMock player = server.addPlayer();
long progress = configured.checkInitialProgress(player);

assertEquals(0, progress);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject or diagnose invalid advancement keys during configuration loading.

These tests accept malformed advancements entries as a normal zero-progress case. parseConfig retains the invalid strings, and checkInitialProgress skips them without a warning. A malformed key can therefore create an objective that cannot progress without identifying the bad configuration value.

Validate each key in parseConfig. Log a warning with the configuration key and invalid value, then use match-nothing or throw a descriptive exception. Update these tests to assert the selected behavior.

As per coding guidelines, “Config parsers must not silently convert invalid keys into a no-filter or match-all result; log a warning and use match-nothing or throw during loading.”

🤖 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/us/eunoians/mcrpg/quest/objective/type/builtin/AdvancementCompleteObjectiveTypeCoverageTest.java`
around lines 62 - 94, Update AdvancementCompleteObjectiveType.parseConfig to
validate every advancement entry, warn with the configuration key and invalid
value, and enforce a match-nothing result or throw a descriptive configuration
exception instead of silently retaining malformed keys. Revise
checkInitialProgress_specificFilter_skipsInvalidKey and
checkInitialProgress_specificFilter_handlesMixedKeys to assert the selected
invalid-configuration behavior rather than accepting zero progress.

Source: Coding guidelines

@Test
@DisplayName("returns 0 with specific filter when advancements are not registered on server")
void checkInitialProgress_specificFilter_returnsZero_whenAdvancementsNotRegistered() {
Section section = mock(Section.class);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IMPORTANT: All four checkInitialProgress tests in this nested class only exercise the "not yet completed" path and assert progress == 0. The "already granted" branch in AdvancementCompleteObjectiveType.checkInitialProgress (player.getAdvancementProgress(advancement).isDone() → true, incrementing count) is never covered — the PR description says this is skipped because "MockBukkit does not support registering custom advancements," but Player, Advancement, and AdvancementProgress are all mockable interfaces (no real registration needed): mock(Player.class) + mock(Advancement.class) + mock(AdvancementProgress.class), with player.getAdvancementProgress(advancement) stubbed to return it and .isDone() stubbed true, combined with mockStatic(Bukkit.class) for the empty-filter branch's Bukkit.advancementIterator() or the specific-filter branch's Bukkit.getAdvancement(...).

This leaves the entire reason the method exists (retroactive progress grant) permanently unverified — a regression that always returns 0, or double-counts, would pass CI silently.

Fix this →


@BeforeEach
void setUp() {
type = new AdvancementCompleteObjectiveType();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IMPORTANT: This "Coverage" test file (added to raise AdvancementCompleteObjectiveType from ~34%) never exercises describeObjective(McRPGPlayer, long) or its private extractDisplayName helper (production AdvancementCompleteObjectiveType.java:130-168) — the only non-trivial public formatting method on the class, with three branches (empty filter / single advancement / multi-advancement loop) plus MiniMessage tag-escaping and underscore→space conversion. None of it is verified anywhere in the repo.

Fix this →

assertEquals(300, configured.getNumericAmount().orElse(0));
}

@Test

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IMPORTANT: None of the three new reward-type coverage tests (this file, RedeemableExperienceRewardTypeCoverageTest, RedeemableLevelsRewardTypeCoverageTest) test grant(Player) — neither the no-op branch (amount <= 0) nor the success branch that resolves the McRPGPlayer via the registry and calls modifyBoostedExperience/modifyRedeemableExperience/modifyRedeemableLevels. This breaks with the established convention in this same package: ItemRewardTypeCoverageTest, CommandRewardTypeCoverageTest, ExperienceRewardTypeCoverageTest, and ScalableCommandRewardTypeCoverageTest all cover grant() including its no-op branches. grant() is the reward's core side-effecting behavior and is left completely unverified here.

Fix this →

import static org.mockito.Mockito.when;

@DisplayName("RedeemableExperienceRewardType Coverage")
class RedeemableExperienceRewardTypeCoverageTest extends McRPGBaseTest {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IMPORTANT: Only the no-arg describeForDisplay() is tested across all three new reward-type coverage tests. The describeForDisplay(McRPGPlayer) overload (e.g. BoostedExperienceRewardType.java:159-188 and the equivalent methods here / in RedeemableLevelsRewardType) — with its localizationRoute branch (+ exception fallback), inline displayLabel branch, and default LocalizationKey branch (+ exception fallback to describeForDisplay()) — is completely untested, even though this is the player-facing overload actually used in reward GUIs/lore. AbilityUpgradeRewardTypeDisplayTest already establishes the pattern for testing exactly this in the same package.

Fix this →

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