Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.
Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
(DEFAULT_CONFIG smart-approval leaked in) — pinned approval
mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
silently probed live provider catalogs (~2s/test) — stubbed
cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
MagicMock agents — interrupt.side_effect now clears _running_agents
(22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
(24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
with rationale comment — the watchdog-dump assertion needs the loop
blocked well past deadline+grace under parallel load (flaked once in
the 40-worker verification run at a 0.2s margin).
Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
Follow-up hardening for the salvaged #69240 readiness gate (#67498):
- _start_polling_once now returns its (generation, progress_event) pair
so the strict cold-start gate binds to exactly the generation it
started, instead of re-reading self._polling_progress_event which a
concurrent recovery task may have replaced with a newer generation's
event (the G1/G2 race flagged in the #69240 review).
- Strict cold start no longer schedules background polling recovery: a
polling error during the readiness wait is captured by a strict
callback and fails the connect attempt immediately with a loud
OSError, so GatewayRunner disposes the partial adapter and retries
with a fresh one — no more waiting out the full readiness deadline on
a generation that already errored, and no G2-on-partial-app healing.
- After readiness is proven the strict callback delegates every later
polling error to the real background-recovery callback, preserving
the existing degraded/reconnect semantics for the polling lifetime.
- The readiness-timeout error message now states the deadline and that
the gateway will retry with a fresh adapter (loud failure, not a
silent wait).
- Regression tests: current-generation progress connects; a polling
error during strict cold start fails fast without scheduling
background recovery (the #67498 idle-threads shape); stale-generation
progress is rejected.
Progresses #67498
Use wall deadlines for deleteWebhook and start_polling, then fail cold startup unless getUpdates proves progress. This lets the gateway discard partial PTB state and retry with a fresh adapter.\n\nRefs #67498
Follow-up on the salvaged fix, which bounded start_polling() only in
_handle_polling_network_error. The same wedge (#59614) exists at the two
sibling call sites:
1. _start_polling_resilient (bootstrap): an exhausted pool hangs connect()
forever. The TimeoutError from wait_for is a builtins TimeoutError
(OSError subclass), so the existing except classifies it via
_looks_like_network_error and schedules background recovery.
2. _handle_polling_conflict (conflict-retry ladder): identical hang wedges
conflict attempt N forever; timeout now converts to RuntimeError and the
existing except schedules the next attempt.
Tests replaced with a stronger suite: hung-network-ladder repro (RED without
the fix), bootstrap hang schedules recovery, success-path sanity, and a
bug-class contract test asserting EVERY updater.start_polling( call site is
wrapped in wait_for so a new unbounded site can't reintroduce the wedge.
Verified RED (3 failures) with the wrappers removed, GREEN with them.
When the connection pool is in a degraded state after
_drain_polling_connections(), start_polling() can hang indefinitely
when both primary and fallback Telegram endpoints are unreachable. The
httpx client may hold a stale socket that neither connects nor times out
within PTB's internal flow, causing the reconnect ladder to stall at
attempt 1/10 forever.
Wrap start_polling() in asyncio.wait_for() with a 30-second timeout so a
hung call raises asyncio.TimeoutError and feeds back into the existing
retry ladder. This unblocks:
- The 10-retry ladder advances to attempt 2, 3, ...
- The heartbeat loop sees _polling_error_task.done() and can trigger recovery
- The reconnect watcher gets the adapter in _failed_platforms
Fixes#59614