mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-30 01:41:43 +00:00
Fix variable name breakage (run_agent, hermes_constants, etc.) where import rewriter changed 'import X' to 'import hermes_agent.Y' but test code still referenced 'X' as a variable name. Fix package-vs-module confusion (cli.auth, cli.models, cli.ui) where single files became directories. Fix hardcoded file paths in tests pointing to old locations. Fix tool registry to discover tools in subpackage directories. Fix stale import in hermes_agent/tools/__init__.py. Part of #14182, #14183
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
"""Tests for Windows compatibility of process management code.
|
|
|
|
Verifies that os.setsid and os.killpg are never called unconditionally,
|
|
and that each module uses a platform guard before invoking POSIX-only functions.
|
|
"""
|
|
|
|
import ast
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
# Files that must have Windows-safe process management
|
|
GUARDED_FILES = [
|
|
"hermes_agent/backends/local.py",
|
|
"hermes_agent/tools/process_registry.py",
|
|
"hermes_agent/tools/code_execution.py",
|
|
"hermes_agent/gateway/platforms/whatsapp.py",
|
|
]
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
|
|
|
|
def _get_preexec_fn_values(filepath: Path) -> list:
|
|
"""Find all preexec_fn= keyword arguments in Popen calls."""
|
|
source = filepath.read_text(encoding="utf-8")
|
|
tree = ast.parse(source, filename=str(filepath))
|
|
values = []
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.keyword) and node.arg == "preexec_fn":
|
|
values.append(ast.dump(node.value))
|
|
return values
|
|
|
|
|
|
class TestNoUnconditionalSetsid:
|
|
"""preexec_fn must never be a bare os.setsid reference."""
|
|
|
|
@pytest.mark.parametrize("relpath", GUARDED_FILES)
|
|
def test_preexec_fn_is_guarded(self, relpath):
|
|
filepath = PROJECT_ROOT / relpath
|
|
if not filepath.exists():
|
|
pytest.skip(f"{relpath} not found")
|
|
values = _get_preexec_fn_values(filepath)
|
|
for val in values:
|
|
# A bare os.setsid would be: Attribute(value=Name(id='os'), attr='setsid')
|
|
assert "attr='setsid'" not in val or "IfExp" in val or "None" in val, (
|
|
f"{relpath} has unconditional preexec_fn=os.setsid"
|
|
)
|
|
|
|
|
|
class TestIsWindowsConstant:
|
|
"""Each guarded file must define _IS_WINDOWS."""
|
|
|
|
@pytest.mark.parametrize("relpath", GUARDED_FILES)
|
|
def test_has_is_windows(self, relpath):
|
|
filepath = PROJECT_ROOT / relpath
|
|
if not filepath.exists():
|
|
pytest.skip(f"{relpath} not found")
|
|
source = filepath.read_text(encoding="utf-8")
|
|
assert "_IS_WINDOWS" in source, (
|
|
f"{relpath} missing _IS_WINDOWS platform guard"
|
|
)
|
|
|
|
|
|
class TestKillpgGuarded:
|
|
"""os.killpg must always be behind a platform check."""
|
|
|
|
@pytest.mark.parametrize("relpath", GUARDED_FILES)
|
|
def test_no_unguarded_killpg(self, relpath):
|
|
filepath = PROJECT_ROOT / relpath
|
|
if not filepath.exists():
|
|
pytest.skip(f"{relpath} not found")
|
|
source = filepath.read_text(encoding="utf-8")
|
|
lines = source.splitlines()
|
|
for i, line in enumerate(lines):
|
|
stripped = line.strip()
|
|
if "os.killpg" in stripped or "os.getpgid" in stripped:
|
|
# Check that there's an _IS_WINDOWS guard in the surrounding context
|
|
context = "\n".join(lines[max(0, i - 15):i + 1])
|
|
assert "_IS_WINDOWS" in context or "else:" in context, (
|
|
f"{relpath}:{i + 1} has unguarded os.killpg/os.getpgid call"
|
|
)
|