55in-memory replacement for `MqttChannel` and `LocalChannel` during testing.
66"""
77
8- from collections .abc import Callable
8+ from collections .abc import Awaitable , Callable
99from typing import Any
1010from unittest .mock import AsyncMock , MagicMock
1111
@@ -37,6 +37,9 @@ class FakeChannel(Channel):
3737 publish (useful for low-level RPC request/response testing).
3838 - **Push unsolicited messages**: Call ``channel.notify_subscribers(msg)``
3939 to simulate the device broadcasting a state change.
40+ - **Intercept published messages**: Register a handler/callback via
41+ ``channel.publish_handler = my_handler`` (e.g. stateful simulator)
42+ to reactively process commands.
4043 """
4144
4245 subscribe : Any
@@ -49,6 +52,11 @@ def __init__(self, is_local: bool = False):
4952 self ._is_connected = False
5053 self ._is_local = is_local
5154
55+ # A callback to intercept published messages (e.g., bound simulator handler).
56+ # Can be either synchronous or asynchronous: Callable[[RoborockMessage], Awaitable[Any]].
57+ # By default, routes to self._default_publish_handler to handle the response_queue.
58+ self .publish_handler : Callable [[RoborockMessage ], Awaitable [Any ]] | None = self ._default_publish_handler
59+
5260 # Set this to an exception instance to make the next publish raise it.
5361 # This is a convenience shortcut; callers can also replace
5462 # ``publish.side_effect`` directly for more control.
@@ -96,14 +104,16 @@ def is_local_connected(self) -> bool:
96104 async def _publish (self , message : RoborockMessage ) -> None :
97105 """Default publish implementation.
98106
99- Records the message in ``published_messages`` and, if
100- ``response_queue`` is non-empty, pops the first response and
101- delivers it to all current subscribers (simulating a
102- request/response round-trip).
107+ Records the message in ``published_messages`` and executes ``publish_handler``.
103108 """
104109 self .published_messages .append (message )
105110 if self .publish_side_effect :
106111 raise self .publish_side_effect
112+ if self .publish_handler :
113+ await self .publish_handler (message )
114+
115+ async def _default_publish_handler (self , message : RoborockMessage ) -> None :
116+ """Default handler that pops canned responses from response_queue."""
107117 if self .response_queue :
108118 response = self .response_queue .pop (0 )
109119 self .notify_subscribers (response )
@@ -124,3 +134,15 @@ def notify_subscribers(self, message: RoborockMessage) -> None:
124134 """
125135 for subscriber in list (self .subscribers ):
126136 subscriber (message )
137+
138+ def inject_error (self , exception : Exception ) -> None :
139+ """Inject a transient failure into all channel operations (publish, subscribe, connect)."""
140+ self .publish .side_effect = exception
141+ self .subscribe .side_effect = exception
142+ self .connect .side_effect = exception
143+
144+ def clear_error (self ) -> None :
145+ """Restore default success behaviors on all channel operations."""
146+ self .publish .side_effect = self ._publish
147+ self .subscribe .side_effect = self ._subscribe
148+ self .connect .side_effect = self ._connect
0 commit comments