From dca05ce6b15dded7de7c63f310434fcdbd45fa3d Mon Sep 17 00:00:00 2001 From: maziggy Date: Fri, 15 May 2026 10:33:38 +0200 Subject: [PATCH] fix(inventory): break the Reset-Slot deadlock on A1 Mini BMCU / P1S Standard AMS (#1322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original #1322 fix widened empty-slot detection to (state == 11 OR tray_type != ""), which closed the configured-slot reconfig case but didn't help the "Reset Slot on printer screen with spool still inserted" flow. On these firmwares the AMS reports state=3, tray_type="" after a Reset Slot regardless of whether a spool is physically present, so the empty-detection still decided "empty", skipped MQTT, marked pending — and on_ams_change replay never re-fired because the AMS never reported any state change either. RosdasHH traced the path: tray_state=3 falls into the else: branch, slot_is_empty = not (fingerprint_type and fingerprint_type.strip()), fingerprint_type is "", so slot_is_empty=True, MQTT is skipped, and the slot stays unconfigured forever. He verified empirically that removing the gate makes the firmware accept the push when a spool is physically present. Drop the tray_type fallback entirely. Only state in {9, 10} (firmware's explicit "no spool" / "spool present but no feed") short-circuits the MQTT publish. Every other state — including 3 (default-idle, ambiguous) and missing-state (older firmwares) — attempts the publish. Bambu's "firmware silently drops on empty slots" behavior makes the worst case a no-op for a truly-empty slot, and on_ams_change replay still serves as the safety net for state=9/10 slots whose spools get inserted later. pending_config is now (slot_is_definitely_empty OR not configured) so a printer-offline / no-client publish failure correctly flags the assignment for replay instead of falsely showing "configured". --- CHANGELOG.md | 2 + backend/app/api/routes/inventory.py | 65 ++++++++------ .../integration/test_inventory_assign.py | 90 +++++++++++-------- 3 files changed, 94 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 726c854da..aa3ab6ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ All notable changes to Bambuddy will be documented in this file. - **Slice modal: pick the build plate (#1337, reported by @digitalskies)** — Slicing a plain STL through the integrated slicer always defaulted to whatever `curr_bed_type` lived in the chosen process preset (typically `Cool Plate`), which the slicer CLI then rejected for high-temp filaments with `Plate 1: Cool Plate does not support filament 1`. The user had no way to switch plates short of cloning the process preset in BambuStudio, which defeats the point of the in-app slicer. The Slice modal now exposes a `Build plate` dropdown with the six canonical BambuStudio / OrcaSlicer plates (Cool Plate, Cool Plate SuperTack, Engineering Plate, High Temp Plate, Textured PEI Plate, Smooth PEI Plate) plus an explicit `Auto (use process preset)` option that preserves the previous behavior. The dropdown sits between Process profile and Filament rows so it stays visible regardless of how many filament slots the picked plate uses (a long filament list would otherwise push it off the modal's `max-h-[85vh]` scroll viewport) and is **always enabled** — including when the user picks a Printer Preset Bundle from the top BundlePicker. When the user picks a specific plate, the new `bed_type` field on `SliceRequest` ([`backend/app/schemas/slicer.py`](backend/app/schemas/slicer.py)) flows through the dispatcher via two paths: (1) **resolved-preset path** — the route helper `_patch_process_bed_type` in [`backend/app/api/routes/library.py`](backend/app/api/routes/library.py) overwrites `curr_bed_type` on the resolved process JSON before forwarding to the sidecar (no preset cloning required); (2) **bundle dispatch path** — `slice_with_bundle` in [`backend/app/services/slicer_api.py`](backend/app/services/slicer_api.py) adds a `bedType` form field to the sidecar multipart so the sidecar can pass `--curr_bed_type` through to the CLI, which lets the override take effect even though Bambuddy can't patch the bundle's process JSON locally (the sidecar materialises it from the stored .bbscfg). Sidecar versions that don't recognise the field silently no-op — the slice still runs, just with the bundle's default plate; the slicer-API fork at maziggy/orca-slicer-api will need the matching change for the bundle path to take full effect. **i18n parity:** 8 new keys (`slice.bedType.{label,auto,coolPlate,coolPlateSuperTack,engineering,highTemp,texturedPEI,smoothPEI}`) added to all 8 locales — full German translation, English fallbacks elsewhere per project convention. **Regression tests:** 4 in [`test_slice_request_bed_type.py`](backend/tests/unit/test_slice_request_bed_type.py) (`bed_type` defaults to None, accepts the six canonical strings, rejects overlong input via the schema's `max_length=64`; `_patch_process_bed_type` overwrites an existing value, adds the field when missing, and returns the input unchanged for malformed JSON or non-dict roots), 4 in [`test_library_slice_api.py`](backend/tests/integration/test_library_slice_api.py) (resolved-preset path: with `bed_type` set, the sidecar receives `"curr_bed_type": "Textured PEI Plate"` in the presetProfile multipart part; without it, `curr_bed_type` stays out of the body entirely. bundle dispatch path: `bedType` form field carries the override through to the sidecar; omitting `bed_type` keeps the form field out of the request so the bundle's own `curr_bed_type` is preserved), 2 in [`SliceModal.test.tsx`](frontend/src/__tests__/components/SliceModal.test.tsx) (dropdown selection puts `bed_type` on the request; leaving it on Auto omits the field). 59 backend slice tests + 34 SliceModal tests pass; build and i18n parity script clean. ### Fixed +- **Assign Spool now configures the slot even after a "Reset Slot" on A1 Mini BMCU / P1S Standard AMS** ([#1322](https://github.com/maziggy/bambuddy/issues/1322) follow-up, reported by @RosdasHH) — The original fix widened empty-slot detection to `state == 11 OR tray_type != ""`, which closed the configured-slot reconfig case (PETG-over-PLA) but didn't help the "Reset Slot on printer screen with spool still inserted" flow: on these firmwares the AMS reports `state=3, tray_type=""` after a Reset Slot regardless of whether a spool is physically loaded. The empty-detection therefore decided "empty", skipped the MQTT publish, marked the assignment pending, and waited for `on_ams_change` to re-fire when the AMS transitioned to "loaded" — but the AMS never transitioned, because nothing was changing physically. A deadlock with no escape from user actions. Reporter pinned it by removing the `if not slot_is_empty:` gate at [`backend/app/api/routes/inventory.py:1302`](backend/app/api/routes/inventory.py) and verified the firmware accepts the MQTT push when a spool is present, even with `state=3, tray_type=""`. The original guard's rationale — "Bambu firmware silently drops ams_filament_setting / extrusion_cali_sel for unloaded slots" — turned out to be over-cautious: it's load-bearing only for slots that the *firmware itself* explicitly marks empty via `state == 9` ("no spool") or `state == 10` ("spool present but no feed"). For ambiguous states (`state=3` default-idle, missing-state on older firmwares), the AMS doesn't give us a reliable signal at all, so the safest bet is to treat the user's explicit Assign click as their assertion that a spool is there and let the firmware decide what to do with the push. **Fix:** the empty-detection now only short-circuits on `state ∈ {9, 10}` — every other state attempts MQTT. `pending_config` is now driven by either the explicit-empty signal OR `not configured` (so a printer-offline / no-client publish failure still flags the assignment as awaiting follow-up). The `on_ams_change` replay logic at [`backend/app/main.py:1031`](backend/app/main.py) is unchanged and still serves as the safety net for state=9/10 slots whose spools get inserted later (and for any truly-empty slot the firmware dropped — DB `fingerprint_type` stays empty until an AMS push actually provides one, so the replay still fires). **Trade-off:** for the rare case of "assign to a slot that really IS empty + state=3", the badge will show "Configured" even though firmware silently dropped the push. Most users assign right after inserting, so this is a small UI honesty cost in exchange for unblocking the much more common Reset-Slot workflow. **Regression tests** in [`backend/tests/integration/test_inventory_assign.py`](backend/tests/integration/test_inventory_assign.py): `test_post_reset_slot_with_state_3_still_fires_mqtt` (renamed from the previous "marks_pending" test which was pinning the bug) and `test_state_missing_with_empty_tray_type_still_fires_mqtt` (inverted from the legacy "older firmware empty → pending" assertion) pin the new behavior on the two firmware shapes the reporter hit. `test_empty_tray_type_without_state_still_fires_mqtt` covers the no-state SpoolBuddy case. `test_no_ams_data_with_no_client_marks_pending` keeps the printer-offline path producing `pending_config=True` so on_ams_change replay still triggers. `test_state_empty_skips_mqtt_and_marks_pending` (state=9) is unchanged — the firmware's explicit "no spool" still short-circuits correctly. The recent [`dd3e3f80`](https://github.com/maziggy/bambuddy/commit/dd3e3f80) k-profile fix was a separate red-herring path the reporter happened to also hit during testing; it stays as-is. All 28 inventory-assign tests + 312 inventory-tagged tests pass; ruff clean. + - **Firmware update dialog now survives Cloudflare-blocked or transient outages on `bambulab.com`** ([#1350](https://github.com/maziggy/bambuddy/issues/1350), reported by @K1ngJony) — User's X1C on 01.10.00.00 saw "01.11.02.00 newer · Unavailable" plus the error "Firmware file for 01.11.02.00 is not available from Bambu Lab", and the logs showed repeated `Failed to get Bambu Lab page: 403` warnings. Two problems stacked: (1) `https://bambulab.com/en/support/firmware-download/all` (the page Bambuddy scrapes to extract the Next.js `buildId` used to fetch per-model JSON with download URLs) was returning 403 from the reporter's network — Cloudflare bot protection on bambulab.com is stricter than on the wiki and, prior to the 2026-05-12 compliance audit, the firmware-check service still claimed to be Chrome 120 via UA spoofing. The UA was updated to honest `Bambuddy/1.0` in that audit but `Accept` / `Accept-Language` headers were never sent, so the request still tripped the "bare Python client" signal. (2) The `buildId` was cached in-memory only (1 h TTL), so every backend restart forced a fresh page fetch — meaning the first 403 from the user's network permanently broke download-URL resolution for that session even though the previous run had a perfectly valid buildId. **Fix in [`backend/app/services/firmware_check.py`](backend/app/services/firmware_check.py):** (a) the httpx client now sends `Accept: text/html,application/json,*/*;q=0.8` and `Accept-Language: en-US,en;q=0.9` alongside the existing honest `Bambuddy/1.0` UA — both headers any normal client sends, no impersonation. (b) `_get_build_id()` gained a disk-cache layer at `/firmware/build_id.json`: successful fetches persist `{build_id, fetched_at}` to disk; the in-memory cache (fresh path, 1 h TTL) is checked first, then the disk cache seeds the in-memory slot on cold start, then the live fetch tries to refresh. On 403 or network error, we keep the cached buildId and set a new `download_page_unreachable` flag so callers can render an honest error. (c) `_fetch_all_versions_from_download_page` now retries **once** when a cached buildId returns 404 (Bambu rebuilt the page → invalidate + refetch + retry); on 403 it sets the unreachable flag and gives up gracefully without churning. **Better error message in [`backend/app/services/firmware_update.py`](backend/app/services/firmware_update.py):** when a wiki-listed version has no download URL because `download_page_unreachable` is true, the dialog now says `"Could not reach Bambu Lab's firmware download page to fetch the file URL for X. Version is listed on the Bambu wiki but the download endpoint is unreachable from this network. Try again later, or download the firmware manually from bambulab.com and copy it to the printer's SD card."` instead of the misleading `"Firmware file for X is not available from Bambu Lab"` (which implied Bambu didn't have the file, when actually we just couldn't reach Bambu). Version genuinely not in the catalog still gets the original message. **Regression tests** in [`backend/tests/unit/test_firmware_versions.py`](backend/tests/unit/test_firmware_versions.py): `test_client_headers_identify_honestly_and_send_browser_accept` pins UA + Accept headers, `test_build_id_is_persisted_to_disk` confirms the disk write on success, `test_build_id_falls_back_to_disk_on_403` reproduces the reporter's 403 with a pre-seeded disk cache, `test_download_page_unreachable_flag_set_on_403_json` covers the per-model JSON endpoint 403 path, `test_download_page_retries_once_when_buildid_stale` proves the 404 retry. All 12 firmware tests + ruff clean. - **Subtype dropdown on the Add/Edit Spool form now offers `CF` (carbon fiber) and `GF` (glass fiber)** ([#1345](https://github.com/maziggy/bambuddy/issues/1345), reported by @maziggy) — The Subtype dropdown in [`frontend/src/components/spool-form/FilamentSection.tsx`](frontend/src/components/spool-form/FilamentSection.tsx) is populated from the `KNOWN_VARIANTS` array in [`frontend/src/components/spool-form/constants.ts`](frontend/src/components/spool-form/constants.ts). `CF` and `GF` were missing, so a user adding a third-party PETG-CF spool via the Material=PETG + Subtype=CF flow (the same shape Bambu's "PETG HF" already used) couldn't find the subtype in the list and had to type it freehand into the "create new" tail. Added both — `CF` to match `PETG-CF` / `PLA-CF` / `ASA-CF` / `PA-CF`, and `GF` as the natural pair for `ABS-GF` / `PA6-GF`. `parsePresetName` in [`spool-form/utils.ts`](frontend/src/components/spool-form/utils.ts) is unaffected: its materials list is iterated longest-first, so a cloud preset like `Bambu PETG-CF Black` still resolves to material=`PETG-CF` with empty afterMaterial (the variant loop runs on `""` and finds nothing — no accidental Material=PETG / Subtype=CF rewrite). Frontend build clean. diff --git a/backend/app/api/routes/inventory.py b/backend/app/api/routes/inventory.py index 8a94fc0e5..d26448e60 100644 --- a/backend/app/api/routes/inventory.py +++ b/backend/app/api/routes/inventory.py @@ -1270,36 +1270,40 @@ async def assign_spool( # 4. Auto-configure AMS slot via MQTT. # - # Skip the publish entirely when the target slot is empty: Bambu firmware - # silently drops ams_filament_setting / extrusion_cali_sel for unloaded - # slots (there is no filament context for the cali_idx to attach to). The - # SpoolAssignment row is preserved with an empty fingerprint_type, which - # acts as the "pending config" marker — when the spool is physically - # inserted later, on_ams_change re-fires the full configuration. This is - # the SpoolBuddy primary workflow: weigh-then-assign before insertion. + # Only suppress the publish when the firmware's *explicit* empty signal + # (state ∈ {9, 10}) is set — "no spool" / "spool present but no feed". + # Every other state, including state=3 (the default idle on A1 Mini BMCU / + # P1S Standard AMS for both loaded and unconfigured slots) and missing + # state (older firmwares), is treated as the user's assertion that a + # spool is in the slot and we attempt the MQTT push. # - # Empty-detection: priority order is the firmware's explicit signals - # first, then a tray_type fallback for firmwares that don't use the full - # state enum meaningfully. - # - state == 9 → empty (firmware says "no spool") - # - state == 10 → empty (firmware says "spool present but no feed") - # - state == 11 → loaded (firmware says "filament in extruder") - # - any other state value, including 3 (A1 Mini BMCU / P1S Standard AMS - # always report 3) or None (older firmwares omit the field), falls - # back to tray_type: configured ⇒ loaded, empty ⇒ empty (#1322). - # The tray_type fallback for state==3 fixes the reported bug. The 9/10 - # branch keeps existing behavior intact even if the MQTT relay's - # auto-clearing of tray_type ever fails to fire — the firmware's - # explicit "empty" signal stays authoritative over stale metadata. - if tray_state == 9 or tray_state == 10: - slot_is_empty = True - elif tray_state == 11: - slot_is_empty = False - else: - slot_is_empty = not (fingerprint_type and fingerprint_type.strip()) + # The pre-existing "skip when slot looks empty" guard read state=3 + + # tray_type="" as "empty" and skipped MQTT. On these firmwares that + # combination is the post-"Reset Slot" state with the spool still + # physically inserted — there is NO AMS signal that distinguishes it + # from a truly-empty slot, so the guard created a deadlock: MQTT never + # fired, the AMS never reported any change (because nothing changed + # physically), and on_ams_change replay therefore never re-fired the + # config either. Reporter (#1322 follow-up by @RosdasHH) verified + # empirically that removing the guard makes the slot configure + # correctly because Bambu firmware DOES accept the push for a + # physically-loaded slot, even when tray_type is "" and state is 3. + # + # Trade-off for the truly-empty slot case: firmware drops the push + # silently (per Bambu's documented behavior), the SpoolAssignment row + # still has empty fingerprint_type because nothing in the assign path + # updates that column, and on_ams_change at main.py:1031-1054 still + # fires the deferred config when a spool eventually appears. So the + # SpoolBuddy weigh-then-assign-before-insert workflow continues to + # work — just without the optimization of skipping a no-op MQTT call. + # + # state ∈ {9, 10} stays as an explicit short-circuit so we don't churn + # a doomed MQTT push when the firmware has positively confirmed "no + # spool" — and to keep the on_ams_change replay path as the single + # source of truth for those slots. + slot_is_definitely_empty = tray_state == 9 or tray_state == 10 configured = False - pending_config = slot_is_empty - if not slot_is_empty: + if not slot_is_definitely_empty: try: configured = await apply_spool_to_slot_via_mqtt( db=db, @@ -1313,6 +1317,11 @@ async def assign_spool( ) except Exception as e: logger.warning("MQTT auto-configure failed for spool %d: %s", spool.id, e) + # pending_config is the "config not landed yet" UI marker. True when the + # firmware said empty, OR when MQTT couldn't actually publish (printer + # offline, no client, transient failure). on_ams_change replay re-fires + # the config in either case once the AMS reports a non-empty fingerprint. + pending_config = slot_is_definitely_empty or not configured # Return assignment with spool data result = await db.execute( diff --git a/backend/tests/integration/test_inventory_assign.py b/backend/tests/integration/test_inventory_assign.py index 043771c0a..d8d26a08d 100644 --- a/backend/tests/integration/test_inventory_assign.py +++ b/backend/tests/integration/test_inventory_assign.py @@ -590,21 +590,25 @@ class TestAssignSpoolLiveCaliIdx: class TestAssignSpoolEmptySlotPreConfig: - """SpoolBuddy primary workflow: weigh-then-assign before the spool is in the AMS. + """Assign path under ambiguous / explicit-empty AMS state. - Bambu firmware silently drops ams_filament_setting / extrusion_cali_sel for - unloaded slots — there's no filament context for the cali_idx to attach to. - The endpoint persists the SpoolAssignment row with an empty fingerprint_type - (the "pending config" marker) and skips the MQTT publish; on_ams_change - re-fires the full configuration when filament is later inserted. + Updated for the #1322 follow-up: only the firmware's *explicit* empty + signal (state ∈ {9, 10}) skips MQTT. Anything else — including the + SpoolBuddy weigh-then-assign-before-insert case where state/tray_type + can't tell us whether a spool is loaded — attempts MQTT. The deferred- + config workflow still works because on_ams_change at main.py:1031-1054 + re-fires when an AMS push eventually reports the loaded slot. """ @pytest.mark.asyncio @pytest.mark.integration - async def test_empty_slot_skips_mqtt_but_persists_assignment( + async def test_empty_tray_type_without_state_still_fires_mqtt( self, async_client: AsyncClient, printer_factory, spool_factory ): - """Assigning to an empty slot skips MQTT and returns pending_config=True.""" + """tray_type='' with no state field: AMS can't tell us whether a + spool is loaded. Trust the user's Assign click and fire MQTT — + firmware accepts it when a spool is physically there, drops it + silently otherwise (no harm).""" printer = await printer_factory(name="H2D") spool = await spool_factory(slicer_filament="GFL05", material="PLA") @@ -612,7 +616,6 @@ class TestAssignSpoolEmptySlotPreConfig: mock_client.ams_set_filament_setting.return_value = True mock_client.extrusion_cali_sel.return_value = True - # Slot found but empty (tray_type=""): the SpoolBuddy scenario status = _make_mock_status(ams_data=[{"id": 2, "tray": [{"id": 3, "tray_type": ""}]}]) with patch("backend.app.services.printer_manager.printer_manager") as mock_pm: @@ -625,27 +628,27 @@ class TestAssignSpoolEmptySlotPreConfig: ) assert response.status_code == 200 + mock_client.ams_set_filament_setting.assert_called_once() body = response.json() - assert body["pending_config"] is True - assert body["configured"] is False - # Critical: no MQTT was published (firmware would drop it) - mock_client.ams_set_filament_setting.assert_not_called() - mock_client.extrusion_cali_sel.assert_not_called() + assert body["pending_config"] is False + assert body["configured"] is True @pytest.mark.asyncio @pytest.mark.integration - async def test_empty_slot_no_ams_data_skips_mqtt(self, async_client: AsyncClient, printer_factory, spool_factory): - """No AMS data at all (printer offline, no telemetry yet) → still pre-config.""" + async def test_no_ams_data_with_no_client_marks_pending( + self, async_client: AsyncClient, printer_factory, spool_factory + ): + """No AMS data + no MQTT client (printer offline, no telemetry): + publish can't happen, so configured=False and pending_config=True so + on_ams_change replay picks it up when the printer comes online.""" printer = await printer_factory(name="X1C") spool = await spool_factory(slicer_filament="GFL05", material="PLA") - mock_client = MagicMock() - - # No AMS data — fingerprint_type stays None, treated as empty + # No AMS data — fingerprint_type stays None. 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_client.return_value = None # Printer offline, no MQTT client. mock_pm.get_status.return_value = status response = await async_client.post( @@ -654,8 +657,9 @@ class TestAssignSpoolEmptySlotPreConfig: ) assert response.status_code == 200 - assert response.json()["pending_config"] is True - mock_client.ams_set_filament_setting.assert_not_called() + body = response.json() + assert body["pending_config"] is True + assert body["configured"] is False @pytest.mark.asyncio @pytest.mark.integration @@ -994,15 +998,23 @@ class TestAssignSpoolEmptyDetection: @pytest.mark.asyncio @pytest.mark.integration - async def test_state_missing_falls_back_to_tray_type_empty( + async def test_state_missing_with_empty_tray_type_still_fires_mqtt( self, async_client: AsyncClient, printer_factory, spool_factory ): - """Older firmware without state field + empty tray_type → pending.""" + """Older firmware without state field + empty tray_type still fires MQTT. + + The AMS doesn't tell us whether a spool is physically loaded in this + case (no state, no tray_type), so the assign click is the user's + assertion that a spool is there. Firmware silently drops the push on + a truly empty slot — no harm done, and on_ams_change replay handles + the deferred-config case (#1322 follow-up). + """ printer = await printer_factory() 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 tray_data = {"id": 3, "tray_type": "", "tray_color": ""} status = _make_mock_status(ams_data=[{"id": 2, "tray": [tray_data]}]) @@ -1017,10 +1029,10 @@ class TestAssignSpoolEmptyDetection: ) assert response.status_code == 200 - # Legacy fallback: empty tray_type + no state → treated as empty. - mock_client.ams_set_filament_setting.assert_not_called() + mock_client.ams_set_filament_setting.assert_called_once() body = response.json() - assert body["pending_config"] is True + assert body["pending_config"] is False + assert body["configured"] is True @pytest.mark.asyncio @pytest.mark.integration @@ -1059,19 +1071,26 @@ class TestAssignSpoolEmptyDetection: @pytest.mark.asyncio @pytest.mark.integration - async def test_state_never_eleven_firmware_with_empty_tray_marks_pending( + async def test_post_reset_slot_with_state_3_still_fires_mqtt( self, async_client: AsyncClient, printer_factory, spool_factory ): - """Same firmwares as above, but the slot is truly unconfigured - (tray_type=''). Neither signal points to 'loaded', so this should - still pending-config — the user has to configure or insert filament - before MQTT can fire. Pins that the disjunction didn't accidentally - flip empty slots into the loaded branch.""" + """A1 Mini BMCU / P1S Standard AMS post-"Reset Slot" with spool still + inserted: state=3, tray_type="". The AMS gives us no signal to tell + this apart from a truly-empty slot. We trust the user's Assign click + and fire MQTT — firmware accepts the push because a spool is + physically there (#1322 follow-up by @RosdasHH). + + Replaces the previous "marks_pending" assertion which was the bug: + that gate created a deadlock because the AMS would never report a + state change (nothing physically changed), so on_ams_change replay + never re-fired the deferred config either. + """ printer = await printer_factory() 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 tray_data = {"id": 3, "state": 3, "tray_type": "", "tray_color": "00000000", "tray_info_idx": ""} status = _make_mock_status(ams_data=[{"id": 2, "tray": [tray_data]}]) @@ -1086,9 +1105,10 @@ class TestAssignSpoolEmptyDetection: ) assert response.status_code == 200 - mock_client.ams_set_filament_setting.assert_not_called() + mock_client.ams_set_filament_setting.assert_called_once() body = response.json() - assert body["pending_config"] is True + assert body["pending_config"] is False + assert body["configured"] is True @pytest.mark.asyncio @pytest.mark.integration