mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
fix(inventory): stop popping the unknown-tag modal for slots with no RFID
The 7cb905a follow-up mounted the global unknown-tag modal listener, which
turned an existing always-on broadcast for no-tag slots from a silent no-op
into a perpetual popup loop — every push for a slot with a generic
non-RFID spool (or zero-filled tag) re-prompted, and confirming each one
created a fresh ghost spool with an empty tag.
- main.py on_ams_change: drop the no-tag else-branch broadcast. No identity,
no prompt; the slot stays unassigned until a real tag is read.
- inventory.py + spoolman.py /spools/from-slot: 400 when the slot has no
usable tag_uid / tray_uuid so stale frontends can't recreate the ghost
spool by re-confirming a queued prompt.
- test_inventory_from_slot_no_tag: lock the guard in (zero-filled + empty
string).
This commit is contained in:
@@ -2457,6 +2457,14 @@ async def create_spool_from_slot(
|
||||
if not tray or not tray.get("tray_type"):
|
||||
raise HTTPException(status_code=400, detail="Slot is empty or has no readable tray data")
|
||||
|
||||
# Guard against ghost-spool creation: a slot without any RFID tag has no
|
||||
# stable identity, so creating an inventory row would just duplicate on
|
||||
# every confirm and never re-link to the physical spool.
|
||||
from backend.app.services.spool_tag_matcher import is_valid_tag
|
||||
|
||||
if not is_valid_tag(tray.get("tag_uid", ""), tray.get("tray_uuid", "")):
|
||||
raise HTTPException(status_code=400, detail="Slot has no RFID tag")
|
||||
|
||||
spool = await create_spool_from_tray(db, tray)
|
||||
await auto_assign_spool(
|
||||
req.printer_id,
|
||||
|
||||
@@ -1207,6 +1207,13 @@ async def create_spool_from_slot(
|
||||
if not tray:
|
||||
raise HTTPException(status_code=400, detail="Slot is empty or has no readable tray data")
|
||||
|
||||
# Same ghost-spool guard as the inventory route: no tag → no stable
|
||||
# identity → confirm would just create a fresh Spoolman row per push.
|
||||
from backend.app.services.spool_tag_matcher import is_valid_tag
|
||||
|
||||
if not is_valid_tag(tray.tag_uid or "", tray.tray_uuid or ""):
|
||||
raise HTTPException(status_code=400, detail="Slot has no RFID tag")
|
||||
|
||||
sync_result = await client.sync_ams_tray(
|
||||
tray,
|
||||
printer.name,
|
||||
|
||||
+3
-13
@@ -1819,19 +1819,9 @@ async def on_ams_change(printer_id: int, ams_data: list):
|
||||
tray_sub_brands=tray.get("tray_sub_brands"),
|
||||
tray_count=len(ams_unit.get("tray", [])),
|
||||
)
|
||||
else:
|
||||
# No tag at all — let user choose from inventory
|
||||
await _broadcast_unknown_tag(
|
||||
printer_id=printer_id,
|
||||
ams_id=ams_id,
|
||||
tray_id=tray_id,
|
||||
tag_uid="",
|
||||
tray_uuid="",
|
||||
tray_type=tray.get("tray_type"),
|
||||
tray_color=tray.get("tray_color"),
|
||||
tray_sub_brands=tray.get("tray_sub_brands"),
|
||||
tray_count=len(ams_unit.get("tray", [])),
|
||||
)
|
||||
# No-tag slots (generic non-RFID filament) are left alone:
|
||||
# nothing to identify, prompting "+ Add" would just create
|
||||
# ghost spools with empty tags on every confirm.
|
||||
except Exception as e:
|
||||
logger.warning("RFID spool auto-assign failed: %s", e, exc_info=True)
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""No-tag guard on POST /api/v1/inventory/spools/from-slot.
|
||||
|
||||
A slot that has tray_type set but no RFID tag (generic third-party PLA the
|
||||
user manually configured on the printer panel) has no stable identity, so
|
||||
auto-creating an inventory row for it would just duplicate on every confirm
|
||||
without ever re-linking to the physical spool. Verify the endpoint refuses
|
||||
that case with 400.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
def _mock_status_for_tray(ams_id: int, tray_id: int, tray: dict) -> MagicMock:
|
||||
status = MagicMock()
|
||||
status.raw_data = {"ams": {"ams": [{"id": ams_id, "tray": [{"id": tray_id, **tray}]}]}}
|
||||
return status
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_from_slot_rejects_no_tag(async_client: AsyncClient, printer_factory):
|
||||
printer = await printer_factory(name="X1C-no-tag")
|
||||
|
||||
no_tag_tray = {
|
||||
"tray_type": "PLA",
|
||||
"tray_color": "FF0000FF",
|
||||
"tag_uid": "0000000000000000",
|
||||
"tray_uuid": "00000000000000000000000000000000",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"backend.app.services.printer_manager.printer_manager.get_status",
|
||||
return_value=_mock_status_for_tray(0, 1, no_tag_tray),
|
||||
):
|
||||
resp = await async_client.post(
|
||||
"/api/v1/inventory/spools/from-slot",
|
||||
json={"printer_id": printer.id, "ams_id": 0, "tray_id": 1},
|
||||
)
|
||||
|
||||
assert resp.status_code == 400
|
||||
assert "RFID tag" in resp.json()["detail"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_from_slot_rejects_empty_tag_strings(async_client: AsyncClient, printer_factory):
|
||||
"""Empty strings (not zero-filled) must also be refused."""
|
||||
printer = await printer_factory(name="X1C-empty-tag")
|
||||
|
||||
empty_tag_tray = {
|
||||
"tray_type": "PETG",
|
||||
"tray_color": "00FF00FF",
|
||||
"tag_uid": "",
|
||||
"tray_uuid": "",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"backend.app.services.printer_manager.printer_manager.get_status",
|
||||
return_value=_mock_status_for_tray(0, 2, empty_tag_tray),
|
||||
):
|
||||
resp = await async_client.post(
|
||||
"/api/v1/inventory/spools/from-slot",
|
||||
json={"printer_id": printer.id, "ams_id": 0, "tray_id": 2},
|
||||
)
|
||||
|
||||
assert resp.status_code == 400
|
||||
Reference in New Issue
Block a user