test_octoeverywhere_actions.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. """OctoEverywhere uses the existing printer controls and AI event subscription."""
  2. from unittest.mock import AsyncMock, MagicMock, patch
  3. import pytest
  4. from backend.app.models.notification_template import DEFAULT_TEMPLATES, NotificationTemplate
  5. from backend.app.services.octoeverywhere_actions import execute_action
  6. @pytest.mark.parametrize("action", ["notify", "pause", "pause_and_off"])
  7. @pytest.mark.parametrize("frame", [None, b"flagged-jpeg"])
  8. async def test_dispatches_action_with_quality_without_fake_confidence(action, frame):
  9. with patch("backend.app.services.octoeverywhere_actions.execute_detection_action", new_callable=AsyncMock) as run:
  10. await execute_action(1, action, "benchy", 2, frame)
  11. run.assert_awaited_once_with(1, action, "benchy", None, frame, print_quality=2, printer_name=None)
  12. async def test_prepared_action_reuses_printer_name_without_another_lookup():
  13. with (
  14. patch("backend.app.services.obico_actions._get_printer_name", new_callable=AsyncMock) as lookup,
  15. patch("backend.app.services.obico_actions._pause_print") as pause,
  16. patch("backend.app.services.obico_actions._turn_off_linked_plugs", new_callable=AsyncMock) as off,
  17. patch("backend.app.services.obico_actions._notify", new_callable=AsyncMock) as notify,
  18. ):
  19. await execute_action(1, "pause_and_off", "benchy", 2, b"frame", printer_name="Prepared printer")
  20. lookup.assert_not_awaited()
  21. pause.assert_called_once_with(1)
  22. off.assert_awaited_once_with(1)
  23. notify.assert_awaited_once_with(1, "Prepared printer", "benchy", None, "pause_and_off", b"frame", print_quality=2)
  24. async def test_pause_and_power_off_also_notify():
  25. from backend.app.services.obico_actions import execute_action as run
  26. with (
  27. patch("backend.app.services.obico_actions._get_printer_name", return_value="Printer"),
  28. patch("backend.app.services.obico_actions._pause_print") as pause,
  29. patch("backend.app.services.obico_actions._turn_off_linked_plugs", new_callable=AsyncMock) as off,
  30. patch("backend.app.services.obico_actions._notify", new_callable=AsyncMock) as notify,
  31. ):
  32. await run(1, "pause_and_off", "benchy", None, print_quality=1)
  33. pause.assert_called_once_with(1)
  34. off.assert_awaited_once_with(1)
  35. notify.assert_awaited_once_with(1, "Printer", "benchy", None, "pause_and_off", None, print_quality=1)
  36. async def test_notification_shows_quality_and_preserves_event_subscription():
  37. from backend.app.services.notification_service import NotificationService
  38. service = NotificationService()
  39. db = MagicMock()
  40. template = NotificationTemplate(**next(t for t in DEFAULT_TEMPLATES if t["event_type"] == "ai_failure_detection"))
  41. original_body = template.body_template
  42. with (
  43. patch.object(service, "_get_providers_for_event", return_value=["provider"]) as providers,
  44. patch.object(service, "_get_template", return_value=template),
  45. patch.object(service, "_send_to_providers", new_callable=AsyncMock) as send,
  46. ):
  47. await service.on_ai_failure_detection(1, "Printer", "benchy", None, "notify", db, print_quality=2)
  48. providers.assert_awaited_once_with(db, "on_ai_failure_detection", 1)
  49. assert send.await_args.args[1] == "Possible Print Failure Detected"
  50. assert send.await_args.args[2] == (
  51. "Printer: benchy\nProvider: OctoEverywhere\nPrint quality: 2/10\nConfidence: N/A\nAction taken: notify"
  52. )
  53. assert send.await_args.kwargs["variables"]["confidence"] == "N/A"
  54. assert template.body_template == original_body
  55. @pytest.mark.parametrize(
  56. ("body", "expected"),
  57. [
  58. ("Please check {printer}.", "Please check Printer."),
  59. ("{provider}: {task_name}, quality {print_quality}", "OctoEverywhere: benchy, quality 2/10"),
  60. ],
  61. )
  62. async def test_notification_preserves_custom_title_and_body(body, expected):
  63. from backend.app.services.notification_service import NotificationService
  64. service = NotificationService()
  65. template = NotificationTemplate(
  66. event_type="ai_failure_detection",
  67. name="AI Failure Detection",
  68. title_template="Check {printer}",
  69. body_template=body,
  70. )
  71. with (
  72. patch.object(service, "_get_providers_for_event", return_value=["provider"]),
  73. patch.object(service, "_get_template", return_value=template),
  74. patch.object(service, "_send_to_providers", new_callable=AsyncMock) as send,
  75. ):
  76. await service.on_ai_failure_detection(1, "Printer", "benchy", None, "notify", MagicMock(), print_quality=2)
  77. assert send.await_args.args[1:3] == ("Check Printer", expected)
  78. assert template.body_template == body
  79. async def test_obico_default_notification_keeps_confidence():
  80. from backend.app.services.notification_service import NotificationService
  81. service = NotificationService()
  82. template = NotificationTemplate(**next(t for t in DEFAULT_TEMPLATES if t["event_type"] == "ai_failure_detection"))
  83. with (
  84. patch.object(service, "_get_providers_for_event", return_value=["provider"]),
  85. patch.object(service, "_get_template", return_value=template),
  86. patch.object(service, "_send_to_providers", new_callable=AsyncMock) as send,
  87. ):
  88. await service.on_ai_failure_detection(1, "Printer", "benchy", 0.85, "pause", MagicMock())
  89. assert send.await_args.args[2] == (
  90. "Printer: benchy\nProvider: Obico\nPrint quality: N/A\nConfidence: 0.85\nAction taken: pause"
  91. )
  92. async def test_ai_notification_template_preview_includes_quality_variables():
  93. from backend.app.api.routes.notification_templates import preview_template
  94. from backend.app.schemas.notification_template import EVENT_VARIABLES, EventType, TemplatePreviewRequest
  95. assert {"provider", "print_quality", "task_name", "confidence", "action"} <= set(
  96. EVENT_VARIABLES[EventType.AI_FAILURE_DETECTION]
  97. )
  98. result = await preview_template(
  99. TemplatePreviewRequest(
  100. event_type=EventType.AI_FAILURE_DETECTION,
  101. title_template="{provider}: {printer}",
  102. body_template="{task_name}: {print_quality}, {confidence}, {action}",
  103. )
  104. )
  105. assert result.title == "OctoEverywhere: Bambu X1C"
  106. assert result.body == "Benchy.3mf: 2/10, N/A, pause"
  107. async def test_default_ai_template_preview_matches_delivered_message():
  108. from backend.app.api.routes.notification_templates import preview_template
  109. from backend.app.schemas.notification_template import EventType, TemplatePreviewRequest
  110. from backend.app.services.notification_service import NotificationService
  111. template = NotificationTemplate(**next(t for t in DEFAULT_TEMPLATES if t["event_type"] == "ai_failure_detection"))
  112. preview = await preview_template(
  113. TemplatePreviewRequest(
  114. event_type=EventType.AI_FAILURE_DETECTION,
  115. title_template=template.title_template,
  116. body_template=template.body_template,
  117. )
  118. )
  119. service = NotificationService()
  120. with (
  121. patch.object(service, "_get_providers_for_event", return_value=["provider"]),
  122. patch.object(service, "_get_template", return_value=template),
  123. patch.object(service, "_send_to_providers", new_callable=AsyncMock) as send,
  124. ):
  125. await service.on_ai_failure_detection(1, "Bambu X1C", "Benchy.3mf", None, "pause", MagicMock(), print_quality=2)
  126. assert send.await_args.args[1:3] == (preview.title, preview.body)