test_config.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. """Tests de la configuration (MAX_TOKENS, mémoire et fichiers écrits). Lancer depuis la racine du projet : python -m unittest"""
  2. import tempfile
  3. import unittest
  4. from pathlib import Path
  5. from unittest import mock
  6. from bot.config import ConfigError, load_settings, memory_max_tokens, memory_warning
  7. BASE = {"DISCORD_TOKEN": "x", "ANTHROPIC_API_KEY": "x", "CHANNEL_ID": "1",
  8. "CLAUDE_MODEL": "claude-sonnet-5-5", "MEMORY": "on", "MEMORY_SERVER_LIMIT": "5000"}
  9. def settings(**env):
  10. # Sans le .env du projet : seulement les variables données.
  11. with mock.patch.dict("os.environ", {**BASE, **env}, clear=True), mock.patch("bot.config.load_dotenv"):
  12. return load_settings()
  13. class MemoryMaxTokensTest(unittest.TestCase):
  14. def test_margin_follows_effort(self):
  15. # 5000 caractères : 2000 tokens de contenu, plus 2000 pour le reste de l'appel.
  16. self.assertEqual(memory_max_tokens(settings(THINKING="off")), (4000, 4000))
  17. self.assertEqual(memory_max_tokens(settings(THINKING="on", EFFORT="medium")), (4000, 6000))
  18. self.assertEqual(memory_max_tokens(settings(THINKING="on")), (4000, 8000))
  19. self.assertEqual(memory_max_tokens(settings(THINKING="on", EFFORT="max")), (4000, 12000))
  20. def test_opus_thinks_even_with_thinking_off(self):
  21. s = settings(CLAUDE_MODEL="claude-opus-5-5", THINKING="off", EFFORT="low")
  22. self.assertEqual(memory_max_tokens(s), (4000, 6000))
  23. def test_too_small_refused(self):
  24. with self.assertRaises(ConfigError) as ctx:
  25. settings(MAX_TOKENS="3999")
  26. self.assertIn("au moins 4000", str(ctx.exception))
  27. def test_memory_off_not_checked(self):
  28. self.assertEqual(settings(MEMORY="off", MAX_TOKENS="1000").max_tokens, 1000)
  29. self.assertIsNone(memory_warning(settings(MEMORY="off", MAX_TOKENS="1000", THINKING="on")))
  30. def test_warning_when_reflection_does_not_fit(self):
  31. # Le scénario de l'audit : pile le minimum, mais la réflexion à effort high n'y tient pas.
  32. s = settings(MAX_TOKENS="4000", THINKING="on", EFFORT="high")
  33. self.assertIn("au moins 8000", memory_warning(s))
  34. self.assertIsNone(memory_warning(settings(MAX_TOKENS="8000", THINKING="on", EFFORT="high")))
  35. self.assertIsNone(memory_warning(settings(MAX_TOKENS="4000", THINKING="off")))
  36. class OutputPathsTest(unittest.TestCase):
  37. def setUp(self):
  38. tmp = tempfile.TemporaryDirectory()
  39. self.addCleanup(tmp.cleanup)
  40. self.root = Path(tmp.name)
  41. def test_existing_folders_accepted(self):
  42. s = settings(LOG_FILE=str(self.root / "bot.log"), HISTORY_FILE=str(self.root / "h.json"))
  43. self.assertEqual(s.memory_dir, self.root / "memories")
  44. def test_missing_folder_refused(self):
  45. # Le scénario de l'audit : volume non monté sur /data.
  46. for name in ("LOG_FILE", "HISTORY_FILE", "MEMORY_DIR"):
  47. with self.subTest(name), self.assertRaises(ConfigError) as ctx:
  48. settings(**{name: str(self.root / "absent" / "x")})
  49. self.assertIn(f"Le dossier de {name} n'existe pas", str(ctx.exception))
  50. def test_memory_dir_unchecked_when_off(self):
  51. settings(MEMORY="off", MEMORY_DIR=str(self.root / "absent" / "x"))
  52. def test_file_path_is_folder_refused(self):
  53. with self.assertRaises(ConfigError) as ctx:
  54. settings(HISTORY_FILE=str(self.root))
  55. self.assertIn("HISTORY_FILE désigne un dossier", str(ctx.exception))
  56. def test_unwritable_folder_refused(self):
  57. with mock.patch("bot.config.tempfile.TemporaryFile", side_effect=PermissionError(13, "Permission denied")), self.assertRaises(ConfigError) as ctx:
  58. settings()
  59. self.assertIn("Impossible d'écrire dans le dossier de LOG_FILE", str(ctx.exception))
  60. class MaximumTest(unittest.TestCase):
  61. def test_values_above_maximum_refused(self):
  62. for name, value in (("MAX_TOKENS", "64001"), ("WEB_MAX_USES", "11"), ("MEMORY_MAX_OPERATIONS", "21"),
  63. ("PDF_MAX_PAGES", "101"), ("PRESENCE_SPEED", "100.5")):
  64. with self.subTest(name), self.assertRaises(ConfigError) as ctx:
  65. settings(MEMORY="off", **{name: value})
  66. self.assertIn(f"{name} ne peut pas dépasser", str(ctx.exception))
  67. def test_maximum_accepted(self):
  68. s = settings(MEMORY="off", MAX_TOKENS="64000", WEB_MAX_USES="10", MEMORY_MAX_OPERATIONS="20",
  69. PDF_MAX_PAGES="100", PRESENCE_SPEED="100")
  70. self.assertEqual((s.max_tokens, s.pdf_max_pages, s.presence_speed), (64000, 100, 100))
  71. class PresenceWeightsTest(unittest.TestCase):
  72. ONLY_GAME = "anime=0,musique=0,pensee=0,jeu=100,en_ligne=0,ne_pas_deranger=0,absent=0"
  73. def test_empty_items_ignored(self):
  74. s = settings(MEMORY="off", PRESENCE_WEIGHTS="anime=5,,musique=7,")
  75. self.assertEqual((s.presence_weights["anime"], s.presence_weights["musique"]), (5, 7))
  76. def test_duplicate_refused(self):
  77. with self.assertRaises(ConfigError) as ctx:
  78. settings(MEMORY="off", PRESENCE_WEIGHTS="anime=5,Anime=0")
  79. self.assertIn("le statut anime apparaît plusieurs fois", str(ctx.exception))
  80. def test_only_game_without_steam_key_refused(self):
  81. with self.assertRaises(ConfigError) as ctx:
  82. settings(MEMORY="off", PRESENCE_WEIGHTS=self.ONLY_GAME)
  83. self.assertIn("nécessite STEAM_API_KEY", str(ctx.exception))
  84. def test_only_game_accepted_with_steam_key_or_presence_off(self):
  85. settings(MEMORY="off", PRESENCE_WEIGHTS=self.ONLY_GAME, STEAM_API_KEY="x")
  86. settings(MEMORY="off", PRESENCE_WEIGHTS=self.ONLY_GAME, PRESENCE="off")
  87. if __name__ == "__main__":
  88. unittest.main()