test_notify_callback_isolation.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. """Optional Notify hooks must never interrupt essential printer callbacks."""
  2. from types import SimpleNamespace
  3. from unittest.mock import AsyncMock, MagicMock, patch
  4. import pytest
  5. from backend.app import main
  6. from backend.app.services.bambu_mqtt import PrinterState
  7. class ReachedNextStep(Exception):
  8. """Stop a large callback once its essential post-hook action was reached."""
  9. async def test_status_broadcast_survives_notify_observer_failure(monkeypatch):
  10. monkeypatch.setattr(main, "_last_status_broadcast", {})
  11. monkeypatch.setattr(main, "_printer_last_connected", {})
  12. monkeypatch.setattr(main, "_printer_reconciled_since_connect", {})
  13. printer_manager = MagicMock()
  14. printer_manager.get_printer.return_value = None
  15. printer_manager.get_model.return_value = ""
  16. broadcast = AsyncMock()
  17. with (
  18. patch.object(main.notify_live_activities, "observe", side_effect=RuntimeError("Notify failed")),
  19. patch.object(main, "printer_manager", printer_manager),
  20. patch.object(main.ws_manager, "send_printer_status", broadcast),
  21. patch.object(main, "printer_state_to_dict", return_value={"state": "IDLE"}),
  22. patch.object(main, "spawn_background_task", side_effect=lambda coro, **kwargs: coro.close()),
  23. ):
  24. await main.on_printer_status_change(42, PrinterState())
  25. broadcast.assert_awaited_once()
  26. async def test_print_start_still_resets_milestones_when_notify_fails(monkeypatch):
  27. milestones = {42: 75}
  28. stopped = {42}
  29. monkeypatch.setattr(main, "_last_progress_milestone", milestones)
  30. monkeypatch.setattr(main, "_user_stopped_printers", stopped)
  31. with (
  32. patch.object(main.notify_live_activities, "print_started", side_effect=RuntimeError("Notify failed")),
  33. patch.object(main.printer_manager, "get_status", return_value=PrinterState()),
  34. patch.object(
  35. main, "_kill_switch_notification_tasks", SimpleNamespace(pop=MagicMock(side_effect=ReachedNextStep))
  36. ),
  37. pytest.raises(ReachedNextStep),
  38. ):
  39. await main.on_print_start(42, {})
  40. assert milestones[42] == 0
  41. assert 42 not in stopped
  42. async def test_print_complete_still_requires_plate_clear_when_notify_fails(monkeypatch):
  43. monkeypatch.setattr(main, "_kill_switch_notification_tasks", {})
  44. monkeypatch.setattr(main, "_fallback_3mf_retry_tasks", {})
  45. monkeypatch.setattr(main, "_user_stopped_printers", {42})
  46. printer_manager = MagicMock()
  47. printer_manager.set_awaiting_plate_clear.side_effect = ReachedNextStep
  48. with (
  49. patch.object(
  50. main.notify_live_activities, "print_finished", side_effect=RuntimeError("Notify failed")
  51. ) as notify,
  52. patch.object(main, "printer_manager", printer_manager),
  53. patch.object(main, "_recover_fallback_from_cache_before_eviction", AsyncMock()),
  54. patch.object(main, "clear_3mf_cache"),
  55. patch.object(main.ws_manager, "send_print_complete", AsyncMock()) as broadcast,
  56. pytest.raises(ReachedNextStep),
  57. ):
  58. await main.on_print_complete(42, {"status": "failed"})
  59. printer_manager.set_awaiting_plate_clear.assert_called_once_with(42, True)
  60. assert notify.call_args.args[1]["status"] == "cancelled"
  61. broadcast.assert_awaited_once()