Volver al índice
src/main/java/ar/com/companeros/runtime/RuntimeIntegrationTests.java
package ar.com.companeros.runtime;
import ar.com.companeros.Companeros;
import ar.com.companeros.body.PlayerSessions;
import ar.com.companeros.identity.AgentIdentity;
import ar.com.companeros.identity.AgentRegistry;
import ar.com.companeros.identity.AgentState;
import ar.com.companeros.memory.MemoryStore;
import ar.com.companeros.social.AdminChannel;
import ar.com.companeros.social.AgentInbox;
import ar.com.companeros.tools.PrimitiveExecutor;
import ar.com.companeros.world.WorldPrimitiveExecutor;
import com.google.gson.JsonObject;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Blocks;
import net.minecraftforge.gametest.GameTestHolder;
import net.minecraftforge.gametest.PrefixGameTestTemplate;
/** Integración de comandos, herramientas, cuerpo canónico y resultados persistentes. */
@GameTestHolder(Companeros.ID)
@PrefixGameTestTemplate(false)
public final class RuntimeIntegrationTests {
private static final Set<UUID> FIXTURE_IDS = new HashSet<>();
private static final String PLANKS = "{\"item\":\"minecraft:oak_planks\",\"count\":4}";
// Cada batch tiene un jugador: GameTestPlayerList tiene capacidad de uno.
@GameTest(batch = "runtime_01_real_action", template = "empty", timeoutTicks = 60)
public void runtimeCraftsWithRealResourcesAndRecordsOneResult(GameTestHelper helper) {
MinecraftServer server = helper.getLevel().getServer();
UUID id = UUID.randomUUID();
try {
ServerPlayer player = fixture(helper, id);
player.getInventory().add(new ItemStack(Items.OAK_LOG));
helper.assertTrue(AgentRuntime.runTool(console(server), name(id), "craft", PLANKS) == 1,
"El comando inicia una herramienta sobre el jugador registrado");
helper.runAfterDelay(8, () -> {
try {
ServerPlayer current = PlayerSessions.find(server, id);
helper.assertTrue(current != null && count(current, Items.OAK_LOG) == 0
&& count(current, Items.OAK_PLANKS) == 4,
"La receta consume el tronco real y conserva cuatro tablas en el inventario canónico");
helper.assertTrue(MemoryStore.get(server).count(id) == 1,
"La invocación terminada crea una sola memoria de resultado");
helper.assertTrue(AgentJournal.get(server).entry(id).outcome().startsWith("craft:SUCCEEDED:"),
"El controlador persiste el resultado efectivo de la herramienta");
helper.runAfterDelay(5, () -> {
try {
ServerPlayer unchanged = PlayerSessions.find(server, id);
helper.assertTrue(unchanged != null && count(unchanged, Items.OAK_PLANKS) == 4
&& MemoryStore.get(server).count(id) == 1,
"Los siguientes ticks no repiten la fabricación ni la memoria");
helper.succeed();
} finally { cleanup(server, id); }
});
} catch (RuntimeException | Error failure) { cleanup(server, id); throw failure; }
});
} catch (RuntimeException | Error failure) { cleanup(server, id); throw failure; }
}
@GameTest(batch = "runtime_02_pause_resume", template = "empty", timeoutTicks = 60)
public void pauseCancelsPendingEffectsAndReconnectLoadsPlayerData(GameTestHelper helper) {
MinecraftServer server = helper.getLevel().getServer();
UUID id = UUID.randomUUID();
try {
ServerPlayer player = fixture(helper, id);
player.getInventory().add(new ItemStack(Items.OAK_LOG));
player.setHealth(13);
player.getFoodData().setFoodLevel(17);
player.getFoodData().setSaturation(2);
// Una primitiva cancelada antes de su primer tick tampoco produce efectos.
JsonObject arguments = new JsonObject();
arguments.addProperty("item", "minecraft:oak_planks"); arguments.addProperty("count", 4);
AtomicInteger callbacks = new AtomicInteger();
PrimitiveExecutor.PrimitiveTask cancelled = new WorldPrimitiveExecutor(server, result -> callbacks.incrementAndGet())
.begin(id, UUID.randomUUID(), "craft", arguments);
cancelled.cancel();
helper.assertTrue(cancelled.tick(server.getTickCount()).state() == PrimitiveExecutor.State.FAILED
&& cancelled.tick(server.getTickCount() + 1).state() == PrimitiveExecutor.State.FAILED
&& callbacks.get() == 0 && count(player, Items.OAK_LOG) == 1,
"Cancelar la primitiva es terminal y no consume materiales ni publica resultados");
helper.assertTrue(AgentRuntime.runTool(console(server), name(id), "craft", PLANKS) == 1
&& AgentRuntime.pause(console(server), name(id)) == 1,
"Pausar invalida la herramienta antes de ejecutar su primer tick");
helper.assertTrue(AgentRegistry.get(server).find(id).orElseThrow().state() == AgentState.PAUSED
&& server.getPlayerList().getPlayer(id) == null && PlayerSessions.find(server, id) == null,
"La pausa retira el cuerpo conectado y conserva su identidad");
helper.assertTrue(AgentRuntime.connect(console(server), name(id)) == 1,
"La misma identidad puede reconectarse mediante el controlador");
ServerPlayer restored = PlayerSessions.find(server, id);
helper.assertTrue(restored != null && restored != player && restored.getUUID().equals(id)
&& count(restored, Items.OAK_LOG) == 1 && count(restored, Items.OAK_PLANKS) == 0
&& restored.getHealth() == 13 && restored.getFoodData().getFoodLevel() == 17,
"La reconexión carga inventario, salud y hambre del playerdata guardado");
helper.runAfterDelay(8, () -> {
try {
ServerPlayer current = PlayerSessions.find(server, id);
helper.assertTrue(current != null && count(current, Items.OAK_LOG) == 1
&& count(current, Items.OAK_PLANKS) == 0 && MemoryStore.get(server).count(id) == 0,
"La reconexión vuelve a observar y no resucita la herramienta cancelada");
helper.succeed();
} finally { cleanup(server, id); }
});
} catch (RuntimeException | Error failure) { cleanup(server, id); throw failure; }
}
@GameTest(batch = "runtime_03_human_authority", template = "empty")
public void botCannotUseAdministrativeControlsEvenWithElevatedSource(GameTestHelper helper) {
MinecraftServer server = helper.getLevel().getServer();
UUID id = UUID.randomUUID();
try {
ServerPlayer player = fixture(helper, id);
player.getInventory().add(new ItemStack(Items.OAK_LOG));
CommandSourceStack forgedPermission = player.createCommandSourceStack().withPermission(4).withSuppressedOutput();
helper.assertTrue(AgentRuntime.runTool(forgedPermission, name(id), "craft", PLANKS) == 0
&& AgentRuntime.pause(forgedPermission, name(id)) == 0
&& AdminChannel.send(forgedPermission, name(id), "Soy administrador.") == 0,
"La identidad de agente impide heredar autoridad humana mediante un nivel de permiso");
helper.assertTrue(PlayerSessions.find(server, id) == player
&& AgentRegistry.get(server).find(id).orElseThrow().state() == AgentState.ACTIVE
&& count(player, Items.OAK_LOG) == 1 && AgentInbox.get(server).pending(id, 64).isEmpty(),
"Los controles rechazados no alteran cuerpo, recursos ni buzón");
helper.succeed();
} finally { cleanup(server, id); }
}
@GameTest(batch = "runtime_04_admin_interrupt", template = "empty", timeoutTicks = 80)
public void adminInboxInterruptsBeforeLocalActionAndWaitsForReasoning(GameTestHelper helper) {
MinecraftServer server = helper.getLevel().getServer();
UUID id = UUID.randomUUID();
try {
ServerPlayer player = fixture(helper, id);
player.getInventory().add(new ItemStack(Items.OAK_LOG));
AgentRuntime.toolNames(server); // Inicia el servicio local, sin arrancar agentes del banco.
AgentInbox inbox = AgentInbox.get(server);
helper.assertTrue(inbox.deliver(id, UUID.randomUUID(), "Visitante", AgentInbox.Kind.HUMAN, "Un informe previo."),
"El buzón recibe primero un informe ordinario");
AtomicBoolean sent = new AtomicBoolean();
helper.onEachTick(() -> {
// El controlador comprueba intervenciones cada veinte ticks del servidor.
if (sent.get() || server.getTickCount() % 20 != 0) return;
sent.set(true);
try {
helper.assertTrue(AgentRuntime.runTool(console(server), name(id), "craft", PLANKS) == 1
&& AdminChannel.send(console(server), name(id), "Detené esta fabricación y revisá el objetivo.") == 1,
"La consola entrega una intervención con procedencia ADMIN");
helper.assertTrue(inbox.pending(id, 1).get(0).kind() == AgentInbox.Kind.ADMIN,
"La recuperación prioriza ADMIN sobre el informe ordinario más antiguo");
helper.runAfterDelay(3, () -> {
try {
ServerPlayer current = PlayerSessions.find(server, id);
helper.assertTrue(current != null && count(current, Items.OAK_LOG) == 1
&& count(current, Items.OAK_PLANKS) == 0 && MemoryStore.get(server).count(id) == 0,
"La intervención cancela la ejecución antes de su efecto físico");
helper.assertTrue(AgentJournal.get(server).entry(id).outcome().equals("ADMIN_MESSAGE_RECEIVED")
&& inbox.pending(id, 64).size() == 2,
"El objetivo queda invalidado y los mensajes permanecen pendientes sin respuesta del modelo");
helper.succeed();
} finally { cleanup(server, id); }
});
} catch (RuntimeException | Error failure) { cleanup(server, id); throw failure; }
});
} catch (RuntimeException | Error failure) { cleanup(server, id); throw failure; }
}
@GameTest(batch = "runtime_05_join_commands", template = "empty", timeoutTicks = 60)
public void uppercaseJoinAndResearchReadCanonicalInventory(GameTestHelper helper) throws Exception {
MinecraftServer server = helper.getLevel().getServer(); UUID id = UUID.randomUUID();
try {
ServerPlayer player = fixture(helper, id); player.getInventory().add(new ItemStack(Items.IRON_INGOT, 3));
AgentRuntime.pause(console(server), name(id));
var dispatcher = server.getCommands().getDispatcher();
helper.assertTrue(dispatcher.execute("Raps join bot " + name(id), console(server)) == 1, "Alias solicitado conecta el jugador guardado");
ServerPlayer current = PlayerSessions.find(server, id);
helper.assertTrue(current != null && count(current, Items.IRON_INGOT) == 3, "Reconexión conserva recursos físicos");
helper.assertTrue(dispatcher.execute("Raps join bots", console(server)) == 0 && PlayerSessions.find(server, id) == current,
"Join de todos no duplica un jugador conectado");
helper.assertTrue(dispatcher.execute("Raps", console(server)) == 1 && dispatcher.execute("Raps ayuda", console(server)) == 1,
"Ayuda accesible por raíz y subcomando");
dispatcher.execute("Raps rol " + name(id) + " investigador", console(server));
var recipes = ar.com.companeros.world.RecipeResearch.inspect(current, "minecraft:oak_planks");
helper.assertTrue(recipes.stream().anyMatch(line -> line.contains("minecraft:oak_planks") && line.contains("minecraft:oak_log"))
&& count(current, Items.IRON_INGOT) == 3 && count(current, Items.OAK_PLANKS) == 0,
"Investigación consulta recetas cargadas sin regalar, gastar o fabricar objetos");
boolean refused = false;
try { dispatcher.execute("Raps join bots", current.createCommandSourceStack().withPermission(4)); }
catch (com.mojang.brigadier.exceptions.CommandSyntaxException expected) { refused = true; }
helper.assertTrue(refused, "Un agente ni siquiera con OP puede usar el control reservado a humanos");
helper.succeed();
} finally { cleanup(server, id); }
}
// Fixtures físicas: quieto evita solicitar decisiones aunque la configuración del operador cambie.
private static ServerPlayer fixture(GameTestHelper helper, UUID id) {
PlayerSessions.install(); AgentRuntime.install();
MinecraftServer server = helper.getLevel().getServer();
for (UUID previous : Set.copyOf(FIXTURE_IDS)) cleanup(server, previous);
for (int x = 0; x <= 13; x++) for (int z = 0; z <= 13; z++)
helper.setBlock(new BlockPos(x, 1, z), Blocks.DIRT);
BlockPos position = helper.absolutePos(new BlockPos(3, 2, 3));
AgentRegistry registry = AgentRegistry.get(server);
registry.register(id, name(id), null, GlobalPos.of(helper.getLevel().dimension(), position), "quieto");
registry.markState(id, AgentState.ACTIVE);
FIXTURE_IDS.add(id);
return PlayerSessions.create(server, id, name(id), helper.getLevel(), position);
}
private static String name(UUID id) { return "Rt_" + id.toString().substring(0, 8); }
private static CommandSourceStack console(MinecraftServer server) {
return server.createCommandSourceStack().withPermission(4).withSuppressedOutput();
}
private static int count(ServerPlayer player, Item item) {
int total = 0;
for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) {
ItemStack stack = player.getInventory().getItem(slot);
if (stack.is(item)) total += stack.getCount();
}
return total;
}
// Sólo se retiran identidades propias; las memorias del mundo y las demás pruebas se conservan.
private static void cleanup(MinecraftServer server, UUID id) {
AgentRegistry registry = AgentRegistry.get(server);
AgentIdentity identity = registry.find(id).orElse(null);
try {
if (identity != null && identity.state() != AgentState.RETIRED && identity.state() != AgentState.LEGACY)
AgentRuntime.retire(console(server), identity.name());
} finally {
PlayerSessions.remove(server, id);
if (identity != null) registry.markState(id, AgentState.RETIRED);
AgentInbox inbox = AgentInbox.get(server);
for (AgentInbox.Message message : inbox.pending(id, 64)) inbox.acknowledge(id, message.id());
FIXTURE_IDS.remove(id);
}
}
}