test_slot_preset_writer.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. """Tests for the shared slot_preset_mappings writer.
  2. The writer keeps PrintersPage's slot-card preset_name in sync with the
  3. currently-assigned spool from three different inventory paths:
  4. * internal manual assign (inventory.apply_spool_to_slot_via_mqtt)
  5. * internal RFID auto-assign (spool_tag_matcher.auto_assign_spool)
  6. * Spoolman RFID sync (main.auto_sync_spoolman_ams_trays)
  7. Internal-mode regression coverage lives in test_spool_tag_matcher.py
  8. (which exercises the call site end-to-end). This file focuses on the
  9. helper-level contracts — local-preset id formatting, soft-fail on
  10. no preset_id, and the Spoolman derivation path.
  11. """
  12. import pytest
  13. from sqlalchemy import select
  14. from backend.app.models.slot_preset import SlotPresetMapping
  15. from backend.app.services.slot_preset_writer import (
  16. upsert_slot_preset,
  17. upsert_slot_preset_for_spoolman_spool,
  18. )
  19. @pytest.mark.asyncio
  20. async def test_upsert_no_op_when_preset_id_empty(db_session, printer_factory):
  21. """An empty preset_id is not a useful key — the model's column is NOT
  22. NULL and an empty string would overwrite the user's last good preset
  23. with garbage. Skip without raising."""
  24. printer = await printer_factory()
  25. await upsert_slot_preset(
  26. db=db_session,
  27. printer_id=printer.id,
  28. ams_id=0,
  29. tray_id=0,
  30. preset_id="",
  31. preset_name="ignored",
  32. )
  33. result = await db_session.execute(select(SlotPresetMapping).where(SlotPresetMapping.printer_id == printer.id))
  34. assert result.scalar_one_or_none() is None
  35. @pytest.mark.asyncio
  36. async def test_upsert_inserts_then_updates(db_session, printer_factory):
  37. """First call inserts, second call on same (printer, ams, tray) updates
  38. in place rather than violating the unique constraint."""
  39. printer = await printer_factory()
  40. await upsert_slot_preset(
  41. db=db_session,
  42. printer_id=printer.id,
  43. ams_id=0,
  44. tray_id=1,
  45. preset_id="GFSA50",
  46. preset_name="Bambu PLA-CF",
  47. preset_source="cloud",
  48. )
  49. await upsert_slot_preset(
  50. db=db_session,
  51. printer_id=printer.id,
  52. ams_id=0,
  53. tray_id=1,
  54. preset_id="GFSA00",
  55. preset_name="Bambu PLA Basic",
  56. preset_source="cloud",
  57. )
  58. rows = (
  59. (await db_session.execute(select(SlotPresetMapping).where(SlotPresetMapping.printer_id == printer.id)))
  60. .scalars()
  61. .all()
  62. )
  63. assert len(rows) == 1
  64. assert rows[0].preset_id == "GFSA00"
  65. assert rows[0].preset_name == "Bambu PLA Basic"
  66. # -- Spoolman derivation ----------------------------------------------------
  67. @pytest.mark.asyncio
  68. async def test_spoolman_helper_uses_filament_name_and_tray_info_idx(db_session, printer_factory):
  69. """A Spoolman spool with a typical filament shape — name + material —
  70. should land in the row with the AMS-reported tray_info_idx as
  71. preset_id (normalised to setting_id form) and the filament name as
  72. preset_name."""
  73. printer = await printer_factory()
  74. spoolman_spool = {
  75. "id": 42,
  76. "filament": {
  77. "id": 7,
  78. "name": "Bambu PLA-CF Burgundy Red",
  79. "material": "PLA-CF",
  80. },
  81. }
  82. await upsert_slot_preset_for_spoolman_spool(
  83. db=db_session,
  84. spoolman_spool=spoolman_spool,
  85. tray_info_idx="GFA50",
  86. tray_sub_brands="PLA-CF",
  87. tray_type="PLA",
  88. printer_id=printer.id,
  89. ams_id=1,
  90. tray_id=2,
  91. )
  92. mapping = (
  93. await db_session.execute(
  94. select(SlotPresetMapping).where(
  95. SlotPresetMapping.printer_id == printer.id,
  96. SlotPresetMapping.ams_id == 1,
  97. SlotPresetMapping.tray_id == 2,
  98. )
  99. )
  100. ).scalar_one()
  101. assert mapping.preset_id == "GFSA50"
  102. assert mapping.preset_name == "Bambu PLA-CF Burgundy Red"
  103. assert mapping.preset_source == "cloud"
  104. @pytest.mark.asyncio
  105. async def test_spoolman_helper_falls_back_to_material_when_name_missing(db_session, printer_factory):
  106. """Some Spoolman setups have unnamed filaments — fall back through
  107. material → tray_sub_brands → tray_type so we never write an empty
  108. preset_name."""
  109. printer = await printer_factory()
  110. spoolman_spool = {"id": 99, "filament": {"material": "PETG"}}
  111. await upsert_slot_preset_for_spoolman_spool(
  112. db=db_session,
  113. spoolman_spool=spoolman_spool,
  114. tray_info_idx="GFG00",
  115. tray_sub_brands="PETG Basic",
  116. tray_type="PETG",
  117. printer_id=printer.id,
  118. ams_id=0,
  119. tray_id=0,
  120. )
  121. mapping = (
  122. await db_session.execute(select(SlotPresetMapping).where(SlotPresetMapping.printer_id == printer.id))
  123. ).scalar_one()
  124. assert mapping.preset_name == "PETG"
  125. @pytest.mark.asyncio
  126. async def test_spoolman_helper_overwrites_stale_internal_row(db_session, printer_factory):
  127. """Mirror of the internal-mode regression: pre-seed a stale row with a
  128. previous spool's name, run the Spoolman helper, verify the row now
  129. reflects the freshly-synced Spoolman spool. This is the bug shape
  130. that would surface on a Spoolman user with a manually-set preset."""
  131. printer = await printer_factory()
  132. db_session.add(
  133. SlotPresetMapping(
  134. printer_id=printer.id,
  135. ams_id=1,
  136. tray_id=2,
  137. preset_id="GFSA06_09",
  138. preset_name="Bambu PLA Silk+",
  139. preset_source="cloud",
  140. )
  141. )
  142. await db_session.commit()
  143. await upsert_slot_preset_for_spoolman_spool(
  144. db=db_session,
  145. spoolman_spool={"id": 42, "filament": {"name": "Bambu PLA-CF"}},
  146. tray_info_idx="GFA50",
  147. tray_sub_brands="PLA-CF",
  148. tray_type="PLA",
  149. printer_id=printer.id,
  150. ams_id=1,
  151. tray_id=2,
  152. )
  153. mapping = (
  154. await db_session.execute(
  155. select(SlotPresetMapping).where(
  156. SlotPresetMapping.printer_id == printer.id,
  157. SlotPresetMapping.ams_id == 1,
  158. SlotPresetMapping.tray_id == 2,
  159. )
  160. )
  161. ).scalar_one()
  162. assert mapping.preset_name == "Bambu PLA-CF"
  163. assert mapping.preset_id == "GFSA50"
  164. @pytest.mark.asyncio
  165. async def test_spoolman_helper_skips_when_tray_info_idx_unknown(db_session, printer_factory):
  166. """No tray_info_idx → no preset_id → upsert skips. Caller must not
  167. write an empty-string preset_id (would clobber any existing row)."""
  168. printer = await printer_factory()
  169. await upsert_slot_preset_for_spoolman_spool(
  170. db=db_session,
  171. spoolman_spool={"id": 1, "filament": {"name": "Random"}},
  172. tray_info_idx="",
  173. tray_sub_brands="",
  174. tray_type="PLA",
  175. printer_id=printer.id,
  176. ams_id=0,
  177. tray_id=0,
  178. )
  179. result = await db_session.execute(select(SlotPresetMapping).where(SlotPresetMapping.printer_id == printer.id))
  180. assert result.scalar_one_or_none() is None