Compare commits

...
1 Commits
Author SHA1 Message Date
dmg 539900eb14 fix(tests): synchronize health saves before reentry and transfer
Release / release (push) Successful in 8m26s
CI / build (push) Successful in 4m19s
2026-09-19 14:32:35 -04:00
3 changed files with 46 additions and 23 deletions

No files matched your search

@@ -166,7 +166,7 @@ class CharacterBookNativeTest {
f.players.put(a.core.id, a.player); f.players.put(b.core.id, b.player); f.activate(a);
click(f, a); drainReads(f); // Old holder has a queued result, not a delivered book.
a.core.inventory.setItemInOffHand(null); f.changed(a);
f.receipts().find(a.core.id).toCompletableFuture().get(5, TimeUnit.SECONDS);
f.awaitCharacterAndReceipt(a);
var renamed = characters(f).renameCharacter(a.core.record.id(), "Traded 雪 character").get(5, TimeUnit.SECONDS).orElseThrow();
runQueued(f); assertTrue(a.books.isEmpty());
b.core.inventory.setItemInOffHand(new games.dmg.fantasystick.items.CharacterSheets().create(renamed));
@@ -721,6 +721,7 @@ class FantasyStickLifecycleNativeTest {
assertNull(saved.pending()); assertNull(saved.lastExit());
} else {
f.activate(a); sessions.exit(a.player);
f.awaitCharacterAndReceipt(a);
assertEquals(org.bukkit.GameMode.ADVENTURE, a.player.getGameMode());
var saved = f.receipts().find(a.core.id).toCompletableFuture().get().orElseThrow();
assertNotNull(saved.pending()); assertNotNull(saved.lastExit());
@@ -790,7 +791,7 @@ class FantasyStickLifecycleNativeTest {
var sessions = (games.dmg.fantasystick.session.EquipmentSessions) Fixture.field(f.plugin, "sessions");
sessions.exit(a.player);
assertEquals(org.bukkit.GameMode.CREATIVE, a.player.getGameMode());
f.receipts().find(a.core.id).toCompletableFuture().get();
f.awaitCharacterAndReceipt(a);
var b = new Actor(data); f.players.put(b.core.id, b.player);
b.core.gameMode = org.bukkit.GameMode.SURVIVAL;
f.activate(b);
@@ -807,27 +808,43 @@ class FantasyStickLifecycleNativeTest {
if (!value.equals("default")) java.nio.file.Files.writeString(directory.resolve("config.yml"), "character-swap-cooldown-seconds: " + value + "\n");
try (var f = new Fixture(directory)) {
f.plugin.setEnabled(true); var a = new Actor(directory); f.players.put(a.core.id, a.player); f.activate(a);
f.dispatcher.accept(new org.bukkit.event.player.PlayerQuitEvent(a.player, (net.kyori.adventure.text.Component) null));
var exit = f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow().lastExit();
assertEquals(java.time.Duration.ofSeconds(value.equals("default") ? 900 : Long.parseLong(value)),
java.time.Duration.between(exit.exitedAt(), exit.deadline()));
assertEquals(a.core.record.id(), exit.characterId());
var other = new Actor(directory);
a.core.inventory.setItemInOffHand(other.core.inventory.getItemInOffHand().clone());
var before = a.core.snapshot();
f.dispatcher.accept(new org.bukkit.event.player.PlayerJoinEvent(a.player, (net.kyori.adventure.text.Component) null));
f.tick(); f.tick(); // Join receipt reconciliation, then actual coordinator eligibility.
if (value.equals("0")) {
f.tick(); assertTrue(a.core.marker().endsWith("|PACKED"), "Configured zero reaches entry, not only persisted arithmetic");
} else {
assertArrayEquals(before, a.core.snapshot()); assertTrue(a.core.marker().endsWith("|RESTORED"));
assertTrue(a.messages.getLast().matches("(?s).*Wait [1-9][0-9]* seconds.*same character may be resumed now.*"), a.messages::toString);
var state = f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow();
assertNull(state.pending()); assertEquals(exit, state.lastExit());
a.core.inventory.setItemInOffHand(new CharacterSheets().create(a.core.record));
var characters = (CharacterStorage) Fixture.field(f.plugin, "characters");
var worker = (java.util.concurrent.ExecutorService) Fixture.field(characters, "worker");
var entered = new java.util.concurrent.CountDownLatch(1);
var release = new java.util.concurrent.CountDownLatch(1);
worker.execute(() -> {
entered.countDown();
try { release.await(); }
catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); }
});
try {
assertTrue(entered.await(5, java.util.concurrent.TimeUnit.SECONDS));
f.dispatcher.accept(new org.bukkit.event.player.PlayerQuitEvent(a.player, (net.kyori.adventure.text.Component) null));
assertNotNull(f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow().pending(),
"Quit restores native state but must retain ownership until health persistence completes");
release.countDown();
f.awaitCharacterAndReceipt(a);
var exit = f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow().lastExit();
assertEquals(java.time.Duration.ofSeconds(value.equals("default") ? 900 : Long.parseLong(value)),
java.time.Duration.between(exit.exitedAt(), exit.deadline()));
assertEquals(a.core.record.id(), exit.characterId());
var other = new Actor(directory);
a.core.inventory.setItemInOffHand(other.core.inventory.getItemInOffHand().clone());
var before = a.core.snapshot();
f.dispatcher.accept(new org.bukkit.event.player.PlayerJoinEvent(a.player, (net.kyori.adventure.text.Component) null));
f.tick(); f.tick(); f.tick(); assertTrue(a.core.marker().endsWith("|PACKED"));
}
f.tick(); f.tick(); // Join receipt reconciliation, then actual coordinator eligibility.
if (value.equals("0")) {
f.tick(); assertTrue(a.core.marker().endsWith("|PACKED"), "Configured zero reaches entry, not only persisted arithmetic");
} else {
assertArrayEquals(before, a.core.snapshot()); assertTrue(a.core.marker().endsWith("|RESTORED"));
assertTrue(a.messages.getLast().matches("(?s).*Wait [1-9][0-9]* seconds.*same character may be resumed now.*"), a.messages::toString);
var state = f.receipts().find(a.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS).orElseThrow();
assertNull(state.pending()); assertEquals(exit, state.lastExit());
a.core.inventory.setItemInOffHand(new CharacterSheets().create(a.core.record));
f.dispatcher.accept(new org.bukkit.event.player.PlayerJoinEvent(a.player, (net.kyori.adventure.text.Component) null));
f.tick(); f.tick(); f.tick(); assertTrue(a.core.marker().endsWith("|PACKED"));
}
} finally { release.countDown(); }
}
}
}
@@ -1144,6 +1161,12 @@ class FantasyStickLifecycleNativeTest {
games.dmg.fantasystick.persistence.EquipmentSessionStore receipts() {
return (games.dmg.fantasystick.persistence.EquipmentSessionStore) field(plugin, "receipts");
}
void awaitCharacterAndReceipt(Actor actor) throws Exception {
// A receipt read alone cannot await work submitted later by the character-save callback.
((CharacterStorage) field(plugin, "characters")).find(actor.core.record.id())
.get(5, java.util.concurrent.TimeUnit.SECONDS);
receipts().find(actor.core.id).toCompletableFuture().get(5, java.util.concurrent.TimeUnit.SECONDS);
}
void tick() throws Exception {
var task = tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS);
assertNotNull(task, "Missing deferred main-thread callback"); task.run();
@@ -234,7 +234,7 @@ class PartyBookNativeTest {
open(f, a); var oldDisband = button(a, "[Disband party]");
var sheet = a.core.inventory.getItemInOffHand().clone();
a.core.inventory.setItemInOffHand(null); f.changed(a);
f.receipts().find(a.core.id).toCompletableFuture().get(5, TimeUnit.SECONDS);
f.awaitCharacterAndReceipt(a);
b.core.inventory.setItemInOffHand(sheet); f.activate(b);
open(f, b);
assertTrue(text(b).contains(a.core.record.name()));