build: harden controller v2 runtime

This commit is contained in:
Media
2026-08-06 19:23:02 -05:00
parent 4e185b2e1c
commit 63ea314b18
15 changed files with 818 additions and 103 deletions
+57 -2
View File
@@ -14,8 +14,9 @@ from controller.config import (
StorageConfig,
)
from controller.downloads import canonical_playlist_path
from controller.models import SlotProtectedError
from controller.mqtt_adapter import MqttEvent, PublishReceipt
from controller.service import ControllerService
from controller.service import ControllerService, ControllerUnavailableError
from controller.store import ControllerStore
@@ -82,12 +83,20 @@ class FakeMqttAdapter:
class FakeDownloadManager:
def __init__(self):
self.recovered = False
self.recovery_calls = 0
self.expiry_calls = 0
self.stopped = False
def recover_pending_deletions(self):
self.recovered = True
def schedule_pending_deletion_recovery(self):
self.recovered = True
self.recovery_calls += 1
return True
def expire_due_slots(self):
self.expiry_calls += 1
return []
def shutdown(self, *, wait):
@@ -144,7 +153,7 @@ class ControllerServiceTests(unittest.TestCase):
wall_clock=self.wall,
)
self.service.start()
self.assertTrue(self.downloads.recovered)
self.assertFalse(self.downloads.recovered)
return self.service
def ready_slot(self, slot=1):
@@ -414,6 +423,52 @@ class ControllerServiceTests(unittest.TestCase):
self.assertTrue(health["coordinator_alive"])
self.assertEqual(health["status"], "degraded")
self.assertIn("mqtt_disconnected", health["degraded_reasons"])
self.assertIn("awaiting_player_evidence", health["degraded_reasons"])
def test_cleanup_and_downloads_wait_for_post_start_player_evidence(self):
self.start_service()
self.service._next_expiry = self.monotonic()
time.sleep(0.2)
self.assertEqual(self.downloads.expiry_calls, 0)
self.assertFalse(self.downloads.recovered)
with self.assertRaisesRegex(
ControllerUnavailableError,
"deferred until fresh Player evidence",
):
self.service.enqueue_download(
"https://example.test/deferred",
None,
None,
)
self.send_message(
self.config.topics.player_state,
"idle",
retain=True,
)
self.wait_for(
lambda: self.downloads.recovered,
"retained Player idle did not release deferred recovery",
)
self.wait_for(
lambda: self.downloads.expiry_calls > 0,
"Player idle did not release deferred expiry",
)
self.assertNotIn(
"awaiting_player_evidence",
self.service.health()["degraded_reasons"],
)
def test_manual_delete_requires_explicit_override_while_player_is_unknown(self):
self.start_service()
self.ready_slot(1)
with self.assertRaisesRegex(
SlotProtectedError,
"confirm_player_unknown=true",
):
self.service.submit_control({"action": "delete_slot", "slot": 1})
def test_administrator_can_clear_exact_uncertain_playback_review(self):
self.start_service()