hermes-hub/tests/conftest.py
Hermes Team 6e6d912a50 fix(tests): адреса провайдеров из окружения ломали прогон на машине владельца
На машине владельца задана ANTHROPIC_BASE_URL=https://api.anthropic.com, и
из-за неё test_claude_health_check_real_probe падал: адаптер строил
".../models" вместо ".../v1/models". Код при этом верен — подставлялось
значение из окружения вместо умолчания.

Проверено: со снятой переменной 486 passed, с заданной — одно падение.
Набор обязан давать одинаковый результат на любой машине; это то же
требование герметичности, ради которого делался A33.

Добавлена автоматическая фикстура, убирающая восемь переменных *_BASE_URL на
время каждого теста. После правки прогон с заданной переменной даёт
486 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 01:35:57 +07:00

125 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Pytest configuration and global hermetic test isolation fixtures.
Enforces:
1. Zero modification to real user credentials or router_profiles.yaml.
2. Complete filesystem sandboxing in temporary directory via HERMES_HOME.
3. Offline execution for default test runs (network / live require explicit -m markers).
4. No accidental connects to local llama.cpp (8081/8082) or non-loopback hosts.
"""
from __future__ import annotations
import os
import socket
import sys
from pathlib import Path
import pytest
REPO_SRC = Path(__file__).resolve().parent.parent / "src"
if str(REPO_SRC) not in sys.path or sys.path[0] != str(REPO_SRC):
sys.path.insert(0, str(REPO_SRC))
BLOCKED_INFERENCE_PORTS = {8081, 8082}
LOOPBACK_HOSTS = {"127.0.0.1", "::1", "localhost", "0.0.0.0", "::"}
@pytest.fixture(autouse=True)
def isolate_hermes_environment(tmp_path, monkeypatch):
"""Automatically sandbox all file I/O to a temporary HERMES_HOME directory."""
temp_hermes = tmp_path / "hermes_test_home"
temp_hermes.mkdir(parents=True, exist_ok=True)
monkeypatch.setenv("HERMES_HOME", str(temp_hermes))
# Isolate all provider profile dirs
(temp_hermes / "agy_profiles").mkdir(exist_ok=True)
(temp_hermes / "codex_profiles").mkdir(exist_ok=True)
(temp_hermes / "opengo_profiles").mkdir(exist_ok=True)
(temp_hermes / "claude_profiles").mkdir(exist_ok=True)
(temp_hermes / "grok_profiles").mkdir(exist_ok=True)
(temp_hermes / "logs").mkdir(exist_ok=True)
yield temp_hermes
def _socket_host_port(address) -> tuple[str | None, int | None]:
if isinstance(address, tuple) and len(address) >= 2:
host = address[0]
port = address[1]
if isinstance(host, bytes):
host = host.decode("utf-8", errors="replace")
try:
return str(host), int(port)
except (TypeError, ValueError):
return str(host), None
return None, None
def _reject_hermetic_connect(address) -> None:
host, port = _socket_host_port(address)
if port in BLOCKED_INFERENCE_PORTS:
raise RuntimeError(
f"hermetic tests must not contact local inference at {host}:{port}; "
"mock check_local_servers / urllib.request.urlopen"
)
if host and host not in LOOPBACK_HOSTS and not host.startswith("127."):
raise RuntimeError(
f"hermetic tests must not open network connections to {host}:{port}; "
"mark the test live/network or mock the call"
)
@pytest.fixture(autouse=True)
def block_external_network_in_hermetic_tests(request, monkeypatch):
"""Fail fast instead of hanging on llama.cpp or cloud APIs."""
if request.node.get_closest_marker("live") or request.node.get_closest_marker("network"):
yield
return
real_connect = socket.socket.connect
real_connect_ex = socket.socket.connect_ex
real_create_connection = socket.create_connection
def guarded_connect(self, address):
_reject_hermetic_connect(address)
return real_connect(self, address)
def guarded_connect_ex(self, address):
_reject_hermetic_connect(address)
return real_connect_ex(self, address)
def guarded_create_connection(address, *args, **kwargs):
_reject_hermetic_connect(address)
return real_create_connection(address, *args, **kwargs)
monkeypatch.setattr(socket.socket, "connect", guarded_connect)
monkeypatch.setattr(socket.socket, "connect_ex", guarded_connect_ex)
monkeypatch.setattr(socket, "create_connection", guarded_create_connection)
yield
def pytest_configure(config):
config.addinivalue_line("markers", "ui: mark test as requiring UI environment")
# Переменные окружения провайдеров не должны протекать в тесты.
#
# На машине владельца задана ANTHROPIC_BASE_URL=https://api.anthropic.com, и
# из-за неё test_claude_health_check_real_probe падал: адаптер строил
# ".../models" вместо ".../v1/models", хотя код верен, а подставилось значение
# из окружения. Набор обязан давать одинаковый результат на любой машине —
# это то же требование герметичности, ради которого делался A33.
_PROVIDER_BASE_URL_VARS = (
"ANTHROPIC_BASE_URL",
"CODEX_BASE_URL",
"DEEPSEEK_BASE_URL",
"LOCAL_LLM_BASE_URL",
"NVIDIA_BASE_URL",
"OLLAMA_BASE_URL",
"OPENCODE_GO_BASE_URL",
"OPENROUTER_BASE_URL",
)
@pytest.fixture(autouse=True)
def _isolate_provider_base_urls(monkeypatch):
"""Убрать адреса провайдеров из окружения на время каждого теста."""
for name in _PROVIDER_BASE_URL_VARS:
monkeypatch.delenv(name, raising=False)