diff --git a/roborock/devices/traits/v1/clean_summary.py b/roborock/devices/traits/v1/clean_summary.py index a03468fa..80d0d2a2 100644 --- a/roborock/devices/traits/v1/clean_summary.py +++ b/roborock/devices/traits/v1/clean_summary.py @@ -39,6 +39,8 @@ def convert(self, response: common.V1ResponseData) -> CleanRecord: if isinstance(response, dict): return CleanRecord.from_dict(response) if isinstance(response, list): + if not response: + raise ValueError("Empty clean record response") if isinstance(response[-1], dict): records = [CleanRecord.from_dict(rec) for rec in response] final_record = records[-1] diff --git a/tests/devices/traits/v1/test_clean_summary.py b/tests/devices/traits/v1/test_clean_summary.py index f5d74a7c..35d9b71a 100644 --- a/tests/devices/traits/v1/test_clean_summary.py +++ b/tests/devices/traits/v1/test_clean_summary.py @@ -7,7 +7,7 @@ from roborock.data import CleanSummary from roborock.devices.device import RoborockDevice from roborock.devices.traits.v1.clean_summary import CleanSummaryTrait -from roborock.exceptions import RoborockException +from roborock.exceptions import RoborockException, RoborockParsingException from roborock.roborock_typing import RoborockCommand from tests.mock_data import CLEAN_RECORD @@ -193,6 +193,58 @@ async def test_get_clean_record_success( ) +@pytest.mark.parametrize("response", [[], [[]]], ids=["empty", "wrapped-empty"]) +async def test_get_clean_record_empty_response( + clean_summary_trait: CleanSummaryTrait, mock_rpc_channel: AsyncMock, response: list +) -> None: + """Report empty records through the public parsing exception boundary.""" + mock_rpc_channel.send_command.return_value = response + + with pytest.raises(RoborockParsingException, match="GET_CLEAN_RECORD") as exc: + await clean_summary_trait.get_clean_record(1738864366) + + assert isinstance(exc.value.__cause__, ValueError) + assert f"Payload: {response!r}" in str(exc.value) + mock_rpc_channel.send_command.assert_called_once_with(RoborockCommand.GET_CLEAN_RECORD, params=[1738864366]) + + +@pytest.mark.parametrize( + "response", + [ + [CLEAN_RECORD], + [ + { + "begin": 1700000000, + "end": 1700000100, + "duration": 100, + "area": 1000000, + "avoid_count": 1, + "wash_count": 1, + }, + { + "begin": 1700000200, + "end": 1700000400, + "duration": 200, + "area": 2000000, + "avoid_count": 2, + "wash_count": 2, + }, + ], + ], + ids=["wrapped-dict", "multi-phase"], +) +async def test_get_clean_record_structured_responses( + clean_summary_trait: CleanSummaryTrait, mock_rpc_channel: AsyncMock, response: list[dict] +) -> None: + """Keep wrapped and multi-phase records working through the public API.""" + mock_rpc_channel.send_command.return_value = response + + record = await clean_summary_trait.get_clean_record(1738864366) + + assert record.begin == response[0]["begin"] + assert record.end == response[-1]["end"] + + async def test_get_clean_record_dict_response( clean_summary_trait: CleanSummaryTrait, mock_rpc_channel: AsyncMock,