Kaynağa Gözat

Personnalité : un fichier illisible ne rend plus Rika muette

Un fichier de personnalité pas en UTF-8, illisible, ou un
PERSONALITY_FILE qui désigne un dossier faisait échouer chaque
message. Rika garde la dernière personnalité valide (ou celle par
défaut) et le journal explique une seule fois comment corriger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Johnounet 1 gün önce
ebeveyn
işleme
b3492b32e9
3 değiştirilmiş dosya ile 75 ekleme ve 6 silme
  1. 2 0
      CHANGELOG.md
  2. 26 5
      bot/claude_client.py
  3. 47 1
      tests/test_prompts.py

+ 2 - 0
CHANGELOG.md

@@ -18,6 +18,8 @@ Les versions les plus récentes sont en haut. Chaque entrée commence par un ré
 
 - **« Modifier » ne signale plus de faux conflit.** Quand l'écriture sur le disque prenait plus de 3 secondes, Discord considérait l'envoi comme échoué alors que le texte était enregistré : la fenêtre restait ouverte, et un second clic répondait que le fichier avait changé entre-temps. Le bot répond maintenant tout de suite à Discord, avant d'enregistrer, et un même texte envoyé deux fois est accepté.
 
+- **Une personnalité illisible ne rend plus Rika muette.** Un fichier de personnalité qui n'est pas en UTF-8 (enregistré en ANSI avec le Bloc-notes, par exemple), illisible, ou un `PERSONALITY_FILE` qui désigne un dossier (ConfigMap montée comme dossier sur OpenShift) faisait échouer chaque message sans réponse. Rika garde maintenant la dernière personnalité valide, ou celle par défaut au démarrage, et le journal indique une seule fois le problème et comment le corriger. Le fichier est relu dès qu'il est modifié.
+
 ---
 
 ## Version 5.11

+ 26 - 5
bot/claude_client.py

@@ -208,16 +208,37 @@ class ClaudeClient:
         path: Path = self.settings.personality_file
         try:
             mtime = path.stat().st_mtime
-            if self._personality_cache is None or self._personality_cache[0] != mtime:
-                text = path.read_text(encoding="utf-8").strip()
-                self._personality_cache = (mtime, text or DEFAULT_PERSONALITY)
-                logger.info("Personnalité chargée depuis %s", path)
-            return self._personality_cache[1]
         except FileNotFoundError:
             if self._personality_cache != (0.0, DEFAULT_PERSONALITY):
                 logger.warning("%s introuvable : utilisation de la personnalité par défaut.", path)
                 self._personality_cache = (0.0, DEFAULT_PERSONALITY)
             return DEFAULT_PERSONALITY
+        except OSError as e:
+            return self._personality_unreadable(-1.0, f"{path} illisible ({e})")
+        if self._personality_cache is not None and self._personality_cache[0] == mtime:
+            return self._personality_cache[1]
+        if path.is_dir():  # ConfigMap montée comme dossier (Windows lèverait une PermissionError peu parlante)
+            return self._personality_unreadable(
+                mtime, f"{path} est un dossier : PERSONALITY_FILE doit désigner le fichier lui-même")
+        try:
+            text = path.read_text(encoding="utf-8").strip()
+        except UnicodeDecodeError:
+            return self._personality_unreadable(
+                mtime, f"{path} n'est pas en UTF-8 : enregistre-le en UTF-8 (Bloc-notes : Enregistrer sous, Encodage)")
+        except OSError as e:
+            return self._personality_unreadable(mtime, f"{path} illisible ({e})")
+        self._personality_cache = (mtime, text or DEFAULT_PERSONALITY)
+        logger.info("Personnalité chargée depuis %s", path)
+        return self._personality_cache[1]
+
+    def _personality_unreadable(self, mtime: float, reason: str) -> str:
+        """Garde la dernière personnalité valide (ou celle par défaut) ; une seule erreur par version du fichier."""
+        text = self._personality_cache[1] if self._personality_cache else DEFAULT_PERSONALITY
+        if self._personality_cache is None or self._personality_cache[0] != mtime:
+            kept = "celle par défaut" if text == DEFAULT_PERSONALITY else "la dernière valide"
+            logger.error("%s. Personnalité conservée : %s.", reason, kept)
+            self._personality_cache = (mtime, text)
+        return text
 
     # ------------------------------------------------------------------ appel
 

+ 47 - 1
tests/test_prompts.py

@@ -1,8 +1,12 @@
 """Tests des textes envoyés à Claude et aux membres. Lancer depuis la racine du projet : python -m unittest"""
 
+import os
+import tempfile
 import unittest
+from pathlib import Path
+from types import SimpleNamespace
 
-from bot.claude_client import MEMBER_MEMORY_NOTE
+from bot.claude_client import DEFAULT_PERSONALITY, MEMBER_MEMORY_NOTE, ClaudeClient
 
 
 class PromptsTest(unittest.TestCase):
@@ -13,5 +17,47 @@ class PromptsTest(unittest.TestCase):
         self.assertIn("ne récite jamais la fiche", note)
 
 
+class PersonalityTest(unittest.TestCase):
+    def setUp(self):
+        self._tmp = tempfile.TemporaryDirectory()
+        self.path = Path(self._tmp.name) / "personality_prompt.txt"
+        # Sans __init__ : pas de client Anthropic à créer.
+        self.client = ClaudeClient.__new__(ClaudeClient)
+        self.client.settings = SimpleNamespace(personality_file=self.path)
+        self.client._personality_cache = None
+
+    def tearDown(self):
+        self._tmp.cleanup()
+
+    def write(self, data: bytes, mtime: int):
+        self.path.write_bytes(data)
+        os.utime(self.path, (mtime, mtime))  # deux écritures rapprochées auraient la même date
+
+    def test_not_utf8_keeps_last_valid(self):
+        self.write("Tu es Rika, très gentille.".encode("utf-8"), 1000)
+        self.assertEqual(self.client.personality(), "Tu es Rika, très gentille.")
+        self.write("Tu es Rika, très méchante.".encode("cp1252"), 2000)
+        with self.assertLogs("chatbot.claude", "ERROR") as logs:
+            self.assertEqual(self.client.personality(), "Tu es Rika, très gentille.")
+        self.assertIn("UTF-8", logs.output[0])
+        self.assertIn("dernière valide", logs.output[0])
+        with self.assertNoLogs("chatbot.claude", "ERROR"):  # une seule erreur par version du fichier
+            self.client.personality()
+        self.write("Tu es Rika, réparée.".encode("utf-8"), 3000)
+        self.assertEqual(self.client.personality(), "Tu es Rika, réparée.")
+
+    def test_not_utf8_at_start_uses_default(self):
+        self.write("Tu es Rika, très méchante.".encode("cp1252"), 1000)
+        with self.assertLogs("chatbot.claude", "ERROR") as logs:
+            self.assertEqual(self.client.personality(), DEFAULT_PERSONALITY)
+        self.assertIn("par défaut", logs.output[0])
+
+    def test_directory(self):
+        self.path.mkdir()
+        with self.assertLogs("chatbot.claude", "ERROR") as logs:
+            self.assertEqual(self.client.personality(), DEFAULT_PERSONALITY)
+        self.assertIn("est un dossier", logs.output[0])
+
+
 if __name__ == "__main__":
     unittest.main()