Files
bambuddy/backend/tests/integration/test_inventory_assign.py
T
maziggy 294bd74940 Fix AMS slot auto-config falling back to Generic instead of spool's slicer preset
Two bugs caused spool assignments to always configure AMS slots with
generic Bambu filament IDs (e.g. GFB99 "Generic ABS") instead of the
spool's actual slicer preset:

1. PFUS* IDs (cloud-synced custom presets) were blanket-rejected and
   replaced with generic IDs in both assign_spool and configure_ams_slot
2. Generic fallback IDs (GFB99, GFL99, etc.) were treated as "good"
   presets by the slot-reuse logic, making them sticky once set

New priority: spool's own slicer_filament > slot's non-generic preset
(same material) > generic fallback.
2026-02-21 13:06:10 +01:00

327 lines
14 KiB
Python

"""Integration tests for inventory spool assignment — tray_info_idx resolution.
Tests that the spool's own slicer_filament (including PFUS* cloud-synced
custom presets) takes priority, with slot reuse and generic fallback as
lower-priority fallbacks.
"""
from unittest.mock import MagicMock, patch
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.models.spool import Spool
@pytest.fixture
async def spool_factory(db_session: AsyncSession):
"""Factory to create test spools."""
_counter = [0]
async def _create_spool(**kwargs):
_counter[0] += 1
defaults = {
"material": "PLA",
"subtype": "Basic",
"brand": "Devil Design",
"color_name": "Red",
"rgba": "FF0000FF",
"label_weight": 1000,
"weight_used": 0,
"slicer_filament": "PFUS9ac902733670a9",
}
defaults.update(kwargs)
spool = Spool(**defaults)
db_session.add(spool)
await db_session.commit()
await db_session.refresh(spool)
return spool
return _create_spool
def _make_mock_status(ams_data=None, vt_tray=None, nozzles=None, ams_extruder_map=None):
"""Build a mock printer status with optional AMS/nozzle data."""
status = MagicMock()
raw = {}
if ams_data is not None:
raw["ams"] = {"ams": ams_data}
if vt_tray is not None:
raw["vt_tray"] = vt_tray
status.raw_data = raw
status.nozzles = nozzles or [MagicMock(nozzle_diameter="0.4")]
status.ams_extruder_map = ams_extruder_map
return status
class TestAssignSpoolTrayInfoIdx:
"""Tests for tray_info_idx resolution during spool assignment."""
@pytest.mark.asyncio
@pytest.mark.integration
async def test_pfus_slicer_filament_used_directly(self, async_client: AsyncClient, printer_factory, spool_factory):
"""PFUS* cloud-synced custom preset IDs are sent to the printer."""
printer = await printer_factory(name="H2D")
spool = await spool_factory(slicer_filament="PFUS9ac902733670a9", material="PLA")
mock_client = MagicMock()
mock_client.ams_set_filament_setting.return_value = True
mock_client.extrusion_cali_sel.return_value = True
status = _make_mock_status(ams_data=[{"id": 2, "tray": [{"id": 3, "tray_info_idx": "", "tray_type": ""}]}])
with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
mock_pm.get_client.return_value = mock_client
mock_pm.get_status.return_value = status
response = await async_client.post(
"/api/v1/inventory/assignments",
json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 2, "tray_id": 3},
)
assert response.status_code == 200
call_kwargs = mock_client.ams_set_filament_setting.call_args
assert call_kwargs.kwargs["tray_info_idx"] == "PFUS9ac902733670a9"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_spool_preset_takes_priority_over_slot(
self, async_client: AsyncClient, printer_factory, spool_factory
):
"""Spool's own slicer_filament takes priority over slot's existing preset."""
printer = await printer_factory(name="H2D")
spool = await spool_factory(slicer_filament="PFUS9ac902733670a9", material="PLA")
mock_client = MagicMock()
mock_client.ams_set_filament_setting.return_value = True
mock_client.extrusion_cali_sel.return_value = True
# Slot already configured by slicer with cloud-synced preset
status = _make_mock_status(
ams_data=[{"id": 2, "tray": [{"id": 3, "tray_info_idx": "P4d64437", "tray_type": "PLA"}]}]
)
with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
mock_pm.get_client.return_value = mock_client
mock_pm.get_status.return_value = status
response = await async_client.post(
"/api/v1/inventory/assignments",
json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 2, "tray_id": 3},
)
assert response.status_code == 200
call_kwargs = mock_client.ams_set_filament_setting.call_args
# Spool's own preset wins over slot's existing one
assert call_kwargs.kwargs["tray_info_idx"] == "PFUS9ac902733670a9"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_spool_preset_used_even_if_different_material_on_slot(
self, async_client: AsyncClient, printer_factory, spool_factory
):
"""Spool's own slicer_filament is used regardless of what's on the slot."""
printer = await printer_factory(name="H2D")
spool = await spool_factory(slicer_filament="PFUS9ac902733670a9", material="PETG")
mock_client = MagicMock()
mock_client.ams_set_filament_setting.return_value = True
mock_client.extrusion_cali_sel.return_value = True
# Slot currently has PLA but spool is PETG
status = _make_mock_status(
ams_data=[{"id": 2, "tray": [{"id": 3, "tray_info_idx": "P4d64437", "tray_type": "PLA"}]}]
)
with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
mock_pm.get_client.return_value = mock_client
mock_pm.get_status.return_value = status
response = await async_client.post(
"/api/v1/inventory/assignments",
json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 2, "tray_id": 3},
)
assert response.status_code == 200
call_kwargs = mock_client.ams_set_filament_setting.call_args
assert call_kwargs.kwargs["tray_info_idx"] == "PFUS9ac902733670a9"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_gf_slicer_filament_kept(self, async_client: AsyncClient, printer_factory, spool_factory):
"""Standard GF* IDs from spool.slicer_filament are used directly."""
printer = await printer_factory(name="X1C")
spool = await spool_factory(slicer_filament="GFL05", material="PLA")
mock_client = MagicMock()
mock_client.ams_set_filament_setting.return_value = True
mock_client.extrusion_cali_sel.return_value = True
status = _make_mock_status(ams_data=[])
with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
mock_pm.get_client.return_value = mock_client
mock_pm.get_status.return_value = status
response = await async_client.post(
"/api/v1/inventory/assignments",
json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 0, "tray_id": 0},
)
assert response.status_code == 200
call_kwargs = mock_client.ams_set_filament_setting.call_args
assert call_kwargs.kwargs["tray_info_idx"] == "GFL05"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_empty_slicer_filament_uses_generic(self, async_client: AsyncClient, printer_factory, spool_factory):
"""Spool with no slicer_filament gets a generic ID from material type."""
printer = await printer_factory(name="X1C")
spool = await spool_factory(slicer_filament=None, material="ABS")
mock_client = MagicMock()
mock_client.ams_set_filament_setting.return_value = True
mock_client.extrusion_cali_sel.return_value = True
status = _make_mock_status(ams_data=[])
with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
mock_pm.get_client.return_value = mock_client
mock_pm.get_status.return_value = status
response = await async_client.post(
"/api/v1/inventory/assignments",
json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 0, "tray_id": 0},
)
assert response.status_code == 200
call_kwargs = mock_client.ams_set_filament_setting.call_args
assert call_kwargs.kwargs["tray_info_idx"] == "GFB99"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_spool_pfus_used_over_slot_pfus(self, async_client: AsyncClient, printer_factory, spool_factory):
"""Spool's own PFUS preset is used even when slot has a different PFUS."""
printer = await printer_factory(name="H2D")
spool = await spool_factory(slicer_filament="PFUS1111111111", material="PLA")
mock_client = MagicMock()
mock_client.ams_set_filament_setting.return_value = True
mock_client.extrusion_cali_sel.return_value = True
# Slot has a PFUS* ID from some previous config
status = _make_mock_status(
ams_data=[{"id": 0, "tray": [{"id": 0, "tray_info_idx": "PFUS2222222222", "tray_type": "PLA"}]}]
)
with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
mock_pm.get_client.return_value = mock_client
mock_pm.get_status.return_value = status
response = await async_client.post(
"/api/v1/inventory/assignments",
json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 0, "tray_id": 0},
)
assert response.status_code == 200
call_kwargs = mock_client.ams_set_filament_setting.call_args
# Spool's own preset wins
assert call_kwargs.kwargs["tray_info_idx"] == "PFUS1111111111"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_generic_on_slot_not_reused_over_spool_preset(
self, async_client: AsyncClient, printer_factory, spool_factory
):
"""Generic ID on slot (e.g. GFB99) must not override spool's own preset."""
printer = await printer_factory(name="P2S")
spool = await spool_factory(slicer_filament="PFUScda4c46fc9031", material="ABS")
mock_client = MagicMock()
mock_client.ams_set_filament_setting.return_value = True
mock_client.extrusion_cali_sel.return_value = True
# Slot stuck on generic ABS from a previous assignment
status = _make_mock_status(
ams_data=[{"id": 0, "tray": [{"id": 1, "tray_info_idx": "GFB99", "tray_type": "ABS"}]}]
)
with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
mock_pm.get_client.return_value = mock_client
mock_pm.get_status.return_value = status
response = await async_client.post(
"/api/v1/inventory/assignments",
json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 0, "tray_id": 1},
)
assert response.status_code == 200
call_kwargs = mock_client.ams_set_filament_setting.call_args
# Spool's preset wins — generic on slot must not be sticky
assert call_kwargs.kwargs["tray_info_idx"] == "PFUScda4c46fc9031"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_no_preset_with_generic_on_slot_still_uses_generic(
self, async_client: AsyncClient, printer_factory, spool_factory
):
"""Spool without preset + generic on slot → generic fallback (not slot reuse)."""
printer = await printer_factory(name="P2S")
spool = await spool_factory(slicer_filament=None, material="ABS")
mock_client = MagicMock()
mock_client.ams_set_filament_setting.return_value = True
mock_client.extrusion_cali_sel.return_value = True
# Slot has generic ABS
status = _make_mock_status(
ams_data=[{"id": 0, "tray": [{"id": 1, "tray_info_idx": "GFB99", "tray_type": "ABS"}]}]
)
with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
mock_pm.get_client.return_value = mock_client
mock_pm.get_status.return_value = status
response = await async_client.post(
"/api/v1/inventory/assignments",
json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 0, "tray_id": 1},
)
assert response.status_code == 200
call_kwargs = mock_client.ams_set_filament_setting.call_args
# Still gets generic, but via fallback — not via sticky reuse
assert call_kwargs.kwargs["tray_info_idx"] == "GFB99"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_no_preset_reuses_specific_slot_preset(
self, async_client: AsyncClient, printer_factory, spool_factory
):
"""Spool without preset + specific preset on slot → reuse slot's preset."""
printer = await printer_factory(name="X1C")
spool = await spool_factory(slicer_filament=None, material="PLA")
mock_client = MagicMock()
mock_client.ams_set_filament_setting.return_value = True
mock_client.extrusion_cali_sel.return_value = True
# Slot has a specific Bambu PLA preset (not generic)
status = _make_mock_status(
ams_data=[{"id": 0, "tray": [{"id": 0, "tray_info_idx": "GFA05", "tray_type": "PLA"}]}]
)
with patch("backend.app.services.printer_manager.printer_manager") as mock_pm:
mock_pm.get_client.return_value = mock_client
mock_pm.get_status.return_value = status
response = await async_client.post(
"/api/v1/inventory/assignments",
json={"spool_id": spool.id, "printer_id": printer.id, "ams_id": 0, "tray_id": 0},
)
assert response.status_code == 200
call_kwargs = mock_client.ams_set_filament_setting.call_args
# Slot's specific preset is reused when spool has no own preset
assert call_kwargs.kwargs["tray_info_idx"] == "GFA05"