| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649 |
- # -*- coding: utf-8 -*-
- """
- Tests fuer das AdminCommands Plugin.
- Testet alle 27 Intent-Handler mit Mock-Application.
- """
- import socket
- import time
- from pathlib import Path
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from trixy_core.nlp.handler import IntentReceivedData, IntentResult
- from plugins.admin_commands.main import AdminCommandsPlugin
- # ============================================================================
- # Fixtures
- # ============================================================================
- def _make_plugin(tmp_path, **config_overrides) -> tuple[AdminCommandsPlugin, MagicMock]:
- """Erstellt ein AdminCommandsPlugin mit Mock-Application."""
- mock_app = MagicMock()
- mock_app.events = MagicMock()
- mock_app.events.emit = AsyncMock()
- mock_app.events.trigger = AsyncMock()
- mock_app.events.register = MagicMock()
- mock_app.debug = True
- # Mock ServiceContainer
- mock_app.services = MagicMock()
- mock_app.services.get_service = MagicMock(return_value=None)
- # Mock SatelliteManager
- mock_app.satellites = MagicMock()
- mock_app.satellites.count = 3
- mock_app.satellites.connected_count = 2
- mock_app.satellites.get_all = MagicMock(return_value=[])
- # Mock RegistrationManager
- mock_app.registration = MagicMock()
- mock_app.registration.registration_mode = False
- mock_app.registration.enable_registration_mode = MagicMock()
- mock_app.registration.disable_registration_mode = MagicMock()
- mock_app.registration.delete_registration = MagicMock(return_value=True)
- # Mock PluginManager
- mock_app.plugins = MagicMock()
- mock_app.plugins.count = 5
- mock_app.plugins.enabled_count = 4
- config = {
- "enabled": True,
- "retention_days": 7,
- "registration_timeout_seconds": 60,
- **config_overrides,
- }
- plugin_path = tmp_path / "admin_commands"
- plugin_path.mkdir(exist_ok=True)
- plugin = AdminCommandsPlugin(
- application=mock_app,
- plugin_path=plugin_path,
- config=config,
- )
- plugin._start_time = time.time() - 3661 # ~1h 1min
- return plugin, mock_app
- def _make_data(**kwargs) -> IntentReceivedData:
- """Erstellt IntentReceivedData mit Defaults."""
- defaults = {
- "intent": "test",
- "satellite_id": "sat-1",
- "room_id": "wohnzimmer",
- "is_authenticated": True,
- }
- defaults.update(kwargs)
- return IntentReceivedData(**defaults)
- @pytest.fixture
- def plugin_and_app(tmp_path):
- """Standard Plugin + App Fixture."""
- return _make_plugin(tmp_path)
- # ============================================================================
- # Tests: System Info (1-6)
- # ============================================================================
- class TestSystemInfo:
- """Tests fuer System-Info Intents."""
- @pytest.mark.asyncio
- async def test_hostname_query(self, tmp_path):
- """admin_hostname_query gibt Hostnamen zurueck."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data(intent="admin_hostname_query")
- result = await plugin.handle_hostname_query(data)
- assert result.success is True
- assert socket.gethostname() in result.response_text
- @pytest.mark.asyncio
- async def test_domain_query(self, tmp_path):
- """admin_domain_query gibt FQDN zurueck."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data(intent="admin_domain_query")
- result = await plugin.handle_domain_query(data)
- assert result.success is True
- assert socket.getfqdn() in result.response_text
- @pytest.mark.asyncio
- async def test_ip_query(self, tmp_path):
- """admin_ip_query gibt eine IP-Adresse zurueck."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data(intent="admin_ip_query")
- result = await plugin.handle_ip_query(data)
- assert result.success is True
- # Antwort enthaelt eine IP (mindestens Punkt-Notation)
- assert "." in result.response_text
- @pytest.mark.asyncio
- async def test_uptime_query(self, tmp_path):
- """admin_uptime_query gibt Laufzeit zurueck."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data(intent="admin_uptime_query")
- result = await plugin.handle_uptime_query(data)
- assert result.success is True
- assert "1 Stunde" in result.response_text
- @pytest.mark.asyncio
- async def test_system_time_set_no_value(self, tmp_path):
- """admin_system_time_set ohne Wert gibt Fehler."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data(intent="admin_system_time_set")
- result = await plugin.handle_system_time_set(data, time_value="")
- assert result.success is False
- @pytest.mark.asyncio
- async def test_system_date_set_no_value(self, tmp_path):
- """admin_system_date_set ohne Wert gibt Fehler."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data(intent="admin_system_date_set")
- result = await plugin.handle_system_date_set(data, date_value="")
- assert result.success is False
- # ============================================================================
- # Tests: Satellite Management (7-13)
- # ============================================================================
- class TestSatelliteManagement:
- """Tests fuer Satellite-Verwaltung Intents."""
- @pytest.mark.asyncio
- async def test_registration_enable(self, tmp_path):
- """admin_registration_enable aktiviert Registrierung."""
- plugin, app = _make_plugin(tmp_path)
- data = _make_data(intent="admin_registration_enable")
- result = await plugin.handle_registration_enable(data)
- assert result.success is True
- app.registration.enable_registration_mode.assert_called_once_with(60)
- @pytest.mark.asyncio
- async def test_registration_enable_custom_timeout(self, tmp_path):
- """admin_registration_enable mit konfiguriertem Timeout."""
- plugin, app = _make_plugin(tmp_path, registration_timeout_seconds=120)
- data = _make_data()
- result = await plugin.handle_registration_enable(data)
- assert result.success is True
- app.registration.enable_registration_mode.assert_called_once_with(120)
- @pytest.mark.asyncio
- async def test_registration_disable(self, tmp_path):
- """admin_registration_disable deaktiviert Registrierung."""
- plugin, app = _make_plugin(tmp_path)
- data = _make_data()
- result = await plugin.handle_registration_disable(data)
- assert result.success is True
- app.registration.disable_registration_mode.assert_called_once()
- @pytest.mark.asyncio
- async def test_registration_status_active(self, tmp_path):
- """admin_registration_status bei aktivem Modus."""
- plugin, app = _make_plugin(tmp_path)
- app.registration.registration_mode = True
- data = _make_data()
- result = await plugin.handle_registration_status(data)
- assert result.success is True
- assert "aktiv" in result.response_text.lower()
- @pytest.mark.asyncio
- async def test_registration_status_inactive(self, tmp_path):
- """admin_registration_status bei inaktivem Modus."""
- plugin, app = _make_plugin(tmp_path)
- app.registration.registration_mode = False
- data = _make_data()
- result = await plugin.handle_registration_status(data)
- assert result.success is True
- assert "nicht aktiv" in result.response_text.lower()
- @pytest.mark.asyncio
- async def test_satellite_count_registered(self, tmp_path):
- """admin_satellite_count_registered gibt Anzahl zurueck."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data()
- result = await plugin.handle_satellite_count_registered(data)
- assert result.success is True
- assert "3" in result.response_text
- @pytest.mark.asyncio
- async def test_satellite_count_connected(self, tmp_path):
- """admin_satellite_count_connected gibt Anzahl zurueck."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data()
- result = await plugin.handle_satellite_count_connected(data)
- assert result.success is True
- assert "2" in result.response_text
- @pytest.mark.asyncio
- async def test_satellite_last_connected_no_alias(self, tmp_path):
- """admin_satellite_last_connected ohne Alias gibt Fehler."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data()
- result = await plugin.handle_satellite_last_connected(data, alias="")
- assert result.success is False
- @pytest.mark.asyncio
- async def test_satellite_last_connected_with_tracker(self, tmp_path):
- """admin_satellite_last_connected mit Tracker und Historie."""
- plugin, app = _make_plugin(tmp_path)
- mock_tracker = MagicMock()
- mock_tracker.get_satellite_connection_history = MagicMock(return_value=[
- {"connected_at": "2026-03-14T10:00:00+00:00", "alias": "Wohnzimmer"}
- ])
- app.services.get_service = MagicMock(return_value=mock_tracker)
- data = _make_data()
- result = await plugin.handle_satellite_last_connected(data, alias="Wohnzimmer")
- assert result.success is True
- assert "14.03.2026" in result.response_text
- @pytest.mark.asyncio
- async def test_satellite_last_connected_no_history(self, tmp_path):
- """admin_satellite_last_connected ohne Historie."""
- plugin, app = _make_plugin(tmp_path)
- mock_tracker = MagicMock()
- mock_tracker.get_satellite_connection_history = MagicMock(return_value=[])
- app.services.get_service = MagicMock(return_value=mock_tracker)
- data = _make_data()
- result = await plugin.handle_satellite_last_connected(data, alias="Unknown")
- assert result.success is True
- assert "keine" in result.response_text.lower()
- @pytest.mark.asyncio
- async def test_satellite_unregister(self, tmp_path):
- """admin_satellite_unregister loescht Registrierung."""
- plugin, app = _make_plugin(tmp_path)
- mock_sat = MagicMock()
- mock_sat.alias = "Kueche"
- mock_sat.mac_address = "AA:BB:CC:DD:EE:FF"
- mock_sat.id = "sat-2"
- app.satellites.get_all = MagicMock(return_value=[mock_sat])
- data = _make_data()
- result = await plugin.handle_satellite_unregister(data, alias="Kueche")
- assert result.success is True
- app.registration.delete_registration.assert_called_once_with("AA:BB:CC:DD:EE:FF")
- app.satellites.remove.assert_called_once_with("sat-2")
- @pytest.mark.asyncio
- async def test_satellite_unregister_not_found(self, tmp_path):
- """admin_satellite_unregister bei unbekanntem Satellite."""
- plugin, app = _make_plugin(tmp_path)
- app.satellites.get_all = MagicMock(return_value=[])
- data = _make_data()
- result = await plugin.handle_satellite_unregister(data, alias="Unbekannt")
- assert result.success is True
- assert "kein satellite" in result.response_text.lower()
- @pytest.mark.asyncio
- async def test_satellite_unregister_no_alias(self, tmp_path):
- """admin_satellite_unregister ohne Alias gibt Fehler."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data()
- result = await plugin.handle_satellite_unregister(data, alias="")
- assert result.success is False
- # ============================================================================
- # Tests: Plugin Management (14)
- # ============================================================================
- class TestPluginManagement:
- """Tests fuer Plugin-Management Intents."""
- @pytest.mark.asyncio
- async def test_plugin_count(self, tmp_path):
- """admin_plugin_count gibt geladen/aktiv zurueck."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data()
- result = await plugin.handle_plugin_count(data)
- assert result.success is True
- assert "5" in result.response_text
- assert "4" in result.response_text
- # ============================================================================
- # Tests: Training (15-19)
- # ============================================================================
- class TestTraining:
- """Tests fuer Training-Intents."""
- @pytest.mark.asyncio
- async def test_training_status_active(self, tmp_path):
- """admin_training_status bei laufendem Training."""
- plugin, app = _make_plugin(tmp_path)
- mock_trainer = MagicMock()
- mock_trainer.active_trainer_id = "wakeword"
- mock_trainer.current_progress = MagicMock()
- mock_trainer.current_progress.state = MagicMock()
- mock_trainer.current_progress.state.name = "TRAINING"
- app.services.get_service = MagicMock(return_value=mock_trainer)
- data = _make_data()
- result = await plugin.handle_training_status(data)
- assert result.success is True
- assert "wakeword" in result.response_text
- @pytest.mark.asyncio
- async def test_training_status_idle(self, tmp_path):
- """admin_training_status ohne aktives Training."""
- plugin, app = _make_plugin(tmp_path)
- mock_trainer = MagicMock()
- mock_trainer.active_trainer_id = ""
- app.services.get_service = MagicMock(return_value=mock_trainer)
- data = _make_data()
- result = await plugin.handle_training_status(data)
- assert result.success is True
- assert "nein" in result.response_text.lower()
- @pytest.mark.asyncio
- async def test_training_start(self, tmp_path):
- """admin_training_start startet Training."""
- plugin, app = _make_plugin(tmp_path)
- mock_trainer = MagicMock()
- mock_trainer.start_training = AsyncMock(return_value=(True, ""))
- app.services.get_service = MagicMock(return_value=mock_trainer)
- data = _make_data()
- result = await plugin.handle_training_start(data, model_type="wakeword")
- assert result.success is True
- mock_trainer.start_training.assert_called_once_with("wakeword")
- @pytest.mark.asyncio
- async def test_training_start_no_type(self, tmp_path):
- """admin_training_start ohne model_type gibt Fehler."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data()
- result = await plugin.handle_training_start(data, model_type="")
- assert result.success is False
- @pytest.mark.asyncio
- async def test_training_stop(self, tmp_path):
- """admin_training_stop stoppt Training."""
- plugin, app = _make_plugin(tmp_path)
- mock_trainer = MagicMock()
- mock_trainer.stop_training = AsyncMock(return_value=(True, ""))
- app.services.get_service = MagicMock(return_value=mock_trainer)
- data = _make_data()
- result = await plugin.handle_training_stop(data)
- assert result.success is True
- @pytest.mark.asyncio
- async def test_training_last(self, tmp_path):
- """admin_training_last zeigt letztes Training."""
- plugin, app = _make_plugin(tmp_path)
- mock_tracker = MagicMock()
- mock_tracker.get_last_training = MagicMock(return_value={
- "trainer_id": "wakeword",
- "started_at": "2026-03-14T10:00:00+00:00",
- "success": True,
- "duration_seconds": 3600,
- })
- app.services.get_service = MagicMock(return_value=mock_tracker)
- data = _make_data()
- result = await plugin.handle_training_last(data)
- assert result.success is True
- assert "wakeword" in result.response_text
- assert "erfolgreich" in result.response_text
- @pytest.mark.asyncio
- async def test_training_last_none(self, tmp_path):
- """admin_training_last ohne Training-Historie."""
- plugin, app = _make_plugin(tmp_path)
- mock_tracker = MagicMock()
- mock_tracker.get_last_training = MagicMock(return_value=None)
- app.services.get_service = MagicMock(return_value=mock_tracker)
- data = _make_data()
- result = await plugin.handle_training_last(data)
- assert result.success is True
- assert "noch kein" in result.response_text.lower()
- @pytest.mark.asyncio
- async def test_training_last_specific(self, tmp_path):
- """admin_training_last_specific filtert nach Typ."""
- plugin, app = _make_plugin(tmp_path)
- mock_tracker = MagicMock()
- mock_tracker.get_last_training = MagicMock(return_value={
- "trainer_id": "voice",
- "started_at": "2026-03-13T08:00:00+00:00",
- "success": False,
- "duration_seconds": 120,
- })
- app.services.get_service = MagicMock(return_value=mock_tracker)
- data = _make_data()
- result = await plugin.handle_training_last_specific(data, model_type="voice")
- assert result.success is True
- assert "fehlgeschlagen" in result.response_text
- # ============================================================================
- # Tests: History / Logs (20-27)
- # ============================================================================
- class TestHistoryLogs:
- """Tests fuer History/Log-Intents."""
- def _make_tracker_plugin(self, tmp_path, tracker_setup=None):
- """Plugin mit Mock-Tracker erstellen."""
- plugin, app = _make_plugin(tmp_path)
- mock_tracker = MagicMock()
- if tracker_setup:
- tracker_setup(mock_tracker)
- app.services.get_service = MagicMock(return_value=mock_tracker)
- return plugin, mock_tracker
- @pytest.mark.asyncio
- async def test_last_requests(self, tmp_path):
- """admin_last_requests zeigt letzte Anfragen."""
- plugin, tracker = self._make_tracker_plugin(tmp_path)
- tracker.get_last_intents = MagicMock(return_value=[
- {"intent": "wetter", "original_text": "Wie ist das Wetter?"},
- {"intent": "timer", "original_text": "Stell einen Timer"},
- ])
- data = _make_data()
- result = await plugin.handle_last_requests(data, count=2)
- assert result.success is True
- assert "wetter" in result.response_text
- assert "timer" in result.response_text
- @pytest.mark.asyncio
- async def test_last_requests_empty(self, tmp_path):
- """admin_last_requests ohne Anfragen."""
- plugin, tracker = self._make_tracker_plugin(tmp_path)
- tracker.get_last_intents = MagicMock(return_value=[])
- data = _make_data()
- result = await plugin.handle_last_requests(data, count=5)
- assert result.success is True
- assert "keine" in result.response_text.lower()
- @pytest.mark.asyncio
- async def test_last_request_room(self, tmp_path):
- """admin_last_request_room zeigt letzte Anfrage im Raum."""
- plugin, tracker = self._make_tracker_plugin(tmp_path)
- tracker.get_last_intents_by_room = MagicMock(return_value=[
- {"original_text": "Timer auf 5 Minuten"},
- ])
- data = _make_data()
- result = await plugin.handle_last_request_room(data, room="kueche")
- assert result.success is True
- assert "Timer" in result.response_text
- @pytest.mark.asyncio
- async def test_last_request_room_no_room(self, tmp_path):
- """admin_last_request_room ohne Raum gibt Fehler."""
- plugin, _ = _make_plugin(tmp_path)
- data = _make_data()
- result = await plugin.handle_last_request_room(data, room="")
- assert result.success is False
- @pytest.mark.asyncio
- async def test_last_conversation(self, tmp_path):
- """admin_last_conversation zeigt letzte Conversation."""
- plugin, tracker = self._make_tracker_plugin(tmp_path)
- tracker.get_last_conversations = MagicMock(return_value=[
- {"started_at": "2026-03-14T10:00:00+00:00", "satellite_id": "sat-1"},
- ])
- data = _make_data()
- result = await plugin.handle_last_conversation(data)
- assert result.success is True
- assert "14.03.2026" in result.response_text
- @pytest.mark.asyncio
- async def test_last_conversations(self, tmp_path):
- """admin_last_conversations zeigt mehrere Conversations."""
- plugin, tracker = self._make_tracker_plugin(tmp_path)
- tracker.get_last_conversations = MagicMock(return_value=[
- {"started_at": "2026-03-13T08:00:00+00:00", "satellite_id": "sat-1"},
- {"started_at": "2026-03-14T10:00:00+00:00", "satellite_id": "sat-2"},
- ])
- data = _make_data()
- result = await plugin.handle_last_conversations(data, count=2)
- assert result.success is True
- assert "2" in result.response_text
- @pytest.mark.asyncio
- async def test_last_errors(self, tmp_path):
- """admin_last_errors zeigt letzte Fehler."""
- plugin, tracker = self._make_tracker_plugin(tmp_path)
- tracker.get_last_errors = MagicMock(return_value=[
- {"source": "NetworkService", "message": "Connection timeout"},
- ])
- data = _make_data()
- result = await plugin.handle_last_errors(data, count=3)
- assert result.success is True
- assert "NetworkService" in result.response_text
- @pytest.mark.asyncio
- async def test_error_count_hours(self, tmp_path):
- """admin_error_count_hours zaehlt Fehler."""
- plugin, tracker = self._make_tracker_plugin(tmp_path)
- tracker.get_error_count_since = MagicMock(return_value=7)
- data = _make_data()
- result = await plugin.handle_error_count_hours(data, hours=24)
- assert result.success is True
- assert "7" in result.response_text
- @pytest.mark.asyncio
- async def test_config_tool_last(self, tmp_path):
- """admin_config_tool_last zeigt letzte Config-Tool-Verbindung."""
- plugin, tracker = self._make_tracker_plugin(tmp_path)
- tracker.get_last_config_connection = MagicMock(return_value={
- "connected_at": "2026-03-14T10:00:00+00:00",
- "peer_address": "10.0.0.5:54321",
- })
- data = _make_data()
- result = await plugin.handle_config_tool_last(data)
- assert result.success is True
- assert "10.0.0.5" in result.response_text
- @pytest.mark.asyncio
- async def test_config_tool_last_none(self, tmp_path):
- """admin_config_tool_last ohne Historie."""
- plugin, tracker = self._make_tracker_plugin(tmp_path)
- tracker.get_last_config_connection = MagicMock(return_value=None)
- data = _make_data()
- result = await plugin.handle_config_tool_last(data)
- assert result.success is True
- assert "kein" in result.response_text.lower()
- @pytest.mark.asyncio
- async def test_cache_clear(self, tmp_path):
- """admin_cache_clear ruft tracker.clear_all auf."""
- plugin, tracker = self._make_tracker_plugin(tmp_path)
- data = _make_data()
- result = await plugin.handle_cache_clear(data)
- assert result.success is True
- tracker.clear_all.assert_called_once()
- @pytest.mark.asyncio
- async def test_cache_clear_no_tracker(self, tmp_path):
- """admin_cache_clear ohne Tracker gibt Fehler."""
- plugin, app = _make_plugin(tmp_path)
- app.services.get_service = MagicMock(return_value=None)
- data = _make_data()
- result = await plugin.handle_cache_clear(data)
- assert result.success is False
- # ============================================================================
- # Tests: Hilfsmethoden
- # ============================================================================
- class TestHelpers:
- """Tests fuer Plugin-interne Hilfsmethoden."""
- def test_format_duration_hours_minutes(self):
- """_format_duration formatiert Stunden und Minuten."""
- result = AdminCommandsPlugin._format_duration(3661)
- assert "1 Stunde" in result
- assert "1 Minute" in result
- def test_format_duration_days(self):
- """_format_duration mit Tagen."""
- result = AdminCommandsPlugin._format_duration(90061)
- assert "1 Tag" in result # 1 Tag, 1 Stunde, 1 Minute
- def test_format_duration_zero(self):
- """_format_duration bei 0 Sekunden."""
- result = AdminCommandsPlugin._format_duration(0)
- assert "weniger" in result
- def test_format_duration_plural(self):
- """_format_duration mit Plural."""
- result = AdminCommandsPlugin._format_duration(7200 + 120)
- assert "2 Stunden" in result
- assert "2 Minuten" in result
- def test_get_primary_ip(self):
- """_get_primary_ip gibt eine IP zurueck."""
- ip = AdminCommandsPlugin._get_primary_ip()
- assert "." in ip
- def test_format_training_entry_success(self):
- """_format_training_entry bei erfolgreichem Training."""
- entry = {
- "trainer_id": "wakeword",
- "started_at": "2026-03-14T10:00:00+00:00",
- "success": True,
- "duration_seconds": 3600,
- }
- result = AdminCommandsPlugin._format_training_entry(entry)
- assert "wakeword" in result
- assert "erfolgreich" in result
- def test_format_training_entry_failed(self):
- """_format_training_entry bei fehlgeschlagenem Training."""
- entry = {
- "trainer_id": "voice",
- "started_at": "2026-03-13T08:00:00+00:00",
- "success": False,
- "duration_seconds": 60,
- }
- result = AdminCommandsPlugin._format_training_entry(entry)
- assert "fehlgeschlagen" in result
|