ソースを参照

Mémoire : la fenêtre « Modifier » n'accepte plus que la limite du fichier

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Johnounet 2 日 前
親
コミット
ee0ce0e1b5
3 ファイル変更、62 行追加、11 行削除
  1. 26 10
      bot/memory_views.py
  2. 3 0
      changelog.txt
  3. 33 1
      tests/test_memory.py

+ 26 - 10
bot/memory_views.py

@@ -224,16 +224,16 @@ class EditModal(discord.ui.Modal):
             "Modifier le fichier général" if card.target.member_id is None else f"Fiche de {card.target.name}")
         super().__init__(title=title[:45], timeout=VIEW_TIMEOUT)
         self.card = card
-        limit = card.store.limit(card.target.file)
-        count = min(MAX_INPUTS, -(-limit // INPUT_MAX))
+        sizes = input_sizes(card.store.limit(card.target.file))
+        count = len(sizes)
         self.original = card.store.read(card.target.file)  # comparé à l'enregistrement (voir overwrite)
-        parts = split_for_inputs(self.original, count)
+        parts = split_for_inputs(self.original, sizes)
         self.inputs = []
         for i in range(count):
             label = "Contenu" if count == 1 else f"Contenu ({i + 1}/{count})"
             text_input = discord.ui.TextInput(
                 label=label, style=discord.TextStyle.paragraph, required=False,
-                max_length=min(INPUT_MAX, limit), default=parts[i] or None,
+                max_length=sizes[i], default=parts[i] or None,
                 placeholder="Une information par ligne. Tout vider efface le fichier." if i == 0 else None,
             )
             self.inputs.append(text_input)
@@ -249,12 +249,28 @@ class EditModal(discord.ui.Modal):
             await interaction.response.send_message("La modification a échoué, regarde les logs.", ephemeral=True)
 
 
-def split_for_inputs(text: str, count: int) -> list[str]:
-    """Répartit un texte dans count zones de texte, de préférence entre les lignes."""
-    parts = split_message(text, INPUT_MAX)
-    if len(parts) > count:  # lignes trop mal réparties : découpage exact
-        parts = [text[i:i + INPUT_MAX] for i in range(0, len(text), INPUT_MAX)]
-    return (parts + [""] * count)[:count]
+def input_sizes(limit: int) -> list[int]:
+    """Taille maximale de chaque zone de texte : INPUT_MAX, sauf la dernière, réduite pour que le total ne
+    dépasse pas la limite du fichier (5000 : 4000 + 1000). Sinon le compteur de Discord laisserait écrire
+    un texte refusé à l'enregistrement."""
+    count = min(MAX_INPUTS, -(-limit // INPUT_MAX))
+    return [min(INPUT_MAX, limit - INPUT_MAX * i) for i in range(count)]
+
+
+def split_for_inputs(text: str, sizes: list[int]) -> list[str]:
+    """Répartit un texte dans des zones de texte de ces tailles, de préférence entre les lignes. Une zone
+    ne reçoit jamais plus que sa taille : Discord refuserait d'ouvrir la fenêtre."""
+    parts, rest = [], text
+    for size in sizes:
+        cut = len(rest) if len(rest) <= size else rest.rfind("\n", 0, size + 1)
+        if cut <= 0 < len(rest):  # ligne plus longue que la zone : coupée au milieu
+            cut = min(size, len(rest))
+        parts.append(rest[:cut])
+        rest = rest[cut:].lstrip("\n")
+    if rest:  # lignes trop mal réparties : découpage exact
+        starts = [sum(sizes[:i]) for i in range(len(sizes))]
+        parts = [text[start:start + size] for start, size in zip(starts, sizes)]
+    return parts
 
 
 def member_target(store: MemoryStore, member: discord.User | discord.Member, owner_id: int) -> Target:

+ 3 - 0
changelog.txt

@@ -123,6 +123,9 @@ Corrections
   perdue, ou la ligne retirée revenait. Quand un texte n'est pas
   enregistré (fichier changé, trop long…), il est renvoyé en fichier
   joint pour ne pas être perdu.
+- Mémoire : la fenêtre « Modifier » du fichier général n'accepte plus
+  que 5000 caractères (MEMORY_SERVER_LIMIT) : sa deuxième zone de texte
+  laissait en écrire 8000, refusés ensuite à l'enregistrement.
 - Mémoire : un membre ne peut plus prendre le nom Discord d'un autre
   (pseudo, nom d'utilisateur ou ancien pseudo) comme surnom dans sa
   fiche. Ce surnom est ignoré dans la liste des membres : Rika ne note

+ 33 - 1
tests/test_memory.py

@@ -9,7 +9,7 @@ from unittest import mock
 
 from bot.members import MemberRegistry, with_username
 from bot.discord_bot import memory_command
-from bot.memory_views import ADMINS_NOTE, EditModal, MemoryCard, Target
+from bot.memory_views import ADMINS_NOTE, EditModal, MemoryCard, Target, input_sizes, split_for_inputs
 from bot.memory import (MAX_ATTACHED_NOTES, MAX_FORGOTTEN_LINES, MAX_NICKNAME_LENGTH, MAX_NICKNAMES,
                         SERVER_FILE, MemoryStore)
 
@@ -547,6 +547,38 @@ class NotEnabledWriteTest(MemoryTestCase):
         self.assertFalse({item.label: item.disabled for item in self.card.view().children}["Modifier"])
 
 
+class InputSizesTest(MemoryTestCase):
+    """La fenêtre « Modifier » n'accepte pas plus que la limite du fichier."""
+
+    def test_sizes(self):
+        self.assertEqual(input_sizes(3000), [3000])
+        self.assertEqual(input_sizes(5000), [4000, 1000])
+        self.assertEqual(input_sizes(8000), [4000, 4000])
+        self.assertEqual(input_sizes(30000), [4000] * 5)
+
+    def test_server_window_limited(self):
+        # Scénario de l'audit : limite de 5000, deux zones de 4000 laissaient écrire 8000 caractères.
+        self.store.server_limit = 5000
+        window = EditModal(MemoryCard(self.store, Target(SERVER_FILE, None), owner_id=999, bot_name="Rika"))
+        self.assertEqual([text_input.max_length for text_input in window.inputs], [4000, 1000])
+
+    def test_split_between_lines(self):
+        text = "\n".join(["a" * 2000, "b" * 1500, "c" * 900])
+        self.assertEqual(split_for_inputs(text, [4000, 1000]), ["a" * 2000 + "\n" + "b" * 1500, "c" * 900])
+
+    def test_split_never_exceeds_sizes(self):
+        # Coupé entre les lignes, le reste (1400) ne tiendrait pas dans la zone de 1000 : découpage exact.
+        for text in ["\n".join(["a" * 3500, "b" * 1399]), "x" * 5000, "\n" + "y" * 4500]:
+            with self.subTest(text=text[:5]):
+                parts = split_for_inputs(text, [4000, 1000])
+                self.assertTrue(all(len(part) <= size for part, size in zip(parts, [4000, 1000])))
+                self.assertEqual(len("".join(parts).replace("\n", "")), len(text.replace("\n", "")))
+
+    def test_short_text(self):
+        self.assertEqual(split_for_inputs("habite à Lyon", [4000, 1000]), ["habite à Lyon", ""])
+        self.assertEqual(split_for_inputs("", [3000]), [""])
+
+
 class AdminAccessTest(MemoryTestCase):
     """Les membres savent que les administrateurs voient leur fiche, et chaque consultation est tracée."""