From 600d14406f01fb2c6ec2c06c8d2dee59ed4bde2f Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:11:09 +0000 Subject: [PATCH 1/3] fix: stop node deactivation when refunds cannot progress --- .../net/tfminecraft/dowsing/objects/Node.java | 3 + .../dowsing/objects/NodeDeactivationTest.java | 111 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java diff --git a/src/main/java/net/tfminecraft/dowsing/objects/Node.java b/src/main/java/net/tfminecraft/dowsing/objects/Node.java index 9da7d0a..eab9fb2 100644 --- a/src/main/java/net/tfminecraft/dowsing/objects/Node.java +++ b/src/main/java/net/tfminecraft/dowsing/objects/Node.java @@ -461,7 +461,10 @@ public void deActivate() { if(isClaimable()) return; if(this.cycleTime > 0 && this.cycleTime < Cache.cycleLength) { while(this.getInputCounter() > 0) { + int pendingInputs = this.getInputCounter(); refund(); + // A missing barrel or hopper prevents refunds. Keep the pending inputs for retry. + if(this.getInputCounter() >= pendingInputs) break; } } NodeManager.requestNodeBenefitSync(); diff --git a/src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java b/src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java new file mode 100644 index 0000000..f499140 --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java @@ -0,0 +1,111 @@ +package net.tfminecraft.dowsing.objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import net.tfminecraft.dowsing.Cache; + +class NodeDeactivationTest { + + private Integer previousCycleLength; + + @BeforeEach + void setCycleLength() { + previousCycleLength = Cache.cycleLength; + Cache.cycleLength = 60; + } + + @AfterEach + void restoreCycleLength() { + Cache.cycleLength = previousCycleLength; + } + + @Test + void failedRefundStopsAndKeepsPendingInputsForRetry() { + RefundNode node = new RefundNode(3, 0); + + node.deActivate(); + + assertFalse(node.getIsActive()); + assertEquals(1, node.attempts); + assertEquals(3, node.getInputCounter()); + + node.successfulRefundsRemaining = 3; + node.deActivate(); + + assertEquals(4, node.attempts); + assertEquals(0, node.getInputCounter()); + } + + @Test + void partialRefundStopsAtFirstFailure() { + RefundNode node = new RefundNode(3, 1); + + node.deActivate(); + + assertFalse(node.getIsActive()); + assertEquals(2, node.attempts); + assertEquals(2, node.getInputCounter()); + } + + @Test + void successfulRefundsDrainPendingInputs() { + RefundNode node = new RefundNode(3, 3); + + node.deActivate(); + + assertFalse(node.getIsActive()); + assertEquals(3, node.attempts); + assertEquals(0, node.getInputCounter()); + } + + @Test + void noPendingInputsSkipsRefunds() { + RefundNode node = new RefundNode(0, 0); + + node.deActivate(); + + assertFalse(node.getIsActive()); + assertEquals(0, node.attempts); + } + + private static final class RefundNode extends Node { + private int attempts; + private int successfulRefundsRemaining; + private final int maximumAttempts; + + RefundNode(int pendingInputs, int successfulRefunds) { + super(UUID.randomUUID(), null, null, null, true, 1, 1, null, 10, pendingInputs, 50); + successfulRefundsRemaining = successfulRefunds; + maximumAttempts = pendingInputs + 1; + } + + @Override + public boolean isClaimable() { + return false; + } + + @Override + public void update() { + // No world or production configuration is needed for the refund loop. + } + + @Override + public void refund() { + // Fail deterministically on the old loop instead of hanging the test JVM. + assertTrue(++attempts <= maximumAttempts, "Deactivation kept retrying a failed refund"); + if (successfulRefundsRemaining > 0) { + successfulRefundsRemaining--; + setInputCounter(getInputCounter() - 1); + } + // Missing barrel/hopper returns without consuming a pending input. + } + } +} From 863fbbc42394a469b165e984f7b7228e0429d3f1 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:23:01 +0000 Subject: [PATCH 2/3] fix: make pending node refunds retryable before edits --- pom.xml | 6 + .../dowsing/managers/InventoryManager.java | 8 +- .../dowsing/managers/NodeManager.java | 14 ++ .../net/tfminecraft/dowsing/objects/Node.java | 9 ++ .../dowsing/managers/NodeRefundMenuTest.java | 133 ++++++++++++++++++ .../dowsing/objects/NodeDeactivationTest.java | 27 ++++ 6 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 src/test/java/net/tfminecraft/dowsing/managers/NodeRefundMenuTest.java diff --git a/pom.xml b/pom.xml index e8a3e6d..a8cd1c4 100644 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,12 @@ 5.14.0 test + + org.mockito + mockito-core + 5.20.0 + test + net.tfminecraft magic diff --git a/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java b/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java index 21a5027..4b45d2b 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java @@ -334,7 +334,13 @@ ItemStack createStatus(Node n) { i.setType(Material.RED_STAINED_GLASS_PANE); ItemMeta m = i.getItemMeta(); m.setDisplayName("§cINACTIVE"); - if(n.getErrors().size() > 0) { + if(n.hasPendingRefund()) { + m.setDisplayName("§eRetry Refund"); + List lore = new ArrayList<>(n.getErrors()); + lore.add("§7Restore the barrel and hopper, then click to retry."); + lore.add("§7Refunds must finish before changing or activating this node."); + m.setLore(lore); + } else if(n.getErrors().size() > 0) { m.setLore(n.getErrors()); } i.setItemMeta(m); diff --git a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java index e916ca5..269f210 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java @@ -231,6 +231,7 @@ public void validate() { } } public void confirmClick(Player p, Node n, ConfirmType t) { + if(t != ConfirmType.DEACTIVATE && blockPendingRefund(p, n)) return; if(t.equals(ConfirmType.DEACTIVATE)) { n.deActivate(); InventoryManager inv = new InventoryManager(); @@ -254,6 +255,11 @@ public void confirmClick(Player p, Node n, ConfirmType t) { currentNode.put(p, n); } } + private boolean blockPendingRefund(Player p, Node n) { + if(!n.hasPendingRefund()) return false; + p.sendMessage("§cRestore the barrel and hopper, then click Retry Refund before changing this node."); + return true; + } @EventHandler(ignoreCancelled = true) public void placeVanillaNode(BlockPlaceEvent e) { NodeBlock b = BlockLoader.getByBlock(e.getBlock().getType()); @@ -399,6 +405,7 @@ public void invenClick(InventoryClickEvent e) { p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f); return; } + if(e.getSlot() != 17 && blockPendingRefund(p, n)) return; if(e.getSlot() == 8) { if(n.getIsActive()) { p.sendMessage("§cCannot upgrade while node is active"); @@ -436,6 +443,11 @@ public void invenClick(InventoryClickEvent e) { if(n.getIsActive()) { confirm.put(p, ConfirmType.DEACTIVATE); inv.confirmView(p); + } else if(n.hasPendingRefund()) { + n.deActivate(); + p.sendMessage(n.hasPendingRefund() + ? "§cRefund still pending. Restore the barrel and hopper, then retry." + : "§aRefund complete. The node remains inactive."); } else { n.activate(); if(n.getIsActive()) { @@ -482,6 +494,7 @@ public void invenClick(InventoryClickEvent e) { } } else if(currentSlot.get(p) != null && e.getView().getTitle().equalsIgnoreCase("§7"+n.getBlock().getResource()+" Node: "+WordUtils.capitalize(currentSlot.get(p).getId().replace("_", " ")))) { e.setCancelled(true); + if(e.getSlot() != 26 && blockPendingRefund(p, n)) return; if(!n.hasGuild()) { n.breakNode(); nodes.remove(n); @@ -517,6 +530,7 @@ public void invenClick(InventoryClickEvent e) { currentNode.put(p, n); } else if(e.getView().getTitle().equalsIgnoreCase("§7"+n.getBlock().getResource()+" Node: Type")) { e.setCancelled(true); + if(e.getSlot() != 26 && blockPendingRefund(p, n)) return; if(!n.hasGuild()) { n.breakNode(); nodes.remove(n); diff --git a/src/main/java/net/tfminecraft/dowsing/objects/Node.java b/src/main/java/net/tfminecraft/dowsing/objects/Node.java index eab9fb2..fa601cf 100644 --- a/src/main/java/net/tfminecraft/dowsing/objects/Node.java +++ b/src/main/java/net/tfminecraft/dowsing/objects/Node.java @@ -404,6 +404,10 @@ public void refund() { ng.refund(this);; } public void activate() { + if(hasPendingRefund()) { + deActivate(); + return; + } if(isClaimable()) return; NodeEngine ng = new NodeEngine(); Boolean failed = false; @@ -456,6 +460,11 @@ public void activate() { ng.takeInputs(this); NodeManager.requestNodeBenefitSync(); } + /** Whether an inactive node still owes refunds from an interrupted cycle. */ + public boolean hasPendingRefund() { + return !this.isActive && this.inputCounter > 0 + && this.cycleTime > 0 && this.cycleTime < Cache.cycleLength; + } public void deActivate() { this.isActive = false; if(isClaimable()) return; diff --git a/src/test/java/net/tfminecraft/dowsing/managers/NodeRefundMenuTest.java b/src/test/java/net/tfminecraft/dowsing/managers/NodeRefundMenuTest.java new file mode 100644 index 0000000..ddf52e8 --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/managers/NodeRefundMenuTest.java @@ -0,0 +1,133 @@ +package net.tfminecraft.dowsing.managers; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; + +import net.tfminecraft.dowsing.enums.ConfirmType; +import net.tfminecraft.dowsing.objects.Node; +import net.tfminecraft.dowsing.objects.NodeBlock; +import net.tfminecraft.dowsing.objects.NodeSlot; + +class NodeRefundMenuTest { + private NodeManager manager; + private Node node; + private Player player; + private InventoryClickEvent click; + private InventoryView view; + + @BeforeEach + void setUp() { + manager = new NodeManager(); + node = mock(Node.class); + player = mock(Player.class); + click = mock(InventoryClickEvent.class); + view = mock(InventoryView.class); + NodeBlock block = mock(NodeBlock.class); + when(node.getBlock()).thenReturn(block); + when(block.getResource()).thenReturn("Iron"); + when(node.hasGuild()).thenReturn(true); + when(node.hasPendingRefund()).thenReturn(true); + when(node.getIsActive()).thenReturn(false); + when(player.hasPermission("dowsing.admin")).thenReturn(true); + when(click.getWhoClicked()).thenReturn(player); + when(click.getView()).thenReturn(view); + when(view.getTitle()).thenReturn("§7Iron Node"); + when(click.getClickedInventory()).thenReturn(mock(Inventory.class)); + manager.currentNode.put(player, node); + } + + @ParameterizedTest + @ValueSource(ints = {6, 8, 9, 18, 24, 26, 10}) + void pendingRefundBlocksMainMenuChanges(int slot) { + when(click.getSlot()).thenReturn(slot); + + manager.invenClick(click); + + verify(click).setCancelled(true); + verify(player).sendMessage(contains("Retry Refund")); + verify(node, never()).setGuild(any()); + verify(node, never()).breakNode(); + verify(node, never()).activate(); + } + + @ParameterizedTest + @EnumSource(value = ConfirmType.class, names = {"DELETE_NODE", "CHANGE_TYPE"}) + void pendingRefundBlocksStaleConfirmation(ConfirmType action) { + manager.confirmClick(player, node, action); + + verify(player).sendMessage(contains("Retry Refund")); + verify(node, never()).breakNode(); + verify(node, never()).setCurrentType(any()); + } + + @ParameterizedTest + @ValueSource(strings = {"§7Iron Node: Type", "§7Iron Node: Fuel"}) + void pendingRefundBlocksAlreadyOpenSubmenus(String title) { + NodeSlot slot = mock(NodeSlot.class); + when(slot.getId()).thenReturn("fuel"); + manager.currentSlot.put(player, slot); + when(view.getTitle()).thenReturn(title); + + manager.invenClick(click); + + verify(click).setCancelled(true); + verify(player).sendMessage(contains("Retry Refund")); + verify(slot, never()).setActivePm(any()); + } + + @Test + void statusButtonRetriesRefundWithoutActivating() { + when(click.getSlot()).thenReturn(17); + + manager.invenClick(click); + + verify(node).deActivate(); + verify(node, never()).activate(); + verify(player).sendMessage(contains("Refund still pending")); + } + + @Test + void successfulRetryLeavesNodeInactive() { + when(click.getSlot()).thenReturn(17); + when(node.hasPendingRefund()).thenReturn(true, false); + + manager.invenClick(click); + + verify(node).deActivate(); + verify(node, never()).activate(); + verify(player).sendMessage(contains("Refund complete")); + } + + @Test + void statusButtonStillActivatesWhenNoRefundIsPending() { + when(click.getSlot()).thenReturn(17); + when(node.hasPendingRefund()).thenReturn(false); + // Stop at the production boundary before Bukkit's server-backed sound registry is needed. + RuntimeException activationReached = new RuntimeException("Activation reached"); + doThrow(activationReached).when(node).activate(); + + assertSame(activationReached, assertThrows(RuntimeException.class, () -> manager.invenClick(click))); + + verify(node).activate(); + verify(node, never()).deActivate(); + verify(player, never()).sendMessage(anyString()); + } +} diff --git a/src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java b/src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java index f499140..48f94e7 100644 --- a/src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java +++ b/src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java @@ -36,12 +36,39 @@ void failedRefundStopsAndKeepsPendingInputsForRetry() { assertFalse(node.getIsActive()); assertEquals(1, node.attempts); assertEquals(3, node.getInputCounter()); + assertTrue(node.hasPendingRefund()); node.successfulRefundsRemaining = 3; node.deActivate(); assertEquals(4, node.attempts); assertEquals(0, node.getInputCounter()); + assertFalse(node.hasPendingRefund()); + } + + @Test + void activationRetriesPendingRefundWithoutStartingANewCycle() { + RefundNode node = new RefundNode(3, 0); + node.deActivate(); + node.successfulRefundsRemaining = 3; + + node.activate(); + + assertEquals(0, node.getInputCounter()); + assertFalse(node.getIsActive()); + assertFalse(node.hasPendingRefund()); + } + + @Test + void onlyInterruptedInactiveCyclesHavePendingRefunds() { + RefundNode node = new RefundNode(3, 0); + assertFalse(node.hasPendingRefund()); + node.isActive = false; + assertTrue(node.hasPendingRefund()); + node.setCycleTime(0); + assertFalse(node.hasPendingRefund()); + node.setCycleTime(Cache.cycleLength); + assertFalse(node.hasPendingRefund()); } @Test From bf11534d0dcdbbfaba8ad7c4fdc6db4672aa30f4 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:33:12 +0000 Subject: [PATCH 3/3] fix: guard node menu clicks and refund initial cycle inputs --- .../dowsing/managers/NodeManager.java | 4 ++++ .../net/tfminecraft/dowsing/objects/Node.java | 4 ++-- .../dowsing/managers/NodeRefundMenuTest.java | 24 ++++++++++++++++++- .../dowsing/objects/NodeDeactivationTest.java | 16 +++++++++---- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java index 269f210..0b9e595 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java @@ -393,6 +393,7 @@ public void invenClick(InventoryClickEvent e) { InventoryManager inv = new InventoryManager(); if(e.getView().getTitle().equalsIgnoreCase("§7"+n.getBlock().getResource()+ " Node")) { e.setCancelled(true); + if(e.getClickedInventory() != e.getView().getTopInventory()) return; if(!n.hasGuild()) { n.breakNode(); nodes.remove(n); @@ -494,6 +495,7 @@ public void invenClick(InventoryClickEvent e) { } } else if(currentSlot.get(p) != null && e.getView().getTitle().equalsIgnoreCase("§7"+n.getBlock().getResource()+" Node: "+WordUtils.capitalize(currentSlot.get(p).getId().replace("_", " ")))) { e.setCancelled(true); + if(e.getClickedInventory() != e.getView().getTopInventory()) return; if(e.getSlot() != 26 && blockPendingRefund(p, n)) return; if(!n.hasGuild()) { n.breakNode(); @@ -530,6 +532,7 @@ public void invenClick(InventoryClickEvent e) { currentNode.put(p, n); } else if(e.getView().getTitle().equalsIgnoreCase("§7"+n.getBlock().getResource()+" Node: Type")) { e.setCancelled(true); + if(e.getClickedInventory() != e.getView().getTopInventory()) return; if(e.getSlot() != 26 && blockPendingRefund(p, n)) return; if(!n.hasGuild()) { n.breakNode(); @@ -564,6 +567,7 @@ public void invenClick(InventoryClickEvent e) { inv.confirmView(p); } else if(e.getView().getTitle().equalsIgnoreCase("§7Confirm Action")) { e.setCancelled(true); + if(e.getClickedInventory() != e.getView().getTopInventory()) return; if(!n.hasGuild()) { n.breakNode(); nodes.remove(n); diff --git a/src/main/java/net/tfminecraft/dowsing/objects/Node.java b/src/main/java/net/tfminecraft/dowsing/objects/Node.java index fa601cf..d1a86ee 100644 --- a/src/main/java/net/tfminecraft/dowsing/objects/Node.java +++ b/src/main/java/net/tfminecraft/dowsing/objects/Node.java @@ -463,12 +463,12 @@ public void activate() { /** Whether an inactive node still owes refunds from an interrupted cycle. */ public boolean hasPendingRefund() { return !this.isActive && this.inputCounter > 0 - && this.cycleTime > 0 && this.cycleTime < Cache.cycleLength; + && this.cycleTime >= 0 && this.cycleTime < Cache.cycleLength; } public void deActivate() { this.isActive = false; if(isClaimable()) return; - if(this.cycleTime > 0 && this.cycleTime < Cache.cycleLength) { + if(this.cycleTime >= 0 && this.cycleTime < Cache.cycleLength) { while(this.getInputCounter() > 0) { int pendingInputs = this.getInputCounter(); refund(); diff --git a/src/test/java/net/tfminecraft/dowsing/managers/NodeRefundMenuTest.java b/src/test/java/net/tfminecraft/dowsing/managers/NodeRefundMenuTest.java index ddf52e8..5a963b0 100644 --- a/src/test/java/net/tfminecraft/dowsing/managers/NodeRefundMenuTest.java +++ b/src/test/java/net/tfminecraft/dowsing/managers/NodeRefundMenuTest.java @@ -50,10 +50,32 @@ void setUp() { when(click.getWhoClicked()).thenReturn(player); when(click.getView()).thenReturn(view); when(view.getTitle()).thenReturn("§7Iron Node"); - when(click.getClickedInventory()).thenReturn(mock(Inventory.class)); + Inventory top = mock(Inventory.class); + when(click.getClickedInventory()).thenReturn(top); + when(view.getTopInventory()).thenReturn(top); manager.currentNode.put(player, node); } + @ParameterizedTest + @ValueSource(strings = {"§7Iron Node", "§7Iron Node: Type", "§7Iron Node: Fuel", "§7Confirm Action"}) + void lowerInventoryClicksCannotTriggerNodeActions(String title) { + NodeSlot slot = mock(NodeSlot.class); + when(slot.getId()).thenReturn("fuel"); + manager.currentSlot.put(player, slot); + when(view.getTitle()).thenReturn(title); + when(click.getSlot()).thenReturn(17); + when(click.getClickedInventory()).thenReturn(mock(Inventory.class)); + + manager.invenClick(click); + + verify(click).setCancelled(true); + verify(node, never()).deActivate(); + verify(node, never()).activate(); + verify(node, never()).update(); + verify(node, never()).breakNode(); + verify(player, never()).sendMessage(anyString()); + } + @ParameterizedTest @ValueSource(ints = {6, 8, 9, 18, 24, 26, 10}) void pendingRefundBlocksMainMenuChanges(int slot) { diff --git a/src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java b/src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java index 48f94e7..d1ec256 100644 --- a/src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java +++ b/src/test/java/net/tfminecraft/dowsing/objects/NodeDeactivationTest.java @@ -9,6 +9,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import net.tfminecraft.dowsing.Cache; @@ -27,9 +29,11 @@ void restoreCycleLength() { Cache.cycleLength = previousCycleLength; } - @Test - void failedRefundStopsAndKeepsPendingInputsForRetry() { + @ParameterizedTest + @ValueSource(ints = {0, 1}) + void failedRefundStopsAndKeepsPendingInputsForRetry(int cycleTime) { RefundNode node = new RefundNode(3, 0); + node.setCycleTime(cycleTime); node.deActivate(); @@ -66,6 +70,8 @@ void onlyInterruptedInactiveCyclesHavePendingRefunds() { node.isActive = false; assertTrue(node.hasPendingRefund()); node.setCycleTime(0); + assertTrue(node.hasPendingRefund()); + node.setCycleTime(-1); assertFalse(node.hasPendingRefund()); node.setCycleTime(Cache.cycleLength); assertFalse(node.hasPendingRefund()); @@ -82,9 +88,11 @@ void partialRefundStopsAtFirstFailure() { assertEquals(2, node.getInputCounter()); } - @Test - void successfulRefundsDrainPendingInputs() { + @ParameterizedTest + @ValueSource(ints = {0, 1}) + void successfulRefundsDrainPendingInputs(int cycleTime) { RefundNode node = new RefundNode(3, 3); + node.setCycleTime(cycleTime); node.deActivate();