Skip to content

Bring the add/edit/remove command tests to coverage parity - #23

Merged
dmccoystephenson merged 2 commits into
mainfrom
feature/command-trio-test-parity
Aug 25, 2026
Merged

Bring the add/edit/remove command tests to coverage parity#23
dmccoystephenson merged 2 commits into
mainfrom
feature/command-trio-test-parity

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Member

Summary

AddCommand, EditCommand and RemoveCommand are a parallel series: each one runs the same four guards before it touches an item's lore.

  1. The sender must be a Player, or "This command can only be used by a player." is sent.
  2. The main hand must not hold Material.AIR, or "You aren't holding anything." is sent.
  3. ItemStack.getItemMeta() must not return null, or "That item's meta information wasn't found." is sent.
  4. The no-argument execute(CommandSender) overload must send that command's usage string.

Only AddCommandTest exercised most of that set. EditCommandTest and RemoveCommandTest covered index and quoting validation but none of the four guards above, so a regression in either class's copy of a guard would have gone unnoticed until it was reported from a live server.

The missing cases have been added so that all three test classes now cover the same guard set:

  • EditCommandTest — non-player sender, empty main hand, item without meta, and the execute(CommandSender) usage overload (4 new tests).
  • RemoveCommandTest — the same four cases (4 new tests).
  • AddCommandTest — item without meta (1 new test), plus the player-only message assertion that its existing non-player-sender case was missing.

These are characterization tests. No production code was changed by this PR, and no existing assertion was weakened.

The test count rises from 26 to 35.

Test plan

  • mvn -B test — 35 tests run, 0 failures, 0 errors, 0 skipped, BUILD SUCCESS.
  • The two new guard tests in EditCommandTest were confirmed non-vacuous empirically rather than by reasoning: with EditCommand's AIR and null-meta guards mutated away, execute_rejectsWhenNotHoldingAnItem failed and execute_rejectsItemWithoutMeta errored on a NullPointerException; both pass again with the guards restored. The mutation was reverted before commit, and git status confirms no file under src/main is modified.
  • git diff --stat origin/main — 3 files, 90 insertions, 0 deletions, all of it test code.

Notes

No documentation source of truth is affected. No command syntax, permission node, config key or user-facing behaviour was changed, so HelpCommand.java, COMMANDS.md, USER_GUIDE.md, CONFIG.md and CHANGELOG.md are all still accurate as written. CHANGELOG.md was deliberately left untouched: this PR carries no change a user of the plugin could observe.

No tracking issue exists for this work — the gap was found during triage of the existing test suite. The open backlog was deferred for the reasons below rather than picked up this cycle:


This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).


drafted by Claude on behalf of Daniel Stephenson

dmccoystephenson and others added 2 commits August 25, 2026 02:00
AddCommand, EditCommand and RemoveCommand run the same four guards before
touching an item's lore: sender must be a Player, the main hand must not be
empty, the item must have meta, and the no-argument overload must send the
usage string. Only AddCommandTest exercised most of them, so a regression in
EditCommand's or RemoveCommand's copy of a guard would have gone unnoticed.

Adds the missing cases so all three test classes cover the same guard set:

- EditCommandTest and RemoveCommandTest: non-player sender, empty main hand,
  item without meta, and the execute(CommandSender) usage overload
- AddCommandTest: item without meta, plus the player-only message assertion
  its non-player-sender case was missing

Characterization only; no production code changed. The two new guard tests
were verified against a mutated EditCommand to confirm they fail when the
guard is removed. 26 tests to 35, all passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review found the parity incomplete: the three classes covered the same
guards, but five of the pre-existing cases asserted only the boolean return
(or matched the message with anyString()), so a guard could send the wrong
text and still pass.

Each of those now asserts the exact string the command sends, matching the
cases added in the previous commit:

- AddCommandTest: the empty-main-hand and no-argument-overload cases
- EditCommandTest and RemoveCommandTest: the missing-arguments case

The now-unused anyString() import is dropped from AddCommandTest. Assertions
were only strengthened, never relaxed. 35 tests, all passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmccoystephenson

Copy link
Copy Markdown
Member Author

Self-review rubric

Scored adversarially against the diff at 9770584, on the assumption of FAIL unless direct evidence of PASS was found. Three items initially failed and were fixed in 9770584; the scores below are post-fix.

  • Scope: PASSgit diff --name-only origin/main...HEAD returns exactly three paths, all under src/test/java/dansplugins/playerlore/commands/. No file under src/main is touched, no formatting sweep is included, and no rename is present.
  • Tests-new: PASS (inverted) — no new public method is introduced by this PR, so the usual direction of this item does not apply. The inverse was checked instead: nine new test methods were added, and each targets a production branch that previously had no test covering it in that class.
  • Tests-fix (empirical, not judged): PASS — no bug is fixed here, so the stash-and-run procedure was replaced by a mutation check on the equivalent question of whether the new tests are vacuous. With EditCommand's Material.AIR guard and its null-ItemMeta guard both mutated to if (false), execute_rejectsWhenNotHoldingAnItem failed on its message assertion and execute_rejectsItemWithoutMeta errored with NullPointer Cannot invoke "org.bukkit.inventory.meta.ItemMeta.getLore()" because "itemMeta" is null. Both passed again once the guards were restored, and git status confirmed EditCommand.java clean before either commit was made.
  • Sibling structure: PASS — no new file is created. The added methods follow the arrange/act/assert shape, the execute_<condition> naming, and the static-import style already used by the three sibling test classes.
  • Sibling renames: PASS — no identifier is renamed.
  • Docs: PASS — every row of the documentation sources-of-truth table was checked against this diff and none is affected. HelpCommand.java lists the same four commands; COMMANDS.md's four rows still match the constructors and usage strings; USER_GUIDE.md's permissions table still matches the four nodes in plugin.yml; CONFIG.md's version and debugMode still match ConfigService.saveMissingConfigDefaultsIfNotPresent. CHANGELOG.md was deliberately not amended, because a test-only change produces nothing a user of the plugin can observe.
  • Issue resolution: PASS (not applicable) — no Closes #N reference is claimed, so no issue can be falsely auto-closed. The gap was found while triaging the existing suite.
  • CI: PASS — the build job passed in 35s on head 977058420ca62c6cc0c4d4cc2fda080b5081ebd7, which is the exact SHA reviewed here (run 32824616324). Locally, mvn -B test reports Tests run: 35, Failures: 0, Errors: 0, Skipped: 0 and BUILD SUCCESS; the executed-test count was read from the output rather than inferred from the exit status, and it rose from 26 on main.
  • Command trio parity (repo-specific): PASS — this item normally guards against a validation or permission change landing in one of AddCommand/EditCommand/RemoveCommand without the other two being checked. No validation or permission logic changed here, and the PR's entire purpose is to close the corresponding gap in the tests, so the three classes now cover the identical guard set with identical assertion strength.

Findings that were fixed during review

Three assertion-strength gaps were found that made the parity claim weaker than it read. All were mechanical and were fixed in 9770584 rather than deferred.

  • src/test/java/dansplugins/playerlore/commands/AddCommandTest.java:89execute_rejectsWhenNotHoldingAnItem asserted only the false return. AddCommand could have sent the wrong text, or no text at all, and the test would still have passed. The exact "You aren't holding anything." message is now asserted, matching the equivalent new cases in the other two classes.
  • src/test/java/dansplugins/playerlore/commands/AddCommandTest.java:117execute_noArgs_sendsUsageMessage matched with anyString(), which cannot distinguish the usage string from any other message. The exact "Usage: /pl add \"line of lore\"" string is now asserted; this also locks the string that COMMANDS.md documents. The anyString import became unused and was removed.
  • src/test/java/dansplugins/playerlore/commands/EditCommandTest.java:98 and src/test/java/dansplugins/playerlore/commands/RemoveCommandTest.java:88 — both execute_rejectsMissingArgs cases asserted only the return value. Each now asserts its command's exact usage string.

No assertion was relaxed in the course of these fixes.

Observations outside the diff

Recorded here rather than acted on, since neither belongs in a test-expansion change.

  • None of the three commands has a test covering the permission node it declares in its constructor (pl.add, pl.edit, pl.remove). That is not an oversight in these test classes: enforcement lives in Ponder's CommandService, not in AbstractPluginCommand.execute, so the node is not observable from a unit test that calls execute directly. This is the same structural fact that pl.default permission is declared in DefaultCommand but is absent from plugin.yml and USER_GUIDE.md #12 describes for pl.default, and it is worth keeping in mind when pl.default permission is declared in DefaultCommand but is absent from plugin.yml and USER_GUIDE.md #12 is decided — routing DefaultCommand through the command service would make its node enforced but no more unit-testable than the other three.
  • PlayerLore.isVersionMismatched() and dansplugins.playerlore.utils.Logger remain entirely untested, and ConfigService.saveMissingConfigDefaultsIfNotPresent, sendConfigList and the three get*OrDefault helpers are untested as well. The get*OrDefault trio has a genuine quirk worth locking in characterization tests — getIntOrDefault and getDoubleOrDefault treat a legitimately-configured 0 as absent and return the default instead. These are candidates for a following test-expansion cycle.

This review was posted during a Gardener session (https://github.com/Stephenson-Software/gardener). It is a self-review by the agent that wrote the change, not an independent review.


drafted by Claude on behalf of Daniel Stephenson

@dmccoystephenson
dmccoystephenson merged commit 7197867 into main Aug 25, 2026
1 check passed
@dmccoystephenson
dmccoystephenson deleted the feature/command-trio-test-parity branch August 25, 2026 08:05
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.

1 participant