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