You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: bound each addon HA-link probe by the readiness deadline (#2028)
* fix: bound each addon HA-link probe by the readiness deadline
`_probe_addon_ha_link` passed no timeout to `Client` or `call_tool`, so an
attempt inherited FastMCP's Streamable HTTP default of
`httpx.Timeout(30.0, read=300.0)` (client/transports/http.py). The poll loop
only consulted the deadline between attempts, so a listener that accepted the
connection but stalled on initialize or the tool response could hold
`wait_for_addon_ha_link_ready(timeout=180)` for ~300s per attempt — delaying the
fixture failure and its HAOS diagnostics well past the advertised budget, and
making the docstring's ceiling untrue.
Pass the time left on the deadline into each attempt and apply it at all three
layers that can stall independently (connect/initialize via `init_timeout`, the
tool response via `call_tool(timeout=...)`, and the surrounding coroutine via
`asyncio.wait_for`). Cap the inter-attempt sleep the same way so the budget is
exact rather than exact-plus-one-poll.
Reported by Codex on #2025.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: run the e2e suites when the change classifier does not succeed
The classifier is written to fail closed — any error inside the script leaves
`run=true` — but that only covers the script choosing a value. It does not cover
the job itself not succeeding. `if: needs.changes.outputs.run == 'true'` carries
an implicit `success()` on `needs`, so a classifier job that fails (on #2028 it
failed with no steps and no runner assigned) skips the suite, and a skipped
required check reports Success to branch protection. A runner hiccup in a
five-minute filter job could therefore wave code through untested — the exact
hole the classifier comment says the design avoids.
Gate the four suites on `!cancelled() && (needs.changes.result != 'success' ||
needs.changes.outputs.run == 'true')` instead: a classifier that succeeded and
said docs-only still skips (unchanged), a classifier that did not succeed now
runs the suite, and a real cancellation still cancels.
pr.yml's `e2e-validation-gate` had the same hole from the other side: it reads
`needs.changes.outputs.run != 'true'` to detect a docs-only PR, and a failed
classifier leaves that output empty, so the gate passed without judging any
lane. It now requires the classifier to have succeeded before honoring the skip
and otherwise falls through to the lane results.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: retry stalled link probes and classify their teardown errors
Review follow-ups on the per-attempt budget, all in the same failure path:
Bounding an attempt means cancelling a live fastmcp session, which cancels the
SDK's anyio task group; its teardown groups that CancelledError with any child
failure. A group carrying a BaseException leaf is not an Exception, so it
escaped the poll loop as a raw anyio traceback — losing the remaining retry
budget and the caller's diagnostics pointer, which is exactly the failure mode
this helper exists to prevent. Classify groups the way
custom_components/ha_mcp_tools/llm_api.py already does: catch
BaseExceptionGroup, but retry only when EVERY leaf is transient so a real bug
inside the group still propagates. Also add the anyio stream errors
(ClosedResourceError, BrokenResourceError, EndOfStream) that a mid-read
teardown raises — plain Exceptions, not OSError.
Handing each attempt all the remaining time made the first stall monopolize the
window, so a stall was retried zero times while the docstring promised
"transient errors are retried". Cap each attempt at _ADDON_HA_LINK_PROBE_S and
retry within the overall budget.
Set fastmcp's own deadlines strictly tighter than the outer backstop. Each
starts its clock later than the one around it, so with equal values the
backstop always won the race and the phase-naming errors ("Failed to initialize
server session", the session read timeout) could never fire — every stall
degraded to a bare TimeoutError() whose repr says nothing. Name the target when
the backstop does fire, and log the first transient at INFO, since the e2e
harness runs at INFO and the cause was previously DEBUG-only: a failed link
reported no reason at all.
Correct two overstated comments: the total can overshoot by fastmcp's shielded
disconnect timeout, and only the read component of the connect phase is
budget-scoped.
Skip gates now require an explicit `run == 'false'` rather than treating any
non-`true` value as docs-only, so a future edit that lets the classifier exit 0
without writing its output cannot silently reopen the hole this PR closes.
tests/src/unit/test_e2e_skip_gate_shape.py walks every classifier-gated lane and
pins both clauses plus the pr.yml gate step.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
# the update-path lane must all pass. Any one failing wedges the
105
115
# required gate.
106
116
if [ "$container_result" != "success" ] || [ "$embedded_result" != "success" ] || [ "$update_path_result" != "success" ]; then
107
-
echo "::error::E2E Validation did not succeed (container=$container_result, embedded=$embedded_result, update-path=$update_path_result)."
117
+
# `skipped` here is NOT the docs-only skip (that exited 0 above): the
118
+
# lanes never started, e.g. the run was cancelled.
119
+
case "$container_result$embedded_result$update_path_result" in
120
+
*skipped*) echo "::error::E2E Validation lanes did not start (results: container=$container_result, embedded=$embedded_result, update-path=$update_path_result) — the run may have been cancelled." ;;
121
+
*) echo "::error::E2E Validation did not succeed (container=$container_result, embedded=$embedded_result, update-path=$update_path_result)." ;;
122
+
esac
108
123
exit 1
109
124
fi
110
125
@@ -337,7 +352,11 @@ jobs:
337
352
needs: changes
338
353
# A skipped job reports Success to the required status check, so a
339
354
# docs/website-only PR doesn't wedge the merge waiting on these lanes.
340
-
if: needs.changes.outputs.run == 'true'
355
+
# Skipping requires a classifier that succeeded and said `false` out loud;
356
+
# anything else (a failed classifier leaves `run` empty) runs this lane.
357
+
# Keep this predicate equivalent to the skip branch in e2e-validation-gate:
358
+
# if they diverge, the gate demands success from a lane that never ran.
0 commit comments