Procházet zdrojové kódy

fix(spoolbuddy): close #1815 — preserve PFUS/PFCN setting_id in resolver

  SpoolBuddy "Assign to AMS" with a Bambu Cloud user preset (PFUS) left
  Bambu Studio showing "Generic <Material>" instead of the user's
  custom preset. Root cause: the defensive filter that catches
  PFUS/PFCN leaks into tray_info_idx also cleared setting_id —
  but PFUS/PFCN are VALID setting_id values, just not valid
  tray_info_idx values. When the cloud detail lookup didn't return
  a filament_id (cloud unauth on the on_ams_change replay path,
  transient failure, or older custom presets), both fields got
  cleared and the caller's generic-material fallback overwrote
  setting_id with GFSG99 — slicer resolved to Generic PETG.

  Fix: the filter still clears tray_info_idx for PFUS/PFCN/material-
  name leaks, but preserves setting_id when it's a valid slicer
  reference (PFUS / PFCN / GFS). Material-name leaks still clear
  both. Post-fix MQTT carries tray_info_idx=GFG99 (firmware-acceptable
  for HMS/drying/colour) AND setting_id=PFUS<hash> (slicer uses this
  to load the actual user preset).

  What stays the same: Bambuddy's own AMS card still displays
  the generic material on cloud-unauth paths — same fundamental
  limitation as today. Fixing that needs a deeper layered fallback
  (LocalPreset name match, printer kprofile query, cloud-detail
  cache) and is out of scope for this drop. Slicer-side fix is
  the reporter's explicit ask.
maziggy před 2 měsíci
rodič
revize
261c376d1f

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
CHANGELOG.md


+ 14 - 1
backend/app/services/slicer_filament_resolver.py

@@ -200,6 +200,19 @@ async def resolve_slicer_filament(
         or tray_info_idx.startswith("PFCN")
     ):
         tray_info_idx = ""
-        setting_id = ""
+        # Preserve setting_id when it's still a valid slicer reference
+        # (PFUS / PFCN cloud user/shared preset, or GFS Bambu official
+        # preset). The slicer accepts these as setting_id even though
+        # they're rejected as tray_info_idx; without preservation the
+        # slicer falls back to whatever generic filament the caller's
+        # tray_info_idx fallback produces and shows "Generic <Material>"
+        # instead of the user's actual custom preset (#1815). Material-name
+        # leaks (e.g. setting_id="PETG") are still cleared — those are
+        # never valid slicer references.
+        if not (
+            setting_id
+            and (setting_id.startswith("PFUS") or setting_id.startswith("PFCN") or setting_id.startswith("GFS"))
+        ):
+            setting_id = ""
 
     return (tray_info_idx, setting_id, sub_brand_override)

+ 142 - 0
backend/tests/unit/services/test_slicer_filament_resolver.py

@@ -0,0 +1,142 @@
+"""Tests for ``resolve_slicer_filament`` (#1815).
+
+The defensive filter at the end of the resolver clears ``tray_info_idx``
+when its value isn't slicer-acceptable (literal material names + PFUS /
+PFCN cloud-preset prefixes that the printer's calibration table can't
+key on). Pre-#1815 it cleared ``setting_id`` alongside, which dropped
+the slicer's only handle on the user's actual custom preset and forced
+the caller into the generic-material fallback — Bambu Studio then
+displayed "Generic <Material>" for spools whose Bambu Cloud detail
+lookup didn't resolve a ``filament_id`` (cloud unauth on the on_ams_change
+replay path, transient cloud failure, or custom presets whose detail
+JSON omits ``filament_id``).
+
+Post-#1815 the filter preserves a setting_id that's still a valid
+slicer reference (PFUS / PFCN cloud user/shared preset, or GFS Bambu
+official preset) even when ``tray_info_idx`` is cleared.
+"""
+
+from __future__ import annotations
+
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+
+from backend.app.services.slicer_filament_resolver import resolve_slicer_filament
+
+
+@pytest.mark.asyncio
+async def test_pfus_cloud_unavailable_preserves_setting_id():
+    """Reporter scenario: PFUS cloud user preset, cloud lookup fails to
+    return a filament_id. setting_id must survive so the slicer can
+    still find the user's actual custom preset."""
+    db = MagicMock()
+    with patch(
+        "backend.app.api.routes.cloud.build_authenticated_cloud",
+        AsyncMock(return_value=None),
+    ):
+        tray_info_idx, setting_id, sub_brand = await resolve_slicer_filament(
+            db=db,
+            current_user=None,
+            slicer_filament="PFUS990b6e19965353",
+            slicer_filament_name="Jayo PETG HF",
+            material="PETG",
+        )
+    assert tray_info_idx == ""
+    assert setting_id == "PFUS990b6e19965353"
+    assert sub_brand is None
+
+
+@pytest.mark.asyncio
+async def test_pfcn_cloud_unavailable_preserves_setting_id():
+    """PFCN partner/shared cloud preset (e.g. Polymaker H2D variants,
+    #1648) shares the same shape problem as PFUS."""
+    db = MagicMock()
+    with patch(
+        "backend.app.api.routes.cloud.build_authenticated_cloud",
+        AsyncMock(return_value=None),
+    ):
+        tray_info_idx, setting_id, sub_brand = await resolve_slicer_filament(
+            db=db,
+            current_user=None,
+            slicer_filament="PFCN1234567890",
+            slicer_filament_name="Polymaker PolyTerra PLA",
+            material="PLA",
+        )
+    assert tray_info_idx == ""
+    assert setting_id == "PFCN1234567890"
+    assert sub_brand is None
+
+
+@pytest.mark.asyncio
+async def test_pfus_cloud_resolves_filament_id_regression_guard():
+    """When cloud auth works and returns a filament_id, the resolver
+    keeps its existing behaviour: tray_info_idx = real filament_id,
+    setting_id = original PFUS reference."""
+    db = MagicMock()
+    cloud_mock = MagicMock()
+    cloud_mock.is_authenticated = True
+    cloud_mock.get_setting_detail = AsyncMock(return_value={"filament_id": "P285e239", "name": "Jayo PETG HF @P1S"})
+    cloud_mock.close = AsyncMock()
+    with patch(
+        "backend.app.api.routes.cloud.build_authenticated_cloud",
+        AsyncMock(return_value=cloud_mock),
+    ):
+        tray_info_idx, setting_id, sub_brand = await resolve_slicer_filament(
+            db=db,
+            current_user=MagicMock(),
+            slicer_filament="PFUS990b6e19965353",
+            slicer_filament_name="Jayo PETG HF",
+            material="PETG",
+        )
+    assert tray_info_idx == "P285e239"
+    assert setting_id == "PFUS990b6e19965353"
+    assert sub_brand == "Jayo PETG HF"
+
+
+@pytest.mark.asyncio
+async def test_gfs_cloud_unavailable_resolves_via_normalize():
+    """GFS Bambu official preset + cloud unavailable: normalize strips
+    the 'S' to give a real filament_id ('GFG02'), so tray_info_idx is
+    valid and the defensive filter doesn't trigger. setting_id stays as
+    the original GFS reference. Regression guard for the cloud-down
+    Bambu-official path."""
+    db = MagicMock()
+    with patch(
+        "backend.app.api.routes.cloud.build_authenticated_cloud",
+        AsyncMock(return_value=None),
+    ):
+        tray_info_idx, setting_id, sub_brand = await resolve_slicer_filament(
+            db=db,
+            current_user=None,
+            slicer_filament="GFSG02",
+            slicer_filament_name=None,
+            material="PETG",
+        )
+    assert tray_info_idx == "GFG02"
+    assert setting_id == "GFSG02"
+    assert sub_brand is None
+
+
+@pytest.mark.asyncio
+async def test_literal_material_name_clears_both():
+    """slicer_filament='PETG' (free-text material leak from legacy
+    spools): both tray_info_idx and setting_id must be cleared so the
+    caller's generic-material fallback rescues the slot. Regression
+    guard that the PFUS preservation doesn't accidentally preserve
+    literal material names."""
+    db = MagicMock()
+    with patch(
+        "backend.app.api.routes.cloud.build_authenticated_cloud",
+        AsyncMock(return_value=None),
+    ):
+        tray_info_idx, setting_id, sub_brand = await resolve_slicer_filament(
+            db=db,
+            current_user=None,
+            slicer_filament="PETG",
+            slicer_filament_name=None,
+            material="PETG",
+        )
+    assert tray_info_idx == ""
+    assert setting_id == ""
+    assert sub_brand is None

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů