| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891 |
- """What reaches an AMS slot when a spool's material is a product line (#2902).
- The reporter assigned an eSUN PLA+ spool and the slot came out unusable: any
- plate sliced with a PLA profile refused it. Four routes configure a slot and
- all four wrote the spool's material straight into ``tray_type``, where "PLA+"
- matches nothing -- not the slicer, and not Bambuddy's own dispatch matcher,
- which compares the printer's reported ``tray_type`` to the 3MF's declared type
- as plain equality.
- Each test below asserts the whole slot, not just the type: an unrecognised
- material also missed the generic-filament-id lookup, so the slot went out with
- an empty ``tray_info_idx`` -- the half-configured state #2604 documents the
- printer as reverting from -- and took the 200/240 catch-all temperatures
- instead of PLA's.
- """
- import json
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from httpx import AsyncClient
- from sqlalchemy.ext.asyncio import AsyncSession
- from backend.app.models.spool import Spool
- def _mqtt_mock():
- client = MagicMock()
- client.ams_set_filament_setting.return_value = True
- client.extrusion_cali_sel.return_value = True
- return client
- def _status(ams_data=None):
- status = MagicMock()
- status.raw_data = {"ams": {"ams": ams_data if ams_data is not None else []}}
- status.nozzles = [MagicMock(nozzle_diameter="0.4")]
- status.ams_extruder_map = None
- status.kprofiles = []
- return status
- def _spoolman_spool(material, spool_id=11, slicer_filament=None):
- extra = {}
- if slicer_filament is not None:
- extra["bambu_slicer_filament"] = json.dumps(str(slicer_filament))
- return {
- "id": spool_id,
- "filament": {
- "id": 1,
- "name": "Cool White",
- "material": material,
- "color_hex": "E1E9E9",
- "weight": 1000,
- "vendor": {"id": 1, "name": "eSUN"},
- },
- "remaining_weight": 800.0,
- "used_weight": 200.0,
- "archived": False,
- "extra": extra,
- }
- def _spoolman_client(spool):
- client = MagicMock()
- client.base_url = "http://localhost:7912"
- client.health_check = AsyncMock(return_value=True)
- client.get_spool = AsyncMock(return_value=spool)
- client.get_spools = AsyncMock(return_value=[spool])
- client.merge_spool_extra = AsyncMock(return_value=spool)
- return client
- class TestInternalInventoryAssign:
- async def _assign(self, async_client, db_session, material, **spool_kwargs):
- from backend.app.models.printer import Printer
- printer = Printer(
- name="P1S",
- serial_number=f"MAT2902{material[:4]}",
- ip_address="192.168.1.77",
- access_code="12345678",
- )
- db_session.add(printer)
- spool = Spool(
- material=material,
- brand="eSUN",
- color_name="Cool White",
- rgba="E1E9E9FF",
- label_weight=1000,
- weight_used=0,
- **spool_kwargs,
- )
- db_session.add(spool)
- await db_session.commit()
- await db_session.refresh(printer)
- await db_session.refresh(spool)
- client = _mqtt_mock()
- with patch("backend.app.services.printer_manager.printer_manager") as pm:
- pm.get_client.return_value = client
- 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
- client.ams_set_filament_setting.assert_called_once()
- return client.ams_set_filament_setting.call_args.kwargs
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_a_pla_plus_spool_configures_the_slot_as_pla(
- self, async_client: AsyncClient, db_session: AsyncSession
- ):
- sent = await self._assign(async_client, db_session, "PLA+")
- assert sent["tray_type"] == "PLA"
- # Not just the label: the id and its setting_id are what stop the
- # printer treating the slot as half configured, and the temperatures
- # are PLA's rather than the catch-all.
- assert sent["tray_info_idx"] == "GFL99"
- assert sent["setting_id"] == "GFSL99"
- assert (sent["nozzle_temp_min"], sent["nozzle_temp_max"]) == (190, 230)
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_the_product_name_is_not_lost_it_moves_to_the_sub_brand(
- self, async_client: AsyncClient, db_session: AsyncSession
- ):
- """Which is where Bambu itself puts it -- their own catalogue has a
- preset named "eSUN PLA+" (GFL03) whose type is PLA."""
- sent = await self._assign(async_client, db_session, "PLA+")
- assert "PLA+" in sent["tray_sub_brands"]
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_a_material_that_already_resolved_keeps_its_own_preset(
- self, async_client: AsyncClient, db_session: AsyncSession
- ):
- """ "PETG HF" has a generic preset of its own (GFG96, "Generic PETG HF").
- Reducing the material before the id lookup rather than after it would
- trade that away for plain PETG's GFG99 -- a quiet downgrade of slots
- that work today."""
- sent = await self._assign(async_client, db_session, "PETG HF")
- assert sent["tray_info_idx"] == "GFG96"
- assert sent["tray_type"] == "PETG"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_it_can_now_reuse_the_calibrated_preset_already_in_the_slot(
- self, async_client: AsyncClient, db_session: AsyncSession
- ):
- """A slot already holding a specific preset keeps it when the incoming
- spool is the same material -- that is how a printer's calibration
- context survives an assignment. The comparison is against the slot's
- reported type, so a PLA+ spool could never match a PLA slot and the
- reuse branch was dead for every spool this issue is about."""
- from backend.app.models.printer import Printer
- printer = Printer(
- name="Reuse P1S",
- serial_number="MAT2902RU",
- ip_address="192.168.1.81",
- access_code="12345678",
- )
- db_session.add(printer)
- spool = Spool(material="PLA+", brand="eSUN", rgba="E1E9E9FF", label_weight=1000, weight_used=0)
- db_session.add(spool)
- await db_session.commit()
- await db_session.refresh(printer)
- await db_session.refresh(spool)
- client = _mqtt_mock()
- live_slot = [{"id": 0, "tray": [{"id": 1, "tray_info_idx": "P4d64437", "tray_type": "PLA"}]}]
- with patch("backend.app.services.printer_manager.printer_manager") as pm:
- pm.get_client.return_value = client
- pm.get_status.return_value = _status(live_slot)
- 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
- sent = client.ams_set_filament_setting.call_args.kwargs
- assert sent["tray_info_idx"] == "P4d64437"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_but_it_does_not_reuse_a_product_name_a_previous_version_left_there(
- self, async_client: AsyncClient, db_session: AsyncSession
- ):
- """A spool whose slicer_filament was free text could send that text to
- the printer as the slot's filament id, and the printer reports it
- straight back -- so an upgraded install can be looking at a slot that
- says type PLA, id "PLA+". Reuse has always refused a bare material name
- in that field; refusing a product line too is what stops the bad id
- being carried forward on every assignment instead of replaced."""
- from backend.app.models.printer import Printer
- printer = Printer(
- name="Stale P1S",
- serial_number="MAT2902ST",
- ip_address="192.168.1.82",
- access_code="12345678",
- )
- db_session.add(printer)
- spool = Spool(material="PLA", brand="eSUN", rgba="E1E9E9FF", label_weight=1000, weight_used=0)
- db_session.add(spool)
- await db_session.commit()
- await db_session.refresh(printer)
- await db_session.refresh(spool)
- client = _mqtt_mock()
- stale_slot = [{"id": 0, "tray": [{"id": 1, "tray_info_idx": "PLA+", "tray_type": "PLA"}]}]
- with patch("backend.app.services.printer_manager.printer_manager") as pm:
- pm.get_client.return_value = client
- pm.get_status.return_value = _status(stale_slot)
- 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
- sent = client.ams_set_filament_setting.call_args.kwargs
- assert sent["tray_info_idx"] == "GFL99"
- assert sent["tray_type"] == "PLA"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_a_material_nothing_can_be_made_of_is_sent_unchanged(
- self, async_client: AsyncClient, db_session: AsyncSession
- ):
- """The catalogue ships a few names with no filament type in them at all.
- Guessing at those would be worse than leaving them: this route behaved
- exactly this way before #2902, and still does."""
- sent = await self._assign(async_client, db_session, "CPE HG100")
- assert sent["tray_type"] == "CPE HG100"
- assert sent["tray_info_idx"] == ""
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_a_free_text_slicer_filament_naming_a_product_is_not_a_filament_id(
- self, async_client: AsyncClient, db_session: AsyncSession
- ):
- """slicer_filament is free text on older spools, so "PLA+" can be sitting
- in it. It is as unusable a tray_info_idx as the bare "PLA" the resolver
- already discarded, and letting it through would put a product name in
- the field the printer keys its calibration table by."""
- sent = await self._assign(async_client, db_session, "PLA+", slicer_filament="PLA+")
- assert sent["tray_info_idx"] == "GFL99"
- class TestSpoolmanInventoryAssign:
- @pytest.fixture
- async def settings(self, db_session):
- from backend.app.models.settings import Settings
- db_session.add(Settings(key="spoolman_enabled", value="true"))
- db_session.add(Settings(key="spoolman_url", value="http://localhost:7912"))
- await db_session.commit()
- @pytest.fixture
- async def printer(self, db_session):
- from backend.app.models.printer import Printer
- p = Printer(
- name="Spoolman P1S",
- serial_number="MAT2902SM",
- ip_address="192.168.1.78",
- access_code="12345678",
- )
- db_session.add(p)
- await db_session.commit()
- await db_session.refresh(p)
- return p
- async def _assign(self, async_client, printer, material, slicer_filament=None):
- mqtt = _mqtt_mock()
- spool = _spoolman_spool(material, slicer_filament=slicer_filament)
- with (
- patch("backend.app.api.routes.spoolman_inventory.printer_manager") as pm,
- patch(
- "backend.app.api.routes.spoolman_inventory.get_spoolman_client",
- AsyncMock(return_value=_spoolman_client(spool)),
- ),
- ):
- pm.get_client.return_value = mqtt
- pm.get_status.return_value = _status()
- response = await async_client.post(
- "/api/v1/spoolman/inventory/slot-assignments",
- json={"spoolman_spool_id": 11, "printer_id": printer.id, "ams_id": 0, "tray_id": 2},
- )
- assert response.status_code == 200
- return mqtt.ams_set_filament_setting.call_args.kwargs
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_spoolmans_free_text_material_is_reduced_the_same_way(
- self, async_client: AsyncClient, settings, printer
- ):
- """Spoolman's material field is free text too, so the same product names
- arrive by this route -- and it is the route the reporter used."""
- sent = await self._assign(async_client, printer, "PLA+")
- assert sent["tray_type"] == "PLA"
- assert sent["tray_info_idx"] == "GFL99"
- # Exactly, not just "contains PLA+": the filament's own name is in this
- # string too, so a substring check would pass even if the material had
- # been reduced before it was built.
- assert sent["tray_sub_brands"] == "eSUN PLA+ Cool White"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_it_keeps_this_routes_own_preset_for_a_material_that_had_one(
- self, async_client: AsyncClient, settings, printer
- ):
- sent = await self._assign(async_client, printer, "PETG HF")
- assert sent["tray_info_idx"] == "GFG96"
- assert sent["tray_type"] == "PETG"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_the_resolver_is_handed_the_spools_own_wording_not_the_type(
- self, async_client: AsyncClient, settings, printer, db_session
- ):
- """This route also passes the material down to the slicer-filament
- resolver. Handing that the reduced type instead would look harmless and
- quietly downgrade GFG96 to GFG99 whenever the spool points at a local
- preset with no filament_id of its own."""
- from backend.app.models.local_preset import LocalPreset
- lp = LocalPreset(name="Generic PETG HF", preset_type="filament", source="orcaslicer", setting="{}")
- db_session.add(lp)
- await db_session.commit()
- await db_session.refresh(lp)
- sent = await self._assign(async_client, printer, "PETG HF", slicer_filament=lp.id)
- assert sent["tray_info_idx"] == "GFG96"
- class TestConfigureSlotModal:
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_a_product_line_typed_into_the_modal_is_reduced_too(self, async_client: AsyncClient, printer_factory):
- """The Configure Slot modal derives tray_type from a preset name or the
- spool's material, so it can hand the backend a product line as readily
- as the assignment routes can."""
- printer = await printer_factory(model="P1S")
- client = _mqtt_mock()
- with patch("backend.app.api.routes.printers.printer_manager") as pm:
- pm.get_client.return_value = client
- pm.get_status.return_value = _status()
- response = await async_client.post(
- f"/api/v1/printers/{printer.id}/slots/0/1/configure",
- params={
- "tray_info_idx": "",
- "tray_type": "PLA+",
- "tray_sub_brands": "eSUN PLA+",
- "tray_color": "E1E9E9FF",
- "nozzle_temp_min": 190,
- "nozzle_temp_max": 230,
- },
- )
- assert response.status_code == 200
- sent = client.ams_set_filament_setting.call_args.kwargs
- assert sent["tray_type"] == "PLA"
- # The empty tray_info_idx the modal sent for a generic material is what
- # the reduced type now rescues.
- assert sent["tray_info_idx"] == "GFL99"
- assert sent["tray_sub_brands"] == "eSUN PLA+"
- class TestSpoolmanLink:
- """The fourth route that configures a slot: linking a Spoolman spool to a
- slot's tag auto-configures it too, from the same free-text material."""
- @pytest.fixture
- async def settings(self, db_session):
- from backend.app.models.settings import Settings
- db_session.add(Settings(key="spoolman_enabled", value="true"))
- db_session.add(Settings(key="spoolman_url", value="http://localhost:7912"))
- await db_session.commit()
- @pytest.fixture
- async def printer(self, db_session):
- from backend.app.models.printer import Printer
- p = Printer(
- name="Link P1S",
- serial_number="MAT2902LK",
- ip_address="192.168.1.79",
- access_code="12345678",
- )
- db_session.add(p)
- await db_session.commit()
- await db_session.refresh(p)
- return p
- async def _link(self, async_client, printer, material):
- client = _spoolman_client(_spoolman_spool(material, spool_id=12))
- mqtt = _mqtt_mock()
- with (
- patch("backend.app.api.routes.spoolman.get_spoolman_client", AsyncMock(return_value=client)),
- patch("backend.app.api.routes.spoolman.init_spoolman_client", AsyncMock(return_value=client)),
- patch("backend.app.api.routes.spoolman.printer_manager") as pm,
- ):
- pm.get_client.return_value = mqtt
- pm.get_status.return_value = _status()
- response = await async_client.post(
- "/api/v1/spoolman/spools/12/link",
- json={
- "tray_uuid": "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4",
- "printer_id": printer.id,
- "ams_id": 0,
- "tray_id": 3,
- },
- )
- assert response.status_code == 200
- return mqtt.ams_set_filament_setting.call_args.kwargs
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_linking_a_pla_plus_spool_configures_the_slot_as_pla(
- self, async_client: AsyncClient, settings, printer
- ):
- sent = await self._link(async_client, printer, "PLA+")
- assert sent["tray_type"] == "PLA"
- assert sent["tray_info_idx"] == "GFL99"
- assert sent["tray_sub_brands"] == "eSUN PLA+ Cool White"
- assert (sent["nozzle_temp_min"], sent["nozzle_temp_max"]) == (190, 230)
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_it_keeps_this_routes_own_preset_for_a_material_that_had_one(
- self, async_client: AsyncClient, settings, printer
- ):
- sent = await self._link(async_client, printer, "PETG HF")
- assert sent["tray_info_idx"] == "GFG96"
- assert sent["tray_type"] == "PETG"
- class TestALocalPresetThatNamesNoFilamentId:
- """The one path where the material reaches the slicer-filament resolver
- rather than the route's own fallback: a spool pointing at an imported local
- preset whose setting JSON carries no filament_id. The resolver then has only
- the material to go on, so it has to read it the same way -- and be handed
- the spool's own wording, not the reduced type."""
- @pytest.fixture
- async def preset(self, db_session):
- from backend.app.models.local_preset import LocalPreset
- lp = LocalPreset(
- name="eSUN PLA+ @BBL P1S",
- preset_type="filament",
- source="orcaslicer",
- filament_type=None,
- setting="{}",
- )
- db_session.add(lp)
- await db_session.commit()
- await db_session.refresh(lp)
- return lp
- @pytest.fixture
- async def printer(self, db_session):
- from backend.app.models.printer import Printer
- p = Printer(
- name="LP P1S",
- serial_number="MAT2902LP",
- ip_address="192.168.1.80",
- access_code="12345678",
- )
- db_session.add(p)
- await db_session.commit()
- await db_session.refresh(p)
- return p
- async def _assign(self, async_client, db_session, printer, preset, material):
- spool = Spool(
- material=material,
- brand="eSUN",
- rgba="E1E9E9FF",
- label_weight=1000,
- weight_used=0,
- slicer_filament=str(preset.id),
- )
- db_session.add(spool)
- await db_session.commit()
- await db_session.refresh(spool)
- client = _mqtt_mock()
- with patch("backend.app.services.printer_manager.printer_manager") as pm:
- pm.get_client.return_value = client
- 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
- return client.ams_set_filament_setting.call_args.kwargs
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_the_resolver_places_a_product_line_too(
- self, async_client: AsyncClient, db_session: AsyncSession, printer, preset
- ):
- sent = await self._assign(async_client, db_session, printer, preset, "PLA+")
- assert sent["tray_info_idx"] == "GFL99"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_and_still_prefers_a_material_that_has_its_own_preset(
- self, async_client: AsyncClient, db_session: AsyncSession, printer, preset
- ):
- sent = await self._assign(async_client, db_session, printer, preset, "PETG HF")
- assert sent["tray_info_idx"] == "GFG96"
- class TestTheAssignmentSurvivesTheSlotItJustConfigured:
- """The other side of the same coin, and the one that bites hardest.
- on_ams_change auto-unlinks an assignment whose slot no longer looks like it
- did when the spool was assigned. The fingerprint is snapshotted *before* the
- MQTT config goes out, so the very next AMS push after an assignment is a
- mismatch by construction -- and what saves the assignment is a second check:
- does the tray match the assigned spool now? That check read the spool's raw
- material, which the slot no longer carries, so every spool this issue is
- about would have been silently unlinked from the slot it had just been
- assigned to. Correct in isolation, ruinous together.
- """
- async def _push(
- self,
- db_session,
- printer_factory,
- spool_material,
- reported_type,
- fingerprint_type="PETG",
- **spool_kwargs,
- ):
- from unittest.mock import AsyncMock
- from backend.app.main import on_ams_change
- from backend.app.models.spool_assignment import SpoolAssignment
- printer = await printer_factory(name="H2D")
- spool = Spool(
- material=spool_material,
- brand="eSUN",
- rgba="E1E9E9FF",
- label_weight=1000,
- weight_used=0,
- **spool_kwargs,
- )
- db_session.add(spool)
- await db_session.commit()
- await db_session.refresh(spool)
- assignment = SpoolAssignment(
- spool_id=spool.id,
- printer_id=printer.id,
- ams_id=0,
- tray_id=2,
- fingerprint_color="E1E9E9FF",
- fingerprint_type=fingerprint_type,
- )
- db_session.add(assignment)
- await db_session.commit()
- assignment_id = assignment.id
- ams_data = [{"id": 0, "tray": [{"id": 2, "tray_type": reported_type, "tray_color": "E1E9E9FF", "state": 11}]}]
- status = _status(ams_data)
- status.state = "IDLE"
- with (
- patch("backend.app.main.printer_manager") as pm,
- patch("backend.app.main.mqtt_relay") as relay,
- patch("backend.app.main.ws_manager") as ws,
- ):
- pm.get_printer.return_value = MagicMock(name="H2D", serial_number="0948BB540200427")
- pm.get_status.return_value = status
- pm.get_model.return_value = "H2D"
- relay.on_ams_change = AsyncMock()
- ws.send_printer_status = AsyncMock()
- ws.broadcast = AsyncMock()
- await on_ams_change(printer.id, ams_data)
- # on_ams_change commits through its own session.
- db_session.expunge_all()
- return await db_session.get(SpoolAssignment, assignment_id)
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_a_pla_plus_spool_is_not_unlinked_from_the_slot_now_reporting_pla(
- self, async_client: AsyncClient, db_session: AsyncSession, printer_factory
- ):
- surviving = await self._push(db_session, printer_factory, "PLA+", reported_type="PLA")
- assert surviving is not None, "the slot reports what we wrote to it -- that is a match, not a swap"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_nor_is_one_in_a_slot_an_older_version_configured(
- self, async_client: AsyncClient, db_session: AsyncSession, printer_factory
- ):
- """An install upgrading into this fix has slots still reporting "PLA+"
- until something reconfigures them. Reducing only the spool's side would
- break those the moment they were left alone."""
- surviving = await self._push(db_session, printer_factory, "PLA+", reported_type="PLA+")
- assert surviving is not None
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_nor_is_one_whose_slot_took_its_presets_type_rather_than_its_material(
- self, async_client: AsyncClient, db_session: AsyncSession, printer_factory
- ):
- """The preset outranks the material column when the spool has one, so
- the slot can legitimately carry a type the material never named. The
- check has to recognise that as its own handiwork or it unlinks the
- assignment on the very next AMS push."""
- surviving = await self._push(
- db_session,
- printer_factory,
- "PLA",
- reported_type="PLA-AERO",
- slicer_filament_name="Bambu PLA Aero @BBL H2D",
- )
- assert surviving is not None
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_even_when_the_preset_name_was_never_stored(
- self, async_client: AsyncClient, db_session: AsyncSession, printer_factory
- ):
- """slicer_filament_name is optional. An imported local preset carries
- its type outright, which is the value the assign path actually used."""
- from backend.app.models.local_preset import LocalPreset
- lp = LocalPreset(
- name="Bambu PLA Aero @BBL H2D",
- preset_type="filament",
- source="orcaslicer",
- filament_type="PLA-AERO",
- setting="{}",
- )
- db_session.add(lp)
- await db_session.commit()
- await db_session.refresh(lp)
- surviving = await self._push(
- db_session,
- printer_factory,
- "PLA",
- reported_type="PLA-AERO",
- slicer_filament=str(lp.id),
- )
- assert surviving is not None
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_a_genuinely_different_filament_still_unlinks(
- self, async_client: AsyncClient, db_session: AsyncSession, printer_factory
- ):
- """The check still has to do its job: someone swapping PLA for ABS in
- the slot must lose the assignment, or usage gets charged to the wrong
- spool."""
- surviving = await self._push(db_session, printer_factory, "PLA+", reported_type="ABS")
- assert surviving is None
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_and_a_preset_name_does_not_excuse_an_unrelated_slot(
- self, async_client: AsyncClient, db_session: AsyncSession, printer_factory
- ):
- """Widening the check to the preset only accepts the types the assign
- path could actually have written. Anything else is still a swap."""
- surviving = await self._push(
- db_session,
- printer_factory,
- "PLA",
- reported_type="ABS",
- slicer_filament_name="Bambu PLA Aero @BBL H2D",
- )
- assert surviving is None
- class TestAFilledOrFoamedVariantIsATypeOfItsOwn:
- """The first cut of this fix reduced PLA-AERO, PLA-GF, ASA-GF and PPS-GF
- onto their base material, because the reduction table was assembled from
- the cloud filament names and the frontend preset parser and never checked
- against ``filament_fields.json`` -- the list Bambuddy itself offers when a
- preset is created. @doncaruana caught PLA Aero on the issue.
- That is worse than the bug it replaced. "PLA-AERO" matched nothing before,
- which was useless but honest; "PLA" matches every plain PLA plate in the
- queue, so the dispatcher would have sent one to foaming filament.
- """
- @pytest.fixture
- async def printer(self, db_session):
- from backend.app.models.printer import Printer
- p = Printer(
- name="Aero P1S",
- serial_number="MAT2902AERO",
- ip_address="192.168.1.81",
- access_code="12345678",
- )
- db_session.add(p)
- await db_session.commit()
- await db_session.refresh(p)
- return p
- async def _assign(self, async_client, db_session, printer, material, tray_id, **spool_kwargs):
- spool = Spool(
- material=material,
- brand="Bambu Lab",
- rgba="E1E9E9FF",
- label_weight=1000,
- weight_used=0,
- **spool_kwargs,
- )
- db_session.add(spool)
- await db_session.commit()
- await db_session.refresh(spool)
- client = _mqtt_mock()
- with patch("backend.app.services.printer_manager.printer_manager") as pm:
- pm.get_client.return_value = client
- 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": tray_id},
- )
- assert response.status_code == 200
- return client.ams_set_filament_setting.call_args.kwargs
- @pytest.mark.asyncio
- @pytest.mark.integration
- @pytest.mark.parametrize(
- ("material", "tray_id"),
- [("PLA-AERO", 0), ("PLA-GF", 1), ("ASA-GF", 2), ("PPS-GF", 3)],
- )
- async def test_it_reaches_the_slot_intact(
- self, async_client: AsyncClient, db_session: AsyncSession, printer, material, tray_id
- ):
- sent = await self._assign(async_client, db_session, printer, material, tray_id)
- assert sent["tray_type"] == material
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_written_with_a_space_it_still_reaches_the_slot_intact(
- self, async_client: AsyncClient, db_session: AsyncSession, printer
- ):
- """The table hyphenates because the slicers do; a spool says "PLA Aero"
- and so does every Bambu preset name."""
- sent = await self._assign(async_client, db_session, printer, "PLA Aero", 0)
- assert sent["tray_type"] == "PLA-AERO"
- class TestThePresetOutranksTheMaterialColumn:
- """#2902 again, from @doncaruana: a preset has to be picked from a list the
- slicer defines, so it already knows its own type and nothing has to be read
- out of a product name. When a spool points at one, that answer wins.
- It cannot be the only answer. ``material`` is required on a spool and
- ``slicer_filament`` is not -- the spool this issue was reported for had no
- preset at all -- so the reduction stays as the fallback.
- """
- @pytest.fixture
- async def printer(self, db_session):
- from backend.app.models.printer import Printer
- p = Printer(
- name="Preset P1S",
- serial_number="MAT2902PRE",
- ip_address="192.168.1.82",
- access_code="12345678",
- )
- db_session.add(p)
- await db_session.commit()
- await db_session.refresh(p)
- return p
- async def _preset(self, db_session, name, filament_type):
- from backend.app.models.local_preset import LocalPreset
- lp = LocalPreset(
- name=name,
- preset_type="filament",
- source="orcaslicer",
- filament_type=filament_type,
- setting="{}",
- )
- db_session.add(lp)
- await db_session.commit()
- await db_session.refresh(lp)
- return lp
- async def _assign(self, async_client, db_session, printer, material, preset, tray_id):
- spool = Spool(
- material=material,
- brand="Bambu Lab",
- rgba="E1E9E9FF",
- label_weight=1000,
- weight_used=0,
- slicer_filament=str(preset.id) if preset else None,
- )
- db_session.add(spool)
- await db_session.commit()
- await db_session.refresh(spool)
- client = _mqtt_mock()
- with patch("backend.app.services.printer_manager.printer_manager") as pm:
- pm.get_client.return_value = client
- 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": tray_id},
- )
- assert response.status_code == 200
- return client.ams_set_filament_setting.call_args.kwargs
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_the_slot_gets_the_presets_type_not_one_read_from_the_material(
- self, async_client: AsyncClient, db_session: AsyncSession, printer
- ):
- """The material column says "PLA", which the reduction would happily
- accept. The preset says the spool is foaming PLA, and it is right."""
- preset = await self._preset(db_session, "Bambu PLA Aero @BBL P1S", "PLA-AERO")
- sent = await self._assign(async_client, db_session, printer, "PLA", preset, 0)
- assert sent["tray_type"] == "PLA-AERO"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_a_preset_that_names_no_type_leaves_the_reduction_in_charge(
- self, async_client: AsyncClient, db_session: AsyncSession, printer
- ):
- preset = await self._preset(db_session, "eSUN PLA+ @BBL P1S", None)
- sent = await self._assign(async_client, db_session, printer, "PLA+", preset, 1)
- assert sent["tray_type"] == "PLA"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_and_a_spool_with_no_preset_at_all_still_gets_one(
- self, async_client: AsyncClient, db_session: AsyncSession, printer
- ):
- sent = await self._assign(async_client, db_session, printer, "PLA+", None, 2)
- assert sent["tray_type"] == "PLA"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_a_hand_edited_preset_naming_a_product_line_is_still_reduced(
- self, async_client: AsyncClient, db_session: AsyncSession, printer
- ):
- """Preferring the preset does not mean trusting it blindly. A profile
- whose filament_type is a product line puts that product line in the
- slot, which is the exact failure this issue is about."""
- preset = await self._preset(db_session, "My PLA+ @BBL P1S", "PLA+")
- sent = await self._assign(async_client, db_session, printer, "PLA", preset, 3)
- assert sent["tray_type"] == "PLA"
|