fix(honcho): network-hermetic unit tests + lazy async writer start

Re-port of PR #67576:
- plugins/memory/honcho/session.py: start the async writer thread lazily on
  first enqueue via _ensure_async_writer (idempotent, lock-guarded) instead
  of eagerly in __init__; shutdown tolerates a never-started thread
- tests/honcho_plugin/conftest.py: package-wide socket guard so no honcho
  unit test can reach a live server
- tests/honcho_plugin/test_network_isolation.py: regression tests
- test_async_memory.py / test_oauth_flow.py: rebased hunks onto the pruned
  suite (pruned tests not resurrected)

Salvaged-from: #67576
Co-authored-by: eapwrk <eapwrk@gmail.com>
This commit is contained in:
eapwrk 2026-07-29 18:23:40 -07:00 committed by Teknium
parent 8d009e4f3e
commit bd1a850fa2
5 changed files with 356 additions and 45 deletions

View file

@ -140,17 +140,16 @@ class HonchoSessionManager:
config.dialectic_max_input_chars if config else 10000
)
# Async write queue — started lazily on first enqueue
# Async write queue — the writer thread starts lazily on first enqueue
# (see _ensure_async_writer). Constructing a manager must not spawn
# background work or touch the network: unit tests build managers with
# mocked clients, and an eagerly-started writer raced ahead of the mock
# and wrote test messages to a live local Honcho.
self._async_queue: queue.Queue | None = None
self._async_thread: threading.Thread | None = None
self._async_thread_lock = threading.Lock()
if write_frequency == "async":
self._async_queue = queue.Queue()
self._async_thread = threading.Thread(
target=self._async_writer_loop,
name="honcho-async-writer",
daemon=True,
)
self._async_thread.start()
@property
def honcho(self) -> Honcho:
@ -511,6 +510,7 @@ class HonchoSessionManager:
if wf == "async":
if self._async_queue is not None:
self._ensure_async_writer()
self._async_queue.put(session)
elif wf == "turn":
self._flush_session(session)
@ -545,12 +545,26 @@ class HonchoSessionManager:
except queue.Empty:
break
def _ensure_async_writer(self) -> None:
"""Start the async writer on first enqueue (idempotent, thread-safe)."""
if self._async_thread is not None and self._async_thread.is_alive():
return
with self._async_thread_lock:
if self._async_thread is None or not self._async_thread.is_alive():
self._async_thread = threading.Thread(
target=self._async_writer_loop,
name="honcho-async-writer",
daemon=True,
)
self._async_thread.start()
def shutdown(self) -> None:
"""Gracefully shut down the async writer thread."""
if self._async_queue is not None and self._async_thread is not None:
if self._async_queue is not None:
self.flush_all()
self._async_queue.put(_ASYNC_SHUTDOWN)
self._async_thread.join(timeout=10)
if self._async_thread is not None and self._async_thread.is_alive():
self._async_queue.put(_ASYNC_SHUTDOWN)
self._async_thread.join(timeout=10)
def delete(self, key: str) -> bool:
"""Delete a session from local cache."""