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