test_inventory_assign.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. """Integration tests for inventory spool assignment — tray_info_idx resolution.
  2. Tests that PFUS* user-local preset IDs are replaced with generic Bambu IDs,
  3. and that existing recognised presets on slots are reused when the material matches.
  4. """
  5. from unittest.mock import MagicMock, patch
  6. import pytest
  7. from httpx import AsyncClient
  8. from sqlalchemy.ext.asyncio import AsyncSession
  9. from backend.app.models.spool import Spool
  10. @pytest.fixture
  11. async def spool_factory(db_session: AsyncSession):
  12. """Factory to create test spools."""
  13. _counter = [0]
  14. async def _create_spool(**kwargs):
  15. _counter[0] += 1
  16. defaults = {
  17. "material": "PLA",
  18. "subtype": "Basic",
  19. "brand": "Devil Design",
  20. "color_name": "Red",
  21. "rgba": "FF0000FF",
  22. "label_weight": 1000,
  23. "weight_used": 0,
  24. "slicer_filament": "PFUS9ac902733670a9",
  25. }
  26. defaults.update(kwargs)
  27. spool = Spool(**defaults)
  28. db_session.add(spool)
  29. await db_session.commit()
  30. await db_session.refresh(spool)
  31. return spool
  32. return _create_spool
  33. def _make_mock_status(ams_data=None, vt_tray=None, nozzles=None, ams_extruder_map=None):
  34. """Build a mock printer status with optional AMS/nozzle data."""
  35. status = MagicMock()
  36. raw = {}
  37. if ams_data is not None:
  38. raw["ams"] = {"ams": ams_data}
  39. if vt_tray is not None:
  40. raw["vt_tray"] = vt_tray
  41. status.raw_data = raw
  42. status.nozzles = nozzles or [MagicMock(nozzle_diameter="0.4")]
  43. status.ams_extruder_map = ams_extruder_map
  44. return status
  45. class TestAssignSpoolTrayInfoIdx:
  46. """Tests for tray_info_idx resolution during spool assignment."""
  47. @pytest.mark.asyncio
  48. @pytest.mark.integration
  49. async def test_pfus_replaced_with_generic(self, async_client: AsyncClient, printer_factory, spool_factory):
  50. """PFUS* user-local IDs are replaced with generic Bambu IDs."""
  51. printer = await printer_factory(name="H2D")
  52. spool = await spool_factory(slicer_filament="PFUS9ac902733670a9", material="PLA")
  53. mock_client = MagicMock()
  54. mock_client.ams_set_filament_setting.return_value = True
  55. mock_client.extrusion_cali_sel.return_value = True
  56. status = _make_mock_status(ams_data=[{"id": 2, "tray": [{"id": 3, "tray_info_idx": "", "tray_type": ""}]}])
  57. with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
  58. mock_pm.get_client.return_value = mock_client
  59. mock_pm.get_status.return_value = status
  60. response = await async_client.post(
  61. "/api/v1/inventory/assignments",
  62. json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 2, "tray_id": 3},
  63. )
  64. assert response.status_code == 200
  65. call_kwargs = mock_client.ams_set_filament_setting.call_args
  66. assert call_kwargs.kwargs["tray_info_idx"] == "GFL99"
  67. @pytest.mark.asyncio
  68. @pytest.mark.integration
  69. async def test_reuses_existing_recognised_preset(self, async_client: AsyncClient, printer_factory, spool_factory):
  70. """When slot already has a recognised preset for same material, reuse it."""
  71. printer = await printer_factory(name="H2D")
  72. spool = await spool_factory(slicer_filament="PFUS9ac902733670a9", material="PLA")
  73. mock_client = MagicMock()
  74. mock_client.ams_set_filament_setting.return_value = True
  75. mock_client.extrusion_cali_sel.return_value = True
  76. # Slot already configured by slicer with cloud-synced preset
  77. status = _make_mock_status(
  78. ams_data=[{"id": 2, "tray": [{"id": 3, "tray_info_idx": "P4d64437", "tray_type": "PLA"}]}]
  79. )
  80. with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
  81. mock_pm.get_client.return_value = mock_client
  82. mock_pm.get_status.return_value = status
  83. response = await async_client.post(
  84. "/api/v1/inventory/assignments",
  85. json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 2, "tray_id": 3},
  86. )
  87. assert response.status_code == 200
  88. call_kwargs = mock_client.ams_set_filament_setting.call_args
  89. # Should reuse the slicer's cloud-synced ID
  90. assert call_kwargs.kwargs["tray_info_idx"] == "P4d64437"
  91. @pytest.mark.asyncio
  92. @pytest.mark.integration
  93. async def test_different_material_uses_generic(self, async_client: AsyncClient, printer_factory, spool_factory):
  94. """When slot has a preset for a DIFFERENT material, use generic ID."""
  95. printer = await printer_factory(name="H2D")
  96. spool = await spool_factory(slicer_filament="PFUS9ac902733670a9", material="PETG")
  97. mock_client = MagicMock()
  98. mock_client.ams_set_filament_setting.return_value = True
  99. mock_client.extrusion_cali_sel.return_value = True
  100. # Slot currently has PLA but spool is PETG
  101. status = _make_mock_status(
  102. ams_data=[{"id": 2, "tray": [{"id": 3, "tray_info_idx": "P4d64437", "tray_type": "PLA"}]}]
  103. )
  104. with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
  105. mock_pm.get_client.return_value = mock_client
  106. mock_pm.get_status.return_value = status
  107. response = await async_client.post(
  108. "/api/v1/inventory/assignments",
  109. json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 2, "tray_id": 3},
  110. )
  111. assert response.status_code == 200
  112. call_kwargs = mock_client.ams_set_filament_setting.call_args
  113. assert call_kwargs.kwargs["tray_info_idx"] == "GFG99"
  114. @pytest.mark.asyncio
  115. @pytest.mark.integration
  116. async def test_gf_slicer_filament_kept(self, async_client: AsyncClient, printer_factory, spool_factory):
  117. """Standard GF* IDs from spool.slicer_filament are used directly."""
  118. printer = await printer_factory(name="X1C")
  119. spool = await spool_factory(slicer_filament="GFL05", material="PLA")
  120. mock_client = MagicMock()
  121. mock_client.ams_set_filament_setting.return_value = True
  122. mock_client.extrusion_cali_sel.return_value = True
  123. status = _make_mock_status(ams_data=[])
  124. with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
  125. mock_pm.get_client.return_value = mock_client
  126. mock_pm.get_status.return_value = status
  127. response = await async_client.post(
  128. "/api/v1/inventory/assignments",
  129. json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 0, "tray_id": 0},
  130. )
  131. assert response.status_code == 200
  132. call_kwargs = mock_client.ams_set_filament_setting.call_args
  133. assert call_kwargs.kwargs["tray_info_idx"] == "GFL05"
  134. @pytest.mark.asyncio
  135. @pytest.mark.integration
  136. async def test_empty_slicer_filament_uses_generic(self, async_client: AsyncClient, printer_factory, spool_factory):
  137. """Spool with no slicer_filament gets a generic ID from material type."""
  138. printer = await printer_factory(name="X1C")
  139. spool = await spool_factory(slicer_filament=None, material="ABS")
  140. mock_client = MagicMock()
  141. mock_client.ams_set_filament_setting.return_value = True
  142. mock_client.extrusion_cali_sel.return_value = True
  143. status = _make_mock_status(ams_data=[])
  144. with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
  145. mock_pm.get_client.return_value = mock_client
  146. mock_pm.get_status.return_value = status
  147. response = await async_client.post(
  148. "/api/v1/inventory/assignments",
  149. json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 0, "tray_id": 0},
  150. )
  151. assert response.status_code == 200
  152. call_kwargs = mock_client.ams_set_filament_setting.call_args
  153. assert call_kwargs.kwargs["tray_info_idx"] == "GFB99"
  154. @pytest.mark.asyncio
  155. @pytest.mark.integration
  156. async def test_existing_pfus_on_slot_not_reused(self, async_client: AsyncClient, printer_factory, spool_factory):
  157. """A PFUS* ID already on the slot should NOT be reused (it's also user-local)."""
  158. printer = await printer_factory(name="H2D")
  159. spool = await spool_factory(slicer_filament="PFUS1111111111", material="PLA")
  160. mock_client = MagicMock()
  161. mock_client.ams_set_filament_setting.return_value = True
  162. mock_client.extrusion_cali_sel.return_value = True
  163. # Slot has a PFUS* ID from some previous config
  164. status = _make_mock_status(
  165. ams_data=[{"id": 0, "tray": [{"id": 0, "tray_info_idx": "PFUS2222222222", "tray_type": "PLA"}]}]
  166. )
  167. with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
  168. mock_pm.get_client.return_value = mock_client
  169. mock_pm.get_status.return_value = status
  170. response = await async_client.post(
  171. "/api/v1/inventory/assignments",
  172. json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 0, "tray_id": 0},
  173. )
  174. assert response.status_code == 200
  175. call_kwargs = mock_client.ams_set_filament_setting.call_args
  176. # Should NOT reuse the PFUS on the slot — use generic instead
  177. assert call_kwargs.kwargs["tray_info_idx"] == "GFL99"