55in-memory replacement for `MqttChannel` and `LocalChannel` during testing.
66"""
77
8+ import inspect
89from collections .abc import Callable
910from typing import Any
1011from unittest .mock import AsyncMock , MagicMock
@@ -37,6 +38,9 @@ class FakeChannel(Channel):
3738 publish (useful for low-level RPC request/response testing).
3839 - **Push unsolicited messages**: Call ``channel.notify_subscribers(msg)``
3940 to simulate the device broadcasting a state change.
41+ - **Intercept published messages**: Register a handler/callback via
42+ ``channel.publish_handler = my_handler`` (e.g. stateful simulator)
43+ to reactively process commands.
4044 """
4145
4246 subscribe : Any
@@ -49,6 +53,11 @@ def __init__(self, is_local: bool = False):
4953 self ._is_connected = False
5054 self ._is_local = is_local
5155
56+ # A callback to intercept published messages (e.g., bound simulator handler).
57+ # Can be either synchronous or asynchronous: Callable[[RoborockMessage], Any].
58+ # By default, routes to self._default_publish_handler to handle the response_queue.
59+ self .publish_handler : Callable [[RoborockMessage ], Any ] | None = self ._default_publish_handler
60+
5261 # Set this to an exception instance to make the next publish raise it.
5362 # This is a convenience shortcut; callers can also replace
5463 # ``publish.side_effect`` directly for more control.
@@ -96,14 +105,18 @@ def is_local_connected(self) -> bool:
96105 async def _publish (self , message : RoborockMessage ) -> None :
97106 """Default publish implementation.
98107
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).
108+ Records the message in ``published_messages`` and executes ``publish_handler``.
103109 """
104110 self .published_messages .append (message )
105111 if self .publish_side_effect :
106112 raise self .publish_side_effect
113+ if self .publish_handler :
114+ res = self .publish_handler (message )
115+ if inspect .isawaitable (res ):
116+ await res
117+
118+ def _default_publish_handler (self , message : RoborockMessage ) -> None :
119+ """Default handler that pops canned responses from response_queue."""
107120 if self .response_queue :
108121 response = self .response_queue .pop (0 )
109122 self .notify_subscribers (response )
@@ -124,3 +137,15 @@ def notify_subscribers(self, message: RoborockMessage) -> None:
124137 """
125138 for subscriber in list (self .subscribers ):
126139 subscriber (message )
140+
141+ def inject_error (self , exception : Exception ) -> None :
142+ """Inject a transient failure into all channel operations (publish, subscribe, connect)."""
143+ self .publish .side_effect = exception
144+ self .subscribe .side_effect = exception
145+ self .connect .side_effect = exception
146+
147+ def clear_error (self ) -> None :
148+ """Restore default success behaviors on all channel operations."""
149+ self .publish .side_effect = self ._publish
150+ self .subscribe .side_effect = self ._subscribe
151+ self .connect .side_effect = self ._connect
0 commit comments