| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113 |
- """Regression tests for obico_actions (#1794).
- Before #1794, `obico_actions._notify` routed AI failure-detection events
- through `notification_service.on_printer_error`, multiplexing them with
- HMS hardware errors. Users couldn't subscribe to one without the other,
- and the reporter on #1794 found that turning OFF the "Printer Error"
- toggle on a Discord provider silently disabled spaghetti alerts too.
- This file pins the post-#1794 wiring: `execute_action` calls
- `on_ai_failure_detection`, not `on_printer_error`.
- """
- from contextlib import asynccontextmanager
- from types import SimpleNamespace
- from unittest.mock import AsyncMock, patch
- import pytest
- from backend.app.services.obico_actions import execute_action
- @asynccontextmanager
- async def _fake_session(printer):
- result = SimpleNamespace(scalar_one_or_none=lambda: printer)
- session = SimpleNamespace(execute=AsyncMock(return_value=result))
- yield session
- @pytest.fixture
- def fake_printer():
- return SimpleNamespace(id=7, name="X1 Carbon")
- @pytest.fixture(autouse=True)
- def _patch_session(fake_printer):
- with patch("backend.app.services.obico_actions.async_session", lambda: _fake_session(fake_printer)):
- yield
- async def test_notify_routes_to_on_ai_failure_detection(fake_printer):
- """Regression guard for #1794: action='notify' must call
- on_ai_failure_detection, not on_printer_error. If anyone reverts the
- handoff, the reporter's symptom (Discord silent when "Printer Error"
- is OFF and "AI Failure Detection" is ON) returns."""
- with (
- patch(
- "backend.app.services.notification_service.notification_service.on_ai_failure_detection",
- new_callable=AsyncMock,
- ) as mock_ai,
- patch(
- "backend.app.services.notification_service.notification_service.on_printer_error",
- new_callable=AsyncMock,
- ) as mock_err,
- ):
- await execute_action(
- printer_id=fake_printer.id,
- action="notify",
- task_name="benchy.3mf",
- score=0.91,
- )
- mock_ai.assert_awaited_once()
- mock_err.assert_not_awaited() # the bug the user reported
- call_kwargs = mock_ai.await_args.kwargs
- assert call_kwargs["printer_id"] == fake_printer.id
- assert call_kwargs["printer_name"] == fake_printer.name
- assert call_kwargs["task_name"] == "benchy.3mf"
- assert call_kwargs["confidence"] == 0.91
- assert call_kwargs["action"] == "notify"
- async def test_pause_action_still_pauses_and_notifies(fake_printer):
- """`pause` calls pause_print AND fires the AI notification — the
- notification fan-out shape isn't different for the pause action."""
- fake_client = SimpleNamespace(pause_print=lambda: True)
- with (
- patch(
- "backend.app.services.printer_manager.printer_manager.get_client",
- return_value=fake_client,
- ),
- patch(
- "backend.app.services.notification_service.notification_service.on_ai_failure_detection",
- new_callable=AsyncMock,
- ) as mock_ai,
- ):
- await execute_action(
- printer_id=fake_printer.id,
- action="pause",
- task_name="benchy.3mf",
- score=0.5,
- )
- mock_ai.assert_awaited_once()
- assert mock_ai.await_args.kwargs["action"] == "pause"
- async def test_notify_swallows_notification_service_exceptions(fake_printer):
- """Notification failure must not propagate — Obico's detection loop
- keeps polling; one transient Discord blip shouldn't kill it."""
- with patch(
- "backend.app.services.notification_service.notification_service.on_ai_failure_detection",
- new_callable=AsyncMock,
- side_effect=RuntimeError("discord 502"),
- ):
- # Must not raise.
- await execute_action(
- printer_id=fake_printer.id,
- action="notify",
- task_name="benchy.3mf",
- score=0.91,
- )
|