Beta/test_bot.py

479 lines
14 KiB
Python
Raw Normal View History

2026-06-16 17:09:34 +00:00
#!/usr/bin/env python3
"""Tests unitaires pour Superviseur Bot."""
2026-06-16 17:09:34 +00:00
import sys
import os
import json
import asyncio
import tempfile
import shutil
2026-06-16 17:09:34 +00:00
import logging
# Add the project root to the path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
logging.basicConfig(level=logging.INFO)
2026-06-16 17:09:34 +00:00
logger = logging.getLogger('TestBot')
# ==================== Tests d'import ====================
2026-06-16 17:09:34 +00:00
def test_imports():
"""Test que tous les modules principaux peuvent être importés."""
from core.bot import Bot
from core.llm import LLMManager
from core.action_executor import ActionExecutor
from brain.memory import (
add_interaction, build_context_for_model, flush_all,
delete_user_memory, set_llm_manager, _extractive_summary
)
from brain.moderation import init_db, log_infraction
from dotenv import load_dotenv
import discord
logger.info("✓ Tous les imports réussis")
def test_llm_manager_init():
"""Test l'initialisation du LLMManager."""
from core.llm import LLMManager
llm = LLMManager(model_name="test-model")
assert llm.model_name == "test-model"
assert llm.temperature == 0.5
assert "localhost" in llm.base_url
logger.info("✓ LLMManager initialisé correctement")
def test_bot_init():
"""Test l'initialisation du bot Beta."""
from core.bot import Bot
from core.llm import LLMManager
import discord
llm = LLMManager(model_name="test-model")
intents = discord.Intents.default()
intents.message_content = True
bot = Bot(
llama_model=llm,
command_prefix="!",
guild_id=123456789,
system_prompt="Test prompt",
intents=intents
)
assert bot.guild_id == 123456789
assert bot.system_name == "Bêta"
logger.info("✓ Bot Superviseur initialisé correctement")
# ==================== Tests LLM (JSON Extraction) ====================
def test_extract_json_simple_object():
"""Test extraction JSON simple."""
from core.llm import LLMManager
llm = LLMManager()
text = 'Voici mon analyse : {"thought": "test", "action": "NONE", "response": "Salut !"}'
result = llm.extract_json_actions(text)
assert result is not None
data = json.loads(result)
assert data["action"] == "NONE"
assert data["response"] == "Salut !"
logger.info("✓ Extraction JSON simple OK")
def test_extract_json_array():
"""Test extraction JSON tableau."""
from core.llm import LLMManager
llm = LLMManager()
text = 'Actions : [{"action": "CREATE_CHANNEL", "channel_name": "test"}, {"action": "CREATE_ROLE", "role_name": "mod"}]'
result = llm.extract_json_actions(text)
assert result is not None
data = json.loads(result)
assert isinstance(data, list)
assert len(data) == 2
assert data[0]["action"] == "CREATE_CHANNEL"
logger.info("✓ Extraction JSON tableau OK")
def test_extract_json_with_comments():
"""Test extraction JSON avec commentaires GPT-OSS."""
from core.llm import LLMManager
llm = LLMManager()
text = '''Voici la réponse :
// Ceci est un commentaire
{"action": "KICK", "user_id": "123", "reason": "spam"}
// Autre commentaire'''
result = llm.extract_json_actions(text)
assert result is not None
data = json.loads(result)
assert data["action"] == "KICK"
logger.info("✓ Extraction JSON avec commentaires OK")
def test_extract_json_none():
"""Test extraction quand pas de JSON."""
from core.llm import LLMManager
llm = LLMManager()
text = "Salut, comment ça va ? Pas de JSON ici."
result = llm.extract_json_actions(text)
assert result is None
logger.info("✓ Absence de JSON détectée correctement")
def test_extract_json_empty():
"""Test extraction avec texte vide."""
from core.llm import LLMManager
llm = LLMManager()
assert llm.extract_json_actions("") is None
assert llm.extract_json_actions(None) is None
logger.info("✓ Texte vide géré correctement")
def test_extract_json_nested_braces():
"""Test extraction avec accolades imbriquées."""
from core.llm import LLMManager
llm = LLMManager()
text = 'Analyse : {"action": "SEND_MESSAGE", "channel_id": "123", "response": "Contenu avec {des} accolades"}'
result = llm.extract_json_actions(text)
assert result is not None
data = json.loads(result)
assert data["action"] == "SEND_MESSAGE"
logger.info("✓ Extraction JSON avec accolades imbriquées OK")
# ==================== Tests LLM (Payload) ====================
def test_build_payload():
"""Test la construction de payload Ollama."""
from core.llm import LLMManager
llm = LLMManager(model_name="test-model", temperature=0.3)
payload = llm.build_payload(
prompt="Test prompt",
system_prompt="Test system"
)
assert payload["model"] == "test-model"
assert payload["prompt"] == "Test prompt"
assert payload["system"] == "Test system"
assert payload["options"]["temperature"] == 0.3
assert payload["options"]["num_ctx"] == 16384
logger.info("✓ Payload Ollama construit correctement")
# ==================== Tests Mémoire ====================
def _run_async(coro):
"""Helper pour exécuter du code async dans les tests sync."""
return asyncio.get_event_loop().run_until_complete(coro)
def test_memory_save_load():
"""Test sauvegarde et chargement mémoire."""
import brain.memoire as mem
test_dir = tempfile.mkdtemp()
original_dir = mem.MEMORY_DIR
mem.MEMORY_DIR = test_dir
mem.memory_cache.clear()
mem._dirty.clear()
2026-06-16 17:09:34 +00:00
try:
uid = "test_user_123"
data = {"resume_global": "Test résumé", "historique_recent": []}
_run_async(mem.save_user_memory_async(uid, data, persist_immediately=True))
loaded = _run_async(mem.load_user_memory_async(uid))
assert loaded == data, f"Load/save failed: {loaded} != {data}"
logger.info("✓ Sauvegarde/chargement mémoire OK")
finally:
mem.MEMORY_DIR = original_dir
mem.memory_cache.clear()
shutil.rmtree(test_dir)
def test_memory_add_interaction():
"""Test ajout d'interaction."""
import brain.memoire as mem
test_dir = tempfile.mkdtemp()
original_dir = mem.MEMORY_DIR
mem.MEMORY_DIR = test_dir
mem.memory_cache.clear()
mem._dirty.clear()
2026-06-16 17:09:34 +00:00
try:
uid = "test_user_456"
_run_async(mem.add_interaction(uid, 111, "Hello", "Hi there!"))
loaded = _run_async(mem.load_user_memory_async(uid))
assert len(loaded["historique_recent"]) == 1
assert loaded["historique_recent"][0]["user_message"] == "Hello"
assert loaded["historique_recent"][0]["bot_message"] == "Hi there!"
logger.info("✓ Ajout d'interaction OK")
finally:
mem.MEMORY_DIR = original_dir
mem.memory_cache.clear()
shutil.rmtree(test_dir)
def test_memory_build_context():
"""Test construction du contexte pour le modèle."""
import brain.memoire as mem
test_dir = tempfile.mkdtemp()
original_dir = mem.MEMORY_DIR
mem.MEMORY_DIR = test_dir
mem.memory_cache.clear()
mem._dirty.clear()
2026-06-16 17:09:34 +00:00
try:
uid = "test_user_789"
_run_async(mem.add_interaction(uid, 111, "Question?", "Réponse!"))
ctx = _run_async(mem.build_context_for_model(uid))
assert "Question?" in ctx
assert "Réponse!" in ctx
logger.info("✓ Construction contexte OK")
finally:
mem.MEMORY_DIR = original_dir
mem.memory_cache.clear()
shutil.rmtree(test_dir)
def test_memory_delete():
"""Test suppression mémoire avec guild_id."""
import brain.memoire as mem
test_dir = tempfile.mkdtemp()
original_dir = mem.MEMORY_DIR
mem.MEMORY_DIR = test_dir
mem.memory_cache.clear()
mem._dirty.clear()
try:
uid = "test_user_del"
_run_async(mem.add_interaction(uid, 111, "Test", "Response", guild_id="TestGuild"))
# Forcer la sauvegarde immédiate (add_interaction schedule en background)
_run_async(asyncio.sleep(0.5))
# Vérifier que le fichier existe
path = mem._user_filepath(uid, "TestGuild")
assert os.path.exists(path), f"Fichier non trouvé: {path}"
2026-06-16 17:09:34 +00:00
# Supprimer
mem.delete_user_memory(uid, "TestGuild")
# Vérifier suppression (recharger crée un fichier vide)
mem.memory_cache.clear()
loaded = _run_async(mem.load_user_memory_async(uid, "TestGuild"))
assert loaded["historique_recent"] == []
logger.info("✓ Suppression mémoire avec guild_id OK")
finally:
mem.MEMORY_DIR = original_dir
mem.memory_cache.clear()
shutil.rmtree(test_dir)
def test_memory_extractive_summary():
"""Test le résumé extractif (fallback)."""
from brain.memoire import _extractive_summary
messages = [
{"user_message": "Salut, comment configurer le bot ?", "bot_message": "Tu peux utiliser la commande !config."},
{"user_message": "Merci beaucoup !", "bot_message": "De rien, n'hésite pas."},
]
summary = _extractive_summary(messages)
assert len(summary) > 0
assert "Mots-clés:" in summary
logger.info("✓ Résumé extractif OK")
def test_memory_flush_all():
"""Test flush de toutes les mémoires dirty."""
import brain.memoire as mem
test_dir = tempfile.mkdtemp()
original_dir = mem.MEMORY_DIR
mem.MEMORY_DIR = test_dir
mem.memory_cache.clear()
mem._dirty.clear()
try:
# Créer quelques entrées
_run_async(mem.add_interaction("user_a", 111, "Test A", "Response A"))
_run_async(mem.add_interaction("user_b", 222, "Test B", "Response B"))
# Marquer dirty
assert any(mem._dirty.values())
# Flush
_run_async(mem.flush_all())
# Vérifier que tout est clean
assert not any(mem._dirty.values())
logger.info("✓ Flush mémoire OK")
finally:
mem.MEMORY_DIR = original_dir
mem.memory_cache.clear()
shutil.rmtree(test_dir)
# ==================== Tests Bot (Response Cleaning) ====================
def test_clean_response_text():
"""Test le nettoyage de texte de réponse."""
from core.response_handler import clean_response_text
# Test nettoyage basique
clean = clean_response_text("Salut ! Voici ma r\u00e9ponse.", None)
assert clean == "Salut ! Voici ma r\u00e9ponse."
# Test nettoyage avec JSON extrait
clean = clean_response_text('Texte avant {"action": "NONE"} texte apr\u00e8s', '{"action": "NONE"}')
assert "NONE" not in clean
assert "avant" in clean
assert "apr\u00e8s" in clean
# Test nettoyage balises
clean = clean_response_text("R\u00e9ponse <|channel|> normale", None)
assert "<|channel|>" not in clean
logger.info("OK Nettoyage r\u00e9ponse")
def test_parse_json_actions():
"""Test le parsing des actions JSON."""
from core.response_handler import parse_json_actions
# Test parsing valide
actions, responses, is_none = parse_json_actions(
'{"action": "KICK", "response": "Utilisateur kick\u00e9"}'
)
assert len(actions) == 1
assert actions[0]["action"] == "KICK"
assert "kick\u00e9" in responses[0]
assert not is_none
# Test parsing NONE
actions, responses, is_none = parse_json_actions(
'{"action": "NONE"}'
)
assert is_none
# Test parsing tableau
actions, responses, is_none = parse_json_actions(
'[{"action": "A", "response": "R1"}, {"action": "B", "response": "R2"}]'
)
assert len(actions) == 2
assert len(responses) == 2
# Test parsing None
actions, responses, is_none = parse_json_actions(None)
assert actions == []
assert responses == []
assert not is_none
logger.info("OK Parsing actions JSON")
# ==================== Tests Action Router ====================
def test_action_executor_init():
"""Test l'initialisation de l'ActionExecutor."""
from core.action_executor import ActionExecutor
from core.bot import Bot
from core.llm import LLMManager
import discord
llm = LLMManager()
intents = discord.Intents.default()
bot = Bot(
llama_model=llm, command_prefix="!", guild_id=123,
system_prompt="Test", intents=intents
)
executor = ActionExecutor(bot)
logger.info("✓ ActionExecutor initialisé OK")
# ==================== Tests Modération ====================
def test_moderation_db_init():
"""Test l'initialisation de la DB de modération."""
import tempfile
import brain.moderation as mod
from brain import moderation
original_path = mod.DB_PATH
test_dir = tempfile.mkdtemp()
mod.DB_PATH = os.path.join(test_dir, "test_mod.db")
try:
mod.init_db()
2026-06-16 17:09:34 +00:00
# Vérifier que la table existe
import sqlite3
conn = sqlite3.connect(mod.DB_PATH)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='infractions'")
assert cursor.fetchone() is not None
conn.close()
2026-06-16 17:09:34 +00:00
logger.info("✓ Initialisation DB modération OK")
finally:
mod.DB_PATH = original_path
shutil.rmtree(test_dir)
# ==================== Point d'entrée ====================
2026-06-16 17:09:34 +00:00
if __name__ == "__main__":
logger.info("=" * 50)
logger.info("Tests Superviseur Bot")
logger.info("=" * 50)
tests = [
test_imports,
test_llm_manager_init,
test_bot_init,
test_extract_json_simple_object,
test_extract_json_array,
test_extract_json_with_comments,
test_extract_json_none,
test_extract_json_empty,
test_extract_json_nested_braces,
test_build_payload,
test_memory_save_load,
test_memory_add_interaction,
test_memory_build_context,
test_memory_delete,
test_memory_extractive_summary,
test_memory_flush_all,
test_clean_response_text,
test_parse_json_actions,
test_action_executor_init,
test_moderation_db_init,
]
passed = 0
failed = 0
for test in tests:
try:
test()
passed += 1
except Exception as e:
import traceback
logger.error(f"{test.__name__}: {e}")
traceback.print_exc()
failed += 1
logger.info("=" * 50)
logger.info(f"Résultats: {passed}/{passed + failed} tests réussis")
logger.info("=" * 50)
sys.exit(0 if failed == 0 else 1)