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