Skip to content

[Tests] Add unit tests for PlayerStatisticData, ReloadableParser, ItemPluginType, ChainPlayerContextFilter#33

Open
DiamondDagger590 wants to merge 1 commit into
developfrom
claude/stoic-cori-vo6nk7
Open

[Tests] Add unit tests for PlayerStatisticData, ReloadableParser, ItemPluginType, ChainPlayerContextFilter#33
DiamondDagger590 wants to merge 1 commit into
developfrom
claude/stoic-cori-vo6nk7

Conversation

@DiamondDagger590

@DiamondDagger590 DiamondDagger590 commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • PlayerStatisticDataAdditionalTest: Tests getUUID(), getValue() (raw Object accessor for all types), bulkIncrementLong() delegation and accumulation, getOrCreateSet with plain HashSet (non-LinkedHashSet) populated via populateFromEntries, cancellation paths for incrementInt, incrementDouble, incrementLong, setMaxLong, setMaxInt, setMaxDouble, setTimestampIfAbsent, addToSet, removeFromSet, and StatisticNotRegisteredException for all mutators (90.3% → 96.7% line coverage)
  • ReloadableParserTest: Tests constructor-based loading with simple equations, variable equations, reload lifecycle after YAML mutation, and constant expressions (0% → 100% line coverage)
  • ItemPluginTypeTest: Tests fromName() for all known names (nexo, itemsadder, items_adder, none), case-insensitive matching, unrecognized name fallback to NONE via parameterized test, and enum completeness (0% → 23.6% line coverage; remaining 76% are CorePlugin-dependent lambda bodies)
  • ChainPlayerContextFilterTest: Tests single-filter chain, multi-filter chain ordering, three-filter sequential application, empty input handling, filter-that-removes-all propagation, and player context accessibility (0% → 100% line coverage)

Classes covered (avoiding PR #27, PR #28, PR #29, PR #30, PR #31, and PR #32 overlap)

PR #27 covers GetItemRequest, StatisticEntry, ReloadableContent, ReloadableContentManager, StartupProfile. PR #28 covers Methods, PlayerSettingRegistry, ReloadableContent subtypes. PR #29 covers Mutexable, ParseError, EvaluationException, and exception classes. PR #30 covers RegistryAccess, StatisticModifyEvent, PostStatisticModifyEvent, Transaction. PR #31 covers database events, GUI events, SQLiteDatabaseDriver, DatabaseDriver, ManagerKey/PluginHookKey constants. PR #32 covers player events, CorePlayer, Credentials, ConnectionDetails, CorePlayerOfflineException. This PR targets entirely different classes with no overlap.

Coverage impact

4 classes improved: 2 brought from 0% to 100%, 1 brought from 0% to 23.6%, 1 improved from 90.3% to 96.7%. Overall project coverage: 27.0% → 28.3%.

Test plan

  • All new tests pass via ./gradlew test
  • JaCoCo coverage report confirms expected coverage on all 4 targeted classes
  • No existing tests broken by these additions
  • No production code changes — test-only PR

https://claude.ai/code/session_012thebA7utDjU8bJZqBUAB9


Generated by Claude Code

Summary by CodeRabbit

  • Tests
    • Added comprehensive test coverage for item type resolution, including multiple input format variations.
    • Added test coverage for configuration document content loading and variable substitution functionality.
    • Added extensive test coverage for player statistics operations including increments, bulk updates, set operations, and event handling.
    • Added test coverage for player filtering functionality across multiple chained filters in sequence.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds four new JUnit 5 test classes covering enum validation, YAML configuration parsing, statistic data mutations with event handling, and filter chain composition. The tests verify correct behavior across edge cases including case-insensitive input handling, event cancellation semantics, and sequential filter application.

Changes

Test Coverage Expansion

Layer / File(s) Summary
ItemPluginType enum tests
src/test/java/com/diamonddagger590/mccore/builder/item/ItemPluginTypeTest.java
Validates ItemPluginType constant count, valueOf resolution, and fromName behavior across case variations and formats, with fallback to NONE for unrecognized input and non-empty name assertion.
ReloadableParser YAML evaluation tests
src/test/java/com/diamonddagger590/mccore/configuration/common/ReloadableParserTest.java
Verifies arithmetic expression evaluation, variable substitution via setVariable, parser replacement after reloadContent, and constant equation resolution from YAML documents.
PlayerStatisticData comprehensive tests
src/test/java/com/diamonddagger590/mccore/statistic/PlayerStatisticDataAdditionalTest.java
Extensive coverage including UUID/value retrieval, bulk increment with pre/post events, set operations, event-driven mutation cancellation, modified-entry tracking, unregistered key exceptions, and test isolation via registry reset and custom CorePlayer stub.
ChainPlayerContextFilter order and composition tests
src/test/java/com/diamonddagger590/mccore/util/filter/ChainPlayerContextFilterTest.java
Validates single and multi-filter chaining with order verification, empty input handling, early elimination cascading, and CorePlayer context propagation through filter chains.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • DiamondDagger590/McCore#9: PlayerStatisticDataAdditionalTest directly extends test coverage for PlayerStatisticData introduced in the statistics framework PR with validation of UUID semantics, mutation operations, event cancellation, modified entries, and exception handling for unregistered keys.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies all four test classes being added and accurately reflects the main purpose of the changeset.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/stoic-cori-vo6nk7

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 and usage tips.

@github-actions

Copy link
Copy Markdown

Testing Review

I'll systematically apply every checklist item to the four test files and the production classes they exercise.


Checklist Application

Coverage Completeness

CONCERN: ItemPluginTypeTest asserts that ItemPluginType.values() has exactly 3 constants as a hardcoded literal, but no production file for ItemPluginType is included in the diff. There is no test for any behaviour other than fromName and enum membership — specifically, no test verifies that the enum participates correctly in any plugin-detection or item-lookup logic (e.g., isAvailable(), getItem(), or any factory method that uses the type). If those methods exist and are non-trivial, they are uncovered.
WHY: Coverage gap — non-trivial public methods on the production enum are not exercised if they exist.
WHERE: ItemPluginTypeTest.java / ItemPluginType (production class absent from diff)


CONCERN: ReloadableParserTest never tests what happens when the YAML route is missing entirely (i.e., the key does not exist in the document). If ReloadableParser reads a null or absent value from the route and attempts to construct a Parser, the behavior (exception? empty string parse? NPE?) is untested.
WHY: Edge case — null/absent input to constructor is not covered.
WHERE: ReloadableParserTest.java / ReloadableParser


CONCERN: ReloadableParserTest does not test reloadContent when the YAML route is removed or set to null between construction and reload. The reload path exercised by reloadContent_updatesParser_whenYamlChanges only covers the happy path of a valid new equation.
WHY: Edge case gap — reloadContent with a null/missing route value is untested.
WHERE: ReloadableParserTest.java / ReloadableParser


CONCERN: PlayerStatisticDataAdditionalTest calls Instant.now() directly in two test methods (getValue_returnsInstant_whenTimestampStatStored and setTimestampIfAbsent_doesNotChange_whenCancelled) instead of going through a TimeProvider.
WHY: TimeProvider violation — tests that assert time-dependent behavior must inject a fixed clock. If wall-clock time is used, the returned Instant is non-deterministic and the assertions comparing against it can mask drift bugs or produce flaky behavior when production code also reads the clock.
WHERE: PlayerStatisticDataAdditionalTest.java lines in getValue_returnsInstant_whenTimestampStatStored and setTimestampIfAbsent_doesNotChange_whenCancelled


CONCERN: PlayerStatisticDataAdditionalTest.setTimestampIfAbsent_doesNotChange_whenCancelled asserts that the value remains Instant.EPOCH after a cancelled set. However the assertion is assertEquals(Instant.EPOCH, data.getTimestampValue(TIMESTAMP_KEY).orElse(Instant.EPOCH)). If getTimestampValue returns Optional.empty(), orElse(Instant.EPOCH) silently passes even though the data is in an unexpected state. The assertion cannot distinguish "default is EPOCH" from "Optional is empty and we fell back to EPOCH".
WHY: Structural problem — the assertion cannot fail in the empty-Optional case, making the test non-falsifiable for that branch.
WHERE: PlayerStatisticDataAdditionalTest.java / setTimestampIfAbsent_doesNotChange_whenCancelled


CONCERN: There is no test for setTimestampIfAbsent on a key that already has a value (non-absent case). The "if absent" contract — that a second call does not overwrite an existing timestamp — is not verified.
WHY: Coverage gap — the primary semantic of the "if absent" guard is untested.
WHERE: PlayerStatisticDataAdditionalTest.java / PlayerStatisticData.setTimestampIfAbsent


CONCERN: There is no test verifying addToSet respects the capacity limit (registered with maxSize = 3). A SET_STRING stat is registered with size 3, but no test attempts to add a fourth element and asserts that it is rejected or that the oldest element is evicted (depending on the eviction policy).
WHY: Coverage gap — limit/boundary value for the set capacity is not tested.
WHERE: PlayerStatisticDataAdditionalTest.java / PlayerStatisticData.addToSet


CONCERN: getModifiedEntries_skipsKey_whenValueIsNull calls data.populateFromEntries(Map.of()) after an initial setValue, then sets again and asserts. The test comment says "that key is skipped" but the assertion actually asserts the key is present. The test name and intent are contradictory, and the scenario it claims to cover (a dirty key whose value was cleared) is not correctly set up — populateFromEntries(Map.of()) does not clear a value; it populates from an empty map, leaving existing data intact. The actual "skip null value" path (where a dirtyKeys entry has no corresponding value) is therefore not exercised.
WHY: Structural problem — the test does not exercise the code path named in its display name, making it misleading and leaving the real null-skip path uncovered.
WHERE: PlayerStatisticDataAdditionalTest.java / getModifiedEntries_skipsKey_whenValueIsNull


CONCERN: ChainPlayerContextFilterTest calls RegistryResetExtension.setupRegistry() and RegistryResetExtension.resetRegistry() in @BeforeEach/@AfterEach, but the test class never uses a registry. The entire test exercises pure Java logic (ChainPlayerContextFilter) with no Bukkit or registry dependency, yet it incurs registry setup/teardown overhead and couples the test to registry infrastructure.
WHY: Structural problem — a pure-Java test should not spin up framework infrastructure it does not use. This also violates the "does any test spin up MockBukkit/framework infrastructure but call zero of those APIs?" principle. If RegistryResetExtension does anything non-trivial (I/O, static mutation), it is an unnecessary fragility and slowdown.
WHERE: ChainPlayerContextFilterTest.java


CONCERN: ChainPlayerContextFilterTest has no test for the constructor accepting zero filters (an empty varargs call), or for a null filter being passed. If the varargs constructor permits it, the chain's behavior with zero elements (identity pass-through?) is unspecified by tests.
WHY: Edge case gap — zero-element and null-element varargs inputs are not covered.
WHERE: ChainPlayerContextFilterTest.java / ChainPlayerContextFilter


CONCERN: ChainPlayerContextFilterTest uses TestCorePlayer (inner class extending CorePlayer) but CorePlayer's constructor is called with null as the second argument. No test verifies whether the filter chain behaves correctly when the player reference itself is null — a realistic defensive scenario.
WHY: Edge case gap — null player input to filter() is not tested.
WHERE: ChainPlayerContextFilterTest.java / ChainPlayerContextFilter.filter


CONCERN: PlayerStatisticDataAdditionalTest does not test bulkIncrementLong with cancellation. The incrementLong_doesNotChange_whenCancelled test covers incrementLong, and bulkIncrementLong_firesEvents_withIncrementModificationType confirms events fire, but no test confirms that cancelling the pre-event during a bulkIncrementLong call prevents the value change. If bulkIncrementLong has its own dispatch path, this is a gap.
WHY: Coverage gap — cancellation behavior of bulkIncrementLong is not tested.
WHERE: PlayerStatisticDataAdditionalTest.java / PlayerStatisticData.bulkIncrementLong


CONCERN: No test file is present for the setValue method's event-firing behavior. setValue is used as a setup utility in multiple tests, but there is no dedicated test that verifies setValue fires (or deliberately does not fire) events, nor that it marks the object dirty correctly in isolation.
WHY: Coverage gap — setValue public method behavior (dirty flag, event contract) is not tested directly.
WHERE: PlayerStatisticDataAdditionalTest.java / PlayerStatisticData.setValue


MockBukkit Usage

CONCERN: None of the four test files call MockBukkit.mock() or MockBukkit.unmock(). All four use TestCorePlayer (a plain CorePlayer subclass) without a running Bukkit server. This is correct for pure-Java logic. No violation found here.


Naming Convention

CONCERN: Several test method names in ItemPluginTypeTest do not follow givenContext_whenAction_thenOutcome. Examples: values_returnsThreeConstants, values_containsExpectedConstants, fromName_returnsNexo_forNexoString, allConstants_haveNonNullName. The format is subject_outcome_condition rather than given_when_then.
WHY: Structural problem — the naming convention givenContext_whenAction_thenOutcome is a framework requirement for test readability and auditing, not a style preference.
WHERE: ItemPluginTypeTest.java — all test method names


CONCERN: Several test method names in ReloadableParserTest do not follow givenContext_whenAction_thenOutcome. Examples: constructor_createsParser_thatEvaluatesEquation, constructor_handlesConstantEquation.
WHY: Same as above — structural naming convention violation.
WHERE: ReloadableParserTest.java — all test method names


Summary

Production files changed: ItemPluginType (enum), ReloadableParser, PlayerStatisticData, ChainPlayerContextFilter (none of the production files are included in the diff; they are inferred from test imports)

Test files present: ItemPluginTypeTest.java, ReloadableParserTest.java, PlayerStatisticDataAdditionalTest.java, ChainPlayerContextFilterTest.java

Coverage gaps:

  • Non-trivial public methods on ItemPluginType beyond fromName (if any exist)
  • ReloadableParser constructor / reloadContent with absent or null YAML route
  • setTimestampIfAbsent non-absent (already-set) case
  • addToSet at capacity boundary (size = 3, attempt to add 4th element)
  • bulkIncrementLong cancellation path
  • setValue event-firing and dirty-flag contract
  • ChainPlayerContextFilter with zero filters and null player
  • Real null-value skip path in getModifiedEntries
  • Instant.now() used directly instead of via TimeProvider in two test methods
  • Non-falsifiable assertion in setTimestampIfAbsent_doesNotChange_whenCancelled
  • ChainPlayerContextFilterTest unnecessarily couples to registry infrastructure
  • Naming convention violations in ItemPluginTypeTest and ReloadableParserTest

@github-actions

Copy link
Copy Markdown

Security Review

No security concerns found.

The diff contains only test code (src/test/java/…). None of the checklist items apply:

  • SQL Injection / DDL Injection: No DAO methods, PreparedStatement usage, or DDL construction appear anywhere in the diff.
  • MiniMessage Injection: No calls to deserialize() or any MiniMessage API are present.
  • ChatResponse Safety: No ChatResponse implementations or pending chat content handling.
  • onClick() Return Value: No Slot.onClick() overrides.
  • Framework Permission Gating: No framework-level operations affecting player state; all code is unit test scaffolding with no Bukkit server interaction.

The test helpers (TestCorePlayer, RegistryResetExtension) are isolated to the test classpath and introduce no runtime attack surface.

@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: 10

🤖 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/com/diamonddagger590/mccore/builder/item/ItemPluginTypeTest.java`:
- Around line 78-83: Test currently omits the null input case for
ItemPluginType.fromName; update the parameterized test method
fromName_returnsNone_forUnrecognizedName (or add a new dedicated test) to
include a null case by adding `@NullSource` to the `@ValueSource` annotation (or
creating a small test that calls ItemPluginType.fromName(null) and asserts
ItemPluginType.NONE), ensuring the null-path behavior is explicitly verified.

In
`@src/test/java/com/diamonddagger590/mccore/configuration/common/ReloadableParserTest.java`:
- Around line 35-81: Add unit tests covering edge cases for ReloadableParser:
create new test methods that construct YamlDocument instances with (1) missing
or null EQUATION_ROUTE, (2) empty or invalid expressions (e.g., empty string,
non-parsable text), and (3) boundary numeric values (zero, negative numbers,
very large/small doubles) and assert expected behavior (exceptions thrown or
default/constant parser values). Use ReloadableParser(yaml, EQUATION_ROUTE),
call getContent(), parser.getValue(), parser.setVariable(...) and
reloadContent() where appropriate, and
assertNull/assertThrows/assertEquals/assertNotSame to verify correct handling of
null/missing routes, invalid expressions, and numeric boundaries.
- Line 37: Rename the test methods that currently use custom names to follow the
givenContext_whenAction_thenOutcome pattern: change
constructor_createsParser_thatEvaluatesEquation() and the three other test
methods referenced in this file (lines noted in the review) to descriptive names
that start with "given", describe the context, then "_when_" for the action, and
"_then_" for the expected outcome; update the method identifiers (method
declarations) accordingly in ReloadableParserTest so the `@Test` annotations
remain on the renamed methods and any internal references to those method names
are adjusted.

In
`@src/test/java/com/diamonddagger590/mccore/statistic/PlayerStatisticDataAdditionalTest.java`:
- Around line 81-412: The test method names in PlayerStatisticDataAdditionalTest
(e.g., getUUID_returnsConstructorUUID,
getValue_returnsDefault_whenNoStoredValue,
bulkIncrementLong_delegatesToIncrementLong,
addToSet_worksCorrectly_whenPopulatedWithPlainHashSet,
incrementInt_doesNotChange_whenCancelled,
setMaxLong_doesNotChange_whenCancelled,
getModifiedEntries_skipsKey_whenValueIsNull,
incrementLong_throws_forUnregisteredKey, addToSet_throws_forUnregisteredKey,
etc.) do not follow the required givenContext_whenAction_thenOutcome naming
convention; rename each test method to the form
given<Context>_when<Action>_then<Outcome> (for example
getUUID_returnsConstructorUUID ->
givenPlayerStatisticData_whenGetUUID_thenReturnsConstructorUUID,
getValue_returnsDefault_whenNoStoredValue ->
givenNoStoredValueAndRegisteredStat_whenGetValue_thenReturnsDefault,
bulkIncrementLong_delegatesToIncrementLong ->
givenLongStat_whenBulkIncrementLong_thenDelegatesAndAccumulates, and similarly
for the others) so every test method in this class follows the required pattern.
- Around line 89-412: Add unit tests to PlayerStatisticDataAdditionalTest
covering null and boundary numeric inputs: (1) verify null NamespacedKey or null
value passed to incrementInt/incrementLong/incrementDouble,
setMaxInt/setMaxLong/setMaxDouble, setTimestampIfAbsent, addToSet, and
removeFromSet produce the expected exception (e.g., NullPointerException or
IllegalArgumentException) by calling incrementInt/incrementLong/incrementDouble,
setMaxInt/setMaxLong/setMaxDouble, setTimestampIfAbsent, addToSet, removeFromSet
with null key or null element; (2) add tests for zero, negative, and max values
for incrementInt/incrementLong/incrementDouble and setMax* to assert correct
result or thrown exception (use
Integer.MAX_VALUE/Long.MAX_VALUE/Double.MAX_VALUE where appropriate); and (3)
add tests for empty and null collections when populating via populateFromEntries
and when adding/removing from set to ensure addToSet/removeFromSet behave
correctly. Use the existing helper keys (INT_KEY, LONG_KEY, DOUBLE_KEY,
TIMESTAMP_KEY, SET_KEY) and methods like incrementInt, incrementLong,
incrementDouble, setMaxInt, setMaxLong, setMaxDouble, setTimestampIfAbsent,
addToSet, removeFromSet, and populateFromEntries to locate targets.
- Around line 337-347: The test's assertions contradict its description: it
intends to verify that getModifiedEntries() skips a key whose value was
cleared/null, but it currently sets INT_KEY back to 99 and asserts presence;
update the test to actually clear the key (e.g., call the method that clears the
value or setValue(INT_KEY, null) after populateFromEntries(Map.of())) and then
call getModifiedEntries() and assert that the resulting Map does NOT contain
INT_KEY; reference the existing helpers and symbols data.setValue,
data.populateFromEntries, data.getModifiedEntries, INT_KEY, and StatisticEntry
when making this change.

In
`@src/test/java/com/diamonddagger590/mccore/util/filter/ChainPlayerContextFilterTest.java`:
- Around line 138-140: The TestCorePlayer(UUID uuid) constructor parameter
should be annotated with `@NotNull` (IntelliJ annotations v12); update the
constructor signature in class TestCorePlayer to import and apply
org.jetbrains.annotations.NotNull to the uuid parameter so the non-null contract
is explicit and matches project guidelines.
- Around line 35-134: Add a test method (e.g., filter_throwsWhenInputIsNull) in
ChainPlayerContextFilterTest that constructs a ChainPlayerContextFilter (use a
noop PlayerContextFilter or existing noopFilter) and asserts that calling
chain.filter(testPlayer, null) throws the expected exception (use
assertThrows(NullPointerException.class, ...) to verify the null-input
contract). Reference ChainPlayerContextFilter.filter and the test fixture
testPlayer when adding the assertion.
- Around line 37-124: The test method names do not follow the required
givenContext_whenAction_thenOutcome convention; rename each test method to that
pattern (e.g., filter_appliesSingleFilter_correctly ->
givenSingleFilter_whenFiltering_thenAppliesCorrectly,
filter_appliesMultipleFilters_inOrder ->
givenTwoFilters_whenFiltering_thenAppliesInOrder,
filter_orderMatters_forFilterChain ->
givenFiltersInReverseOrder_whenFiltering_thenOrderMatters,
filter_returnsEmpty_whenInputIsEmpty ->
givenEmptyInput_whenFiltering_thenReturnsEmpty,
filter_returnsEmpty_whenFirstFilterRemovesAll ->
givenFirstFilterRemovesAll_whenChaining_thenReturnsEmpty,
filter_appliesThreeFilters_sequentially ->
givenThreeFilters_whenFiltering_thenAllAppliedSequentially,
filter_passesPlayerContext_toEachFilter ->
givenStringFilters_whenFilteringWithPlayerContext_thenPlayerAccessible); update
the method names in ChainPlayerContextFilterTest accordingly and run tests to
ensure no references (e.g., `@DisplayName` remains) are broken.
- Around line 64-77: The test "filter_orderMatters_forFilterChain" currently
uses two commutative predicates (greaterThanThree and evenOnly) so it doesn't
prove order sensitivity; replace the filters with order-dependent ones (for
example a take-first-N filter and an even-only filter) so chaining order changes
the result: implement something like takeFirstThree = (player,list) ->
list.stream().limit(3).collect(...) and keep evenOnly = (player,list) ->
list.stream().filter(i -> i % 2 == 0).collect(...), create the
ChainPlayerContextFilter with (takeFirstThree, evenOnly), call
chain.filter(testPlayer, List.of(...)) and assert the expected different result
(e.g. when using List.of(1,2,3,4,5,6) expect [2] for takeFirstThree then
evenOnly but a different list if reversed), updating the assertion accordingly;
reference ChainPlayerContextFilter, filter_orderMatters_forFilterChain,
takeFirstThree, evenOnly, and the filter(...) method to locate and change the
code.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: d9d2a4aa-4426-4e34-9d50-a8e42b9eaa61

📥 Commits

Reviewing files that changed from the base of the PR and between fcb6a13 and 46d9488.

📒 Files selected for processing (4)
  • src/test/java/com/diamonddagger590/mccore/builder/item/ItemPluginTypeTest.java
  • src/test/java/com/diamonddagger590/mccore/configuration/common/ReloadableParserTest.java
  • src/test/java/com/diamonddagger590/mccore/statistic/PlayerStatisticDataAdditionalTest.java
  • src/test/java/com/diamonddagger590/mccore/util/filter/ChainPlayerContextFilterTest.java

Comment on lines +78 to +83
@ParameterizedTest
@ValueSource(strings = {"unknown", "oraxen", "mmoitems", "customitems", "", " "})
@DisplayName("Given an unrecognized name, when calling fromName, then returns NONE as fallback")
void fromName_returnsNone_forUnrecognizedName(String name) {
assertEquals(ItemPluginType.NONE, ItemPluginType.fromName(name));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add null-input coverage for fromName edge-case contract.

Line 78-83 currently covers unknown/blank values but not null. Please extend this parameterized test with @NullSource (or add a dedicated null test) so the null-path behavior is explicitly verified.

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/builder/item/ItemPluginTypeTest.java`
around lines 78 - 83, Test currently omits the null input case for
ItemPluginType.fromName; update the parameterized test method
fromName_returnsNone_forUnrecognizedName (or add a new dedicated test) to
include a null case by adding `@NullSource` to the `@ValueSource` annotation (or
creating a small test that calls ItemPluginType.fromName(null) and asserts
ItemPluginType.NONE), ensuring the null-path behavior is explicitly verified.

Source: Coding guidelines

Comment on lines +35 to +81
@Test
@DisplayName("Given a YAML with a simple equation, when constructing ReloadableParser, then parser evaluates correctly")
void constructor_createsParser_thatEvaluatesEquation() throws IOException {
YamlDocument yaml = createYamlDocument("equation: '2 + 3'");
ReloadableParser reloadableParser = new ReloadableParser(yaml, EQUATION_ROUTE);

Parser parser = reloadableParser.getContent();
assertNotNull(parser);
assertEquals(5.0, parser.getValue(), 0.001);
}

@Test
@DisplayName("Given a YAML with a variable equation, when constructing ReloadableParser, then parser accepts variables")
void constructor_createsParser_thatAcceptsVariables() throws IOException {
YamlDocument yaml = createYamlDocument("equation: 'x * 2'");
ReloadableParser reloadableParser = new ReloadableParser(yaml, EQUATION_ROUTE);

Parser parser = reloadableParser.getContent();
assertNotNull(parser);
parser.setVariable("x", 5.0);
assertEquals(10.0, parser.getValue(), 0.001);
}

@Test
@DisplayName("Given an updated YAML, when reloading, then parser reflects new equation")
void reloadContent_updatesParser_whenYamlChanges() throws IOException {
YamlDocument yaml = createYamlDocument("equation: '1 + 1'");
ReloadableParser reloadableParser = new ReloadableParser(yaml, EQUATION_ROUTE);

assertEquals(2.0, reloadableParser.getContent().getValue(), 0.001);

yaml.set(EQUATION_ROUTE, "10 * 3");
Parser oldParser = reloadableParser.getContent();
reloadableParser.reloadContent();

assertNotSame(oldParser, reloadableParser.getContent());
assertEquals(30.0, reloadableParser.getContent().getValue(), 0.001);
}

@Test
@DisplayName("Given a YAML with a constant equation, when constructing ReloadableParser, then returns constant value")
void constructor_handlesConstantEquation() throws IOException {
YamlDocument yaml = createYamlDocument("equation: '42'");
ReloadableParser reloadableParser = new ReloadableParser(yaml, EQUATION_ROUTE);

assertEquals(42.0, reloadableParser.getContent().getValue(), 0.001);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add edge-case tests for parser inputs and route/value states.

The suite currently validates only happy paths; add edge cases (e.g., missing/null equation route, invalid/empty expression, and boundary numeric values) to satisfy test coverage requirements.

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/configuration/common/ReloadableParserTest.java`
around lines 35 - 81, Add unit tests covering edge cases for ReloadableParser:
create new test methods that construct YamlDocument instances with (1) missing
or null EQUATION_ROUTE, (2) empty or invalid expressions (e.g., empty string,
non-parsable text), and (3) boundary numeric values (zero, negative numbers,
very large/small doubles) and assert expected behavior (exceptions thrown or
default/constant parser values). Use ReloadableParser(yaml, EQUATION_ROUTE),
call getContent(), parser.getValue(), parser.setVariable(...) and
reloadContent() where appropriate, and
assertNull/assertThrows/assertEquals/assertNotSame to verify correct handling of
null/missing routes, invalid expressions, and numeric boundaries.

Source: Coding guidelines


@Test
@DisplayName("Given a YAML with a simple equation, when constructing ReloadableParser, then parser evaluates correctly")
void constructor_createsParser_thatEvaluatesEquation() throws IOException {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Rename test methods to the required givenContext_whenAction_thenOutcome pattern.

Line 37, Line 48, Line 60, and Line 76 currently use custom naming that does not follow the project’s required test method convention.

As per coding guidelines, "Follow the givenContext_whenAction_thenOutcome naming convention for test methods."

Also applies to: 48-48, 60-60, 76-76

🤖 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/configuration/common/ReloadableParserTest.java`
at line 37, Rename the test methods that currently use custom names to follow
the givenContext_whenAction_thenOutcome pattern: change
constructor_createsParser_thatEvaluatesEquation() and the three other test
methods referenced in this file (lines noted in the review) to descriptive names
that start with "given", describe the context, then "_when_" for the action, and
"_then_" for the expected outcome; update the method identifiers (method
declarations) accordingly in ReloadableParserTest so the `@Test` annotations
remain on the renamed methods and any internal references to those method names
are adjusted.

Source: Coding guidelines

Comment on lines +81 to +412
@Test
@DisplayName("Given a PlayerStatisticData, when calling getUUID, then returns the UUID passed to constructor")
void getUUID_returnsConstructorUUID() {
assertEquals(PLAYER_UUID, data.getUUID());
}

// --- getValue (raw Object accessor) ---

@Test
@DisplayName("Given no stored value and a registered stat, when calling getValue, then returns the default value")
void getValue_returnsDefault_whenNoStoredValue() {
assertTrue(data.getValue(INT_KEY).isPresent());
assertEquals(0, data.getValue(INT_KEY).get());
}

@Test
@DisplayName("Given a stored value, when calling getValue, then returns that stored value")
void getValue_returnsStoredValue_afterSet() {
data.setValue(INT_KEY, 42);
assertEquals(42, data.getValue(INT_KEY).get());
}

@Test
@DisplayName("Given an unregistered key, when calling getValue, then returns empty")
void getValue_returnsEmpty_forUnregisteredKey() {
assertTrue(data.getValue(key("test", "nonexistent")).isEmpty());
}

@Test
@DisplayName("Given a string stat with stored value, when calling getValue, then returns the string")
void getValue_returnsString_whenStringStatStored() {
data.setValue(STRING_KEY, "hello");
assertEquals("hello", data.getValue(STRING_KEY).get());
}

@Test
@DisplayName("Given a timestamp stat with stored value, when calling getValue, then returns the Instant")
void getValue_returnsInstant_whenTimestampStatStored() {
Instant now = Instant.now();
data.setValue(TIMESTAMP_KEY, now);
assertEquals(now, data.getValue(TIMESTAMP_KEY).get());
}

// --- bulkIncrementLong ---

@Test
@DisplayName("Given a long stat, when calling bulkIncrementLong, then it delegates to incrementLong")
void bulkIncrementLong_delegatesToIncrementLong() {
data.bulkIncrementLong(LONG_KEY, 50L);
assertEquals(50L, data.getLongValue(LONG_KEY).orElse(0L));
}

@Test
@DisplayName("Given a long stat, when calling bulkIncrementLong multiple times, then values accumulate")
void bulkIncrementLong_accumulates_whenCalledMultipleTimes() {
data.bulkIncrementLong(LONG_KEY, 10L);
data.bulkIncrementLong(LONG_KEY, 20L);
assertEquals(30L, data.getLongValue(LONG_KEY).orElse(0L));
}

@Test
@DisplayName("Given a long stat, when calling bulkIncrementLong, then fires pre and post events with INCREMENT type")
void bulkIncrementLong_firesEvents_withIncrementModificationType() {
data.bulkIncrementLong(LONG_KEY, 5L);
assertEquals(2, firedEvents.size());
assertTrue(firedEvents.get(0) instanceof StatisticModifyEvent);
StatisticModifyEvent preEvent = (StatisticModifyEvent) firedEvents.get(0);
assertEquals(ModificationType.INCREMENT, preEvent.getModificationType());
assertTrue(firedEvents.get(1) instanceof PostStatisticModifyEvent);
}

// --- getOrCreateSet with plain HashSet (non-LinkedHashSet) ---

@Test
@DisplayName("Given a set stat populated with a plain HashSet via entries, when adding to the set, then works correctly")
void addToSet_worksCorrectly_whenPopulatedWithPlainHashSet() {
Set<String> plainSet = new HashSet<>();
plainSet.add("existing");
data.populateFromEntries(Map.of(SET_KEY, new StatisticEntry(SET_KEY, StatisticType.SET_STRING, plainSet)));
assertTrue(data.addToSet(SET_KEY, "new_element"));
Set<String> result = data.getSetValue(SET_KEY).get();
assertTrue(result.contains("existing"));
assertTrue(result.contains("new_element"));
}

@Test
@DisplayName("Given a set stat populated with a plain HashSet, when removing from the set, then works correctly")
void removeFromSet_worksCorrectly_whenPopulatedWithPlainHashSet() {
Set<String> plainSet = new HashSet<>();
plainSet.add("target");
data.populateFromEntries(Map.of(SET_KEY, new StatisticEntry(SET_KEY, StatisticType.SET_STRING, plainSet)));
assertTrue(data.removeFromSet(SET_KEY, "target"));
assertFalse(data.getSetValue(SET_KEY).get().contains("target"));
}

// --- Increment cancellation ---

@Test
@DisplayName("Given a cancelling event dispatcher, when incrementing int, then value does not change")
void incrementInt_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.incrementInt(INT_KEY, 10);
assertEquals(0, data.getIntValue(INT_KEY).orElse(0));
assertFalse(data.isDirty());
}

@Test
@DisplayName("Given a cancelling event dispatcher, when incrementing double, then value does not change")
void incrementDouble_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.incrementDouble(DOUBLE_KEY, 5.0);
assertEquals(0.0, data.getDoubleValue(DOUBLE_KEY).orElse(0.0), 0.001);
assertFalse(data.isDirty());
}

@Test
@DisplayName("Given a cancelling event dispatcher, when incrementing long, then value does not change")
void incrementLong_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.incrementLong(LONG_KEY, 100L);
assertEquals(0L, data.getLongValue(LONG_KEY).orElse(0L));
assertFalse(data.isDirty());
}

// --- setMax cancellation ---

@Test
@DisplayName("Given a cancelling dispatcher, when setting max long, then value does not change")
void setMaxLong_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.setMaxLong(LONG_KEY, 100L);
assertEquals(0L, data.getLongValue(LONG_KEY).orElse(0L));
}

@Test
@DisplayName("Given a cancelling dispatcher, when setting max int, then value does not change")
void setMaxInt_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.setMaxInt(INT_KEY, 100);
assertEquals(0, data.getIntValue(INT_KEY).orElse(0));
}

@Test
@DisplayName("Given a cancelling dispatcher, when setting max double, then value does not change")
void setMaxDouble_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.setMaxDouble(DOUBLE_KEY, 100.0);
assertEquals(0.0, data.getDoubleValue(DOUBLE_KEY).orElse(0.0), 0.001);
}

// --- setTimestampIfAbsent cancellation ---

@Test
@DisplayName("Given a cancelling dispatcher, when setting timestamp if absent, then value does not change")
void setTimestampIfAbsent_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.setTimestampIfAbsent(TIMESTAMP_KEY, Instant.now());
assertEquals(Instant.EPOCH, data.getTimestampValue(TIMESTAMP_KEY).orElse(Instant.EPOCH));
}

// --- addToSet cancellation ---

@Test
@DisplayName("Given a cancelling dispatcher, when adding to set, then returns false and set is unchanged")
void addToSet_returnsFalse_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
assertFalse(data.addToSet(SET_KEY, "value"));
assertTrue(data.getSetValue(SET_KEY).get().isEmpty());
}

// --- removeFromSet cancellation ---

@Test
@DisplayName("Given a set with an element and a cancelling dispatcher, when removing, then element remains")
void removeFromSet_doesNotRemove_whenCancelled() {
data.addToSet(SET_KEY, "keep_me");
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.populateFromEntries(Map.of(SET_KEY, new StatisticEntry(SET_KEY, StatisticType.SET_STRING, new LinkedHashSet<>(Set.of("keep_me")))));
assertFalse(data.removeFromSet(SET_KEY, "keep_me"));
assertTrue(data.getSetValue(SET_KEY).get().contains("keep_me"));
}

// --- getModifiedEntries with null value ---

@Test
@DisplayName("Given a dirty key whose value was cleared, when getting modified entries, then that key is skipped")
void getModifiedEntries_skipsKey_whenValueIsNull() {
data.setValue(INT_KEY, 42);
assertTrue(data.isDirty());
data.populateFromEntries(Map.of());
data.setValue(INT_KEY, 99);
Map<NamespacedKey, StatisticEntry> modified = data.getModifiedEntries();
assertTrue(modified.containsKey(INT_KEY));
assertEquals(99, modified.get(INT_KEY).getAsInt());
}

// --- Increment throws for unregistered key ---

@Test
@DisplayName("Given an unregistered key, when incrementing long, then throws StatisticNotRegisteredException")
void incrementLong_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.incrementLong(key("test", "nonexistent"), 1L));
}

@Test
@DisplayName("Given an unregistered key, when incrementing int, then throws StatisticNotRegisteredException")
void incrementInt_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.incrementInt(key("test", "nonexistent"), 1));
}

@Test
@DisplayName("Given an unregistered key, when incrementing double, then throws StatisticNotRegisteredException")
void incrementDouble_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.incrementDouble(key("test", "nonexistent"), 1.0));
}

@Test
@DisplayName("Given an unregistered key, when setting max long, then throws StatisticNotRegisteredException")
void setMaxLong_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.setMaxLong(key("test", "nonexistent"), 1L));
}

@Test
@DisplayName("Given an unregistered key, when setting max int, then throws StatisticNotRegisteredException")
void setMaxInt_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.setMaxInt(key("test", "nonexistent"), 1));
}

@Test
@DisplayName("Given an unregistered key, when setting max double, then throws StatisticNotRegisteredException")
void setMaxDouble_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.setMaxDouble(key("test", "nonexistent"), 1.0));
}

@Test
@DisplayName("Given an unregistered key, when setting timestamp if absent, then throws StatisticNotRegisteredException")
void setTimestampIfAbsent_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.setTimestampIfAbsent(key("test", "nonexistent"), Instant.now()));
}

@Test
@DisplayName("Given an unregistered key, when adding to set, then throws StatisticNotRegisteredException")
void addToSet_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.addToSet(key("test", "nonexistent"), "value"));
}

@Test
@DisplayName("Given an unregistered key, when removing from set, then throws StatisticNotRegisteredException")
void removeFromSet_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.removeFromSet(key("test", "nonexistent"), "value"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Rename test methods to the required givenContext_whenAction_thenOutcome format.

Current names (for example, getUUID_returnsConstructorUUID) do not follow the required repository naming convention for test methods.

As per coding guidelines, "Follow the givenContext_whenAction_thenOutcome naming convention for test methods (e.g., givenEmptyPage_whenGetSlots_thenReturnsEmpty)."

🤖 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/statistic/PlayerStatisticDataAdditionalTest.java`
around lines 81 - 412, The test method names in
PlayerStatisticDataAdditionalTest (e.g., getUUID_returnsConstructorUUID,
getValue_returnsDefault_whenNoStoredValue,
bulkIncrementLong_delegatesToIncrementLong,
addToSet_worksCorrectly_whenPopulatedWithPlainHashSet,
incrementInt_doesNotChange_whenCancelled,
setMaxLong_doesNotChange_whenCancelled,
getModifiedEntries_skipsKey_whenValueIsNull,
incrementLong_throws_forUnregisteredKey, addToSet_throws_forUnregisteredKey,
etc.) do not follow the required givenContext_whenAction_thenOutcome naming
convention; rename each test method to the form
given<Context>_when<Action>_then<Outcome> (for example
getUUID_returnsConstructorUUID ->
givenPlayerStatisticData_whenGetUUID_thenReturnsConstructorUUID,
getValue_returnsDefault_whenNoStoredValue ->
givenNoStoredValueAndRegisteredStat_whenGetValue_thenReturnsDefault,
bulkIncrementLong_delegatesToIncrementLong ->
givenLongStat_whenBulkIncrementLong_thenDelegatesAndAccumulates, and similarly
for the others) so every test method in this class follows the required pattern.

Source: Coding guidelines

Comment on lines +89 to +412
@Test
@DisplayName("Given no stored value and a registered stat, when calling getValue, then returns the default value")
void getValue_returnsDefault_whenNoStoredValue() {
assertTrue(data.getValue(INT_KEY).isPresent());
assertEquals(0, data.getValue(INT_KEY).get());
}

@Test
@DisplayName("Given a stored value, when calling getValue, then returns that stored value")
void getValue_returnsStoredValue_afterSet() {
data.setValue(INT_KEY, 42);
assertEquals(42, data.getValue(INT_KEY).get());
}

@Test
@DisplayName("Given an unregistered key, when calling getValue, then returns empty")
void getValue_returnsEmpty_forUnregisteredKey() {
assertTrue(data.getValue(key("test", "nonexistent")).isEmpty());
}

@Test
@DisplayName("Given a string stat with stored value, when calling getValue, then returns the string")
void getValue_returnsString_whenStringStatStored() {
data.setValue(STRING_KEY, "hello");
assertEquals("hello", data.getValue(STRING_KEY).get());
}

@Test
@DisplayName("Given a timestamp stat with stored value, when calling getValue, then returns the Instant")
void getValue_returnsInstant_whenTimestampStatStored() {
Instant now = Instant.now();
data.setValue(TIMESTAMP_KEY, now);
assertEquals(now, data.getValue(TIMESTAMP_KEY).get());
}

// --- bulkIncrementLong ---

@Test
@DisplayName("Given a long stat, when calling bulkIncrementLong, then it delegates to incrementLong")
void bulkIncrementLong_delegatesToIncrementLong() {
data.bulkIncrementLong(LONG_KEY, 50L);
assertEquals(50L, data.getLongValue(LONG_KEY).orElse(0L));
}

@Test
@DisplayName("Given a long stat, when calling bulkIncrementLong multiple times, then values accumulate")
void bulkIncrementLong_accumulates_whenCalledMultipleTimes() {
data.bulkIncrementLong(LONG_KEY, 10L);
data.bulkIncrementLong(LONG_KEY, 20L);
assertEquals(30L, data.getLongValue(LONG_KEY).orElse(0L));
}

@Test
@DisplayName("Given a long stat, when calling bulkIncrementLong, then fires pre and post events with INCREMENT type")
void bulkIncrementLong_firesEvents_withIncrementModificationType() {
data.bulkIncrementLong(LONG_KEY, 5L);
assertEquals(2, firedEvents.size());
assertTrue(firedEvents.get(0) instanceof StatisticModifyEvent);
StatisticModifyEvent preEvent = (StatisticModifyEvent) firedEvents.get(0);
assertEquals(ModificationType.INCREMENT, preEvent.getModificationType());
assertTrue(firedEvents.get(1) instanceof PostStatisticModifyEvent);
}

// --- getOrCreateSet with plain HashSet (non-LinkedHashSet) ---

@Test
@DisplayName("Given a set stat populated with a plain HashSet via entries, when adding to the set, then works correctly")
void addToSet_worksCorrectly_whenPopulatedWithPlainHashSet() {
Set<String> plainSet = new HashSet<>();
plainSet.add("existing");
data.populateFromEntries(Map.of(SET_KEY, new StatisticEntry(SET_KEY, StatisticType.SET_STRING, plainSet)));
assertTrue(data.addToSet(SET_KEY, "new_element"));
Set<String> result = data.getSetValue(SET_KEY).get();
assertTrue(result.contains("existing"));
assertTrue(result.contains("new_element"));
}

@Test
@DisplayName("Given a set stat populated with a plain HashSet, when removing from the set, then works correctly")
void removeFromSet_worksCorrectly_whenPopulatedWithPlainHashSet() {
Set<String> plainSet = new HashSet<>();
plainSet.add("target");
data.populateFromEntries(Map.of(SET_KEY, new StatisticEntry(SET_KEY, StatisticType.SET_STRING, plainSet)));
assertTrue(data.removeFromSet(SET_KEY, "target"));
assertFalse(data.getSetValue(SET_KEY).get().contains("target"));
}

// --- Increment cancellation ---

@Test
@DisplayName("Given a cancelling event dispatcher, when incrementing int, then value does not change")
void incrementInt_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.incrementInt(INT_KEY, 10);
assertEquals(0, data.getIntValue(INT_KEY).orElse(0));
assertFalse(data.isDirty());
}

@Test
@DisplayName("Given a cancelling event dispatcher, when incrementing double, then value does not change")
void incrementDouble_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.incrementDouble(DOUBLE_KEY, 5.0);
assertEquals(0.0, data.getDoubleValue(DOUBLE_KEY).orElse(0.0), 0.001);
assertFalse(data.isDirty());
}

@Test
@DisplayName("Given a cancelling event dispatcher, when incrementing long, then value does not change")
void incrementLong_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.incrementLong(LONG_KEY, 100L);
assertEquals(0L, data.getLongValue(LONG_KEY).orElse(0L));
assertFalse(data.isDirty());
}

// --- setMax cancellation ---

@Test
@DisplayName("Given a cancelling dispatcher, when setting max long, then value does not change")
void setMaxLong_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.setMaxLong(LONG_KEY, 100L);
assertEquals(0L, data.getLongValue(LONG_KEY).orElse(0L));
}

@Test
@DisplayName("Given a cancelling dispatcher, when setting max int, then value does not change")
void setMaxInt_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.setMaxInt(INT_KEY, 100);
assertEquals(0, data.getIntValue(INT_KEY).orElse(0));
}

@Test
@DisplayName("Given a cancelling dispatcher, when setting max double, then value does not change")
void setMaxDouble_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.setMaxDouble(DOUBLE_KEY, 100.0);
assertEquals(0.0, data.getDoubleValue(DOUBLE_KEY).orElse(0.0), 0.001);
}

// --- setTimestampIfAbsent cancellation ---

@Test
@DisplayName("Given a cancelling dispatcher, when setting timestamp if absent, then value does not change")
void setTimestampIfAbsent_doesNotChange_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.setTimestampIfAbsent(TIMESTAMP_KEY, Instant.now());
assertEquals(Instant.EPOCH, data.getTimestampValue(TIMESTAMP_KEY).orElse(Instant.EPOCH));
}

// --- addToSet cancellation ---

@Test
@DisplayName("Given a cancelling dispatcher, when adding to set, then returns false and set is unchanged")
void addToSet_returnsFalse_whenCancelled() {
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
assertFalse(data.addToSet(SET_KEY, "value"));
assertTrue(data.getSetValue(SET_KEY).get().isEmpty());
}

// --- removeFromSet cancellation ---

@Test
@DisplayName("Given a set with an element and a cancelling dispatcher, when removing, then element remains")
void removeFromSet_doesNotRemove_whenCancelled() {
data.addToSet(SET_KEY, "keep_me");
data = new PlayerStatisticData(
PLAYER_UUID,
event -> {
if (event instanceof StatisticModifyEvent sme) {
sme.setCancelled(true);
}
},
id -> mockPlayer
);
data.populateFromEntries(Map.of(SET_KEY, new StatisticEntry(SET_KEY, StatisticType.SET_STRING, new LinkedHashSet<>(Set.of("keep_me")))));
assertFalse(data.removeFromSet(SET_KEY, "keep_me"));
assertTrue(data.getSetValue(SET_KEY).get().contains("keep_me"));
}

// --- getModifiedEntries with null value ---

@Test
@DisplayName("Given a dirty key whose value was cleared, when getting modified entries, then that key is skipped")
void getModifiedEntries_skipsKey_whenValueIsNull() {
data.setValue(INT_KEY, 42);
assertTrue(data.isDirty());
data.populateFromEntries(Map.of());
data.setValue(INT_KEY, 99);
Map<NamespacedKey, StatisticEntry> modified = data.getModifiedEntries();
assertTrue(modified.containsKey(INT_KEY));
assertEquals(99, modified.get(INT_KEY).getAsInt());
}

// --- Increment throws for unregistered key ---

@Test
@DisplayName("Given an unregistered key, when incrementing long, then throws StatisticNotRegisteredException")
void incrementLong_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.incrementLong(key("test", "nonexistent"), 1L));
}

@Test
@DisplayName("Given an unregistered key, when incrementing int, then throws StatisticNotRegisteredException")
void incrementInt_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.incrementInt(key("test", "nonexistent"), 1));
}

@Test
@DisplayName("Given an unregistered key, when incrementing double, then throws StatisticNotRegisteredException")
void incrementDouble_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.incrementDouble(key("test", "nonexistent"), 1.0));
}

@Test
@DisplayName("Given an unregistered key, when setting max long, then throws StatisticNotRegisteredException")
void setMaxLong_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.setMaxLong(key("test", "nonexistent"), 1L));
}

@Test
@DisplayName("Given an unregistered key, when setting max int, then throws StatisticNotRegisteredException")
void setMaxInt_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.setMaxInt(key("test", "nonexistent"), 1));
}

@Test
@DisplayName("Given an unregistered key, when setting max double, then throws StatisticNotRegisteredException")
void setMaxDouble_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.setMaxDouble(key("test", "nonexistent"), 1.0));
}

@Test
@DisplayName("Given an unregistered key, when setting timestamp if absent, then throws StatisticNotRegisteredException")
void setTimestampIfAbsent_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.setTimestampIfAbsent(key("test", "nonexistent"), Instant.now()));
}

@Test
@DisplayName("Given an unregistered key, when adding to set, then throws StatisticNotRegisteredException")
void addToSet_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.addToSet(key("test", "nonexistent"), "value"));
}

@Test
@DisplayName("Given an unregistered key, when removing from set, then throws StatisticNotRegisteredException")
void removeFromSet_throws_forUnregisteredKey() {
assertThrows(StatisticNotRegisteredException.class,
() -> data.removeFromSet(key("test", "nonexistent"), "value"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add missing null/negative/max boundary tests for this suite.

This suite covers many happy/error paths, but it still misses required edge cases for null input and negative/max numeric boundaries on new test coverage (for example increment*, setMax*, and set operations).

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/statistic/PlayerStatisticDataAdditionalTest.java`
around lines 89 - 412, Add unit tests to PlayerStatisticDataAdditionalTest
covering null and boundary numeric inputs: (1) verify null NamespacedKey or null
value passed to incrementInt/incrementLong/incrementDouble,
setMaxInt/setMaxLong/setMaxDouble, setTimestampIfAbsent, addToSet, and
removeFromSet produce the expected exception (e.g., NullPointerException or
IllegalArgumentException) by calling incrementInt/incrementLong/incrementDouble,
setMaxInt/setMaxLong/setMaxDouble, setTimestampIfAbsent, addToSet, removeFromSet
with null key or null element; (2) add tests for zero, negative, and max values
for incrementInt/incrementLong/incrementDouble and setMax* to assert correct
result or thrown exception (use
Integer.MAX_VALUE/Long.MAX_VALUE/Double.MAX_VALUE where appropriate); and (3)
add tests for empty and null collections when populating via populateFromEntries
and when adding/removing from set to ensure addToSet/removeFromSet behave
correctly. Use the existing helper keys (INT_KEY, LONG_KEY, DOUBLE_KEY,
TIMESTAMP_KEY, SET_KEY) and methods like incrementInt, incrementLong,
incrementDouble, setMaxInt, setMaxLong, setMaxDouble, setTimestampIfAbsent,
addToSet, removeFromSet, and populateFromEntries to locate targets.

Source: Coding guidelines

Comment on lines +337 to +347
@Test
@DisplayName("Given a dirty key whose value was cleared, when getting modified entries, then that key is skipped")
void getModifiedEntries_skipsKey_whenValueIsNull() {
data.setValue(INT_KEY, 42);
assertTrue(data.isDirty());
data.populateFromEntries(Map.of());
data.setValue(INT_KEY, 99);
Map<NamespacedKey, StatisticEntry> modified = data.getModifiedEntries();
assertTrue(modified.containsKey(INT_KEY));
assertEquals(99, modified.get(INT_KEY).getAsInt());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Test intent and assertions are inconsistent for null-skip behavior.

Line 338 says the key should be skipped when value is null, but Lines 345-346 assert the key is present with value 99. This currently validates overwrite behavior, not null-skip filtering in getModifiedEntries().

Suggested fix
 `@Test`
 `@DisplayName`("Given a dirty key whose value was cleared, when getting modified entries, then that key is skipped")
 void getModifiedEntries_skipsKey_whenValueIsNull() {
     data.setValue(INT_KEY, 42);
     assertTrue(data.isDirty());
-    data.populateFromEntries(Map.of());
-    data.setValue(INT_KEY, 99);
-    Map<NamespacedKey, StatisticEntry> modified = data.getModifiedEntries();
-    assertTrue(modified.containsKey(INT_KEY));
-    assertEquals(99, modified.get(INT_KEY).getAsInt());
+    data.populateFromEntries(Map.of( // clear backing value for dirty key
+            INT_KEY, new StatisticEntry(INT_KEY, StatisticType.INT, null)
+    ));
+    Map<NamespacedKey, StatisticEntry> modified = data.getModifiedEntries();
+    assertFalse(modified.containsKey(INT_KEY));
 }
🤖 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/statistic/PlayerStatisticDataAdditionalTest.java`
around lines 337 - 347, The test's assertions contradict its description: it
intends to verify that getModifiedEntries() skips a key whose value was
cleared/null, but it currently sets INT_KEY back to 99 and asserts presence;
update the test to actually clear the key (e.g., call the method that clears the
value or setValue(INT_KEY, null) after populateFromEntries(Map.of())) and then
call getModifiedEntries() and assert that the resulting Map does NOT contain
INT_KEY; reference the existing helpers and symbols data.setValue,
data.populateFromEntries, data.getModifiedEntries, INT_KEY, and StatisticEntry
when making this change.

Comment on lines +35 to +134
@Test
@DisplayName("Given a single filter, when filtering, then applies that filter correctly")
void filter_appliesSingleFilter_correctly() {
PlayerContextFilter<Integer, TestCorePlayer> evenOnly = (player, list) ->
list.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());

ChainPlayerContextFilter<Integer, TestCorePlayer> chain = new ChainPlayerContextFilter<>(evenOnly);

Collection<Integer> result = chain.filter(testPlayer, List.of(1, 2, 3, 4, 5, 6));
assertEquals(List.of(2, 4, 6), new ArrayList<>(result));
}

@Test
@DisplayName("Given two filters, when filtering, then applies both in order")
void filter_appliesMultipleFilters_inOrder() {
PlayerContextFilter<Integer, TestCorePlayer> evenOnly = (player, list) ->
list.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());
PlayerContextFilter<Integer, TestCorePlayer> greaterThanThree = (player, list) ->
list.stream().filter(i -> i > 3).collect(Collectors.toList());

@SuppressWarnings("unchecked")
ChainPlayerContextFilter<Integer, TestCorePlayer> chain =
new ChainPlayerContextFilter<>(evenOnly, greaterThanThree);

Collection<Integer> result = chain.filter(testPlayer, List.of(1, 2, 3, 4, 5, 6, 7, 8));
assertEquals(List.of(4, 6, 8), new ArrayList<>(result));
}

@Test
@DisplayName("Given two filters in reverse order, when filtering, then order matters")
void filter_orderMatters_forFilterChain() {
PlayerContextFilter<Integer, TestCorePlayer> greaterThanThree = (player, list) ->
list.stream().filter(i -> i > 3).collect(Collectors.toList());
PlayerContextFilter<Integer, TestCorePlayer> evenOnly = (player, list) ->
list.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());

@SuppressWarnings("unchecked")
ChainPlayerContextFilter<Integer, TestCorePlayer> chain =
new ChainPlayerContextFilter<>(greaterThanThree, evenOnly);

Collection<Integer> result = chain.filter(testPlayer, List.of(1, 2, 3, 4, 5, 6, 7, 8));
assertEquals(List.of(4, 6, 8), new ArrayList<>(result));
}

@Test
@DisplayName("Given an empty input collection, when filtering, then returns empty collection")
void filter_returnsEmpty_whenInputIsEmpty() {
PlayerContextFilter<String, TestCorePlayer> noopFilter = (player, list) -> list;

ChainPlayerContextFilter<String, TestCorePlayer> chain = new ChainPlayerContextFilter<>(noopFilter);

Collection<String> result = chain.filter(testPlayer, List.of());
assertTrue(result.isEmpty());
}

@Test
@DisplayName("Given a filter that reduces to empty, when chaining with another, then result is empty")
void filter_returnsEmpty_whenFirstFilterRemovesAll() {
PlayerContextFilter<Integer, TestCorePlayer> removeAll = (player, list) -> List.of();
PlayerContextFilter<Integer, TestCorePlayer> identity = (player, list) -> list;

@SuppressWarnings("unchecked")
ChainPlayerContextFilter<Integer, TestCorePlayer> chain =
new ChainPlayerContextFilter<>(removeAll, identity);

Collection<Integer> result = chain.filter(testPlayer, List.of(1, 2, 3));
assertTrue(result.isEmpty());
}

@Test
@DisplayName("Given three chained filters, when filtering, then all three are applied sequentially")
void filter_appliesThreeFilters_sequentially() {
PlayerContextFilter<Integer, TestCorePlayer> removeNegatives = (player, list) ->
list.stream().filter(i -> i >= 0).collect(Collectors.toList());
PlayerContextFilter<Integer, TestCorePlayer> removeLargeThan10 = (player, list) ->
list.stream().filter(i -> i <= 10).collect(Collectors.toList());
PlayerContextFilter<Integer, TestCorePlayer> evenOnly = (player, list) ->
list.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());

@SuppressWarnings("unchecked")
ChainPlayerContextFilter<Integer, TestCorePlayer> chain =
new ChainPlayerContextFilter<>(removeNegatives, removeLargeThan10, evenOnly);

Collection<Integer> result = chain.filter(testPlayer, List.of(-5, -2, 0, 1, 2, 3, 4, 8, 11, 20));
assertEquals(List.of(0, 2, 4, 8), new ArrayList<>(result));
}

@Test
@DisplayName("Given string filters, when filtering with player context, then player object is accessible")
void filter_passesPlayerContext_toEachFilter() {
PlayerContextFilter<String, TestCorePlayer> filterByPlayerUUID = (player, list) ->
list.stream().filter(s -> !s.equals(player.getUUID().toString())).collect(Collectors.toList());

ChainPlayerContextFilter<String, TestCorePlayer> chain =
new ChainPlayerContextFilter<>(filterByPlayerUUID);

String playerUuidString = PLAYER_UUID.toString();
Collection<String> result = chain.filter(testPlayer, List.of("hello", playerUuidString, "world"));
assertEquals(List.of("hello", "world"), new ArrayList<>(result));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a null-input edge-case test for filter(...).

This suite covers empty input but not null input. Add a regression asserting the expected behavior (e.g., exception contract) for null collections.

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/util/filter/ChainPlayerContextFilterTest.java`
around lines 35 - 134, Add a test method (e.g., filter_throwsWhenInputIsNull) in
ChainPlayerContextFilterTest that constructs a ChainPlayerContextFilter (use a
noop PlayerContextFilter or existing noopFilter) and asserts that calling
chain.filter(testPlayer, null) throws the expected exception (use
assertThrows(NullPointerException.class, ...) to verify the null-input
contract). Reference ChainPlayerContextFilter.filter and the test fixture
testPlayer when adding the assertion.

Source: Coding guidelines

Comment on lines +37 to +124
void filter_appliesSingleFilter_correctly() {
PlayerContextFilter<Integer, TestCorePlayer> evenOnly = (player, list) ->
list.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());

ChainPlayerContextFilter<Integer, TestCorePlayer> chain = new ChainPlayerContextFilter<>(evenOnly);

Collection<Integer> result = chain.filter(testPlayer, List.of(1, 2, 3, 4, 5, 6));
assertEquals(List.of(2, 4, 6), new ArrayList<>(result));
}

@Test
@DisplayName("Given two filters, when filtering, then applies both in order")
void filter_appliesMultipleFilters_inOrder() {
PlayerContextFilter<Integer, TestCorePlayer> evenOnly = (player, list) ->
list.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());
PlayerContextFilter<Integer, TestCorePlayer> greaterThanThree = (player, list) ->
list.stream().filter(i -> i > 3).collect(Collectors.toList());

@SuppressWarnings("unchecked")
ChainPlayerContextFilter<Integer, TestCorePlayer> chain =
new ChainPlayerContextFilter<>(evenOnly, greaterThanThree);

Collection<Integer> result = chain.filter(testPlayer, List.of(1, 2, 3, 4, 5, 6, 7, 8));
assertEquals(List.of(4, 6, 8), new ArrayList<>(result));
}

@Test
@DisplayName("Given two filters in reverse order, when filtering, then order matters")
void filter_orderMatters_forFilterChain() {
PlayerContextFilter<Integer, TestCorePlayer> greaterThanThree = (player, list) ->
list.stream().filter(i -> i > 3).collect(Collectors.toList());
PlayerContextFilter<Integer, TestCorePlayer> evenOnly = (player, list) ->
list.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());

@SuppressWarnings("unchecked")
ChainPlayerContextFilter<Integer, TestCorePlayer> chain =
new ChainPlayerContextFilter<>(greaterThanThree, evenOnly);

Collection<Integer> result = chain.filter(testPlayer, List.of(1, 2, 3, 4, 5, 6, 7, 8));
assertEquals(List.of(4, 6, 8), new ArrayList<>(result));
}

@Test
@DisplayName("Given an empty input collection, when filtering, then returns empty collection")
void filter_returnsEmpty_whenInputIsEmpty() {
PlayerContextFilter<String, TestCorePlayer> noopFilter = (player, list) -> list;

ChainPlayerContextFilter<String, TestCorePlayer> chain = new ChainPlayerContextFilter<>(noopFilter);

Collection<String> result = chain.filter(testPlayer, List.of());
assertTrue(result.isEmpty());
}

@Test
@DisplayName("Given a filter that reduces to empty, when chaining with another, then result is empty")
void filter_returnsEmpty_whenFirstFilterRemovesAll() {
PlayerContextFilter<Integer, TestCorePlayer> removeAll = (player, list) -> List.of();
PlayerContextFilter<Integer, TestCorePlayer> identity = (player, list) -> list;

@SuppressWarnings("unchecked")
ChainPlayerContextFilter<Integer, TestCorePlayer> chain =
new ChainPlayerContextFilter<>(removeAll, identity);

Collection<Integer> result = chain.filter(testPlayer, List.of(1, 2, 3));
assertTrue(result.isEmpty());
}

@Test
@DisplayName("Given three chained filters, when filtering, then all three are applied sequentially")
void filter_appliesThreeFilters_sequentially() {
PlayerContextFilter<Integer, TestCorePlayer> removeNegatives = (player, list) ->
list.stream().filter(i -> i >= 0).collect(Collectors.toList());
PlayerContextFilter<Integer, TestCorePlayer> removeLargeThan10 = (player, list) ->
list.stream().filter(i -> i <= 10).collect(Collectors.toList());
PlayerContextFilter<Integer, TestCorePlayer> evenOnly = (player, list) ->
list.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());

@SuppressWarnings("unchecked")
ChainPlayerContextFilter<Integer, TestCorePlayer> chain =
new ChainPlayerContextFilter<>(removeNegatives, removeLargeThan10, evenOnly);

Collection<Integer> result = chain.filter(testPlayer, List.of(-5, -2, 0, 1, 2, 3, 4, 8, 11, 20));
assertEquals(List.of(0, 2, 4, 8), new ArrayList<>(result));
}

@Test
@DisplayName("Given string filters, when filtering with player context, then player object is accessible")
void filter_passesPlayerContext_toEachFilter() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test method names don’t follow the required givenContext_whenAction_thenOutcome convention.

Rename test methods to the mandated pattern for consistency and discoverability in this test suite.

As per coding guidelines, "Follow the givenContext_whenAction_thenOutcome naming convention for test methods (e.g., givenEmptyPage_whenGetSlots_thenReturnsEmpty)."

🤖 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/util/filter/ChainPlayerContextFilterTest.java`
around lines 37 - 124, The test method names do not follow the required
givenContext_whenAction_thenOutcome convention; rename each test method to that
pattern (e.g., filter_appliesSingleFilter_correctly ->
givenSingleFilter_whenFiltering_thenAppliesCorrectly,
filter_appliesMultipleFilters_inOrder ->
givenTwoFilters_whenFiltering_thenAppliesInOrder,
filter_orderMatters_forFilterChain ->
givenFiltersInReverseOrder_whenFiltering_thenOrderMatters,
filter_returnsEmpty_whenInputIsEmpty ->
givenEmptyInput_whenFiltering_thenReturnsEmpty,
filter_returnsEmpty_whenFirstFilterRemovesAll ->
givenFirstFilterRemovesAll_whenChaining_thenReturnsEmpty,
filter_appliesThreeFilters_sequentially ->
givenThreeFilters_whenFiltering_thenAllAppliedSequentially,
filter_passesPlayerContext_toEachFilter ->
givenStringFilters_whenFilteringWithPlayerContext_thenPlayerAccessible); update
the method names in ChainPlayerContextFilterTest accordingly and run tests to
ensure no references (e.g., `@DisplayName` remains) are broken.

Source: Coding guidelines

Comment on lines +64 to +77
@DisplayName("Given two filters in reverse order, when filtering, then order matters")
void filter_orderMatters_forFilterChain() {
PlayerContextFilter<Integer, TestCorePlayer> greaterThanThree = (player, list) ->
list.stream().filter(i -> i > 3).collect(Collectors.toList());
PlayerContextFilter<Integer, TestCorePlayer> evenOnly = (player, list) ->
list.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());

@SuppressWarnings("unchecked")
ChainPlayerContextFilter<Integer, TestCorePlayer> chain =
new ChainPlayerContextFilter<>(greaterThanThree, evenOnly);

Collection<Integer> result = chain.filter(testPlayer, List.of(1, 2, 3, 4, 5, 6, 7, 8));
assertEquals(List.of(4, 6, 8), new ArrayList<>(result));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

“Order matters” test currently does not prove order sensitivity.

Both filters are simple predicates (> 3 and even), so applying them in either order yields the same result. This test can pass even if chaining order is broken.

Proposed fix
 `@Test`
 `@DisplayName`("Given two filters in reverse order, when filtering, then order matters")
 void filter_orderMatters_forFilterChain() {
-    PlayerContextFilter<Integer, TestCorePlayer> greaterThanThree = (player, list) ->
-            list.stream().filter(i -> i > 3).collect(Collectors.toList());
-    PlayerContextFilter<Integer, TestCorePlayer> evenOnly = (player, list) ->
-            list.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());
+    PlayerContextFilter<Integer, TestCorePlayer> takeFirstThree = (player, list) ->
+            list.stream().limit(3).collect(Collectors.toList());
+    PlayerContextFilter<Integer, TestCorePlayer> evenOnly = (player, list) ->
+            list.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());

     `@SuppressWarnings`("unchecked")
     ChainPlayerContextFilter<Integer, TestCorePlayer> chain =
-            new ChainPlayerContextFilter<>(greaterThanThree, evenOnly);
+            new ChainPlayerContextFilter<>(takeFirstThree, evenOnly);

-    Collection<Integer> result = chain.filter(testPlayer, List.of(1, 2, 3, 4, 5, 6, 7, 8));
-    assertEquals(List.of(4, 6, 8), new ArrayList<>(result));
+    Collection<Integer> result = chain.filter(testPlayer, List.of(1, 2, 3, 4, 5, 6));
+    assertEquals(List.of(2), new ArrayList<>(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/util/filter/ChainPlayerContextFilterTest.java`
around lines 64 - 77, The test "filter_orderMatters_forFilterChain" currently
uses two commutative predicates (greaterThanThree and evenOnly) so it doesn't
prove order sensitivity; replace the filters with order-dependent ones (for
example a take-first-N filter and an even-only filter) so chaining order changes
the result: implement something like takeFirstThree = (player,list) ->
list.stream().limit(3).collect(...) and keep evenOnly = (player,list) ->
list.stream().filter(i -> i % 2 == 0).collect(...), create the
ChainPlayerContextFilter with (takeFirstThree, evenOnly), call
chain.filter(testPlayer, List.of(...)) and assert the expected different result
(e.g. when using List.of(1,2,3,4,5,6) expect [2] for takeFirstThree then
evenOnly but a different list if reversed), updating the assertion accordingly;
reference ChainPlayerContextFilter, filter_orderMatters_forFilterChain,
takeFirstThree, evenOnly, and the filter(...) method to locate and change the
code.

Comment on lines +138 to +140
TestCorePlayer(UUID uuid) {
super(uuid, null);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Annotate the non-null constructor parameter with @NotNull.

TestCorePlayer(UUID uuid) receives a non-null UUID in this test and should carry the required nullability annotation.

As per coding guidelines, "Add @NotNull annotation from IntelliJ annotations v12 on all non-null return types and parameters."

🤖 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/util/filter/ChainPlayerContextFilterTest.java`
around lines 138 - 140, The TestCorePlayer(UUID uuid) constructor parameter
should be annotated with `@NotNull` (IntelliJ annotations v12); update the
constructor signature in class TestCorePlayer to import and apply
org.jetbrains.annotations.NotNull to the uuid parameter so the non-null contract
is explicit and matches project guidelines.

Source: Coding guidelines

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