Files
bambuddy/backend/tests/unit/test_hms_error_summary.py
maziggy 6988a30eae Carry a fault's description in the status response (issue #2926)
The HMS catalogue has been in the backend all along and the status
response never carried it, so every consumer that wanted to tell a user
why a print halted resolved the same 853 codes from its own duplicate of
the same sentences -- this repo's Python table, the frontend modal's, and
at least one third-party client whose catalogue exists purely because the
server would not say. Each ages separately, and a relay watching a
printer could only manage "your printer needs attention" while the server
already knew it was "Filament ran out. Please load new filament."

hms_errors[] entries now carry a description, defaulting to null so a
client that has never seen the field is unaffected.

It is resolved where the fault is parsed rather than at the boundary that
prompted the request, because there are three serializers of a fault, not
one: the status response, the WebSocket broadcast, and the completion
payload the queue's failure reason is built from. Adding it to only the
first would have handed half the feature to a relay watching the stream,
which is the likelier consumer of the three. The queue's failure reason
now quotes the resolved sentence instead of looking the code up a fourth
time, and the notification path reads it rather than re-deriving. That
they cannot report different text for one fault is the point, and a test
asserts they agree.

describe_fault is the single mapping from either code shape onto the
table. An 8-char print_error is the catalogue's MMMM_EEEE key with the
separator removed -- the parser derives full_code and that key from the
same 32-bit value -- so it resolves exactly. A 16-char hms[] identifier
is tried whole and then collapsed to its first and last groups.

That collapse is lossy, and keeping it was the decision worth making
carefully. #2728 counts 65 documented faults falling onto 0300_0001
alone, so a hit can attribute a neighbour's sentence to this fault, and
refusing it looks like the stricter reading. It is not: the notification
path, the queue's failure-reason helper and the frontend modal have all
resolved hms[] faults this way for as long as they have existed, and it
resolves real ones -- a 0500_4038 nozzle mismatch arrives in that shape.
Declining to collapse would have stopped describing faults that are
described today, silently suppressed the notifications they raise, and
left this field null while the UI showed text for the same fault.
Narrowing it belongs with #2728, where both key spaces can move together.

So the lookup is exactly what it was, verified rather than asserted:
a test walks every catalogue code in both fault shapes across all three
alert levels and checks the result against the derivation this replaces.
A future change to the lookup cannot quietly stop notifications firing.

The catalogue ships one language, so the field is English and
unlocalized, which the schema and the API reference both say next to it.
The camwall feed is deliberately left alone -- it is code-only because
its token travels in a URL on a screen, and a readable sentence discloses
more than the camera picture already does. The frontend keeps resolving
its own text: switching it would change what filterKnownHMSErrors counts
across eight call sites, which is #1840 and #2728's argument to have.

HMSError.message goes with this -- a text field that was never set or
read anywhere, and an invitation to populate the wrong one now that a
live description sits beside it.

-----

Record a failure code the user can actually look up

The queue's failure reason formats a fault's module and error into
MMMM_EEEE, and that one derivation never masked the error to 16 bits. A
fault arriving from the printer's hms[] array carries its alert level in
the code's high half, so the label came out as 0500_24038 -- five digits
in a group that has four. It is not a code anyone can find on Bambu's HMS
index, and because it matches no catalogue key the sentence explaining
the failure was dropped along with it, leaving the bare number alone.

The nozzle-size mismatch behind #1111 is exactly such a fault. Reported
one way it read "[0500_4038] The nozzle diameter in sliced file is not
consistent with the current nozzle setting"; reported the other, the same
physical fault read "[0500_24038]" and nothing else.

There is already a helper that gets this right, used by the archive's own
failure-reason lookup, so this calls it instead of keeping a fourth copy
of the derivation. It also takes the raw integer code the MQTT payload
carries, which the local version only handled as a string.
2026-08-24 09:47:39 +02:00

79 lines
2.9 KiB
Python

"""Tests for main._format_hms_error_summary — the helper that turns MQTT hms_errors
into a human-readable PrintQueueItem.error_message on pre-print failures (#1111)."""
def _format(hms_errors):
from backend.app.main import _format_hms_error_summary
return _format_hms_error_summary(hms_errors)
def test_returns_none_for_empty_list():
assert _format([]) is None
assert _format(None or []) is None
def test_formats_known_nozzle_mismatch_code():
"""0500_4038 is the nozzle-size-mismatch code from the HMS table — the common
trigger for issue #1111."""
summary = _format([{"code": "0x4038", "attr": 0x05000000, "module": 0x5, "severity": 1}])
assert summary is not None
assert "0500_4038" in summary
assert "nozzle diameter" in summary.lower()
def test_formats_unknown_code_as_bare_short_code():
summary = _format([{"code": "0x9999", "attr": 0x99990000, "module": 0x99, "severity": 1}])
assert summary == "[9999_9999]"
def test_joins_multiple_errors_with_semicolons():
summary = _format(
[
{"code": "0x4038", "attr": 0x05000000, "module": 0x5, "severity": 1},
{"code": "0x9999", "attr": 0x99990000, "module": 0x99, "severity": 1},
]
)
assert summary is not None
assert "; " in summary
assert summary.count("[") == 2
def test_tolerates_malformed_entry_and_skips_it():
summary = _format(
[
{"code": "not-hex", "attr": "also-not-int"},
{"code": "0x4038", "attr": 0x05000000, "module": 0x5, "severity": 1},
]
)
assert summary is not None
assert "0500_4038" in summary
def test_all_malformed_returns_none():
assert _format([{"code": "not-hex", "attr": "also-not-int"}]) is None
def test_masks_a_32_bit_code_into_a_four_digit_label():
"""An `hms[]` entry's code carries the alert level in its high 16 bits. The
label used to be formatted from the unmasked value, producing "0500_3000A" —
five digits in a group that has four, so it matched no catalogue key and was
not a code anyone could look up either."""
summary = _format([{"code": "0x3000a", "attr": 0x05000200, "module": 5, "severity": 2}])
assert summary == "[0500_000A]"
def test_masking_lets_a_32_bit_code_resolve_its_description():
"""0500_4038 is the nozzle-size mismatch. Arriving as an `hms[]` entry with
an alert-level group, it went undescribed purely because of the formatting
above; now it reads the same as when it arrives via print_error."""
summary = _format([{"code": "0x00024038", "attr": 0x05000200, "module": 5, "severity": 2}])
assert summary is not None
assert summary.startswith("[0500_4038] ")
assert "nozzle diameter" in summary.lower()
def test_accepts_an_integer_code():
"""`_hms_short_code` takes both shapes; the raw MQTT payload carries ints."""
assert _format([{"code": 0x4038, "attr": 0x05000000, "module": 5, "severity": 1}]).startswith("[0500_4038] ")