test_admin_commands.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests fuer das AdminCommands Plugin.
  4. Testet alle 27 Intent-Handler mit Mock-Application.
  5. """
  6. import socket
  7. import time
  8. from pathlib import Path
  9. from unittest.mock import AsyncMock, MagicMock, patch
  10. import pytest
  11. from trixy_core.nlp.handler import IntentReceivedData, IntentResult
  12. from plugins.admin_commands.main import AdminCommandsPlugin
  13. # ============================================================================
  14. # Fixtures
  15. # ============================================================================
  16. def _make_plugin(tmp_path, **config_overrides) -> tuple[AdminCommandsPlugin, MagicMock]:
  17. """Erstellt ein AdminCommandsPlugin mit Mock-Application."""
  18. mock_app = MagicMock()
  19. mock_app.events = MagicMock()
  20. mock_app.events.emit = AsyncMock()
  21. mock_app.events.trigger = AsyncMock()
  22. mock_app.events.register = MagicMock()
  23. mock_app.debug = True
  24. # Mock ServiceContainer
  25. mock_app.services = MagicMock()
  26. mock_app.services.get_service = MagicMock(return_value=None)
  27. # Mock SatelliteManager
  28. mock_app.satellites = MagicMock()
  29. mock_app.satellites.count = 3
  30. mock_app.satellites.connected_count = 2
  31. mock_app.satellites.get_all = MagicMock(return_value=[])
  32. # Mock RegistrationManager
  33. mock_app.registration = MagicMock()
  34. mock_app.registration.registration_mode = False
  35. mock_app.registration.enable_registration_mode = MagicMock()
  36. mock_app.registration.disable_registration_mode = MagicMock()
  37. mock_app.registration.delete_registration = MagicMock(return_value=True)
  38. # Mock PluginManager
  39. mock_app.plugins = MagicMock()
  40. mock_app.plugins.count = 5
  41. mock_app.plugins.enabled_count = 4
  42. config = {
  43. "enabled": True,
  44. "retention_days": 7,
  45. "registration_timeout_seconds": 60,
  46. **config_overrides,
  47. }
  48. plugin_path = tmp_path / "admin_commands"
  49. plugin_path.mkdir(exist_ok=True)
  50. plugin = AdminCommandsPlugin(
  51. application=mock_app,
  52. plugin_path=plugin_path,
  53. config=config,
  54. )
  55. plugin._start_time = time.time() - 3661 # ~1h 1min
  56. return plugin, mock_app
  57. def _make_data(**kwargs) -> IntentReceivedData:
  58. """Erstellt IntentReceivedData mit Defaults."""
  59. defaults = {
  60. "intent": "test",
  61. "satellite_id": "sat-1",
  62. "room_id": "wohnzimmer",
  63. "is_authenticated": True,
  64. }
  65. defaults.update(kwargs)
  66. return IntentReceivedData(**defaults)
  67. @pytest.fixture
  68. def plugin_and_app(tmp_path):
  69. """Standard Plugin + App Fixture."""
  70. return _make_plugin(tmp_path)
  71. # ============================================================================
  72. # Tests: System Info (1-6)
  73. # ============================================================================
  74. class TestSystemInfo:
  75. """Tests fuer System-Info Intents."""
  76. @pytest.mark.asyncio
  77. async def test_hostname_query(self, tmp_path):
  78. """admin_hostname_query gibt Hostnamen zurueck."""
  79. plugin, _ = _make_plugin(tmp_path)
  80. data = _make_data(intent="admin_hostname_query")
  81. result = await plugin.handle_hostname_query(data)
  82. assert result.success is True
  83. assert socket.gethostname() in result.response_text
  84. @pytest.mark.asyncio
  85. async def test_domain_query(self, tmp_path):
  86. """admin_domain_query gibt FQDN zurueck."""
  87. plugin, _ = _make_plugin(tmp_path)
  88. data = _make_data(intent="admin_domain_query")
  89. result = await plugin.handle_domain_query(data)
  90. assert result.success is True
  91. assert socket.getfqdn() in result.response_text
  92. @pytest.mark.asyncio
  93. async def test_ip_query(self, tmp_path):
  94. """admin_ip_query gibt eine IP-Adresse zurueck."""
  95. plugin, _ = _make_plugin(tmp_path)
  96. data = _make_data(intent="admin_ip_query")
  97. result = await plugin.handle_ip_query(data)
  98. assert result.success is True
  99. # Antwort enthaelt eine IP (mindestens Punkt-Notation)
  100. assert "." in result.response_text
  101. @pytest.mark.asyncio
  102. async def test_uptime_query(self, tmp_path):
  103. """admin_uptime_query gibt Laufzeit zurueck."""
  104. plugin, _ = _make_plugin(tmp_path)
  105. data = _make_data(intent="admin_uptime_query")
  106. result = await plugin.handle_uptime_query(data)
  107. assert result.success is True
  108. assert "1 Stunde" in result.response_text
  109. @pytest.mark.asyncio
  110. async def test_system_time_set_no_value(self, tmp_path):
  111. """admin_system_time_set ohne Wert gibt Fehler."""
  112. plugin, _ = _make_plugin(tmp_path)
  113. data = _make_data(intent="admin_system_time_set")
  114. result = await plugin.handle_system_time_set(data, time_value="")
  115. assert result.success is False
  116. @pytest.mark.asyncio
  117. async def test_system_date_set_no_value(self, tmp_path):
  118. """admin_system_date_set ohne Wert gibt Fehler."""
  119. plugin, _ = _make_plugin(tmp_path)
  120. data = _make_data(intent="admin_system_date_set")
  121. result = await plugin.handle_system_date_set(data, date_value="")
  122. assert result.success is False
  123. # ============================================================================
  124. # Tests: Satellite Management (7-13)
  125. # ============================================================================
  126. class TestSatelliteManagement:
  127. """Tests fuer Satellite-Verwaltung Intents."""
  128. @pytest.mark.asyncio
  129. async def test_registration_enable(self, tmp_path):
  130. """admin_registration_enable aktiviert Registrierung."""
  131. plugin, app = _make_plugin(tmp_path)
  132. data = _make_data(intent="admin_registration_enable")
  133. result = await plugin.handle_registration_enable(data)
  134. assert result.success is True
  135. app.registration.enable_registration_mode.assert_called_once_with(60)
  136. @pytest.mark.asyncio
  137. async def test_registration_enable_custom_timeout(self, tmp_path):
  138. """admin_registration_enable mit konfiguriertem Timeout."""
  139. plugin, app = _make_plugin(tmp_path, registration_timeout_seconds=120)
  140. data = _make_data()
  141. result = await plugin.handle_registration_enable(data)
  142. assert result.success is True
  143. app.registration.enable_registration_mode.assert_called_once_with(120)
  144. @pytest.mark.asyncio
  145. async def test_registration_disable(self, tmp_path):
  146. """admin_registration_disable deaktiviert Registrierung."""
  147. plugin, app = _make_plugin(tmp_path)
  148. data = _make_data()
  149. result = await plugin.handle_registration_disable(data)
  150. assert result.success is True
  151. app.registration.disable_registration_mode.assert_called_once()
  152. @pytest.mark.asyncio
  153. async def test_registration_status_active(self, tmp_path):
  154. """admin_registration_status bei aktivem Modus."""
  155. plugin, app = _make_plugin(tmp_path)
  156. app.registration.registration_mode = True
  157. data = _make_data()
  158. result = await plugin.handle_registration_status(data)
  159. assert result.success is True
  160. assert "aktiv" in result.response_text.lower()
  161. @pytest.mark.asyncio
  162. async def test_registration_status_inactive(self, tmp_path):
  163. """admin_registration_status bei inaktivem Modus."""
  164. plugin, app = _make_plugin(tmp_path)
  165. app.registration.registration_mode = False
  166. data = _make_data()
  167. result = await plugin.handle_registration_status(data)
  168. assert result.success is True
  169. assert "nicht aktiv" in result.response_text.lower()
  170. @pytest.mark.asyncio
  171. async def test_satellite_count_registered(self, tmp_path):
  172. """admin_satellite_count_registered gibt Anzahl zurueck."""
  173. plugin, _ = _make_plugin(tmp_path)
  174. data = _make_data()
  175. result = await plugin.handle_satellite_count_registered(data)
  176. assert result.success is True
  177. assert "3" in result.response_text
  178. @pytest.mark.asyncio
  179. async def test_satellite_count_connected(self, tmp_path):
  180. """admin_satellite_count_connected gibt Anzahl zurueck."""
  181. plugin, _ = _make_plugin(tmp_path)
  182. data = _make_data()
  183. result = await plugin.handle_satellite_count_connected(data)
  184. assert result.success is True
  185. assert "2" in result.response_text
  186. @pytest.mark.asyncio
  187. async def test_satellite_last_connected_no_alias(self, tmp_path):
  188. """admin_satellite_last_connected ohne Alias gibt Fehler."""
  189. plugin, _ = _make_plugin(tmp_path)
  190. data = _make_data()
  191. result = await plugin.handle_satellite_last_connected(data, alias="")
  192. assert result.success is False
  193. @pytest.mark.asyncio
  194. async def test_satellite_last_connected_with_tracker(self, tmp_path):
  195. """admin_satellite_last_connected mit Tracker und Historie."""
  196. plugin, app = _make_plugin(tmp_path)
  197. mock_tracker = MagicMock()
  198. mock_tracker.get_satellite_connection_history = MagicMock(return_value=[
  199. {"connected_at": "2026-03-14T10:00:00+00:00", "alias": "Wohnzimmer"}
  200. ])
  201. app.services.get_service = MagicMock(return_value=mock_tracker)
  202. data = _make_data()
  203. result = await plugin.handle_satellite_last_connected(data, alias="Wohnzimmer")
  204. assert result.success is True
  205. assert "14.03.2026" in result.response_text
  206. @pytest.mark.asyncio
  207. async def test_satellite_last_connected_no_history(self, tmp_path):
  208. """admin_satellite_last_connected ohne Historie."""
  209. plugin, app = _make_plugin(tmp_path)
  210. mock_tracker = MagicMock()
  211. mock_tracker.get_satellite_connection_history = MagicMock(return_value=[])
  212. app.services.get_service = MagicMock(return_value=mock_tracker)
  213. data = _make_data()
  214. result = await plugin.handle_satellite_last_connected(data, alias="Unknown")
  215. assert result.success is True
  216. assert "keine" in result.response_text.lower()
  217. @pytest.mark.asyncio
  218. async def test_satellite_unregister(self, tmp_path):
  219. """admin_satellite_unregister loescht Registrierung."""
  220. plugin, app = _make_plugin(tmp_path)
  221. mock_sat = MagicMock()
  222. mock_sat.alias = "Kueche"
  223. mock_sat.mac_address = "AA:BB:CC:DD:EE:FF"
  224. mock_sat.id = "sat-2"
  225. app.satellites.get_all = MagicMock(return_value=[mock_sat])
  226. data = _make_data()
  227. result = await plugin.handle_satellite_unregister(data, alias="Kueche")
  228. assert result.success is True
  229. app.registration.delete_registration.assert_called_once_with("AA:BB:CC:DD:EE:FF")
  230. app.satellites.remove.assert_called_once_with("sat-2")
  231. @pytest.mark.asyncio
  232. async def test_satellite_unregister_not_found(self, tmp_path):
  233. """admin_satellite_unregister bei unbekanntem Satellite."""
  234. plugin, app = _make_plugin(tmp_path)
  235. app.satellites.get_all = MagicMock(return_value=[])
  236. data = _make_data()
  237. result = await plugin.handle_satellite_unregister(data, alias="Unbekannt")
  238. assert result.success is True
  239. assert "kein satellite" in result.response_text.lower()
  240. @pytest.mark.asyncio
  241. async def test_satellite_unregister_no_alias(self, tmp_path):
  242. """admin_satellite_unregister ohne Alias gibt Fehler."""
  243. plugin, _ = _make_plugin(tmp_path)
  244. data = _make_data()
  245. result = await plugin.handle_satellite_unregister(data, alias="")
  246. assert result.success is False
  247. # ============================================================================
  248. # Tests: Plugin Management (14)
  249. # ============================================================================
  250. class TestPluginManagement:
  251. """Tests fuer Plugin-Management Intents."""
  252. @pytest.mark.asyncio
  253. async def test_plugin_count(self, tmp_path):
  254. """admin_plugin_count gibt geladen/aktiv zurueck."""
  255. plugin, _ = _make_plugin(tmp_path)
  256. data = _make_data()
  257. result = await plugin.handle_plugin_count(data)
  258. assert result.success is True
  259. assert "5" in result.response_text
  260. assert "4" in result.response_text
  261. # ============================================================================
  262. # Tests: Training (15-19)
  263. # ============================================================================
  264. class TestTraining:
  265. """Tests fuer Training-Intents."""
  266. @pytest.mark.asyncio
  267. async def test_training_status_active(self, tmp_path):
  268. """admin_training_status bei laufendem Training."""
  269. plugin, app = _make_plugin(tmp_path)
  270. mock_trainer = MagicMock()
  271. mock_trainer.active_trainer_id = "wakeword"
  272. mock_trainer.current_progress = MagicMock()
  273. mock_trainer.current_progress.state = MagicMock()
  274. mock_trainer.current_progress.state.name = "TRAINING"
  275. app.services.get_service = MagicMock(return_value=mock_trainer)
  276. data = _make_data()
  277. result = await plugin.handle_training_status(data)
  278. assert result.success is True
  279. assert "wakeword" in result.response_text
  280. @pytest.mark.asyncio
  281. async def test_training_status_idle(self, tmp_path):
  282. """admin_training_status ohne aktives Training."""
  283. plugin, app = _make_plugin(tmp_path)
  284. mock_trainer = MagicMock()
  285. mock_trainer.active_trainer_id = ""
  286. app.services.get_service = MagicMock(return_value=mock_trainer)
  287. data = _make_data()
  288. result = await plugin.handle_training_status(data)
  289. assert result.success is True
  290. assert "nein" in result.response_text.lower()
  291. @pytest.mark.asyncio
  292. async def test_training_start(self, tmp_path):
  293. """admin_training_start startet Training."""
  294. plugin, app = _make_plugin(tmp_path)
  295. mock_trainer = MagicMock()
  296. mock_trainer.start_training = AsyncMock(return_value=(True, ""))
  297. app.services.get_service = MagicMock(return_value=mock_trainer)
  298. data = _make_data()
  299. result = await plugin.handle_training_start(data, model_type="wakeword")
  300. assert result.success is True
  301. mock_trainer.start_training.assert_called_once_with("wakeword")
  302. @pytest.mark.asyncio
  303. async def test_training_start_no_type(self, tmp_path):
  304. """admin_training_start ohne model_type gibt Fehler."""
  305. plugin, _ = _make_plugin(tmp_path)
  306. data = _make_data()
  307. result = await plugin.handle_training_start(data, model_type="")
  308. assert result.success is False
  309. @pytest.mark.asyncio
  310. async def test_training_stop(self, tmp_path):
  311. """admin_training_stop stoppt Training."""
  312. plugin, app = _make_plugin(tmp_path)
  313. mock_trainer = MagicMock()
  314. mock_trainer.stop_training = AsyncMock(return_value=(True, ""))
  315. app.services.get_service = MagicMock(return_value=mock_trainer)
  316. data = _make_data()
  317. result = await plugin.handle_training_stop(data)
  318. assert result.success is True
  319. @pytest.mark.asyncio
  320. async def test_training_last(self, tmp_path):
  321. """admin_training_last zeigt letztes Training."""
  322. plugin, app = _make_plugin(tmp_path)
  323. mock_tracker = MagicMock()
  324. mock_tracker.get_last_training = MagicMock(return_value={
  325. "trainer_id": "wakeword",
  326. "started_at": "2026-03-14T10:00:00+00:00",
  327. "success": True,
  328. "duration_seconds": 3600,
  329. })
  330. app.services.get_service = MagicMock(return_value=mock_tracker)
  331. data = _make_data()
  332. result = await plugin.handle_training_last(data)
  333. assert result.success is True
  334. assert "wakeword" in result.response_text
  335. assert "erfolgreich" in result.response_text
  336. @pytest.mark.asyncio
  337. async def test_training_last_none(self, tmp_path):
  338. """admin_training_last ohne Training-Historie."""
  339. plugin, app = _make_plugin(tmp_path)
  340. mock_tracker = MagicMock()
  341. mock_tracker.get_last_training = MagicMock(return_value=None)
  342. app.services.get_service = MagicMock(return_value=mock_tracker)
  343. data = _make_data()
  344. result = await plugin.handle_training_last(data)
  345. assert result.success is True
  346. assert "noch kein" in result.response_text.lower()
  347. @pytest.mark.asyncio
  348. async def test_training_last_specific(self, tmp_path):
  349. """admin_training_last_specific filtert nach Typ."""
  350. plugin, app = _make_plugin(tmp_path)
  351. mock_tracker = MagicMock()
  352. mock_tracker.get_last_training = MagicMock(return_value={
  353. "trainer_id": "voice",
  354. "started_at": "2026-03-13T08:00:00+00:00",
  355. "success": False,
  356. "duration_seconds": 120,
  357. })
  358. app.services.get_service = MagicMock(return_value=mock_tracker)
  359. data = _make_data()
  360. result = await plugin.handle_training_last_specific(data, model_type="voice")
  361. assert result.success is True
  362. assert "fehlgeschlagen" in result.response_text
  363. # ============================================================================
  364. # Tests: History / Logs (20-27)
  365. # ============================================================================
  366. class TestHistoryLogs:
  367. """Tests fuer History/Log-Intents."""
  368. def _make_tracker_plugin(self, tmp_path, tracker_setup=None):
  369. """Plugin mit Mock-Tracker erstellen."""
  370. plugin, app = _make_plugin(tmp_path)
  371. mock_tracker = MagicMock()
  372. if tracker_setup:
  373. tracker_setup(mock_tracker)
  374. app.services.get_service = MagicMock(return_value=mock_tracker)
  375. return plugin, mock_tracker
  376. @pytest.mark.asyncio
  377. async def test_last_requests(self, tmp_path):
  378. """admin_last_requests zeigt letzte Anfragen."""
  379. plugin, tracker = self._make_tracker_plugin(tmp_path)
  380. tracker.get_last_intents = MagicMock(return_value=[
  381. {"intent": "wetter", "original_text": "Wie ist das Wetter?"},
  382. {"intent": "timer", "original_text": "Stell einen Timer"},
  383. ])
  384. data = _make_data()
  385. result = await plugin.handle_last_requests(data, count=2)
  386. assert result.success is True
  387. assert "wetter" in result.response_text
  388. assert "timer" in result.response_text
  389. @pytest.mark.asyncio
  390. async def test_last_requests_empty(self, tmp_path):
  391. """admin_last_requests ohne Anfragen."""
  392. plugin, tracker = self._make_tracker_plugin(tmp_path)
  393. tracker.get_last_intents = MagicMock(return_value=[])
  394. data = _make_data()
  395. result = await plugin.handle_last_requests(data, count=5)
  396. assert result.success is True
  397. assert "keine" in result.response_text.lower()
  398. @pytest.mark.asyncio
  399. async def test_last_request_room(self, tmp_path):
  400. """admin_last_request_room zeigt letzte Anfrage im Raum."""
  401. plugin, tracker = self._make_tracker_plugin(tmp_path)
  402. tracker.get_last_intents_by_room = MagicMock(return_value=[
  403. {"original_text": "Timer auf 5 Minuten"},
  404. ])
  405. data = _make_data()
  406. result = await plugin.handle_last_request_room(data, room="kueche")
  407. assert result.success is True
  408. assert "Timer" in result.response_text
  409. @pytest.mark.asyncio
  410. async def test_last_request_room_no_room(self, tmp_path):
  411. """admin_last_request_room ohne Raum gibt Fehler."""
  412. plugin, _ = _make_plugin(tmp_path)
  413. data = _make_data()
  414. result = await plugin.handle_last_request_room(data, room="")
  415. assert result.success is False
  416. @pytest.mark.asyncio
  417. async def test_last_conversation(self, tmp_path):
  418. """admin_last_conversation zeigt letzte Conversation."""
  419. plugin, tracker = self._make_tracker_plugin(tmp_path)
  420. tracker.get_last_conversations = MagicMock(return_value=[
  421. {"started_at": "2026-03-14T10:00:00+00:00", "satellite_id": "sat-1"},
  422. ])
  423. data = _make_data()
  424. result = await plugin.handle_last_conversation(data)
  425. assert result.success is True
  426. assert "14.03.2026" in result.response_text
  427. @pytest.mark.asyncio
  428. async def test_last_conversations(self, tmp_path):
  429. """admin_last_conversations zeigt mehrere Conversations."""
  430. plugin, tracker = self._make_tracker_plugin(tmp_path)
  431. tracker.get_last_conversations = MagicMock(return_value=[
  432. {"started_at": "2026-03-13T08:00:00+00:00", "satellite_id": "sat-1"},
  433. {"started_at": "2026-03-14T10:00:00+00:00", "satellite_id": "sat-2"},
  434. ])
  435. data = _make_data()
  436. result = await plugin.handle_last_conversations(data, count=2)
  437. assert result.success is True
  438. assert "2" in result.response_text
  439. @pytest.mark.asyncio
  440. async def test_last_errors(self, tmp_path):
  441. """admin_last_errors zeigt letzte Fehler."""
  442. plugin, tracker = self._make_tracker_plugin(tmp_path)
  443. tracker.get_last_errors = MagicMock(return_value=[
  444. {"source": "NetworkService", "message": "Connection timeout"},
  445. ])
  446. data = _make_data()
  447. result = await plugin.handle_last_errors(data, count=3)
  448. assert result.success is True
  449. assert "NetworkService" in result.response_text
  450. @pytest.mark.asyncio
  451. async def test_error_count_hours(self, tmp_path):
  452. """admin_error_count_hours zaehlt Fehler."""
  453. plugin, tracker = self._make_tracker_plugin(tmp_path)
  454. tracker.get_error_count_since = MagicMock(return_value=7)
  455. data = _make_data()
  456. result = await plugin.handle_error_count_hours(data, hours=24)
  457. assert result.success is True
  458. assert "7" in result.response_text
  459. @pytest.mark.asyncio
  460. async def test_config_tool_last(self, tmp_path):
  461. """admin_config_tool_last zeigt letzte Config-Tool-Verbindung."""
  462. plugin, tracker = self._make_tracker_plugin(tmp_path)
  463. tracker.get_last_config_connection = MagicMock(return_value={
  464. "connected_at": "2026-03-14T10:00:00+00:00",
  465. "peer_address": "10.0.0.5:54321",
  466. })
  467. data = _make_data()
  468. result = await plugin.handle_config_tool_last(data)
  469. assert result.success is True
  470. assert "10.0.0.5" in result.response_text
  471. @pytest.mark.asyncio
  472. async def test_config_tool_last_none(self, tmp_path):
  473. """admin_config_tool_last ohne Historie."""
  474. plugin, tracker = self._make_tracker_plugin(tmp_path)
  475. tracker.get_last_config_connection = MagicMock(return_value=None)
  476. data = _make_data()
  477. result = await plugin.handle_config_tool_last(data)
  478. assert result.success is True
  479. assert "kein" in result.response_text.lower()
  480. @pytest.mark.asyncio
  481. async def test_cache_clear(self, tmp_path):
  482. """admin_cache_clear ruft tracker.clear_all auf."""
  483. plugin, tracker = self._make_tracker_plugin(tmp_path)
  484. data = _make_data()
  485. result = await plugin.handle_cache_clear(data)
  486. assert result.success is True
  487. tracker.clear_all.assert_called_once()
  488. @pytest.mark.asyncio
  489. async def test_cache_clear_no_tracker(self, tmp_path):
  490. """admin_cache_clear ohne Tracker gibt Fehler."""
  491. plugin, app = _make_plugin(tmp_path)
  492. app.services.get_service = MagicMock(return_value=None)
  493. data = _make_data()
  494. result = await plugin.handle_cache_clear(data)
  495. assert result.success is False
  496. # ============================================================================
  497. # Tests: Hilfsmethoden
  498. # ============================================================================
  499. class TestHelpers:
  500. """Tests fuer Plugin-interne Hilfsmethoden."""
  501. def test_format_duration_hours_minutes(self):
  502. """_format_duration formatiert Stunden und Minuten."""
  503. result = AdminCommandsPlugin._format_duration(3661)
  504. assert "1 Stunde" in result
  505. assert "1 Minute" in result
  506. def test_format_duration_days(self):
  507. """_format_duration mit Tagen."""
  508. result = AdminCommandsPlugin._format_duration(90061)
  509. assert "1 Tag" in result # 1 Tag, 1 Stunde, 1 Minute
  510. def test_format_duration_zero(self):
  511. """_format_duration bei 0 Sekunden."""
  512. result = AdminCommandsPlugin._format_duration(0)
  513. assert "weniger" in result
  514. def test_format_duration_plural(self):
  515. """_format_duration mit Plural."""
  516. result = AdminCommandsPlugin._format_duration(7200 + 120)
  517. assert "2 Stunden" in result
  518. assert "2 Minuten" in result
  519. def test_get_primary_ip(self):
  520. """_get_primary_ip gibt eine IP zurueck."""
  521. ip = AdminCommandsPlugin._get_primary_ip()
  522. assert "." in ip
  523. def test_format_training_entry_success(self):
  524. """_format_training_entry bei erfolgreichem Training."""
  525. entry = {
  526. "trainer_id": "wakeword",
  527. "started_at": "2026-03-14T10:00:00+00:00",
  528. "success": True,
  529. "duration_seconds": 3600,
  530. }
  531. result = AdminCommandsPlugin._format_training_entry(entry)
  532. assert "wakeword" in result
  533. assert "erfolgreich" in result
  534. def test_format_training_entry_failed(self):
  535. """_format_training_entry bei fehlgeschlagenem Training."""
  536. entry = {
  537. "trainer_id": "voice",
  538. "started_at": "2026-03-13T08:00:00+00:00",
  539. "success": False,
  540. "duration_seconds": 60,
  541. }
  542. result = AdminCommandsPlugin._format_training_entry(entry)
  543. assert "fehlgeschlagen" in result