fix(ia): prevent private notes leak in places tool, guard recordTurn against disk failure, and add tests

This commit is contained in:
Marcos Paulo
2026-08-18 19:15:02 -03:00
parent ea55813019
commit 2220f11e64
3 changed files with 54 additions and 4 deletions
@@ -437,6 +437,7 @@ final class Ai {
return; return;
} }
String clean = String.join(" ", segments); String clean = String.join(" ", segments);
lastAnswer = new Answered(askerId, question, clean);
// Only remember if the asker is still online: a PlayerQuitEvent forgets // Only remember if the asker is still online: a PlayerQuitEvent forgets
// the player's history (carry-forward #6), and re-adding here after the // the player's history (carry-forward #6), and re-adding here after the
// quit would resurrect it. lastAnswer stays regardless, so /ia corrigir // quit would resurrect it. lastAnswer stays regardless, so /ia corrigir
@@ -444,10 +445,13 @@ final class Ai {
if (asker != null) { if (asker != null) {
conversations.remember(askerId, question, clean); conversations.remember(askerId, question, clean);
if (plugin.playerMemory() != null) { if (plugin.playerMemory() != null) {
try {
plugin.playerMemory().recordTurn(askerId, asker.getName(), question, clean); plugin.playerMemory().recordTurn(askerId, asker.getName(), question, clean);
} catch (RuntimeException e) {
plugin.getLogger().warning("Falha ao gravar memória da IA para " + asker.getName() + ": " + e);
}
} }
} }
lastAnswer = new Answered(askerId, question, clean);
if (isPrivate || !settings.aiPublic()) { if (isPrivate || !settings.aiPublic()) {
if (asker != null) { if (asker != null) {
@@ -146,7 +146,20 @@ final class Tools {
} }
String uuidStr = who.uuid(); String uuidStr = who.uuid();
List<DeathLog.Entry> deaths = plugin.deathLog().forPlayer(uuidStr); List<DeathLog.Entry> deaths = plugin.deathLog().forPlayer(uuidStr);
List<Note> notes = plugin.notes().visibleTo(uuidStr, null, ""); List<Note> notes = plugin.notes().visibleTo(uuidStr, Note.Scope.PUBLICA, "");
return formatPlayerPlaces(who, deaths, notes);
}
String formatPlayerPlaces(OfflineStats.Known who, List<DeathLog.Entry> deaths, List<Note> visibleNotes) {
String uuidStr = who.uuid();
List<Note> notes = new java.util.ArrayList<>();
if (visibleNotes != null) {
for (Note n : visibleNotes) {
if (n.scope() == Note.Scope.PUBLICA && n.authorId() != null && n.authorId().equals(uuidStr)) {
notes.add(n);
}
}
}
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("Lugares conhecidos de ").append(who.name()).append(":\n"); sb.append("Lugares conhecidos de ").append(who.name()).append(":\n");
@@ -164,7 +177,7 @@ final class Tools {
sb.append("\n"); sb.append("\n");
} }
if (deaths.isEmpty()) { if (deaths == null || deaths.isEmpty()) {
sb.append("- Mortes recentes: nenhum registro de morte recente."); sb.append("- Mortes recentes: nenhum registro de morte recente.");
} else { } else {
sb.append("- Mortes recentes: "); sb.append("- Mortes recentes: ");
@@ -3,6 +3,8 @@ package dev.marcospaulo.canalhandia;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -42,4 +44,35 @@ class ToolsTest {
String res = tools.run("wiki", "malformed json"); String res = tools.run("wiki", "malformed json");
assertTrue(res.contains("argumentos inválidos")); assertTrue(res.contains("argumentos inválidos"));
} }
@Test
void formatPlayerPlacesFiltersOutPrivateNotesAndOtherAuthors() {
Tools tools = new Tools(null, null, null, s -> {});
OfflineStats.Known who = new OfflineStats.Known("uuid-ana", "ana");
Note privateNote = new Note(1, Note.Scope.PRIVADA, "ana", "uuid-ana", "segredo", "world", 10, 20, 30, 1000L);
Note publicNote = new Note(2, Note.Scope.PUBLICA, "ana", "uuid-ana", "vila do spawn", "world", 100, 64, 200, 1000L);
Note otherAuthorPublicNote = new Note(3, Note.Scope.PUBLICA, "bob", "uuid-bob", "base do bob", "world", 500, 64, 500, 1000L);
String formatted = tools.formatPlayerPlaces(who, List.of(), List.of(privateNote, publicNote, otherAuthorPublicNote));
assertTrue(formatted.contains("vila do spawn"));
assertFalse(formatted.contains("segredo"), "Private notes must never be included in places output");
assertFalse(formatted.contains("base do bob"), "Notes of other players must not be included");
assertTrue(formatted.contains("nenhum registro de morte recente"));
}
@Test
void formatPlayerPlacesIncludesDeathsAndFormatsPlaces() {
Tools tools = new Tools(null, null, null, s -> {});
OfflineStats.Known who = new OfflineStats.Known("uuid-ana", "ana");
DeathLog.Entry d = new DeathLog.Entry("uuid-ana", "abraçou um Creeper", "Mundo normal", 12, 64, -80, 1000L);
String formatted = tools.formatPlayerPlaces(who, List.of(d), List.of());
assertTrue(formatted.contains("Lugares conhecidos de ana:"));
assertTrue(formatted.contains("nenhuma base salva"));
assertTrue(formatted.contains("12, 64, -80 (Mundo normal)"));
assertTrue(formatted.contains("abraçou um Creeper"));
}
} }