test_obico_actions.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  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. async def test_notify_routes_to_on_ai_failure_detection(fake_printer):
  28. """Regression guard for #1794: action='notify' must call
  29. on_ai_failure_detection, not on_printer_error. If anyone reverts the
  30. handoff, the reporter's symptom (Discord silent when "Printer Error"
  31. is OFF and "AI Failure Detection" is ON) returns."""
  32. with (
  33. patch(
  34. "backend.app.services.notification_service.notification_service.on_ai_failure_detection",
  35. new_callable=AsyncMock,
  36. ) as mock_ai,
  37. patch(
  38. "backend.app.services.notification_service.notification_service.on_printer_error",
  39. new_callable=AsyncMock,
  40. ) as mock_err,
  41. ):
  42. await execute_action(
  43. printer_id=fake_printer.id,
  44. action="notify",
  45. task_name="benchy.3mf",
  46. score=0.91,
  47. )
  48. mock_ai.assert_awaited_once()
  49. mock_err.assert_not_awaited() # the bug the user reported
  50. call_kwargs = mock_ai.await_args.kwargs
  51. assert call_kwargs["printer_id"] == fake_printer.id
  52. assert call_kwargs["printer_name"] == fake_printer.name
  53. assert call_kwargs["task_name"] == "benchy.3mf"
  54. assert call_kwargs["confidence"] == 0.91
  55. assert call_kwargs["action"] == "notify"
  56. async def test_pause_action_still_pauses_and_notifies(fake_printer):
  57. """`pause` calls pause_print AND fires the AI notification — the
  58. notification fan-out shape isn't different for the pause action."""
  59. fake_client = SimpleNamespace(pause_print=lambda: True)
  60. with (
  61. patch(
  62. "backend.app.services.printer_manager.printer_manager.get_client",
  63. return_value=fake_client,
  64. ),
  65. patch(
  66. "backend.app.services.notification_service.notification_service.on_ai_failure_detection",
  67. new_callable=AsyncMock,
  68. ) as mock_ai,
  69. ):
  70. await execute_action(
  71. printer_id=fake_printer.id,
  72. action="pause",
  73. task_name="benchy.3mf",
  74. score=0.5,
  75. )
  76. mock_ai.assert_awaited_once()
  77. assert mock_ai.await_args.kwargs["action"] == "pause"
  78. async def test_notify_swallows_notification_service_exceptions(fake_printer):
  79. """Notification failure must not propagate — Obico's detection loop
  80. keeps polling; one transient Discord blip shouldn't kill it."""
  81. with patch(
  82. "backend.app.services.notification_service.notification_service.on_ai_failure_detection",
  83. new_callable=AsyncMock,
  84. side_effect=RuntimeError("discord 502"),
  85. ):
  86. # Must not raise.
  87. await execute_action(
  88. printer_id=fake_printer.id,
  89. action="notify",
  90. task_name="benchy.3mf",
  91. score=0.91,
  92. )