Files
hermes-agent/tests/hermes_cli/test_relay_plugin_migrate.py
T

91 lines
4.1 KiB
Python

"""``hermes update`` / ``hermes migrate relay``: legacy ``HERMES_NEMO_RELAY_ATIF_*``/``ATOF_*`` vars
become a validated ``relay-plugins.toml`` per profile home, selected from ``.env``."""
from __future__ import annotations
import asyncio
import tomllib
from pathlib import Path
import pytest
from hermes_cli.relay_plugin_migrate import (
RELAY_PLUGINS_TOML_NAME, migrate_all_profile_relay_envs, migrate_profile_relay_env)
from hermes_cli.relay_plugin_cutover import RELAY_PLUGINS_CONFIG_ENV, configured_legacy_relay_env_vars
nemo_relay = pytest.importorskip("nemo_relay")
LEGACY_ENV = """OPENAI_API_KEY=sk-test
HERMES_NEMO_RELAY_ATOF_ENABLED=1
HERMES_NEMO_RELAY_ATOF_OUTPUT_DIRECTORY={home}/telemetry/atof
HERMES_NEMO_RELAY_ATOF_FILENAME=hermes-atof.jsonl
HERMES_NEMO_RELAY_ATOF_MODE=append
HERMES_NEMO_RELAY_ATIF_ENABLED=1
HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY={home}/telemetry/atif
HERMES_NEMO_RELAY_ATIF_FILENAME_TEMPLATE=trajectory-{{session_id}}.json
HERMES_NEMO_RELAY_ATIF_SUBAGENT_EXPORT_MODE=all
"""
def _parse_env(path: Path) -> dict[str, str]:
out = {}
for line in path.read_text(encoding="utf-8").splitlines():
if line.strip() and not line.startswith("#") and "=" in line:
k, _, v = line.partition("=")
out[k.strip()] = v.strip().strip('"')
return out
@pytest.fixture
def profile_env(tmp_path, monkeypatch):
home = tmp_path / ".hermes"
home.mkdir()
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(home))
return home
def test_legacy_env_becomes_validated_toml_selected_from_env(profile_env):
(profile_env / ".env").write_text(LEGACY_ENV.format(home=profile_env), encoding="utf-8")
result = migrate_profile_relay_env(profile_env)
assert result.migrated, (result.skipped_reason, result.validation_error)
assert result.diagnostics == []
toml_path = profile_env / RELAY_PLUGINS_TOML_NAME
document = tomllib.loads(toml_path.read_text(encoding="utf-8"))
sink = document["components"][0]["config"]["atof"]["sinks"][0]
assert sink["type"] == "file" and sink["filename"] == "hermes-atof.jsonl"
assert document["components"][0]["config"]["atif"]["filename_template"] == "trajectory-{session_id}.json"
# Relay itself accepts the file Hermes will load at runtime.
report = asyncio.run(nemo_relay.plugin.initialize(document))
asyncio.run(nemo_relay.plugin.clear_async())
assert report.get("diagnostics") == []
# .env now selects the file; the legacy lines survive as comments (not deleted), so the
# runtime warning + doctor finding go quiet and a second run is a no-op.
env = _parse_env(profile_env / ".env")
assert env[RELAY_PLUGINS_CONFIG_ENV] == str(toml_path)
assert env["OPENAI_API_KEY"] == "sk-test"
assert configured_legacy_relay_env_vars(env) == ()
assert "# migrated to relay-plugins.toml: HERMES_NEMO_RELAY_ATOF_ENABLED=1" in (profile_env / ".env").read_text(encoding="utf-8")
assert migrate_profile_relay_env(profile_env).skipped_reason == "no legacy exporter variables"
def test_update_migrates_every_profile_home_separately(profile_env):
"""Multiplex: each profile keeps its own TOML; a profile without legacy vars is untouched."""
(profile_env / ".env").write_text("OPENAI_API_KEY=x\n", encoding="utf-8")
work = profile_env / "profiles" / "work"
idle = profile_env / "profiles" / "idle"
for p in (work, idle):
p.mkdir(parents=True)
(work / ".env").write_text(LEGACY_ENV.format(home=work), encoding="utf-8")
(idle / ".env").write_text("SLACK_BOT_TOKEN=y\n", encoding="utf-8")
results = {r.home: r for r in migrate_all_profile_relay_envs(validate=False)}
assert results[work].migrated and results[work].toml_path == work / RELAY_PLUGINS_TOML_NAME
assert not results[idle].migrated and not (idle / RELAY_PLUGINS_TOML_NAME).exists()
assert not results[profile_env].migrated and not (profile_env / RELAY_PLUGINS_TOML_NAME).exists()
assert _parse_env(work / ".env")[RELAY_PLUGINS_CONFIG_ENV] == str(work / RELAY_PLUGINS_TOML_NAME)
assert (idle / ".env").read_text(encoding="utf-8") == "SLACK_BOT_TOKEN=y\n"