Files
purpur-fantasy-stick/src/nativeTest/java/games/dmg/fantasystick/EquipmentAdminCommandNativeTest.java
T
dmg 1dcc7cfae8
Release / release (push) Successful in 10m2s
feat(training): add durable damage dummies
2026-09-21 15:48:52 -04:00

309 lines
25 KiB
Java

package games.dmg.fantasystick;
import static org.junit.jupiter.api.Assertions.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.bukkit.command.CommandSender;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/** Actual packaged PluginCommand permission/dispatch; only the absent console is a port. */
class EquipmentAdminCommandNativeTest {
@TempDir Path data;
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
@Test void registeredGivePersistsOneFreshSheetThenDeliversActivatesAndReloads() throws Exception {
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
f.plugin.setEnabled(true);
var target = new FantasyStickLifecycleNativeTest.Actor(data); f.players.put(target.core.id, target.player);
var before = target.core.snapshot(); var sender = new Sender();
assertTrue(f.command.execute(sender.port, "fantasystick", new String[]{"give", "Account"}));
assertTrue(sender.messages.isEmpty(), "Queued persistence is not successful delivery");
((games.dmg.fantasystick.persistence.CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters"))
.find(java.util.UUID.randomUUID()).get(5, java.util.concurrent.TimeUnit.SECONDS);
var callback = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(callback); callback.run(); callback.run();
assertTrue(sender.messages.getLast().contains("Issued one new character sheet"));
assertFalse(java.util.Arrays.equals(before, target.core.snapshot()));
var sheets = new games.dmg.fantasystick.items.CharacterSheets();
var delivered = java.util.stream.IntStream.range(0, 36).mapToObj(target.core.inventory::getItem)
.filter(item -> sheets.identify(item).isPresent()).filter(item -> !sheets.identify(item).orElseThrow().equals(target.core.record.id())).toList();
assertEquals(1, delivered.size(), "Repeated callback must not duplicate delivery");
assertEquals(target.core.record.id(),sheets.identify(target.core.inventory.getItemInOffHand()).orElseThrow());
assertTrue(((games.dmg.fantasystick.session.EquipmentSessions) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions")).active(target.core.id).isEmpty());
var id = sheets.identify(delivered.getFirst()).orElseThrow();
var storage = (games.dmg.fantasystick.persistence.CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
var record = storage.find(id).get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow();
assertEquals(java.math.BigInteger.valueOf(40), record.unspentPoints()); assertEquals(1, record.level());
assertEquals(java.math.BigInteger.ZERO, record.experience()); assertFalse(record.dead()); assertTrue(record.learned().isEmpty());
assertTrue(record.stats().values().stream().allMatch(java.math.BigInteger.ZERO::equals));assertEquals(new java.math.BigDecimal("10"),record.health());
assertEquals(java.util.Collections.nCopies(7,""),record.loadout());assertTrue(record.cooldowns().isEmpty());assertFalse(record.name().isBlank());
target.core.inventory.remove(delivered.getFirst()); target.core.inventory.setItemInOffHand(delivered.getFirst());
f.activate(target); assertEquals(id, ((games.dmg.fantasystick.session.EquipmentSessions) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "sessions")).active(target.core.id).orElseThrow().character());
f.plugin.setEnabled(false); f.plugin.setEnabled(true);
assertEquals(record, ((games.dmg.fantasystick.persistence.CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters")).find(id).get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow());
}
}
@Test void giveCompletionAndRejectedExecutionArePermissionAwareStrictAndReadOnly() throws Exception {
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
f.plugin.setEnabled(true); var target = new FantasyStickLifecycleNativeTest.Actor(data); f.players.put(target.core.id,target.player);
var sender=new Sender();
assertEquals(List.of("config","experience","give","inspect","party-registrar","recover","shop","training-dummy"),f.command.tabComplete(sender.port,"fantasystick",new String[]{""}));
assertEquals(List.of("give"),f.command.tabComplete(sender.port,"fantasystick",new String[]{"g"}));
assertEquals(List.of("Account"),f.command.tabComplete(sender.port,"fantasystick",new String[]{"give",""}));
assertEquals(List.of("Account"),f.command.tabComplete(sender.port,"fantasystick",new String[]{"give","Acc"}));
assertTrue(f.command.tabComplete(sender.port,"fantasystick",new String[]{"give","Account",""}).isEmpty());
sender.allowed=false;assertTrue(f.command.tabComplete(sender.port,"fantasystick",new String[]{"give",""}).isEmpty());
var original=target.core.snapshot();f.command.getExecutor().onCommand(sender.port,f.command,"fantasystick",new String[]{"give","Account"});
sender.allowed=true;for(String[] args:List.of(new String[]{"give"},new String[]{"give","Missing"},new String[]{"give","Account","extra"}))f.command.execute(sender.port,"fantasystick",args);
assertArrayEquals(original,target.core.snapshot());assertTrue(f.tasks.isEmpty());
assertEquals(1,java.nio.file.Files.list(data.resolve("characters")).filter(path->path.toString().endsWith(".yml")).count());
for(int slot=0;slot<36;slot++)target.core.inventory.setItem(slot,new org.bukkit.inventory.ItemStack(org.bukkit.Material.STONE));
var full=target.core.snapshot();f.command.execute(sender.port,"fantasystick",new String[]{"give","Account"});assertTrue(sender.messages.getLast().contains("no inventory space"));
assertArrayEquals(full,target.core.snapshot());assertTrue(f.tasks.isEmpty());assertEquals(1,java.nio.file.Files.list(data.resolve("characters")).filter(path->path.toString().endsWith(".yml")).count());
}
}
@Test void giveSaveFailureDisconnectSlotRaceAndLifecycleInterruptionNeverDuplicateOrOverwrite() throws Exception {
for(String route:List.of("save","disconnect","slot","close")){var dir=data.resolve(route);java.nio.file.Files.createDirectories(dir);try(var f=new FantasyStickLifecycleNativeTest.Fixture(dir)){
f.plugin.setEnabled(true);var target=new FantasyStickLifecycleNativeTest.Actor(dir);f.players.put(target.core.id,target.player);var sender=new Sender();var original=target.core.snapshot();
var storage=(games.dmg.fantasystick.persistence.CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");
var worker=(java.util.concurrent.ExecutorService)FantasyStickLifecycleNativeTest.Fixture.field(storage,"worker");var entered=new java.util.concurrent.CountDownLatch(1);var release=new java.util.concurrent.CountDownLatch(1);worker.execute(()->{entered.countDown();try{release.await(5,java.util.concurrent.TimeUnit.SECONDS);}catch(InterruptedException e){Thread.currentThread().interrupt();}});assertTrue(entered.await(5,java.util.concurrent.TimeUnit.SECONDS));
f.command.execute(sender.port,"fantasystick",new String[]{"give","Account"});
if(route.equals("save")){var issuer=FantasyStickLifecycleNativeTest.Fixture.field(f.command.getExecutor(),"characterIssuer");var pending=(java.util.Map<?,?>)FantasyStickLifecycleNativeTest.Fixture.field(issuer,"pending");var attempt=pending.values().iterator().next();var record=(games.dmg.fantasystick.persistence.CharacterRecord)FantasyStickLifecycleNativeTest.Fixture.field(attempt,"record");java.nio.file.Files.createDirectory(dir.resolve("characters").resolve(record.id()+".yml"));}
release.countDown();storage.find(java.util.UUID.randomUUID()).get(5,java.util.concurrent.TimeUnit.SECONDS);var callback=f.tasks.poll(5,java.util.concurrent.TimeUnit.SECONDS);assertNotNull(callback);
if(route.equals("disconnect"))target.core.online=false;if(route.equals("slot"))target.core.inventory.setItem(1,new org.bukkit.inventory.ItemStack(org.bukkit.Material.DIAMOND));if(route.equals("close"))f.plugin.setEnabled(false);callback.run();
if(!route.equals("close"))storage.find(java.util.UUID.randomUUID()).get(5,java.util.concurrent.TimeUnit.SECONDS);
assertFalse(sender.messages.stream().anyMatch(message->message.startsWith("Issued one")));assertEquals(route.equals("slot")?org.bukkit.Material.DIAMOND:original[1]==null?null:original[1].getType(),target.core.inventory.getItem(1)==null?null:target.core.inventory.getItem(1).getType());
if(!route.equals("save"))assertEquals(1,java.nio.file.Files.list(dir.resolve("characters")).filter(path->path.toString().endsWith(".yml")).count());
release.countDown();}}
}
@Test void recoveryCompletionIsPermissionAwarePrefixFilteredUuidOnlyAndReadOnly() throws Exception {
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
f.plugin.setEnabled(true);
var first = new FantasyStickLifecycleNativeTest.Actor(data);
var second = new FantasyStickLifecycleNativeTest.Actor(data);
var offline = new FantasyStickLifecycleNativeTest.Actor(data);
offline.core.online = false;
f.players.put(first.core.id, first.player); f.players.put(second.core.id, second.player); f.players.put(offline.core.id, offline.player);
var sender = new Sender();
var expected = java.util.stream.Stream.of(first.core.id.toString(), second.core.id.toString()).sorted().toList();
assertEquals(List.of("config", "experience", "give", "inspect", "party-registrar", "recover", "shop", "training-dummy"), f.command.tabComplete(sender.port, "fantasystick", new String[]{""}));
assertEquals(List.of("inspect"), f.command.tabComplete(sender.port, "fantasystick", new String[]{"in"}));
assertEquals(List.of("recover"), f.command.tabComplete(sender.port, "fantasystick", new String[]{"rec"}));
assertEquals(expected, f.command.tabComplete(sender.port, "fantasystick", new String[]{"inspect", ""}));
String prefix = expected.getFirst().substring(0, 8);
assertEquals(expected.stream().filter(id -> id.startsWith(prefix)).toList(),
f.command.tabComplete(sender.port, "fantasystick", new String[]{"recover", prefix}));
assertFalse(f.command.tabComplete(sender.port, "fantasystick", new String[]{"inspect", ""}).contains("Account"));
assertTrue(f.command.tabComplete(sender.port, "fantasystick", new String[]{"inspect", expected.getFirst(), ""}).isEmpty());
assertTrue(f.command.tabComplete(sender.port, "fantasystick", new String[]{"unknown", ""}).isEmpty());
assertEquals(List.of("adventure-mode"), f.command.tabComplete(sender.port, "fantasystick", new String[]{"config", "adventure-m"}));
assertEquals(List.of("false", "true"), f.command.tabComplete(sender.port, "fantasystick", new String[]{"config", "adventure-mode", ""}));
assertEquals(List.of("true"), f.command.tabComplete(sender.port, "fantasystick", new String[]{"config", "adventure-mode", "t"}));
assertTrue(f.command.tabComplete(sender.port, "fantasystick", new String[]{"config", "adventure-mode", "true", ""}).isEmpty());
sender.allowed = false;
for (String[] args : List.of(new String[]{""}, new String[]{"inspect", ""}, new String[]{"config", ""},
new String[]{"config", "adventure-mode", ""})) {
assertTrue(f.command.tabComplete(sender.port, "fantasystick", args).isEmpty());
}
assertTrue(f.tasks.isEmpty()); assertFalse(Files.exists(data.resolve("sessions")));
}
}
@Test void adventureConfigurationCommandReadsValidatesPersistsAndReopensWithoutTouchingPlayers() throws Exception {
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
f.plugin.setEnabled(true);
var sender = new Sender();
f.command.execute(sender.port, "fantasystick", new String[]{"config", "adventure-mode"});
assertTrue(sender.messages.getLast().contains("adventure-mode=true"));
sender.messages.clear();
for (String value : List.of("yes", "1", "TRUE", "")) {
f.command.execute(sender.port, "fantasystick", new String[]{"config", "adventure-mode", value});
assertTrue(sender.messages.getLast().contains("true|false"));
}
assertTrue(f.tasks.isEmpty());
assertFalse(Files.exists(data.resolve("config.yml")));
sender.allowed = false;
f.command.getExecutor().onCommand(sender.port, f.command, "fantasystick", new String[]{"config", "adventure-mode", "false"});
assertFalse(Files.exists(data.resolve("config.yml")));
sender.allowed = true; sender.messages.clear();
f.command.execute(sender.port, "fantasystick", new String[]{"config", "adventure-mode", "false"});
assertTrue(sender.messages.isEmpty(), "Do not report success before acknowledgment");
var reply = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(reply); reply.run();
assertTrue(sender.messages.getLast().contains("adventure-mode=false"));
assertTrue(sender.messages.getLast().contains("new sessions"));
assertEquals(0, f.playerLookups);
f.plugin.setEnabled(false); f.plugin.setEnabled(true);
f.command.execute(sender.port, "fantasystick", new String[]{"config", "adventure-mode"});
assertTrue(sender.messages.getLast().contains("adventure-mode=false"));
}
}
@Test void reenableWaitsOffThreadForAcceptedConfigurationWritesBeforeReadingNewPolicy() throws Exception {
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
f.plugin.setEnabled(true);
var old = FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "adventureSettings");
var worker = (java.util.concurrent.ExecutorService) FantasyStickLifecycleNativeTest.Fixture.field(old, "worker");
var entered = new java.util.concurrent.CountDownLatch(1);
var release = new java.util.concurrent.CountDownLatch(1);
worker.execute(() -> {
entered.countDown();
try { release.await(5, java.util.concurrent.TimeUnit.SECONDS); }
catch (InterruptedException error) { Thread.currentThread().interrupt(); }
});
try {
assertTrue(entered.await(5, java.util.concurrent.TimeUnit.SECONDS));
var sender = new Sender();
f.command.execute(sender.port, "fantasystick", new String[]{"config", "adventure-mode", "false"});
f.plugin.setEnabled(false); f.plugin.setEnabled(true);
assertNull(FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "sessions"), "Do not activate from a stale config snapshot");
release.countDown();
f.tick();
assertNotNull(FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "sessions"));
f.command.execute(sender.port, "fantasystick", new String[]{"config", "adventure-mode"});
assertTrue(sender.messages.getLast().contains("adventure-mode=false"));
} finally { release.countDown(); }
}
}
@Test void permissionAndStrictShapeRejectBeforePersistenceOrNativeLookup() throws Exception {
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
f.plugin.setEnabled(true);
assertNotNull(f.command, "Packaged descriptor must install the actual command");
var sender = new Sender();
sender.allowed = false;
assertTrue(f.command.execute(sender.port, "fantasystick", new String[]{"inspect", UUID.randomUUID().toString()}));
// Executor must also defend direct dispatch, not rely solely on PluginCommand.
assertTrue(f.command.getExecutor().onCommand(sender.port, f.command, "fantasystick",
new String[]{"recover", UUID.randomUUID().toString()}));
sender.allowed = true;
for (String[] args : List.of(new String[]{}, new String[]{"inspect"},
new String[]{"reset", UUID.randomUUID().toString()}, new String[]{"inspect", "1-1-1-1-1"},
new String[]{"recover", "not-a-uuid"}, new String[]{"inspect", UUID.randomUUID().toString(), "extra"})) {
assertTrue(f.command.execute(sender.port, "fantasystick", args));
}
assertFalse(Files.exists(data.resolve("sessions")));
assertTrue(f.tasks.isEmpty());
assertEquals(0, f.playerLookups, "Rejected commands must not inspect native players");
assertFalse(sender.messages.isEmpty());
}
assertFalse(Files.exists(data.resolve("sessions")), "No queued IO after worker drain either");
}
@Test void inspectIsDeferredSanitizedAndReturnsToMainThreadAndCloseDropsResponses() throws Exception {
try (var receipts = new games.dmg.fantasystick.persistence.EquipmentSessionStore(data, java.time.Clock.systemUTC());
var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
UUID owner = UUID.randomUUID(), session = UUID.randomUUID(), character = UUID.randomUUID();
receipts.prepare(owner, session, character).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS);
receipts.recordExit(owner, session, java.time.Duration.ofSeconds(900)).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS);
f.plugin.setEnabled(true);
var sender = new Sender();
assertNotNull(f.command);
assertTrue(f.command.execute(sender.port, "fantasystick", new String[]{"inspect", owner.toString()}));
assertTrue(sender.messages.isEmpty(), "No response until the server callback");
var response = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(response);
response.run();
String text = String.join("\n", sender.messages);
assertTrue(text.contains(owner.toString()), text);
assertTrue(text.contains(session.toString()), text);
assertTrue(text.contains(character.toString()), text);
assertTrue(text.contains("pending") && text.contains("deadline") && text.contains("exit"), text);
assertFalse(text.contains("schema-version") || text.contains(data.toString()), text);
sender.messages.clear();
f.command.execute(sender.port, "fantasystick", new String[]{"inspect", owner.toString()});
var stale = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(stale);
f.plugin.setEnabled(false);
f.plugin.setEnabled(true);
stale.run();
assertTrue(sender.messages.isEmpty(), "Previous lifetime must not reply after re-enable");
}
}
@Test void onlineRecoveryUsesNativeProofAndReportsOnlyARequestAndOfflineNeverQueuesIO() throws Exception {
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
f.plugin.setEnabled(true);
var sender = new Sender(); UUID offline = UUID.randomUUID();
f.command.execute(sender.port, "fantasystick", new String[]{"recover", offline.toString()});
assertTrue(sender.messages.getLast().contains("must be online"));
assertFalse(Files.exists(data.resolve("sessions"))); assertTrue(f.tasks.isEmpty());
var a = new FantasyStickLifecycleNativeTest.Actor(data); f.players.put(a.core.id, a.player);
var original = a.core.snapshot(); UUID session = UUID.randomUUID();
f.receipts().prepare(a.core.id, session, a.core.record.id()).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS);
assertEquals(games.dmg.fantasystick.items.EquipmentStorage.PackResult.PACKED,
new games.dmg.fantasystick.items.EquipmentStorage().pack(a.core.inventory, a.core.id, session));
a.core.marker(session, "PACKED"); var packed = a.core.snapshot();
sender.messages.clear();
f.command.execute(sender.port, "fantasystick", new String[]{"recover", a.core.id.toString()});
assertEquals(1, sender.messages.size());
assertTrue(sender.messages.getFirst().contains("requested") && sender.messages.getFirst().contains("not confirmation"));
assertArrayEquals(packed, a.core.snapshot(), "Request is not premature success");
f.tick();
assertArrayEquals(original, a.core.snapshot()); assertTrue(a.core.marker().endsWith("|RESTORED"));
assertNull(f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow().pending());
f.command.execute(sender.port, "fantasystick", new String[]{"recover", a.core.id.toString()}); f.tick();
assertArrayEquals(original, a.core.snapshot());
}
}
@Test void ambiguousRecoveryRetainsEveryItemAndReceiptAndInspectionNeverPrintsRawMarker() throws Exception {
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
f.plugin.setEnabled(true);
var a = new FantasyStickLifecycleNativeTest.Actor(data); f.players.put(a.core.id, a.player);
UUID session = UUID.randomUUID();
var saved = f.receipts().prepare(a.core.id, session, a.core.record.id()).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS);
new games.dmg.fantasystick.items.EquipmentStorage().pack(a.core.inventory, a.core.id, session);
a.core.pdc.set(new org.bukkit.NamespacedKey("fantasystick", "equipment_session"),
org.bukkit.persistence.PersistentDataType.STRING, "secret-not-a-valid-marker");
var before = a.core.snapshot(); var marker = a.core.pdc.serializeToBytes();
var sender = new Sender();
f.command.execute(sender.port, "fantasystick", new String[]{"recover", a.core.id.toString()}); f.tick();
assertArrayEquals(before, a.core.snapshot()); assertArrayEquals(marker, a.core.pdc.serializeToBytes());
assertEquals(saved, f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow());
f.command.execute(sender.port, "fantasystick", new String[]{"inspect", a.core.id.toString()}); f.tick();
var text = String.join("\n", sender.messages);
assertTrue(text.contains("malformed (retained)")); assertTrue(text.contains("active=none"));
assertFalse(text.contains("secret-not-a-valid-marker"));
}
}
@Test void inspectObservesCurrentOnlineActiveStateAndPermissionRevocationDropsDeferredResponse() throws Exception {
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
f.plugin.setEnabled(true);
var a = new FantasyStickLifecycleNativeTest.Actor(data); f.players.put(a.core.id, a.player); f.activate(a);
var sender = new Sender();
f.command.execute(sender.port, "fantasystick", new String[]{"inspect", a.core.id.toString()}); f.tick();
assertTrue(sender.messages.stream().anyMatch(message -> message.contains("active=session=")));
assertTrue(sender.messages.stream().anyMatch(message -> message.contains("phase=PACKED")));
sender.messages.clear();
f.command.execute(sender.port, "fantasystick", new String[]{"inspect", a.core.id.toString()});
var pending = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(pending);
sender.allowed = false; pending.run(); assertTrue(sender.messages.isEmpty());
sender.allowed = true;
f.command.execute(sender.port, "fantasystick", new String[]{"inspect", a.core.id.toString()});
pending = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(pending);
((AutoCloseable) f.command.getExecutor()).close(); pending.run(); assertTrue(sender.messages.isEmpty());
}
}
static final class Sender {
final Thread main = Thread.currentThread();
final List<String> messages = new ArrayList<>();
boolean allowed = true;
final CommandSender port = FantasyStickLifecycleNativeTest.port(CommandSender.class, (name, args) -> {
assertSame(main, Thread.currentThread(), "Worker accessed command sender: " + name);
return switch (name) {
case "hasPermission" -> { assertEquals("fantasystick.admin", args[0]); yield allowed; }
case "sendMessage" -> { messages.add(String.valueOf(args[0])); yield null; }
default -> throw new AssertionError("Unexpected sender call " + name);
};
});
}
}