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 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); } } }