test_obico_actions.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. """Regression tests for obico_actions (#1794).
  2. Before #1794, `obico_actions._notify` routed AI failure-detection events
  3. through `notification_service.on_printer_error`, multiplexing them with
  4. HMS hardware errors. Users couldn't subscribe to one without the other,
  5. and the reporter on #1794 found that turning OFF the "Printer Error"
  6. toggle on a Discord provider silently disabled spaghetti alerts too.
  7. This file pins the post-#1794 wiring: `execute_action` calls
  8. `on_ai_failure_detection`, not `on_printer_error`.
  9. """
  10. from contextlib import asynccontextmanager
  11. from types import SimpleNamespace
  12. from unittest.mock import AsyncMock, patch
  13. import pytest
  14. from backend.app.services.obico_actions import execute_action
  15. @asynccontextmanager
  16. async def _fake_session(printer):
  17. result = SimpleNamespace(scalar_one_or_none=lambda: printer)
  18. session = SimpleNamespace(execute=AsyncMock(return_value=result))
  19. yield session
  20. @pytest.fixture
  21. def fake_printer():
  22. return SimpleNamespace(id=7, name="X1 Carbon")
  23. @pytest.fixture(autouse=True)
  24. def _patch_session(fake_printer):
  25. with patch("backend.app.services.obico_actions.async_session", lambda: _fake_session(fake_printer)):
  26. yield
  27. @pytest.mark.parametrize("frame", [None, b"detected-frame"])
  28. async def test_notify_routes_to_on_ai_failure_detection(fake_printer, frame):
  29. """Regression guard for #1794: action='notify' must call
  30. on_ai_failure_detection, not on_printer_error. If anyone reverts the
  31. handoff, the reporter's symptom (Discord silent when "Printer Error"
  32. is OFF and "AI Failure Detection" is ON) returns."""
  33. with (
  34. patch(
  35. "backend.app.services.notification_service.notification_service.on_ai_failure_detection",
  36. new_callable=AsyncMock,
  37. ) as mock_ai,
  38. patch(
  39. "backend.app.services.notification_service.notification_service.on_printer_error",
  40. new_callable=AsyncMock,
  41. ) as mock_err,
  42. ):
  43. await execute_action(
  44. printer_id=fake_printer.id,
  45. action="notify",
  46. task_name="benchy.3mf",
  47. score=0.91,
  48. frame=frame,
  49. )
  50. mock_ai.assert_awaited_once()
  51. mock_err.assert_not_awaited() # the bug the user reported
  52. call_kwargs = mock_ai.await_args.kwargs
  53. assert call_kwargs["printer_id"] == fake_printer.id
  54. assert call_kwargs["printer_name"] == fake_printer.name
  55. assert call_kwargs["task_name"] == "benchy.3mf"
  56. assert call_kwargs["confidence"] == 0.91
  57. assert call_kwargs["action"] == "notify"
  58. assert call_kwargs["image_data"] == frame
  59. assert call_kwargs["print_quality"] is None
  60. async def test_pause_action_still_pauses_and_notifies(fake_printer):
  61. """`pause` calls pause_print AND fires the AI notification — the
  62. notification fan-out shape isn't different for the pause action."""
  63. fake_client = SimpleNamespace(pause_print=lambda: True)
  64. with (
  65. patch(
  66. "backend.app.services.printer_manager.printer_manager.get_client",
  67. return_value=fake_client,
  68. ),
  69. patch(
  70. "backend.app.services.notification_service.notification_service.on_ai_failure_detection",
  71. new_callable=AsyncMock,
  72. ) as mock_ai,
  73. ):
  74. await execute_action(
  75. printer_id=fake_printer.id,
  76. action="pause",
  77. task_name="benchy.3mf",
  78. score=0.5,
  79. )
  80. mock_ai.assert_awaited_once()
  81. assert mock_ai.await_args.kwargs["action"] == "pause"
  82. async def test_notify_swallows_notification_service_exceptions(fake_printer):
  83. """Notification failure must not propagate — Obico's detection loop
  84. keeps polling; one transient Discord blip shouldn't kill it."""
  85. with patch(
  86. "backend.app.services.notification_service.notification_service.on_ai_failure_detection",
  87. new_callable=AsyncMock,
  88. side_effect=RuntimeError("discord 502"),
  89. ):
  90. # Must not raise.
  91. await execute_action(
  92. printer_id=fake_printer.id,
  93. action="notify",
  94. task_name="benchy.3mf",
  95. score=0.91,
  96. )