fix(transport): no stale protocol error, no connect task outliving its entry, honest banners (t20)
The t19 review's findings on the protocol transport, all of them closed here. O1 (MEDIUM, FIRST). `_note_protocol_state(PROTOCOL_CONNECTED, None)` only assigned `last_error` when its argument was non-None, so a failure kept being published after the transport recovered: the review measured `protocol_last_error == "connect: Net: boom"` beside `protocol_connected: true` — a contradiction on the exact attributes the diagnostic surface was built for, so the user could not answer "is the protocol working?" without a device test. The pair now has ONE stated meaning, in the code that owns it and in the remote's attribute docs: `protocol_state`/`protocol_connected` is the transport's CURRENT condition, and `protocol_last_error` is the failure the current state has NOT recovered from. A session becoming proven CLEARS it — and a session becomes proven in BOTH places that can prove one, so the connect success and the transport's own availability push now take the same route (the push previously assigned the state directly and would have left the stale error again). `protocol_failures` keeps counting, so a recovery stays visible as history rather than as a stale message. A failure that happens AFTER the session came up is still kept: that one is current, not stale, and a test pins that distinction so a future "just clear it everywhere" refactor cannot make failures invisible. O4 + O5 (the lifecycle leaks, now real tests instead of observations). The connect attempt was never in the manager's task collection, so `async_stop` never cancelled it — a reconnect loop could outlive its entry, and since every options save reloads the entry those loops could accumulate across saves. `async_stop` also only SCHEDULED its cancellations. The attempt is now tracked, `async_stop` cancels AND awaits every tracked task, and the stop path refuses to start anything new. A double start is deterministic instead of orphaning the first: a live attempt is reused, an already-handed-over session (connected or disconnected — the library's keep_reconnecting owns it) starts nothing, and only a FINISHED attempt that never got a session may be replaced. The guard sits before the push callbacks are registered, so a second start cannot double-register them either. O2 (LOW). The connect banner claimed liveliness a successful `async_connect()` cannot know. It now says exactly what it proved (the client accepted a session) and hands liveness to what can actually report it (the transport's own availability push). STALE is defined where the fixes live: a banner is stale when a later state change never followed it up, so the log's last word contradicts the entity. Every transition therefore announces itself with both states — INFO for the two a user acts on, DEBUG otherwise — which is what makes the last transport line always match the published state. O3 (LOW). The drift floors now carry an explicit MARGIN POLICY instead of leaving a 2-margin unexplained: a floor is a vacuity-and-blindness alarm, not a size ratchet (it must fail when the walk stops COVERING something, not when a legitimate change removes code). Owner-counters keep a 2 margin (the project's own t62 canary, now stated once), statement_blocks moves to a ~2% band (28 blocks — the t19 complaint was that a 23-statement refactor could trip it), relative_imports ~2.5%, and the awaited-calls floor is called out plainly as the weakest one because a percentage margin on six sites is meaningless. The rule for changing any floor is written down with it. The walk is now 1648 blocks, so the floor is re-derived: 1615. EVIDENCE THAT THE TESTS PIN THE FIX (not merely pass): the same test file was run against the PRE-fix source (`git archive HEAD`, a throwaway copy under /tmp) and 13 of the 15 new tests FAIL there. The two that pass pre-fix are the deliberate anti-overcorrection pins: a failure that is still current keeps its message, and a terminal (auth-rejected) attempt may still be replaced. No regressions: 889 passed / 5 skipped, 890 / 4 with androidtvremote2, the real-HA pattern (3 files) 74 passed with no skips, ruff clean, drift exit 0. NOT VERIFIED HERE: no real Shield and no paired device, so no real TLS session, certificate or push was ever received — the connect path runs against doubles plus the installed library's real source, and the *timing* of a cancellation during a live handshake is inferred from the awaited-cancel path, not observed on hardware.
This commit is contained in:
1 parent
66846b2f82
commit
6b866019ef
5 files changed
+543
-11
No files matched your search
@@ -531,6 +531,39 @@ class TestRemoteActivityContract:
|
||||
assert attrs["protocol_failures"] == 0
|
||||
assert attrs["power_on_route"] == "protocol"
|
||||
|
||||
def test_a_recovered_transport_is_not_published_as_broken(self, modules):
|
||||
"""t20/O1 at the ENTITY: the pair a user reads cannot contradict itself.
|
||||
|
||||
The review measured ``protocol_last_error == "connect: Net: boom"`` beside
|
||||
``protocol_connected: true``. Both halves are driven here through the
|
||||
manager's own paths — a send that could not run while the transport was
|
||||
absent, then the transport reporting a session — and read back from the
|
||||
attributes a user is told to look at.
|
||||
"""
|
||||
entity = _remote_entity(modules)
|
||||
manager = entity._manager
|
||||
client = manager._protocol
|
||||
|
||||
manager._protocol = None # the transport is gone
|
||||
assert asyncio.run(manager.send_protocol_key("POWER")) is False
|
||||
entity._refresh_state()
|
||||
assert entity.extra_state_attributes["protocol_last_error"], (
|
||||
"a failure that still describes the transport is recorded"
|
||||
)
|
||||
|
||||
manager._protocol = client # ...and the transport comes back
|
||||
manager._on_is_available(True) # its own proof of a session
|
||||
entity._refresh_state()
|
||||
|
||||
attrs = entity.extra_state_attributes
|
||||
assert attrs["protocol_state"] == "connected"
|
||||
assert attrs["protocol_connected"] is True
|
||||
assert attrs["protocol_last_error"] is None, (
|
||||
"a recovered failure must not be published beside protocol_connected: "
|
||||
"true (t20/O1)"
|
||||
)
|
||||
assert attrs["protocol_failures"] == 1, "the history stays in the counter"
|
||||
|
||||
def test_no_protocol_reads_as_off_not_as_unpaired(self, modules):
|
||||
entity = _remote_entity(modules)
|
||||
entity._manager._protocol = None
|
||||
|
||||
Reference in new issue
Block a user