Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
"""Regression tests for the config.yaml → env var bridge in gateway/run.py.
|
||||
|
||||
Guards against the 60-vs-500 bug where a stale `.env HERMES_MAX_ITERATIONS=60`
|
||||
entry silently shadowed `agent.max_turns: 500` in config.yaml because the
|
||||
bridge used `if X not in os.environ` guards. After PR#18413 the bridge
|
||||
treats config.yaml as authoritative and unconditionally overwrites .env
|
||||
values for `agent.*`, `display.*`, `timezone`, and `security.*` keys.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _run_gateway_import(hermes_home: Path, initial_env: dict[str, str]) -> dict[str, str]:
|
||||
"""Import gateway.run in a clean subprocess and return the post-import env.
|
||||
|
||||
The bridge runs at module-import time, so simply importing is enough
|
||||
to exercise it. Running in a subprocess isolates the test from other
|
||||
import side effects and makes the "what ends up in os.environ" check
|
||||
deterministic.
|
||||
"""
|
||||
script = textwrap.dedent(
|
||||
f"""
|
||||
import os, sys
|
||||
sys.path.insert(0, {str(PROJECT_ROOT)!r})
|
||||
|
||||
try:
|
||||
from gateway import run # noqa: F401 — module import triggers bridge
|
||||
except Exception as exc:
|
||||
print(f"IMPORT_ERROR:{{type(exc).__name__}}:{{exc}}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
for k in (
|
||||
"HERMES_MAX_ITERATIONS",
|
||||
"HERMES_AGENT_TIMEOUT",
|
||||
"HERMES_AGENT_TIMEOUT_WARNING",
|
||||
"HERMES_TURN_LEASE_TIMEOUT",
|
||||
"HERMES_SESSION_STALL_TIMEOUT",
|
||||
"HERMES_GATEWAY_BUSY_INPUT_MODE",
|
||||
"HERMES_GATEWAY_BUSY_TEXT_MODE",
|
||||
"HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT",
|
||||
"HERMES_TIMEZONE",
|
||||
):
|
||||
v = os.environ.get(k)
|
||||
if v is not None:
|
||||
print(f"{{k}}={{v}}")
|
||||
"""
|
||||
)
|
||||
env = dict(initial_env)
|
||||
env["HERMES_HOME"] = str(hermes_home)
|
||||
# Keep interpreter paths plus the Windows bootstrap variables required by
|
||||
# stdlib platform detection and native dependency loading. The child is
|
||||
# otherwise intentionally clean so stale Hermes settings cannot leak in.
|
||||
for k in (
|
||||
"PATH",
|
||||
"PYTHONPATH",
|
||||
"VIRTUAL_ENV",
|
||||
"HOME",
|
||||
"USERPROFILE",
|
||||
"HOMEDRIVE",
|
||||
"HOMEPATH",
|
||||
"LOCALAPPDATA",
|
||||
"APPDATA",
|
||||
"SYSTEMROOT",
|
||||
"TEMP",
|
||||
"TMP",
|
||||
):
|
||||
if k in os.environ and k not in env:
|
||||
env[k] = os.environ[k]
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
pytest.fail(
|
||||
f"gateway.run import failed (rc={result.returncode})\n"
|
||||
f"stderr:\n{result.stderr}\nstdout:\n{result.stdout}"
|
||||
)
|
||||
out: dict[str, str] = {}
|
||||
for line in result.stdout.splitlines():
|
||||
if "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def _write_config(home: Path, agent_cfg: dict | None = None, display_cfg: dict | None = None,
|
||||
timezone: str | None = None, gateway_cfg: dict | None = None) -> None:
|
||||
import yaml
|
||||
cfg: dict = {}
|
||||
if agent_cfg:
|
||||
cfg["agent"] = agent_cfg
|
||||
if display_cfg:
|
||||
cfg["display"] = display_cfg
|
||||
if gateway_cfg:
|
||||
cfg["gateway"] = gateway_cfg
|
||||
if timezone:
|
||||
cfg["timezone"] = timezone
|
||||
(home / "config.yaml").write_text(yaml.safe_dump(cfg), encoding="utf-8")
|
||||
|
||||
|
||||
def _write_env(home: Path, entries: dict[str, str]) -> None:
|
||||
lines = [f"{k}={v}\n" for k, v in entries.items()]
|
||||
(home / ".env").write_text("".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hermes_home(tmp_path: Path) -> Path:
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
return home
|
||||
|
||||
|
||||
def test_config_gateway_timeout_wins_over_stale_env(hermes_home: Path) -> None:
|
||||
"""Every agent.* bridge key must be config-authoritative, not .env-authoritative."""
|
||||
_write_config(hermes_home, agent_cfg={
|
||||
"gateway_timeout": 1800,
|
||||
"gateway_timeout_warning": 900,
|
||||
"session_stall_timeout": 300,
|
||||
})
|
||||
_write_env(hermes_home, {
|
||||
"HERMES_AGENT_TIMEOUT": "60",
|
||||
"HERMES_AGENT_TIMEOUT_WARNING": "30",
|
||||
"HERMES_SESSION_STALL_TIMEOUT": "15",
|
||||
})
|
||||
|
||||
env = _run_gateway_import(hermes_home, initial_env={})
|
||||
|
||||
assert env.get("HERMES_AGENT_TIMEOUT") == "1800"
|
||||
assert env.get("HERMES_AGENT_TIMEOUT_WARNING") == "900"
|
||||
assert env.get("HERMES_SESSION_STALL_TIMEOUT") == "300"
|
||||
|
||||
|
||||
def test_config_turn_lease_timeout_wins_over_stale_env(hermes_home: Path) -> None:
|
||||
"""The user-facing lease wait budget belongs to config.yaml."""
|
||||
_write_config(
|
||||
hermes_home,
|
||||
agent_cfg={"gateway_turn_lease_timeout": 600},
|
||||
)
|
||||
_write_env(
|
||||
hermes_home,
|
||||
{"HERMES_TURN_LEASE_TIMEOUT": "60"},
|
||||
)
|
||||
|
||||
env = _run_gateway_import(hermes_home, initial_env={})
|
||||
|
||||
assert env.get("HERMES_TURN_LEASE_TIMEOUT") == "600"
|
||||
|
||||
|
||||
def test_default_turn_lease_timeout_overrides_stale_env_when_key_is_omitted(
|
||||
hermes_home: Path,
|
||||
) -> None:
|
||||
"""The internal env mirror must never become a second config source."""
|
||||
_write_env(
|
||||
hermes_home,
|
||||
{"HERMES_TURN_LEASE_TIMEOUT": "60"},
|
||||
)
|
||||
|
||||
env = _run_gateway_import(hermes_home, initial_env={})
|
||||
|
||||
assert env.get("HERMES_TURN_LEASE_TIMEOUT") == "5"
|
||||
|
||||
|
||||
def test_default_turn_lease_timeout_matches_the_runtime_fallback() -> None:
|
||||
"""The advertised config default must match the fail-closed runtime fallback.
|
||||
|
||||
Invariant, not a snapshot: whatever the default becomes, config and the
|
||||
lease registry's DEFAULT_LEASE_WAIT must move together.
|
||||
"""
|
||||
from gateway.turn_lease import DEFAULT_LEASE_WAIT
|
||||
from hermes_cli.config import DEFAULT_CONFIG
|
||||
|
||||
assert (
|
||||
float(DEFAULT_CONFIG["agent"]["gateway_turn_lease_timeout"])
|
||||
== DEFAULT_LEASE_WAIT
|
||||
)
|
||||
|
||||
|
||||
def test_config_platform_connect_timeout_supplies_env_when_unset(hermes_home: Path) -> None:
|
||||
"""config.yaml:gateway.platform_connect_timeout supplies the env var when
|
||||
it isn't already set (#19776 — config surface for the Discord connect
|
||||
timeout, replacing the undocumented env-var-only workaround)."""
|
||||
_write_config(hermes_home, gateway_cfg={"platform_connect_timeout": 90})
|
||||
|
||||
env = _run_gateway_import(hermes_home, initial_env={})
|
||||
|
||||
assert env.get("HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT") == "90"
|
||||
|
||||
|
||||
def test_env_platform_connect_timeout_wins_over_config(hermes_home: Path) -> None:
|
||||
"""Unlike the agent.*/display.*/timezone bridges (config-authoritative),
|
||||
HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT is the manual-override escape hatch:
|
||||
an explicitly-set env var WINS over config.yaml. This divergence is
|
||||
intentional (#19776) — the env var is the operator's emergency knob."""
|
||||
_write_config(hermes_home, gateway_cfg={"platform_connect_timeout": 90})
|
||||
|
||||
env = _run_gateway_import(
|
||||
hermes_home,
|
||||
initial_env={"HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT": "120"},
|
||||
)
|
||||
|
||||
assert env.get("HERMES_GATEWAY_PLATFORM_CONNECT_TIMEOUT") == "120"
|
||||
Reference in New Issue
Block a user