Volver al índice

src/main/java/ar/com/companeros/AgentFoundationTests.java

package ar.com.companeros;

import ar.com.companeros.action.ActionRequest;
import ar.com.companeros.action.ToolResult;
import ar.com.companeros.identity.AgentRegistry;
import ar.com.companeros.identity.AgentState;
import ar.com.companeros.memory.MemoryOrigin;
import ar.com.companeros.memory.MemoryQuery;
import ar.com.companeros.memory.MemoryRecord;
import ar.com.companeros.memory.MemoryStore;
import ar.com.companeros.memory.MemoryType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
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.nbt.CompoundTag;
import net.minecraft.world.level.Level;
import net.minecraftforge.gametest.GameTestHolder;
import net.minecraftforge.gametest.PrefixGameTestTemplate;

/** Pruebas de persistencia y aislamiento de los contratos nuevos, sin consultar modelos. */
@GameTestHolder(Companeros.ID)
@PrefixGameTestTemplate(false)
public final class AgentFoundationTests {
    // Identidad: cambiar cuerpo/dimensión/estado no crea una identidad diferente.
    @GameTest(template = "empty")
    public void globalIdentityRetainsLifecycleAndSettings(GameTestHelper helper) {
        AgentRegistry registry = new AgentRegistry();
        UUID agent = UUID.randomUUID(), owner = UUID.randomUUID();
        registry.register(agent, "Neon", owner, GlobalPos.of(Level.OVERWORLD, new BlockPos(3, 64, 3)), "quieto");
        registry.markState(agent, AgentState.ACTIVE);
        registry.updateSettings(agent, GlobalPos.of(Level.NETHER, new BlockPos(40, 80, -12)), "autonomo");
        AgentRegistry restored = AgentRegistry.load(registry.save(new CompoundTag()));
        var identity = restored.find(agent).orElseThrow();
        helper.assertTrue(restored.worldId().equals(registry.worldId()) && restored.entries().size() == 1
            && restored.isAgent(agent) && !restored.isAgent(owner), "UUID global y mundo permanecen independientes de dimensión");
        helper.assertTrue(identity.autoLogin() && identity.legacyOwner().equals(owner)
            && identity.camp().dimension().equals(Level.NETHER) && identity.mode().equals("autonomo"),
            "Carga estado, configuración, dueño legado y campamento en otra dimensión");
        restored.markState(agent, AgentState.DEAD);
        helper.assertTrue(restored.isAgent(agent) && !restored.find(agent).orElseThrow().autoLogin(),
            "Una muerte conserva identidad sin fingir una sesión activa");
        restored.markState(agent, AgentState.RETIRED);
        helper.assertTrue(restored.isAgent(agent), "Retirar no convierte el UUID conocido en humano");
        helper.succeed();
    }

    @GameTest(template = "empty")
    public void duplicateNameAndFutureSchemasAreRejected(GameTestHelper helper) {
        AgentRegistry registry = new AgentRegistry();
        registry.register(UUID.randomUUID(), "Argon", null, null, "quieto");
        helper.assertTrue(rejected(() -> registry.register(UUID.randomUUID(), "ARGON", null, null, "quieto")),
            "Dos identidades activas no comparten el mismo nombre de jugador");
        CompoundTag future = registry.save(new CompoundTag()); future.putInt("SchemaVersion", 999);
        helper.assertTrue(rejected(() -> AgentRegistry.load(future)), "Un formato futuro no se resetea ni se pisa en silencio");
        helper.succeed();
    }

    // Memoria: recuperación selectiva, contradicción, procedencia y persistencia real del formato.
    @GameTest(template = "empty")
    public void oldImportantMemoryIsRetrievedWithoutLeakingOthers(GameTestHelper helper) {
        UUID world = UUID.randomUUID(), agent = UUID.randomUUID(), stranger = UUID.randomUUID();
        MemoryStore store = new MemoryStore(world);
        MemoryRecord mine = memory(world, agent, UUID.randomUUID(), "Mina de hierro observada", .95, 1,
            10, MemoryOrigin.OBSERVED, null, Set.of("hierro", "mina"));
        store.record(mine);
        store.record(memory(world, agent, UUID.randomUUID(), "Vi una flor", .1, 1,
            10_000_000, MemoryOrigin.OBSERVED, null, Set.of("flor")));
        store.record(memory(world, stranger, UUID.randomUUID(), "Mi mina privada de hierro", 1, 1,
            10_000_000, MemoryOrigin.OBSERVED, null, Set.of("hierro")));
        MemoryStore restored = MemoryStore.load(store.save(new CompoundTag()));
        var selected = restored.query(agent, new MemoryQuery("hierro", Set.of(), null, null, 1, 6000, 10_000_000, false));
        helper.assertTrue(selected.size() == 1 && selected.get(0).id().equals(mine.id())
            && selected.get(0).agentId().equals(agent), "Recupera el lugar importante antiguo y jamás la memoria privada ajena");
        helper.assertTrue(selected.get(0).worldId().equals(world) && selected.get(0).location().dimension().equals(Level.OVERWORLD),
            "El recuerdo preserva mundo y dimensión");
        helper.succeed();
    }

    @GameTest(template = "empty")
    public void verifiedContradictionPreservesHistoryAndSource(GameTestHelper helper) {
        UUID world = UUID.randomUUID(), agent = UUID.randomUUID();
        MemoryStore store = new MemoryStore(world);
        MemoryRecord reported = memory(world, agent, UUID.randomUUID(), "Dicen que la mina tiene hierro", .9, .35,
            20, MemoryOrigin.REPORTED, null, Set.of("mina"));
        store.record(reported);
        store.updateVerification(reported.id(), .1, 300, 5000);
        MemoryRecord depleted = memory(world, agent, UUID.randomUUID(), "Mina inspeccionada y agotada", .9, 1,
            300, MemoryOrigin.OBSERVED, reported.id(), Set.of("mina"));
        store.record(depleted);
        MemoryStore restored = MemoryStore.load(store.save(new CompoundTag()));
        var selected = restored.query(agent, MemoryQuery.relevant("mina", null, 300));
        var original = restored.find(reported.id()).orElseThrow();
        helper.assertTrue(selected.size() == 1 && selected.get(0).id().equals(depleted.id()),
            "La recuperación corriente evita dirigir al bot hacia el informe reemplazado");
        helper.assertTrue(original.origin() == MemoryOrigin.REPORTED && original.confidence() == .1
            && original.lastVerifiedAtUtc() == 5000 && original.lastVerifiedGameTime() == 300
            && restored.count(agent) == 2, "Conserva informe, confianza, verificación e historia sin falsear procedencia");
        helper.succeed();
    }

    @GameTest(template = "empty")
    public void fullMemoryNeverEvictsProtectedExperience(GameTestHelper helper) {
        UUID world = UUID.randomUUID(), agent = UUID.randomUUID(), first = UUID.randomUUID();
        MemoryStore store = new MemoryStore(world);
        for (int i = 0; i < MemoryStore.MAX_PER_AGENT; i++) store.record(memory(world, agent,
            i == 0 ? first : UUID.randomUUID(), "Experiencia importante " + i, .9, 1, i,
            MemoryOrigin.OBSERVED, null, Set.of("experiencia")));
        boolean accepted = store.record(memory(world, agent, UUID.randomUUID(), "Nuevo informe menor", .1, .2,
            1_000_000, MemoryOrigin.REPORTED, null, Set.of("rumor")));
        MemoryStore restored = MemoryStore.load(store.save(new CompoundTag()));
        helper.assertTrue(!accepted && restored.count(agent) == MemoryStore.MAX_PER_AGENT
            && restored.find(first).isPresent() && restored.refusedRecords() == 1,
            "La cuota rechaza exceso y conserva acontecimientos importantes aunque sean antiguos");
        helper.succeed();
    }

    @GameTest(template = "empty")
    public void memoryEvictsMinorRecordsAndHonorsQueryBudget(GameTestHelper helper) {
        UUID world = UUID.randomUUID(), agent = UUID.randomUUID(), minor = UUID.randomUUID();
        MemoryStore store = new MemoryStore(world);
        store.record(memory(world, agent, minor, "Recuerdo menor", .01, .2, 1, MemoryOrigin.REPORTED, null, Set.of("menor")));
        for (int i = 1; i < MemoryStore.MAX_PER_AGENT; i++) store.record(memory(world, agent,
            UUID.randomUUID(), "Lugar importante " + i, .9, 1, i, MemoryOrigin.OBSERVED, null, Set.of("lugar")));
        helper.assertTrue(store.record(memory(world, agent, UUID.randomUUID(), "Nuevo lugar importante", .9, 1,
            1_000_000, MemoryOrigin.OBSERVED, null, Set.of("lugar"))) && store.find(minor).isEmpty()
            && store.evictedRecords() == 1 && store.count(agent) == MemoryStore.MAX_PER_AGENT,
            "Recicla capacidad menor sin borrar hechos importantes");
        helper.assertTrue(store.query(agent, new MemoryQuery("lugar", Set.of(), null, null, 32, 128,
            1_000_000, false)).isEmpty(), "Una consulta pequeña no envía todo el historial por exceder su presupuesto");
        helper.succeed();
    }

    // Acción y aprendizaje: el estado de bloqueo y los efectos provienen del ejecutor.
    @GameTest(template = "empty")
    public void blockedActionIsNotLearnedAsSuccessAndCopiesInputs(GameTestHelper helper) {
        UUID agent = UUID.randomUUID(), world = UUID.randomUUID(), action = UUID.randomUUID();
        Map<String, Integer> sourceDelta = new HashMap<>(); sourceDelta.put("minecraft:oak_log", 0);
        ToolResult result = new ToolResult(action, ToolResult.Status.BLOCKED, ToolResult.Reason.PERMISSION_DENIED,
            sourceDelta, List.of("Protección canceló la rotura"), List.of(action),
            GlobalPos.of(Level.OVERWORLD, BlockPos.ZERO), 1000, 1001);
        sourceDelta.put("minecraft:oak_log", 99);
        ActionRequest request = new ActionRequest(action, agent, null, "companeros:mine", 1,
            Map.of("target", "0,0,0"), 2, 2000, Set.of("mine"));
        helper.assertTrue(!result.succeeded() && result.finished() && result.inventoryDelta().get("minecraft:oak_log") == 0,
            "Un bloqueo permanece diferente del éxito y el resultado no cambia por mutación externa");
        helper.assertTrue(request.expired(2000) && !request.expired(1999), "Una solicitud vencida no autoriza efectos tardíos");
        MemoryStore store = new MemoryStore(world); store.recordResult(agent, result, 50, "Intenté recoger madera");
        MemoryRecord learned = MemoryStore.load(store.save(new CompoundTag())).find(action).orElseThrow();
        helper.assertTrue(learned.origin() == MemoryOrigin.RESULT && learned.evidenceIds().contains(action)
            && learned.content().contains("PERMISSION_DENIED") && !learned.content().contains("99"),
            "La experiencia conserva fallo y delta observados, sin botín ficticio");
        helper.succeed();
    }

    private static MemoryRecord memory(UUID world, UUID agent, UUID id, String content,
            double importance, double confidence, long gameTime, MemoryOrigin origin,
            UUID supersedes, Set<String> keywords) {
        return new MemoryRecord(id, agent, world, GlobalPos.of(Level.OVERWORLD, new BlockPos(10, 64, 10)),
            1000 + gameTime, gameTime, MemoryType.RESOURCE, importance, confidence,
            origin == MemoryOrigin.OBSERVED ? 1000 + gameTime : 0, gameTime, origin, "test-evidence",
            List.of(id), content, keywords, Set.of(), supersedes, false);
    }

    private static boolean rejected(Runnable operation) {
        try { operation.run(); return false; }
        catch (IllegalArgumentException | IllegalStateException expected) { return true; }
    }
}