test_slicer_filament_resolver.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. """Tests for ``resolve_slicer_filament`` (#1815).
  2. The defensive filter at the end of the resolver clears ``tray_info_idx``
  3. when its value isn't slicer-acceptable (literal material names + PFUS /
  4. PFCN cloud-preset prefixes that the printer's calibration table can't
  5. key on). Pre-#1815 it cleared ``setting_id`` alongside, which dropped
  6. the slicer's only handle on the user's actual custom preset and forced
  7. the caller into the generic-material fallback — Bambu Studio then
  8. displayed "Generic <Material>" for spools whose Bambu Cloud detail
  9. lookup didn't resolve a ``filament_id`` (cloud unauth on the on_ams_change
  10. replay path, transient cloud failure, or custom presets whose detail
  11. JSON omits ``filament_id``).
  12. Post-#1815 the filter preserves a setting_id that's still a valid
  13. slicer reference (PFUS / PFCN cloud user/shared preset, or GFS Bambu
  14. official preset) even when ``tray_info_idx`` is cleared.
  15. """
  16. from __future__ import annotations
  17. from unittest.mock import AsyncMock, MagicMock, patch
  18. import pytest
  19. from backend.app.services.slicer_filament_resolver import resolve_slicer_filament
  20. @pytest.mark.asyncio
  21. async def test_pfus_cloud_unavailable_preserves_setting_id():
  22. """Reporter scenario: PFUS cloud user preset, cloud lookup fails to
  23. return a filament_id. setting_id must survive so the slicer can
  24. still find the user's actual custom preset."""
  25. db = MagicMock()
  26. with patch(
  27. "backend.app.api.routes.cloud.build_authenticated_cloud",
  28. AsyncMock(return_value=None),
  29. ):
  30. tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
  31. db=db,
  32. current_user=None,
  33. slicer_filament="PFUS990b6e19965353",
  34. slicer_filament_name="Jayo PETG HF",
  35. material="PETG",
  36. )
  37. assert tray_info_idx == ""
  38. assert setting_id == "PFUS990b6e19965353"
  39. assert sub_brand is None
  40. @pytest.mark.asyncio
  41. async def test_pfcn_cloud_unavailable_preserves_setting_id():
  42. """PFCN partner/shared cloud preset (e.g. Polymaker H2D variants,
  43. #1648) shares the same shape problem as PFUS."""
  44. db = MagicMock()
  45. with patch(
  46. "backend.app.api.routes.cloud.build_authenticated_cloud",
  47. AsyncMock(return_value=None),
  48. ):
  49. tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
  50. db=db,
  51. current_user=None,
  52. slicer_filament="PFCN1234567890",
  53. slicer_filament_name="Polymaker PolyTerra PLA",
  54. material="PLA",
  55. )
  56. assert tray_info_idx == ""
  57. assert setting_id == "PFCN1234567890"
  58. assert sub_brand is None
  59. @pytest.mark.asyncio
  60. async def test_pfus_cloud_resolves_filament_id_regression_guard():
  61. """When cloud auth works and returns a filament_id, the resolver
  62. keeps its existing behaviour: tray_info_idx = real filament_id,
  63. setting_id = original PFUS reference."""
  64. db = MagicMock()
  65. cloud_mock = MagicMock()
  66. cloud_mock.is_authenticated = True
  67. cloud_mock.get_setting_detail = AsyncMock(return_value={"filament_id": "P285e239", "name": "Jayo PETG HF @P1S"})
  68. cloud_mock.close = AsyncMock()
  69. with patch(
  70. "backend.app.api.routes.cloud.build_authenticated_cloud",
  71. AsyncMock(return_value=cloud_mock),
  72. ):
  73. tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
  74. db=db,
  75. current_user=MagicMock(),
  76. slicer_filament="PFUS990b6e19965353",
  77. slicer_filament_name="Jayo PETG HF",
  78. material="PETG",
  79. )
  80. assert tray_info_idx == "P285e239"
  81. assert setting_id == "PFUS990b6e19965353"
  82. assert sub_brand == "Jayo PETG HF"
  83. @pytest.mark.asyncio
  84. async def test_gfs_cloud_unavailable_resolves_via_normalize():
  85. """GFS Bambu official preset + cloud unavailable: normalize strips
  86. the 'S' to give a real filament_id ('GFG02'), so tray_info_idx is
  87. valid and the defensive filter doesn't trigger. setting_id stays as
  88. the original GFS reference. Regression guard for the cloud-down
  89. Bambu-official path."""
  90. db = MagicMock()
  91. with patch(
  92. "backend.app.api.routes.cloud.build_authenticated_cloud",
  93. AsyncMock(return_value=None),
  94. ):
  95. tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
  96. db=db,
  97. current_user=None,
  98. slicer_filament="GFSG02",
  99. slicer_filament_name=None,
  100. material="PETG",
  101. )
  102. assert tray_info_idx == "GFG02"
  103. assert setting_id == "GFSG02"
  104. assert sub_brand is None
  105. @pytest.mark.asyncio
  106. async def test_literal_material_name_clears_both():
  107. """slicer_filament='PETG' (free-text material leak from legacy
  108. spools): both tray_info_idx and setting_id must be cleared so the
  109. caller's generic-material fallback rescues the slot. Regression
  110. guard that the PFUS preservation doesn't accidentally preserve
  111. literal material names."""
  112. db = MagicMock()
  113. with patch(
  114. "backend.app.api.routes.cloud.build_authenticated_cloud",
  115. AsyncMock(return_value=None),
  116. ):
  117. tray_info_idx, setting_id, sub_brand, _type_override = await resolve_slicer_filament(
  118. db=db,
  119. current_user=None,
  120. slicer_filament="PETG",
  121. slicer_filament_name=None,
  122. material="PETG",
  123. )
  124. assert tray_info_idx == ""
  125. assert setting_id == ""
  126. assert sub_brand is None
  127. class TestThePresetsOwnType:
  128. """#2902: a preset is chosen from a list the slicer defines, so its
  129. ``filament_type`` is the slicer's own answer to what the material is --
  130. no reading of a product name required. Raised by @doncaruana on the issue
  131. after the first fix reduced "PLA Aero" to "PLA".
  132. The resolver hands that answer back as the fourth element; the two assign
  133. routes write it into ``tray_type`` in preference to reducing the spool's
  134. material column. ``None`` means no preset said, and the reduction stands.
  135. """
  136. @pytest.mark.asyncio
  137. async def test_a_local_presets_type_is_returned(self):
  138. db = MagicMock()
  139. lp = MagicMock()
  140. lp.filament_type = "PLA-AERO"
  141. lp.setting = None
  142. lp.name = "Bambu PLA Aero @BBL X1C"
  143. result = MagicMock()
  144. result.scalar_one_or_none = MagicMock(return_value=lp)
  145. db.execute = AsyncMock(return_value=result)
  146. _idx, _sid, _brand, type_override = await resolve_slicer_filament(
  147. db=db,
  148. current_user=None,
  149. slicer_filament="38",
  150. slicer_filament_name=None,
  151. material="PLA",
  152. )
  153. assert type_override == "PLA-AERO"
  154. @pytest.mark.asyncio
  155. async def test_a_cloud_presets_type_is_read_out_of_its_profile(self):
  156. """Both slicers store it as a one-element array, and the preset JSON
  157. sits under ``setting`` in the cloud envelope."""
  158. db = MagicMock()
  159. cloud = MagicMock()
  160. cloud.is_authenticated = True
  161. cloud.get_setting_detail = AsyncMock(
  162. return_value={
  163. "filament_id": "GFA11",
  164. "name": "Bambu PLA Aero @BBL X1C",
  165. "setting": {"filament_type": ["PLA-AERO"]},
  166. }
  167. )
  168. cloud.close = AsyncMock()
  169. with patch(
  170. "backend.app.api.routes.cloud.build_authenticated_cloud",
  171. AsyncMock(return_value=cloud),
  172. ):
  173. idx, _sid, _brand, type_override = await resolve_slicer_filament(
  174. db=db,
  175. current_user=None,
  176. slicer_filament="GFSA11",
  177. slicer_filament_name=None,
  178. material="PLA",
  179. )
  180. assert idx == "GFA11"
  181. assert type_override == "PLA-AERO"
  182. @pytest.mark.asyncio
  183. async def test_a_bare_string_filament_type_is_accepted_too(self):
  184. """Hand-written and older profiles store it unwrapped. ``orca_profiles``
  185. accepts both forms, so this has to as well."""
  186. db = MagicMock()
  187. cloud = MagicMock()
  188. cloud.is_authenticated = True
  189. cloud.get_setting_detail = AsyncMock(
  190. return_value={"filament_id": "GFG02", "setting": {"filament_type": "PETG"}}
  191. )
  192. cloud.close = AsyncMock()
  193. with patch(
  194. "backend.app.api.routes.cloud.build_authenticated_cloud",
  195. AsyncMock(return_value=cloud),
  196. ):
  197. _idx, _sid, _brand, type_override = await resolve_slicer_filament(
  198. db=db,
  199. current_user=None,
  200. slicer_filament="GFSG02",
  201. slicer_filament_name=None,
  202. material="PETG",
  203. )
  204. assert type_override == "PETG"
  205. @pytest.mark.asyncio
  206. async def test_no_preset_means_no_answer(self):
  207. """A spool with no slicer_filament -- the case this issue was reported
  208. for. ``material`` is required on a spool and ``slicer_filament`` is
  209. not, so the reduction has to stay as the fallback."""
  210. db = MagicMock()
  211. _idx, _sid, _brand, type_override = await resolve_slicer_filament(
  212. db=db,
  213. current_user=None,
  214. slicer_filament=None,
  215. slicer_filament_name=None,
  216. material="PLA+",
  217. )
  218. assert type_override is None
  219. @pytest.mark.asyncio
  220. async def test_a_preset_that_does_not_say_gets_no_opinion(self):
  221. db = MagicMock()
  222. cloud = MagicMock()
  223. cloud.is_authenticated = True
  224. cloud.get_setting_detail = AsyncMock(return_value={"filament_id": "GFG02", "setting": {}})
  225. cloud.close = AsyncMock()
  226. with patch(
  227. "backend.app.api.routes.cloud.build_authenticated_cloud",
  228. AsyncMock(return_value=cloud),
  229. ):
  230. _idx, _sid, _brand, type_override = await resolve_slicer_filament(
  231. db=db,
  232. current_user=None,
  233. slicer_filament="GFSG02",
  234. slicer_filament_name=None,
  235. material="PETG",
  236. )
  237. assert type_override is None