test_scheduler_backup_gate.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. """Tests for the AMS Filament Backup gate on prefer_lowest sort (#1766).
  2. The reporter set ``prefer_lowest_filament=True`` but the printer kept picking
  3. the first matching spool. Root cause: without the printer's AMS Filament
  4. Backup enabled, switching to the second spool is impossible — so sorting
  5. toward the lowest leaves the print at risk. The gate coerces prefer_lowest
  6. to False whenever the printer reports backup OFF, with None (unknown / A1
  7. family) preserving today's behaviour.
  8. """
  9. from types import SimpleNamespace
  10. from unittest.mock import AsyncMock, MagicMock, patch
  11. import pytest
  12. from backend.app.services.print_scheduler import PrintScheduler
  13. @pytest.fixture
  14. def scheduler():
  15. return PrintScheduler()
  16. def _patch_status(backup):
  17. """Patch ``printer_manager.get_status`` to return a stub PrinterState whose
  18. ``ams_filament_backup`` is the requested tri-state value."""
  19. return patch(
  20. "backend.app.services.print_scheduler.printer_manager.get_status",
  21. return_value=SimpleNamespace(ams_filament_backup=backup, raw_data={}),
  22. )
  23. async def _run_with_backup(scheduler, backup_state, prefer_lowest_setting):
  24. """Drive ``_compute_ams_mapping_for_printer`` past the gate point and
  25. return whatever ``prefer_lowest`` value gets handed to the matcher."""
  26. db = MagicMock()
  27. item = SimpleNamespace(filament_overrides=None)
  28. filament_reqs = [{"slot_id": 1, "type": "PLA", "color": "#000000", "tray_info_idx": ""}]
  29. loaded = [
  30. {
  31. "ams_id": 0,
  32. "tray_id": 0,
  33. "global_tray_id": 0,
  34. "is_external": False,
  35. "type": "PLA",
  36. "color": "#000000",
  37. "tray_info_idx": "",
  38. },
  39. ]
  40. captured: dict = {}
  41. def _capture_match(reqs, loaded_, prefer, overrides, fts_installed=False):
  42. captured["prefer_lowest"] = prefer
  43. return [0]
  44. with (
  45. _patch_status(backup_state),
  46. patch.object(scheduler, "_get_filament_requirements", new=AsyncMock(return_value=filament_reqs)),
  47. patch.object(scheduler, "_build_loaded_filaments", return_value=loaded),
  48. patch.object(scheduler, "_get_bool_setting", new=AsyncMock(return_value=prefer_lowest_setting)),
  49. patch.object(scheduler, "_build_inventory_remain_overrides", new=AsyncMock(return_value={})),
  50. patch.object(scheduler, "_match_filaments_to_slots", side_effect=_capture_match),
  51. ):
  52. await scheduler._compute_ams_mapping_for_printer(db, printer_id=1, item=item)
  53. return captured.get("prefer_lowest")
  54. class TestPreferLowestBackupGate:
  55. @pytest.mark.asyncio
  56. async def test_backup_off_disables_prefer_lowest(self, scheduler):
  57. # User setting ON but printer reports backup OFF — gate must coerce.
  58. # This is the #1766 fix: previously the sort applied and picked the
  59. # near-empty spool, leaving the print to fail mid-job.
  60. out = await _run_with_backup(scheduler, backup_state=False, prefer_lowest_setting=True)
  61. assert out is False
  62. @pytest.mark.asyncio
  63. async def test_backup_on_preserves_prefer_lowest(self, scheduler):
  64. # Backup ON — sort applies as the user intended.
  65. out = await _run_with_backup(scheduler, backup_state=True, prefer_lowest_setting=True)
  66. assert out is True
  67. @pytest.mark.asyncio
  68. async def test_backup_unknown_preserves_prefer_lowest(self, scheduler):
  69. # None = unknown / unsupported (A1 family). Must NOT be treated as OFF
  70. # — that would regress A1 users who currently get the sort applied.
  71. out = await _run_with_backup(scheduler, backup_state=None, prefer_lowest_setting=True)
  72. assert out is True
  73. @pytest.mark.asyncio
  74. async def test_user_setting_off_short_circuits(self, scheduler):
  75. # User setting OFF — backup state is irrelevant; sort never applies.
  76. out = await _run_with_backup(scheduler, backup_state=True, prefer_lowest_setting=False)
  77. assert out is False