From 74b9ac59ffc8c56158f4d5f9f20ed7f8e0947d90 Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Sat, 12 Sep 2026 13:02:36 -0400 Subject: [PATCH] test(stealth): await complete main-thread admin callbacks --- .../StealthAdminCommandTest.java | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/test/java/games/dmg/spigotstealth/StealthAdminCommandTest.java b/src/test/java/games/dmg/spigotstealth/StealthAdminCommandTest.java index ee081da..a5ff43a 100644 --- a/src/test/java/games/dmg/spigotstealth/StealthAdminCommandTest.java +++ b/src/test/java/games/dmg/spigotstealth/StealthAdminCommandTest.java @@ -80,7 +80,10 @@ class StealthAdminCommandTest { fixture.command.onCommand( sender, command, "stealthadmin", new String[] {"sleepcount", "include"}); - verify(sender, org.mockito.Mockito.timeout(1000)).sendMessage(contains("now included")); + verify(sender, never()).sendMessage(contains("now included")); + assertEquals(List.of(), fixture.audit); + fixture.runMainThreadCallback(); + verify(sender).sendMessage(contains("now included")); assertEquals(SleepCountPolicy.INCLUDE, fixture.manager.snapshot().sleepCountPolicy()); verify(fixture.presentation).refreshSleepCountPolicy(); org.junit.jupiter.api.Assertions.assertTrue(fixture.audit.stream() @@ -125,7 +128,10 @@ class StealthAdminCommandTest { try (Fixture fixture = fixture(playerId)) { CommandSender sender = sender(true); fixture.command.onCommand(sender, mock(Command.class), "stealthadmin", new String[] {"grant", playerId.toString()}); - verify(sender, org.mockito.Mockito.timeout(1000)).sendMessage(contains("granted")); + verify(sender, never()).sendMessage(contains("granted")); + assertEquals(List.of(), fixture.audit); + fixture.runMainThreadCallback(); + verify(sender).sendMessage(contains("granted")); org.junit.jupiter.api.Assertions.assertTrue(fixture.manager.snapshot().player(playerId).unlocked()); org.junit.jupiter.api.Assertions.assertTrue(fixture.audit.stream().anyMatch(message -> message.contains("grant") && message.contains(playerId.toString()))); } @@ -167,16 +173,30 @@ class StealthAdminCommandTest { manager, progression, presentation, ignored -> { }, List::of); KnownPlayerResolver resolver = new KnownPlayerResolver(manager::snapshot, List::of); ArrayList audit = new ArrayList<>(); + var mainThread = new java.util.concurrent.LinkedBlockingQueue(); StealthAdminCommand command = new StealthAdminCommand( - administration, resolver, Runnable::run, audit::add, Duration.ofHours(8)); - return new Fixture(manager, command, presentation, audit); + administration, resolver, mainThread::add, audit::add, Duration.ofHours(8)); + return new Fixture(manager, command, presentation, audit, mainThread); } private record Fixture( StealthStateManager manager, StealthAdminCommand command, IdentityPresentation presentation, - ArrayList audit) implements AutoCloseable { + ArrayList audit, + java.util.concurrent.BlockingQueue mainThread) implements AutoCloseable { + void runMainThreadCallback() { + // A reply is emitted before the audit append. Await and run the whole scheduled callback, + // not just the reply invocation, and never read the ArrayList concurrently with the I/O thread. + try { + Runnable callback = mainThread.poll(5, java.util.concurrent.TimeUnit.SECONDS); + org.junit.jupiter.api.Assertions.assertNotNull(callback, "Expected a scheduled main-thread completion"); + callback.run(); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new AssertionError("Interrupted waiting for command completion", exception); + } + } @Override public void close() { manager.close(); } } }