Files
hermes-agent/tests/tools/test_connectors_import_deadlock.py
kshitijk4poor ddc0e65958 refactor(connectors): trim lazy-surface docstrings and test path setup
Gate finding BN21 r1 quality (Lows):
- The race/eager-import snippets put the repo on sys.path three ways; PYTHONPATH and
  cwd already do it, so drop the in-snippet sys.path.insert and the REPO_ROOT templating.
- The deadlock explanation was spelled out in the module docstring, again in the
  __getattr__ docstring, and a third time in the test module. Cut it to a 2-line
  pointer to the test, and give __getattr__ a one-line docstring that describes what
  it does (cache the name in globals()).
- Unquote the dict[str, str] / list[str] annotations: requires-python is >=3.11.

No behaviour change. Both kept tests still pass on HEAD and fail with BASE
tools/connectors/__init__.py.
2026-10-03 16:56:51 +05:30

83 lines
2.8 KiB
Python

"""Regression tests for the ``tools.connectors`` import surface.
The package is imported concurrently with its own submodules: the tool registry's discovery
scan imports ``tools.connectors.tool`` while the gateway's Group Chat worker imports the
package (``tui_gateway.methods_connectors`` -> ``tools.connectors``). When ``__init__``
imported its submodules eagerly, one thread held the package's module lock while waiting for
a submodule another thread already held, and Python 3.14 aborted the loser with
``_frozen_importlib._DeadlockError: deadlock detected by _ModuleLock('tools.connectors.tool')``
— the Group Chat worker then failed to start ("mutating Group Chat commands will fail closed").
Both checks run in a subprocess so the assertions see a pristine ``sys.modules`` and cannot
perturb the test session's own imports.
"""
import os
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
_NO_EAGER_SUBMODULE = """
import sys
import tools.connectors # noqa: F401
loaded = sorted(k for k in sys.modules if k.startswith("tools.connectors."))
print("\\n".join(loaded))
"""
_RACE = """
import importlib, sys, threading
errors = []
ITER = 25
def purge():
for name in [k for k in list(sys.modules) if k.startswith("tools.connectors")]:
sys.modules.pop(name, None)
def run(target):
for _ in range(ITER):
try:
importlib.import_module(target)
except Exception as exc: # noqa: BLE001
errors.append((target, type(exc).__name__))
purge()
threads = [threading.Thread(target=run, args=("tools.connectors",)) for _ in range(3)]
threads += [threading.Thread(target=run, args=("tools.connectors.tool",)) for _ in range(3)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=120)
print("alive", sum(1 for t in threads if t.is_alive()))
print("errors", [e for e in errors if e[1] == "_DeadlockError"])
"""
def _run_snippet(snippet: str) -> subprocess.CompletedProcess:
env = dict(os.environ)
env["PYTHONPATH"] = str(REPO_ROOT)
return subprocess.run(
[sys.executable, "-c", snippet],
capture_output=True,
text=True,
timeout=180,
env=env,
cwd=str(REPO_ROOT),
)
def test_package_import_does_not_load_submodules():
"""Importing the package must not import any submodule: that eager edge is the cycle."""
proc = _run_snippet(_NO_EAGER_SUBMODULE)
assert proc.returncode == 0, proc.stderr
assert proc.stdout.strip() == "", f"eagerly imported: {proc.stdout.strip()}"
def test_concurrent_package_and_submodule_import_never_deadlocks():
"""The two real-world import paths racing must not raise _DeadlockError."""
proc = _run_snippet(_RACE)
assert proc.returncode == 0, proc.stderr
assert "alive 0" in proc.stdout, proc.stdout
assert "errors []" in proc.stdout, proc.stdout