test(revival): fence administrator audit export observations
This commit is contained in:
1 parent
6dc706b6ff
commit
f1b3fc5094
1 file changed
+141
-18
+141
-18
@@ -61,7 +61,6 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
assertEquals(recipient.core.id.toString(), value(issue.getFirst(), "target_player_id"));
|
||||
assertEquals(slot, issue.getFirst().getAsJsonObject("data").get("delivered_slot").getAsInt());
|
||||
assertEquals("native_inventory_acknowledged", value(issue.getFirst(), "delivery_outcome"));
|
||||
settle(f);
|
||||
var rows = actions(data);
|
||||
sequence(rows, kind, "give", "completed");
|
||||
linked(rows, value(issue.getFirst(), "issue_operation_id"));
|
||||
@@ -101,7 +100,6 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
assertEquals(after.id().toString(), value(grant, "target_character_id"));
|
||||
assertEquals(after.experience().toString(), value(grant, "after_xp"));
|
||||
assertEquals("250", value(grant, "requested_xp"));
|
||||
settle(f);
|
||||
var rows = actions(data);
|
||||
sequence(rows, kind, "experience", "completed");
|
||||
linked(rows, value(grant, "operation_id"));
|
||||
@@ -133,7 +131,6 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
assertEquals("pending", value(inspected.getFirst(), "receipt_phase"));
|
||||
assertEquals(pending.sessionId().toString(), value(inspected.getFirst(), "session_id"));
|
||||
assertEquals("packed", value(inspected.getFirst(), "native_phase"));
|
||||
settle(f);
|
||||
var rows = actions(data);
|
||||
sequence(rows, kind, "inspect", "completed");
|
||||
assertEquals(target.core.id.toString(), rows.getFirst().getAsJsonObject("data")
|
||||
@@ -159,7 +156,7 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
assertNotNull(retained.pending());
|
||||
assertEquals(session, retained.pending().sessionId());
|
||||
target.core.inventory.setItem(0, null);
|
||||
int prior = actions(data).size();
|
||||
int prior = actions(f, data).size();
|
||||
assertTrue(entry.map.dispatch(entry.sender, "fantasystick:fstickadmin recover " + target.core.id));
|
||||
ReskillEventsNativeTest.await(f, () -> {
|
||||
try { return f.receipts().find(target.core.id).toCompletableFuture().get(5, TimeUnit.SECONDS)
|
||||
@@ -179,7 +176,6 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
assertEquals("verified_box_and_marker", value(resolved.getFirst(), "custody_quality"));
|
||||
assertEquals(session.toString(), value(resolved.getFirst(), "session_id"));
|
||||
assertNotNull(value(resolved.getFirst(), "equipment_recovery_operation_id"));
|
||||
settle(f);
|
||||
var rows = actions(data).subList(prior, actions(data).size());
|
||||
sequence(rows, kind, "recover", "completed");
|
||||
linked(rows, value(resolved.getFirst(), "recovery_operation_id"));
|
||||
@@ -201,7 +197,7 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
var stored = storage(f).inspect(source.core.record.id()).get(5, TimeUnit.SECONDS).orElseThrow();
|
||||
var current = storage(f).inspect(target.core.record.id()).get(5, TimeUnit.SECONDS).orElseThrow();
|
||||
var checks = new ArrayList<org.junit.jupiter.api.function.Executable>();
|
||||
int prior = actions(data).size();
|
||||
int prior = actions(f, data).size();
|
||||
assertTrue(entry.map.dispatch(entry.sender, "fstickadmin character view CurrentHolder"));
|
||||
ReskillEventsNativeTest.await(f, () -> !fixture.actor.books.isEmpty());
|
||||
assertEquals(Material.WRITTEN_BOOK, fixture.actor.books.getLast().getType());
|
||||
@@ -242,7 +238,7 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
f.activate(target);
|
||||
var saved = storage(f).inspect(target.core.record.id()).get(5, TimeUnit.SECONDS).orElseThrow();
|
||||
var checks = new ArrayList<org.junit.jupiter.api.function.Executable>();
|
||||
int before = actions(data).size();
|
||||
int before = actions(f, data).size();
|
||||
assertTrue(entry.map.dispatch(entry.sender, "fstickadmin character find " + saved.name()));
|
||||
ReskillEventsNativeTest.await(f, () -> entry.messages().stream().anyMatch(m -> m.contains("Saved character results")));
|
||||
assertTrue(entry.messages().stream().anyMatch(m -> m.contains(saved.id().toString())));
|
||||
@@ -275,16 +271,13 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
int messages = fixture.actor.messages.size();
|
||||
assertTrue(entry.map.dispatch(entry.sender, "fstickadmin inspect " + target.core.id));
|
||||
fixture.actor.allowed = false;
|
||||
ReskillEventsNativeTest.await(f, () -> {
|
||||
try { return domain(data, "action.resolved").stream()
|
||||
.anyMatch(row -> "permission_changed".equals(value(row, "reason"))); }
|
||||
catch (Exception error) { throw new AssertionError(error); }
|
||||
});
|
||||
awaitExport(f, () -> domain(data, "action.resolved").stream()
|
||||
.anyMatch(row -> "permission_changed".equals(value(row, "reason"))));
|
||||
assertEquals(messages, fixture.actor.messages.size());
|
||||
assertArrayEquals(nativeBefore, target.core.snapshot());
|
||||
settle(f);
|
||||
assertTrue(domain(data, "recovery.inspected").isEmpty());
|
||||
assertEquals("permission_changed", value(domain(data, "action.resolved").getLast(), "reason"));
|
||||
settle(f);
|
||||
var rows = actions(data);
|
||||
sequence(rows, "player", "inspect", "refused");
|
||||
assertFalse(rows.getLast().getAsJsonObject("data").has("subject_id"));
|
||||
@@ -304,7 +297,7 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
"experience give NotExact 42", "inspect " + fixture.actor.core.id.toString().substring(0, 8),
|
||||
"recover bad-uuid", "character view NotExact", "character show " + saved.id().toString().substring(0, 8),
|
||||
"character show " + UUID.randomUUID())) {
|
||||
int offset = actions(data).size();
|
||||
int offset = actions(f, data).size();
|
||||
int feedback = fixture.actor.messages.size();
|
||||
int books = fixture.actor.books.size();
|
||||
assertTrue(entry.map.dispatch(entry.sender, "fstickadmin " + command));
|
||||
@@ -328,6 +321,111 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test void issuedAuditObservationPumpsPendingNativeDeliveryBeforeExport() throws Exception {
|
||||
try (var fixture = new QuestTrackingAuditNativeFixture(data)) {
|
||||
var f = fixture.f;
|
||||
var entry = new AdminActionEntryNativeTest.Entry(fixture, "command_block", true);
|
||||
var recipient = actor(f, data, "Recipient");
|
||||
int slot = recipient.core.inventory.firstEmpty();
|
||||
assertTrue(entry.map.dispatch(entry.sender, "fantasystick:fstickadmin give Recipient"));
|
||||
storage(f).find(UUID.randomUUID()).get(5, TimeUnit.SECONDS);
|
||||
assertNull(recipient.core.inventory.getItem(slot), "Worker allocation still needs its native delivery callback");
|
||||
assertTrue(observeExport(f, () -> domain(data, "character.issued").isEmpty(), TimeUnit.SECONDS.toNanos(5)),
|
||||
"No success export exists before native delivery");
|
||||
awaitDomain(f, data, "character.issued", 1);
|
||||
var sheets = new CharacterSheets();
|
||||
var item = recipient.core.inventory.getItem(slot);
|
||||
var issued = sheets.identify(item).orElseThrow();
|
||||
assertTrue(sheets.authorizes(item, storage(f).find(issued).get(5, TimeUnit.SECONDS).orElseThrow()));
|
||||
var issue = domain(data, "character.issued");
|
||||
assertEquals(1, issue.size());
|
||||
assertEquals(issued.toString(), value(issue.getFirst(), "character_id"));
|
||||
assertEquals(recipient.core.id.toString(), value(issue.getFirst(), "target_player_id"));
|
||||
sequence(actions(data), "command_block", "give", "completed");
|
||||
}
|
||||
}
|
||||
|
||||
@Test void issuedAuditObservationWaitsForOwnedInFlightAppendBeforeStrictParsing() throws Exception {
|
||||
try (var fixture = new QuestTrackingAuditNativeFixture(data)) {
|
||||
var f = fixture.f;
|
||||
var segment = issueAndSettle(fixture);
|
||||
var complete = java.nio.file.Files.readAllBytes(segment);
|
||||
var writer = (java.util.concurrent.ThreadPoolExecutor) FantasyStickLifecycleNativeTest.Fixture.field(
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "eventLog"), "worker");
|
||||
var partialVisible = new java.util.concurrent.CountDownLatch(1);
|
||||
var release = new java.util.concurrent.CountDownLatch(1);
|
||||
var returned = new java.util.concurrent.CountDownLatch(1);
|
||||
var premature = new java.util.concurrent.atomic.AtomicBoolean();
|
||||
// Replay visibility of the same genuine exported bytes, not a fabricated issue/result.
|
||||
// Only the fixture-owned writer is active; completed bytes/counts must be unchanged.
|
||||
var append = writer.submit(() -> {
|
||||
try {
|
||||
java.nio.file.Files.write(segment, java.util.Arrays.copyOf(complete, complete.length - 2));
|
||||
partialVisible.countDown();
|
||||
assertTrue(release.await(5, TimeUnit.SECONDS));
|
||||
} catch (Exception error) { throw new AssertionError(error); }
|
||||
finally {
|
||||
try { java.nio.file.Files.write(segment, complete); }
|
||||
catch (Exception error) { throw new AssertionError(error); }
|
||||
}
|
||||
});
|
||||
java.util.concurrent.CompletableFuture<Void> observer = null;
|
||||
try {
|
||||
assertTrue(partialVisible.await(5, TimeUnit.SECONDS));
|
||||
observer = java.util.concurrent.CompletableFuture.runAsync(() -> {
|
||||
try { premature.set(returned.await(200, TimeUnit.MILLISECONDS)); }
|
||||
catch (InterruptedException error) { Thread.currentThread().interrupt(); throw new AssertionError(error); }
|
||||
finally { release.countDown(); }
|
||||
});
|
||||
awaitDomain(f, data, "character.issued", 1);
|
||||
returned.countDown(); observer.get(5, TimeUnit.SECONDS);
|
||||
assertFalse(premature.get(), "Audit observation must wait for its owned append, not a visible prefix");
|
||||
} finally {
|
||||
release.countDown();
|
||||
try { if (observer != null) observer.get(5, TimeUnit.SECONDS); }
|
||||
finally { try { append.get(5, TimeUnit.SECONDS); } finally { settle(f); } }
|
||||
}
|
||||
assertArrayEquals(complete, java.nio.file.Files.readAllBytes(segment));
|
||||
assertEquals(1, domain(data, "character.issued").size());
|
||||
sequence(actions(data), "command_block", "give", "completed");
|
||||
}
|
||||
}
|
||||
|
||||
@Test void completedMalformedIssuedAuditStillFailsStrictVerification() throws Exception {
|
||||
try (var fixture = new QuestTrackingAuditNativeFixture(data)) {
|
||||
var f = fixture.f;
|
||||
var segment = issueAndSettle(fixture);
|
||||
var complete = java.nio.file.Files.readAllBytes(segment);
|
||||
try {
|
||||
java.nio.file.Files.write(segment, java.util.Arrays.copyOf(complete, complete.length - 2));
|
||||
assertTrue(FixtureAuditAcknowledgmentNativeTest.snapshot(f).idle());
|
||||
var failure = assertThrows(AssertionError.class, () -> awaitDomain(f, data, "character.issued", 1));
|
||||
assertInstanceOf(com.google.gson.JsonSyntaxException.class, failure.getCause(),
|
||||
"Completed malformed export must not be skipped, swallowed or accepted as success");
|
||||
} finally { java.nio.file.Files.write(segment, complete); }
|
||||
assertEquals(1, domain(data, "character.issued").size());
|
||||
sequence(actions(data), "command_block", "give", "completed");
|
||||
}
|
||||
}
|
||||
|
||||
private Path issueAndSettle(QuestTrackingAuditNativeFixture fixture) throws Exception {
|
||||
var f = fixture.f;
|
||||
var entry = new AdminActionEntryNativeTest.Entry(fixture, "command_block", true);
|
||||
var recipient = actor(f, data, "Recipient");
|
||||
int slot = recipient.core.inventory.firstEmpty();
|
||||
assertTrue(entry.map.dispatch(entry.sender, "fantasystick:fstickadmin give Recipient"));
|
||||
storage(f).find(UUID.randomUUID()).get(5, TimeUnit.SECONDS);
|
||||
ReskillEventsNativeTest.await(f, () -> recipient.core.inventory.getItem(slot) != null);
|
||||
assertTrue(new CharacterSheets().authorizes(recipient.core.inventory.getItem(slot),
|
||||
storage(f).find(new CharacterSheets().identify(recipient.core.inventory.getItem(slot)).orElseThrow())
|
||||
.get(5, TimeUnit.SECONDS).orElseThrow()));
|
||||
settle(f);
|
||||
assertEquals(1, domain(data, "character.issued").size());
|
||||
sequence(actions(data), "command_block", "give", "completed");
|
||||
return (Path) FantasyStickLifecycleNativeTest.Fixture.field(
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "eventLog"), "segment");
|
||||
}
|
||||
|
||||
private static FantasyStickLifecycleNativeTest.Actor actor(FantasyStickLifecycleNativeTest.Fixture f, Path root, String name) throws Exception {
|
||||
var actor = new FantasyStickLifecycleNativeTest.Actor(root);
|
||||
actor.accountName = name;
|
||||
@@ -341,15 +439,40 @@ class AdminActionIdentityAndEquipmentNativeTest {
|
||||
return (EquipmentSessions) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "sessions");
|
||||
}
|
||||
private static List<JsonObject> actions(Path root) throws Exception { return AdminActionEntryNativeTest.rows(root); }
|
||||
private static List<JsonObject> actions(FantasyStickLifecycleNativeTest.Fixture f, Path root) throws Exception {
|
||||
return observeExport(f, () -> actions(root), TimeUnit.SECONDS.toNanos(5));
|
||||
}
|
||||
private static List<JsonObject> domain(Path root, String suffix) throws Exception {
|
||||
return ReskillEventsNativeTest.records(root).stream().filter(row -> row.get("type").getAsString()
|
||||
.equals("games.dmg.minecraft.fantasystick." + suffix)).toList();
|
||||
}
|
||||
private static void awaitDomain(FantasyStickLifecycleNativeTest.Fixture f, Path root, String suffix, int count) throws Exception {
|
||||
ReskillEventsNativeTest.await(f, () -> {
|
||||
try { return domain(root, suffix).size() >= count; }
|
||||
catch (Exception error) { throw new AssertionError(error); }
|
||||
});
|
||||
awaitExport(f, () -> domain(root, suffix).size() >= count);
|
||||
}
|
||||
private static <T> T observeExport(FantasyStickLifecycleNativeTest.Fixture f,
|
||||
java.util.concurrent.Callable<T> read, long remaining) throws Exception {
|
||||
var writer = (java.util.concurrent.ThreadPoolExecutor) FantasyStickLifecycleNativeTest.Fixture.field(
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "eventLog"), "worker");
|
||||
try { return writer.submit(read).get(remaining, TimeUnit.NANOSECONDS); }
|
||||
catch (java.util.concurrent.ExecutionException error) { throw new AssertionError(error.getCause()); }
|
||||
}
|
||||
private static void awaitExport(FantasyStickLifecycleNativeTest.Fixture f,
|
||||
java.util.concurrent.Callable<Boolean> observed) throws Exception {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
while (true) {
|
||||
long remaining = deadline - System.nanoTime();
|
||||
assertTrue(remaining > 0, "Expected audit export did not arrive within its existing five-second bound");
|
||||
// Strict file I/O shares the actual owned serial writer: never parse mid-append.
|
||||
// Missing rows may await native callbacks; malformed completed rows still fail.
|
||||
if (observeExport(f, observed, remaining)) {
|
||||
settle(f); // Relocated existing acknowledgment fence before subsequent strict assertions.
|
||||
return;
|
||||
}
|
||||
remaining = deadline - System.nanoTime();
|
||||
assertTrue(remaining > 0, "Expected audit export did not arrive within its existing five-second bound");
|
||||
var task = f.tasks.poll(Math.min(remaining, TimeUnit.MILLISECONDS.toNanos(50)), TimeUnit.NANOSECONDS);
|
||||
if (task != null) task.run();
|
||||
}
|
||||
}
|
||||
private static void settle(FantasyStickLifecycleNativeTest.Fixture f) throws Exception {
|
||||
FixtureAuditAcknowledgmentNativeTest.awaitOwnQuiescence(f);
|
||||
|
||||
Reference in new issue
Block a user