"""Tests for the empty-AMS alarm gate (#1619). Empty AMS units still emit humidity/temperature sensor readings, but those readings are ambient and not actionable — there's no filament to dry. Without the gate every empty AMS spammed an hourly alarm. ``_ams_has_filament`` inspects the firmware-reported ``tray_exist_bits`` bitmap (fallback: ``tray`` array's ``tray_type`` strings) so the alarm dispatch in ``record_ams_history`` can skip empty units while still alarming on loaded ones in the same printer. """ from backend.app.main import _ams_has_filament class TestAmsHasFilament: def test_tray_exist_bits_zero_means_empty(self): assert _ams_has_filament({"tray_exist_bits": "0"}) is False # Real firmware sometimes pads with extra zeros or prefixes; all # parseable forms of zero should resolve to "empty". assert _ams_has_filament({"tray_exist_bits": "00"}) is False assert _ams_has_filament({"tray_exist_bits": "0x0"}) is False def test_tray_exist_bits_nonzero_means_loaded(self): # Single tray loaded — e.g. AMS-Lite or AMS-HT. assert _ams_has_filament({"tray_exist_bits": "1"}) is True # Four-slot AMS with all slots full (bitmap 0xf == 0b1111). assert _ams_has_filament({"tray_exist_bits": "f"}) is True # Mixed — 0xa == 0b1010, two slots loaded. assert _ams_has_filament({"tray_exist_bits": "a"}) is True # The exact bitmap seen in #1622 / #1602 logs. assert _ams_has_filament({"tray_exist_bits": "ed"}) is True def test_falls_back_to_tray_array_when_bits_missing(self): # Empty tray_type strings across the whole tray array → empty AMS. ams_empty = { "tray": [ {"id": 0, "tray_type": ""}, {"id": 1, "tray_type": ""}, ] } assert _ams_has_filament(ams_empty) is False # Any non-empty tray_type → loaded AMS. ams_loaded = { "tray": [ {"id": 0, "tray_type": ""}, {"id": 1, "tray_type": "PLA"}, ] } assert _ams_has_filament(ams_loaded) is True def test_missing_both_signals_returns_false(self): # No tray_exist_bits AND no tray array — early-pushall shape; we # treat it as "no info → don't alarm" rather than guessing loaded. assert _ams_has_filament({}) is False def test_unparseable_bitmap_falls_back_to_tray_array(self): # Garbage in tray_exist_bits — must not raise and must fall through # to the tray array check. loaded = {"tray_exist_bits": "garbage", "tray": [{"id": 0, "tray_type": "PETG"}]} assert _ams_has_filament(loaded) is True empty = {"tray_exist_bits": "garbage", "tray": []} assert _ams_has_filament(empty) is False def test_empty_bits_string_falls_back_to_tray_array(self): # Some pre-handshake pushall shapes set the field but leave it blank. loaded = {"tray_exist_bits": "", "tray": [{"id": 0, "tray_type": "ABS"}]} assert _ams_has_filament(loaded) is True def test_whitespace_tray_type_is_not_loaded(self): # A tray_type that's all whitespace doesn't count as a real material. assert _ams_has_filament({"tray": [{"id": 0, "tray_type": " "}]}) is False def test_non_dict_tray_entries_are_skipped(self): # Defensive: malformed tray array shouldn't crash the helper. assert _ams_has_filament({"tray": [None, "junk", 42]}) is False def test_non_string_bits_falls_back(self): # Some MQTT shapes send tray_exist_bits as int; we only parse strings, # so an int falls through to the tray array. loaded = {"tray_exist_bits": 0xED, "tray": [{"id": 0, "tray_type": "PLA"}]} assert _ams_has_filament(loaded) is True empty_int = {"tray_exist_bits": 0xED} # no tray array, int ignored assert _ams_has_filament(empty_int) is False