Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,10 @@ jobs:
strategy:
fail-fast: false
matrix:
# 3.13 ONLY, for now. 3.11 is the declared floor (`pyproject.toml`
# requires-python, and pylint's own py-version) and SHOULD be here, but
# four tests in tests/test_bridge_client.py fail on 3.11 today: they
# assert on `client.status` straight after `wait_connected()` and pass
# on 3.13 purely because its event loop happens to have run the attach
# by then. Scheduling order, not logic — and pre-existing, confirmed
# failing at 913ad44, well before the refactor that added this file.
#
# Adding a red leg to a brand-new gate only teaches people to ignore
# the gate. Tracked separately; restore "3.11" here once those four
# tests await the condition instead of assuming it.
python-version: ["3.13"]
# 3.11 is the declared floor (`pyproject.toml` requires-python, and
# pylint's own py-version); 3.13 is what jarvis actually runs. Both
# legs are cheap (~1s each), so both run on every push.
python-version: ["3.11", "3.13"]
steps:
- uses: actions/checkout@v4

Expand Down
2 changes: 1 addition & 1 deletion indigo-matter.indigoPlugin/Contents/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<key>IwsApiVersion</key>
<string>1.0.0</string>
<key>PluginVersion</key>
<string>2026.28.7</string>
<string>2026.28.8</string>
<key>ServerApiVersion</key>
<string>3.6</string>
</dict>
Expand Down
23 changes: 23 additions & 0 deletions indigo-matter.indigoPlugin/Contents/Server Plugin/bridge_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ def __init__(
#: The last StatusReport the node returned (attach / get_status).
self.status: Optional[StatusReport] = None
self._attached = False
self._attached_event = asyncio.Event()
#: True while the node refused the attach with ``endpoint_map_invalid``
#: and we are holding the connection open for the §1.1 recovery trio.
self.recovery = False
Expand All @@ -245,8 +246,27 @@ def attached(self) -> bool:
"""
return self._attached and self.connected

async def wait_attached(self, timeout: float = 30.0) -> None:
"""Block until an ``attach`` the node accepted has landed.

:meth:`wait_connected` only proves the transport is up — the socket
exists the moment the handshake starts, but ``self.status`` and
``self._attached`` are not populated until the attach that follows
it completes, on the run loop, some time later. A caller that reads
``status`` or relies on ``set_state`` being deliverable right after
``wait_connected`` returns is trusting that the node is already
serving its endpoint set — which is exactly the assumption that is
NOT guaranteed: the attach might still be in flight, or might have
been refused (§1.1's ``endpoint_map_invalid``), leaving a live socket
that is connected but not attached. Wait on this instead of
``wait_connected`` whenever what you need is the node actually
serving your endpoints, not merely a live socket to it.
"""
await asyncio.wait_for(self._attached_event.wait(), timeout)

def _mark_disconnected(self) -> None:
self._attached = False
self._attached_event.clear()
self.recovery = False
super()._mark_disconnected()

Expand Down Expand Up @@ -293,6 +313,7 @@ async def _handshake(self, first: Any) -> None:
self._handle_attach_refused(exc)
return
self._attached = True
self._attached_event.set()
self.recovery = False
self._notify(self._on_attached, status, replace_all)

Expand Down Expand Up @@ -437,6 +458,7 @@ async def _retry_with_intent(self) -> bool:
"rather than halting", owed)
status = await self._attach(None, replace_all=True, timeout=None, inline=True)
self._attached = True
self._attached_event.set()
self.recovery = False
self._notify(self._on_attached, status, True)
return True
Expand Down Expand Up @@ -596,6 +618,7 @@ async def _attach(self, endpoints: Optional[list], *, replace_all: bool,
else:
result = await self._request_frame(frame, timeout)
self._attached = True
self._attached_event.set()
self.recovery = False
self.status = bridge_protocol.parse_status(result)
return self.status
Expand Down
Loading
Loading