Files
hermes-agent/tests/agent/test_relay_runtime_plugins.py
ethernet 890bbbda1f Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	apps/desktop/e2e/archived-hidden-session-recoverable.spec.ts
#	apps/desktop/e2e/bot-chat-message-agent-friendly-name.spec.ts
#	apps/desktop/e2e/bot-mailbox-unreadable-ticket.spec.ts
#	apps/desktop/e2e/bot-mode-roster-localized.spec.ts
#	apps/desktop/e2e/bot-mode-row-click-mirrors-registry.spec.ts
#	apps/desktop/e2e/bot-mode-tab-shows-bot-name.spec.ts
#	apps/desktop/e2e/bot-roster-group-row-organisation.spec.ts
#	apps/desktop/e2e/bot-roster-ignores-infra-dirs.spec.ts
#	apps/desktop/e2e/bot-roster-timestamp-meta.spec.ts
#	apps/desktop/e2e/bot-roster-user-sections.spec.ts
#	apps/desktop/e2e/bot-routines-pane-narrow.spec.ts
#	apps/desktop/e2e/bot-row-open-recent-session.spec.ts
#	apps/desktop/e2e/bot-tile-ignores-ambient-composer-model.spec.ts
#	apps/desktop/e2e/group-composer-auto-grow.spec.ts
#	apps/desktop/e2e/group-create-gate-remote-roster.spec.ts
#	apps/desktop/e2e/group-prompt-renamed-primary-handle.spec.ts
#	apps/desktop/e2e/hosted-room-backend-continuity.spec.ts
#	apps/desktop/e2e/hosted-room-legacy-store-migration.spec.ts
#	apps/desktop/e2e/settings-scope-chips-bot-title.spec.ts
#	apps/desktop/e2e/worktree-branch-status.spec.ts
#	apps/desktop/electron/backend-probes.test.ts
#	apps/desktop/electron/connection-apply.test.ts
#	apps/desktop/electron/desktop-electron-pin.test.ts
#	apps/desktop/electron/desktop-uninstall.test.ts
#	apps/desktop/electron/gateway-file-download-transport.test.ts
#	apps/desktop/electron/gateway-stop-before-update.test.ts
#	apps/desktop/electron/github-api-auth.test.ts
#	apps/desktop/electron/registry-primary-profile-scope.test.ts
#	apps/desktop/electron/update-api-check.test.ts
#	apps/desktop/electron/update-handoff-marker.test.ts
#	apps/desktop/electron/venv-blocker-scan.test.ts
#	apps/desktop/scripts/after-extract.test.mjs
#	apps/desktop/scripts/local-pack-publish.test.mjs
#	apps/desktop/scripts/tasks-scroll.test.mjs
#	apps/desktop/src/app/settings/model-settings.test.tsx
#	apps/desktop/src/app/updates-overlay.blockers.test.tsx
#	apps/desktop/src/components/desktop-install-overlay.test.tsx
#	apps/desktop/src/lib/update-copy.test.ts
#	scripts/ci/check_os_marker_fakes.py
#	tests-js/desktop-mac-usage-descriptions.test.ts
#	tests-js/node-engine-alignment.test.ts
#	tests/agent/lsp/test_install_and_lint_fixes.py
#	tests/agent/test_command_token_source.py
#	tests/agent/test_compression_boundary_hook.py
#	tests/agent/test_create_openai_client_ssl_verify.py
#	tests/agent/test_custom_provider_ca_probes.py
#	tests/agent/test_endpoint_blackhole.py
#	tests/agent/test_estimator_parity.py
#	tests/agent/test_in_place_compaction.py
#	tests/agent/test_moa_loop_mode.py
#	tests/agent/test_model_metadata.py
#	tests/agent/test_skill_session_platform_gate.py
#	tests/agent/test_skill_utils.py
#	tests/agent/test_ssl_ca_guard.py
#	tests/computer_use/test_doctor.py
#	tests/cron/test_codex_execution_paths.py
#	tests/cron/test_cron_bot_chat_delivery.py
#	tests/cron/test_cron_script.py
#	tests/cron/test_media_delivery_parity.py
#	tests/cron/test_misfire_catchup.py
#	tests/cron/test_parallel_pool.py
#	tests/cron/test_recurring_eagain_redispatch.py
#	tests/gateway/test_choice_picker.py
#	tests/gateway/test_control_socket_windows_live.py
#	tests/gateway/test_dingtalk.py
#	tests/gateway/test_feishu.py
#	tests/gateway/test_feishu_onboard.py
#	tests/gateway/test_gateway_shutdown.py
#	tests/gateway/test_matrix.py
#	tests/gateway/test_model_command_custom_providers.py
#	tests/gateway/test_reasoning_command.py
#	tests/gateway/test_runtime_footer.py
#	tests/gateway/test_session.py
#	tests/gateway/test_session_hygiene.py
#	tests/gateway/test_status.py
#	tests/gateway/test_teams.py
#	tests/gateway/test_turn_lease.py
#	tests/gateway/test_whatsapp_connect.py
#	tests/hermes_cli/test_approvals_command.py
#	tests/hermes_cli/test_auth_store_lock_concurrent.py
#	tests/hermes_cli/test_backup.py
#	tests/hermes_cli/test_banner_git_state.py
#	tests/hermes_cli/test_certifi_repair.py
#	tests/hermes_cli/test_cmd_update.py
#	tests/hermes_cli/test_compat_manifest_targets.py
#	tests/hermes_cli/test_computer_use_cli.py
#	tests/hermes_cli/test_cpr_local_leak.py
#	tests/hermes_cli/test_dashboard_auth_gate.py
#	tests/hermes_cli/test_dashboard_procs_kill_grace.py
#	tests/hermes_cli/test_desktop_lifecycle_windows_live.py
#	tests/hermes_cli/test_doctor.py
#	tests/hermes_cli/test_doctor_command_install.py
#	tests/hermes_cli/test_fleet_config_migration_windows_live.py
#	tests/hermes_cli/test_gateway.py
#	tests/hermes_cli/test_gateway_platform_gating.py
#	tests/hermes_cli/test_gateway_restart_loop.py
#	tests/hermes_cli/test_gateway_task_probe.py
#	tests/hermes_cli/test_gateway_wsl.py
#	tests/hermes_cli/test_gui_command.py
#	tests/hermes_cli/test_install_cua_driver.py
#	tests/hermes_cli/test_kanban_db.py
#	tests/hermes_cli/test_lazy_command_exports.py
#	tests/hermes_cli/test_lazy_refresh_venv_repair.py
#	tests/hermes_cli/test_linux_desktop_entry.py
#	tests/hermes_cli/test_local_runtime.py
#	tests/hermes_cli/test_local_runtime_updates.py
#	tests/hermes_cli/test_managed_uv.py
#	tests/hermes_cli/test_mcp_reload_confirm_gate.py
#	tests/hermes_cli/test_nous_subscription.py
#	tests/hermes_cli/test_npm_engine.py
#	tests/hermes_cli/test_personality_none.py
#	tests/hermes_cli/test_pet_toggle.py
#	tests/hermes_cli/test_plan_reconciliation_windows_live.py
#	tests/hermes_cli/test_plugin_event_bus.py
#	tests/hermes_cli/test_plugin_manifest_v2.py
#	tests/hermes_cli/test_plugin_packs.py
#	tests/hermes_cli/test_plugins_cmd.py
#	tests/hermes_cli/test_plugins_cmd_enable_disable_nested.py
#	tests/hermes_cli/test_process_identity.py
#	tests/hermes_cli/test_profiles.py
#	tests/hermes_cli/test_profiles_sidebar_cache.py
#	tests/hermes_cli/test_pty_bridge.py
#	tests/hermes_cli/test_resolve_turn_limit.py
#	tests/hermes_cli/test_serve_runtime_inventory.py
#	tests/hermes_cli/test_session_vacuum_config.py
#	tests/hermes_cli/test_set_config_value.py
#	tests/hermes_cli/test_signal_handler_kanban_worker.py
#	tests/hermes_cli/test_slash_confirm_windows.py
#	tests/hermes_cli/test_stale_pid_guard.py
#	tests/hermes_cli/test_startup_fast_guards.py
#	tests/hermes_cli/test_status.py
#	tests/hermes_cli/test_telegram_managed_bot.py
#	tests/hermes_cli/test_tools_config.py
#	tests/hermes_cli/test_update_apply_shallow_count.py
#	tests/hermes_cli/test_update_autostash.py
#	tests/hermes_cli/test_update_concurrent_quarantine.py
#	tests/hermes_cli/test_update_fetch_failure_classifier.py
#	tests/hermes_cli/test_update_fleet_probe_resume_token.py
#	tests/hermes_cli/test_update_handoff_backend_reap.py
#	tests/hermes_cli/test_update_handoff_desktop_rebuild.py
#	tests/hermes_cli/test_update_head_moved_gate.py
#	tests/hermes_cli/test_update_host_obligation.py
#	tests/hermes_cli/test_update_import_guard.py
#	tests/hermes_cli/test_update_interrupted_recovery.py
#	tests/hermes_cli/test_update_inventory.py
#	tests/hermes_cli/test_update_launchd_unloaded_gateway.py
#	tests/hermes_cli/test_update_missing_configured_deps.py
#	tests/hermes_cli/test_update_modified_notice.py
#	tests/hermes_cli/test_update_multiplex_migration_hook.py
#	tests/hermes_cli/test_update_no_gateway_restart.py
#	tests/hermes_cli/test_update_orphan_backend_reap.py
#	tests/hermes_cli/test_update_parked_branch_guard.py
#	tests/hermes_cli/test_update_post_pull_syntax_guard.py
#	tests/hermes_cli/test_update_receipt.py
#	tests/hermes_cli/test_update_self_lock.py
#	tests/hermes_cli/test_update_shim_fail_closed.py
#	tests/hermes_cli/test_update_shim_self_lock.py
#	tests/hermes_cli/test_update_sqlite_remediation.py
#	tests/hermes_cli/test_update_stale_dashboard.py
#	tests/hermes_cli/test_update_stale_virtualenv.py
#	tests/hermes_cli/test_update_venv_health.py
#	tests/hermes_cli/test_update_venv_ownership_preflight.py
#	tests/hermes_cli/test_update_wedged_gateway.py
#	tests/hermes_cli/test_update_yes_flag.py
#	tests/hermes_cli/test_update_zip_two_phase.py
#	tests/hermes_cli/test_urllib_security.py
#	tests/hermes_cli/test_ux_messages_auth_config.py
#	tests/hermes_cli/test_ux_messages_startup.py
#	tests/hermes_cli/test_venv_holder_classifier.py
#	tests/hermes_cli/test_verify_console_scripts.py
#	tests/hermes_cli/test_verify_core_dependencies.py
#	tests/hermes_cli/test_web_server.py
#	tests/hermes_cli/test_web_server_console_ws.py
#	tests/hermes_cli/test_web_server_ws_ping.py
#	tests/hermes_cli/test_web_ui_build.py
#	tests/hermes_state/test_fts_rebuild_admission.py
#	tests/hermes_state/test_hermes_state.py
#	tests/plugins/memory/test_memory_lazy_install.py
#	tests/plugins/test_google_meet_plugin.py
#	tests/plugins/test_langfuse_plugin.py
#	tests/plugins/test_security_guidance_plugin.py
#	tests/plugins/test_transform_llm_output_hook.py
#	tests/scripts/desktop_update/test_desktop_update_windows_gateway_flag.py
#	tests/scripts/desktop_update/test_desktop_update_windows_python_handoff.py
#	tests/scripts/desktop_update/test_desktop_update_windows_timestamp.py
#	tests/scripts/install/test_install_clone_throttle_fallback.py
#	tests/scripts/install/test_install_lockfile_churn.py
#	tests/scripts/install/test_install_no_initial_commit.py
#	tests/scripts/install/test_install_sh_browser_install.py
#	tests/scripts/install/test_install_sh_node_prerelease.py
#	tests/scripts/install/test_install_sh_symlink_stomp.py
#	tests/scripts/install/test_install_sh_uv_lock_config.py
#	tests/scripts/install/test_install_unmerged_index.py
#	tests/scripts/test_contributor_map.py
#	tests/scripts/test_run_tests_parallel.py
#	tests/skills/test_competitor_news_monitor_skill.py
#	tests/skills/test_document_to_action_items_skill.py
#	tests/skills/test_google_workspace_setup.py
#	tests/skills/test_google_workspace_setup_deps.py
#	tests/skills/test_grounded_citations_skill.py
#	tests/skills/test_ip_as_logo_skill.py
#	tests/skills/test_live_dashboard_skill.py
#	tests/skills/test_mcp_oauth_remote_gateway_skill.py
#	tests/skills/test_office_document_skills.py
#	tests/skills/test_openclaw_migration.py
#	tests/skills/test_product_price_monitor_skill.py
#	tests/skills/test_scrollcraft_skill.py
#	tests/skills/test_setup_wizard_generator_skill.py
#	tests/skills/test_weekly_review_planning_skill.py
#	tests/test_engines_satisfiable.py
#	tests/test_fast_safe_load.py
#	tests/test_hermes_bootstrap.py
#	tests/test_hermes_constants.py
#	tests/test_hermes_logging.py
#	tests/test_managed_runtime_resolution.py
#	tests/test_model_tools_async_bridge.py
#	tests/test_packaging_build_guard.py
#	tests/test_packaging_metadata.py
#	tests/test_yaml_indent_consistency.py
#	tests/tools/test_approval_timeout_overflow.py
#	tests/tools/test_base_environment.py
#	tests/tools/test_bot_mode_dm.py
#	tests/tools/test_browser_chromium_check.py
#	tests/tools/test_browser_hardening.py
#	tests/tools/test_browser_homebrew_paths.py
#	tests/tools/test_browser_npx_warmup.py
#	tests/tools/test_browser_orphan_reaper.py
#	tests/tools/test_browser_real_profile.py
#	tests/tools/test_browser_use_cli.py
#	tests/tools/test_clipboard.py
#	tests/tools/test_code_execution.py
#	tests/tools/test_code_execution_modes.py
#	tests/tools/test_code_execution_windows_env.py
#	tests/tools/test_computer_use.py
#	tests/tools/test_delegate_liveness_timeout.py
#	tests/tools/test_execute_code_approval_cluster.py
#	tests/tools/test_execution_flag_detection.py
#	tests/tools/test_fal_common.py
#	tests/tools/test_file_operations.py
#	tests/tools/test_file_tools.py
#	tests/tools/test_file_tools_cwd_resolution.py
#	tests/tools/test_file_tools_live.py
#	tests/tools/test_lazy_deps.py
#	tests/tools/test_lazy_deps_durable_target.py
#	tests/tools/test_lazy_deps_managed.py
#	tests/tools/test_local_env_blocklist.py
#	tests/tools/test_local_tempdir.py
#	tests/tools/test_macos_protected_search.py
#	tests/tools/test_mcp_npx_cached_bin.py
#	tests/tools/test_oneshot_completion_linger.py
#	tests/tools/test_process_registry.py
#	tests/tools/test_read_file_schema_gating.py
#	tests/tools/test_skill_improvements.py
#	tests/tools/test_skills_sync.py
#	tests/tools/test_termux_api_detection.py
#	tests/tools/test_tirith_security.py
#	tests/tools/test_transcription_tools.py
#	tests/tools/test_tts_streaming.py
#	tests/tools/test_wake_word.py
#	tests/tui_gateway/test_compute_host_borrowed_lease.py
#	tests/tui_gateway/test_compute_host_turn_protocol.py
#	tests/tui_gateway/test_isolated_orphan_activity.py
#	tests/tui_gateway/test_protocol.py
#	tests/tui_gateway/test_slash_worker_profile_home.py
#	tests/tui_gateway/test_subprocess_encoding.py
#	tests/tui_gateway/test_tui_gateway_server.py
#	ui-tui/src/__tests__/terminalParity.test.ts
#	ui-tui/src/__tests__/termuxComposerLayout.test.ts
#	ui-tui/src/__tests__/textInputFastEcho.test.ts
2026-09-23 07:02:44 -04:00

1274 lines
40 KiB
Python

"""Tests for native NeMo Relay plugin configuration ownership."""
from __future__ import annotations
import asyncio
import contextvars
import json
import threading
from types import SimpleNamespace
from typing import Any
import pytest
from agent import relay_runtime
class _FakeRelay:
def __init__(
self,
*,
initialize_error: Exception | None = None,
dynamic_initialize_error: Exception | None = None,
activation_close_error: Exception | None = None,
active_report: Any = None,
report_error: Exception | None = None,
) -> None:
self.events: list[tuple[Any, ...]] = []
self.initialize_error = initialize_error
self.dynamic_initialize_error = dynamic_initialize_error
self.activation_close_error = activation_close_error
self.active_report = active_report
self.report_error = report_error
self.dynamic_plugin_specs: list[dict[str, Any]] = []
self.ScopeType = SimpleNamespace(Agent="agent")
self.plugin = SimpleNamespace(
initialize=self._initialize_plugins,
initialize_with_dynamic_plugins=self._initialize_dynamic_plugins,
load_dynamic_plugin_activation_specs=self._load_dynamic_plugin_specs,
clear_async=self._clear_plugins_async,
report=self._report_plugins,
)
self.scope = SimpleNamespace(
push=self._scope_push,
pop=self._scope_pop,
)
self.subscribers = SimpleNamespace(flush_async=self._flush_async)
def get_scope_stack(self) -> None:
return None
async def _initialize_plugins(self, config: dict[str, Any]) -> dict[str, Any]:
self.events.append(("plugin.initialize", config))
if self.initialize_error is not None:
raise self.initialize_error
return {"diagnostics": []}
async def _initialize_dynamic_plugins(
self,
config: dict[str, Any],
dynamic_plugins: list[dict[str, Any]],
) -> Any:
self.events.append(("plugin.initialize_dynamic", config, dynamic_plugins))
if self.dynamic_initialize_error is not None:
raise self.dynamic_initialize_error
relay = self
class _Activation:
async def close(self) -> None:
relay.events.append(("plugin.activation.close",))
if relay.activation_close_error is not None:
raise relay.activation_close_error
return _Activation()
def _load_dynamic_plugin_specs(self, config_path: Any) -> list[dict[str, Any]]:
self.events.append(("plugin.load_dynamic_specs", str(config_path)))
return self.dynamic_plugin_specs
async def _clear_plugins_async(self) -> None:
self.events.append(("plugin.clear_async",))
def _report_plugins(self) -> Any:
if self.report_error is not None:
raise self.report_error
return self.active_report
def _scope_push(self, name: str, scope_type: Any, **kwargs: Any) -> Any:
handle = ("scope", name, len(self.events))
self.events.append(("scope.push", name, scope_type, kwargs))
return handle
def _scope_pop(self, handle: Any, **kwargs: Any) -> None:
self.events.append(("scope.pop", handle, kwargs))
async def _flush_async(self) -> None:
self.events.append(("subscribers.flush_async",))
class _ConcurrentPublicationRelay(_FakeRelay):
def __init__(self) -> None:
super().__init__()
self.publication_finished = threading.Event()
async def _flush_async(self) -> None:
self.events.append(("subscribers.flush_async",))
assert await asyncio.to_thread(self.publication_finished.wait, 5)
class _BehavioralFakeRelay(_FakeRelay):
"""Record plugin interception together with the active session stack."""
def __init__(self) -> None:
super().__init__()
self._scope_stack = contextvars.ContextVar(
"behavioral_fake_relay_scope_stack",
default=None,
)
self._plugin_source: str | None = None
self.tools = SimpleNamespace(request_intercepts=self._request_intercepts)
def get_scope_stack(self) -> Any:
return self._scope_stack.get()
async def _initialize_plugins(self, config: dict[str, Any]) -> dict[str, Any]:
report = await super()._initialize_plugins(config)
self._plugin_source = "static"
return report
async def _initialize_dynamic_plugins(
self,
config: dict[str, Any],
dynamic_plugins: list[dict[str, Any]],
) -> Any:
activation = await super()._initialize_dynamic_plugins(
config,
dynamic_plugins,
)
self._plugin_source = "dynamic"
return activation
def _scope_push(self, name: str, scope_type: Any, **kwargs: Any) -> Any:
handle = super()._scope_push(name, scope_type, **kwargs)
self._scope_stack.set(handle)
return handle
def _request_intercepts(
self,
tool_name: str,
args: dict[str, Any],
) -> dict[str, Any]:
scope_stack = self.get_scope_stack()
self.events.append(
(
"tools.request_intercepts",
tool_name,
args,
self._plugin_source,
scope_stack,
)
)
return {
**args,
"relay_plugin_source": self._plugin_source,
"relay_scope_stack": scope_stack,
}
@pytest.fixture(autouse=True)
def _reset_runtime():
relay_runtime._reset_for_tests()
yield
relay_runtime._reset_for_tests()
@pytest.fixture
def explicit_static_config(tmp_path, monkeypatch):
config = tmp_path / "plugins.toml"
config.write_text("", encoding="utf-8")
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
return config
def test_unset_config_disables_plugin_initialization(monkeypatch):
monkeypatch.delenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, raising=False)
relay = _FakeRelay()
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert not host.managed_execution_enabled()
assert (
host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.DISABLED
)
host.ensure_session({"session_id": "session"})
assert relay.events[0][0:2] == ("scope.push", relay_runtime.SESSION_SCOPE)
assert not any(event[0].startswith("plugin.") for event in relay.events)
finally:
host.shutdown()
assert not any(event[0] == "subscribers.flush_async" for event in relay.events)
def test_first_profile_plugin_decision_applies_to_later_profile(
tmp_path,
monkeypatch,
):
monkeypatch.delenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, raising=False)
relay = _FakeRelay()
host_a = relay_runtime.RelayRuntime(relay=relay, profile_key="profile-a")
config = tmp_path / "plugins.toml"
config.write_text("", encoding="utf-8")
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
host_b = relay_runtime.RelayRuntime(relay=relay, profile_key="profile-b")
try:
assert not host_a.managed_execution_enabled()
assert not host_b.managed_execution_enabled()
assert (
host_a._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.DISABLED
)
assert (
host_b._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.DISABLED
)
assert not any(event[0].startswith("plugin.") for event in relay.events)
finally:
host_a.shutdown()
host_b.shutdown()
def test_relay_initializes_explicit_plugins_before_first_session_scope(
explicit_static_config,
):
relay = _FakeRelay()
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert host.managed_execution_enabled()
host.ensure_session({"session_id": "session"})
assert relay.events[0] == ("plugin.initialize", {})
assert relay.events[1][0:2] == ("scope.push", relay_runtime.SESSION_SCOPE)
finally:
host.shutdown()
def test_foreign_active_plugin_configuration_is_left_unchanged(
explicit_static_config,
caplog,
):
foreign_report = {"diagnostics": [], "source": "embedding-host"}
relay = _FakeRelay(active_report=foreign_report)
with caplog.at_level("WARNING"):
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert not host.managed_execution_enabled()
assert (
host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.FOREIGN
)
assert relay.active_report is foreign_report
assert relay.events == []
assert any(r.levelname == "WARNING" for r in caplog.records)
finally:
host.shutdown()
def test_unreadable_foreign_plugin_state_fails_safe(
explicit_static_config,
caplog,
):
relay = _FakeRelay(report_error=RuntimeError("report unavailable"))
with caplog.at_level("WARNING"):
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert not host.managed_execution_enabled()
assert (
host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.FAILED
)
assert relay.events == []
assert any(r.levelname == "WARNING" for r in caplog.records)
finally:
host.shutdown()
def test_legacy_exporter_env_without_plugins_toml_warns_and_stays_disabled(
monkeypatch,
caplog,
):
monkeypatch.delenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, raising=False)
monkeypatch.setenv("HERMES_NEMO_RELAY_ATOF_ENABLED", "1")
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_EXPORT_TIMEOUT_S", "30")
relay = _FakeRelay()
with caplog.at_level("WARNING"):
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert not host.managed_execution_enabled()
assert (
host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.DISABLED
)
assert relay.events == []
assert "HERMES_NEMO_RELAY_ATOF_ENABLED" in caplog.text
assert "HERMES_NEMO_RELAY_ATIF_EXPORT_TIMEOUT_S" in caplog.text
finally:
host.shutdown()
def test_initialization_failure_is_fail_open(explicit_static_config, caplog):
relay = _FakeRelay(initialize_error=RuntimeError("rejected config"))
with caplog.at_level("WARNING"):
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert not host.managed_execution_enabled()
assert (
host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.FAILED
)
assert any(r.levelname == "WARNING" for r in caplog.records)
finally:
host.shutdown()
def test_later_host_shares_initialization_failure(explicit_static_config):
relay = _FakeRelay(initialize_error=RuntimeError("transient failure"))
failed_host = relay_runtime.RelayRuntime(relay=relay, profile_key="failed")
assert not failed_host.managed_execution_enabled()
assert (
failed_host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.FAILED
)
relay.initialize_error = None
later_host = relay_runtime.RelayRuntime(relay=relay, profile_key="later")
try:
assert not later_host.managed_execution_enabled()
assert (
later_host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.FAILED
)
assert relay.events == [("plugin.initialize", {})]
finally:
failed_host.shutdown()
later_host.shutdown()
retry_host = relay_runtime.RelayRuntime(relay=relay, profile_key="retry")
try:
assert retry_host.managed_execution_enabled()
assert (
retry_host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.ACTIVE
)
assert relay.events.count(("plugin.initialize", {})) == 2
finally:
retry_host.shutdown()
def test_missing_explicit_config_is_failed_for_all_current_hosts(
tmp_path,
monkeypatch,
caplog,
):
missing_config = tmp_path / "missing" / "plugins.toml"
monkeypatch.setenv(
relay_runtime.RELAY_PLUGINS_CONFIG_ENV,
str(missing_config),
)
relay = _FakeRelay()
with caplog.at_level("WARNING"):
first_host = relay_runtime.RelayRuntime(relay=relay, profile_key="first")
missing_config.parent.mkdir()
missing_config.write_text("", encoding="utf-8")
later_host = relay_runtime.RelayRuntime(relay=relay, profile_key="later")
try:
for host in (first_host, later_host):
assert not host.managed_execution_enabled()
assert (
host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.FAILED
)
assert relay.events == []
assert any(r.levelname == "WARNING" for r in caplog.records)
finally:
first_host.shutdown()
later_host.shutdown()
def test_malformed_explicit_config_does_not_fall_back_to_discovery(
tmp_path,
monkeypatch,
caplog,
):
config = tmp_path / "plugins.toml"
config.write_text("[[components]\nkind =", encoding="utf-8")
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
relay = _FakeRelay()
with caplog.at_level("WARNING"):
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert not host.managed_execution_enabled()
assert (
host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.FAILED
)
assert relay.events == []
assert any(r.levelname == "WARNING" for r in caplog.records)
finally:
host.shutdown()
def test_present_plugins_section_is_validated_even_when_falsey(
tmp_path,
monkeypatch,
caplog,
):
config = tmp_path / "plugins.toml"
config.write_text("plugins = []", encoding="utf-8")
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
relay = _FakeRelay()
def reject_invalid_plugins(_config_path):
raise ValueError("'plugins' must be a table")
relay.plugin.load_dynamic_plugin_activation_specs = reject_invalid_plugins
with caplog.at_level("WARNING"):
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert not host.managed_execution_enabled()
assert (
host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.FAILED
)
assert relay.events == []
assert "'plugins' must be a table" in caplog.text
finally:
host.shutdown()
def test_two_profile_hosts_initialize_once_and_clear_after_final_shutdown(
explicit_static_config,
caplog,
):
relay = _BehavioralFakeRelay()
with caplog.at_level("INFO"):
host_a = relay_runtime.RelayRuntime(relay=relay, profile_key="profile-a")
host_b = relay_runtime.RelayRuntime(relay=relay, profile_key="profile-b")
assert relay.events == [("plugin.initialize", {})]
assert host_a.managed_execution_enabled()
assert host_b.managed_execution_enabled()
assert (
caplog.text.count(
"Relay plugins are active process-wide and apply to all profiles "
"hosted by this Hermes process."
)
== 1
)
rewritten_a = host_a.apply_tool_request_intercepts(
session_id="profile-a-session",
tool_name="terminal",
args={"profile": "a"},
)
rewritten_b = host_b.apply_tool_request_intercepts(
session_id="profile-b-session",
tool_name="terminal",
args={"profile": "b"},
)
assert rewritten_a["relay_plugin_source"] == "static"
assert rewritten_b["relay_plugin_source"] == "static"
assert rewritten_a["relay_scope_stack"] != rewritten_b["relay_scope_stack"]
host_a.shutdown()
assert ("plugin.clear_async",) not in relay.events
host_b.shutdown()
assert relay.events[-2:] == [
("subscribers.flush_async",),
("plugin.clear_async",),
]
assert relay.events.count(("plugin.initialize", {})) == 1
assert relay.events.count(("plugin.clear_async",)) == 1
pop_index = next(
index for index, event in enumerate(relay.events) if event[0] == "scope.pop"
)
assert pop_index < relay.events.index(("plugin.clear_async",))
def test_plugin_initialization_inside_running_event_loop(explicit_static_config):
relay = _FakeRelay()
async def construct_host() -> relay_runtime.RelayRuntime:
return relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
host = asyncio.run(construct_host())
try:
assert relay.events == [("plugin.initialize", {})]
assert host.managed_execution_enabled()
finally:
host.shutdown()
def test_static_plugin_cleanup_uses_async_apis_inside_running_event_loop(
explicit_static_config,
):
relay = _FakeRelay()
async def run_lifecycle() -> None:
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
host.shutdown()
asyncio.run(run_lifecycle())
assert relay.events == [
("plugin.initialize", {}),
("subscribers.flush_async",),
("plugin.clear_async",),
]
def test_dynamic_plugins_share_owned_activation_until_final_host_shutdown(
tmp_path,
monkeypatch,
):
config = tmp_path / ".nemo-relay" / "plugins.toml"
config.parent.mkdir()
config.write_text(
"""
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 1
[[plugins.dynamic]]
manifest = "plugins/native/relay-plugin.toml"
[plugins.dynamic.config]
mode = "strict"
[[plugins.dynamic]]
manifest = "plugins/worker/relay-plugin.toml"
""".strip(),
encoding="utf-8",
)
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
relay = _BehavioralFakeRelay()
relay.dynamic_plugin_specs = [
{
"plugin_id": "native.policy",
"kind": "rust_dynamic",
"manifest_ref": str(
config.parent / "plugins/native/relay-plugin.toml"
),
"config": {"mode": "strict"},
},
{
"plugin_id": "worker.policy",
"kind": "worker",
"manifest_ref": str(
config.parent / "plugins/worker/relay-plugin.toml"
),
"environment_ref": str(config.parent / "environments/worker"),
"config": {},
},
]
host_a = relay_runtime.RelayRuntime(relay=relay, profile_key="profile-a")
host_b = relay_runtime.RelayRuntime(relay=relay, profile_key="profile-b")
assert host_a.managed_execution_enabled()
assert host_b.managed_execution_enabled()
assert relay.events == [
("plugin.load_dynamic_specs", str(config)),
(
"plugin.initialize_dynamic",
{
"version": 1,
"components": [
{
"kind": "observability",
"enabled": True,
"config": {"version": 1},
}
],
},
relay.dynamic_plugin_specs,
)
]
rewritten_a = host_a.apply_tool_request_intercepts(
session_id="profile-a-session",
tool_name="terminal",
args={"profile": "a"},
)
rewritten_b = host_b.apply_tool_request_intercepts(
session_id="profile-b-session",
tool_name="terminal",
args={"profile": "b"},
)
assert rewritten_a["relay_plugin_source"] == "dynamic"
assert rewritten_b["relay_plugin_source"] == "dynamic"
assert rewritten_a["relay_scope_stack"] != rewritten_b["relay_scope_stack"]
host_a.shutdown()
assert ("plugin.activation.close",) not in relay.events
host_b.shutdown()
assert relay.events[-2:] == [
("subscribers.flush_async",),
("plugin.activation.close",),
]
assert ("plugin.clear_async",) not in relay.events
assert relay.events.count(("plugin.activation.close",)) == 1
pop_index = next(
index for index, event in enumerate(relay.events) if event[0] == "scope.pop"
)
assert pop_index < relay.events.index(("plugin.activation.close",))
def test_dynamic_activation_failure_disables_plugins(
tmp_path,
monkeypatch,
caplog,
):
config = tmp_path / "plugins.toml"
config.write_text(
"""
[[plugins.dynamic]]
manifest = "relay-plugin.toml"
""".strip(),
encoding="utf-8",
)
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
relay = _FakeRelay(
dynamic_initialize_error=RuntimeError("worker rejected config")
)
relay.dynamic_plugin_specs = [{"plugin_id": "worker.policy"}]
with caplog.at_level("INFO"):
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert not host.managed_execution_enabled()
assert (
host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.FAILED
)
assert [event[0] for event in relay.events] == [
"plugin.load_dynamic_specs",
"plugin.initialize_dynamic",
]
assert any(r.levelname == "WARNING" for r in caplog.records)
assert "Relay plugins are active process-wide" not in caplog.text
finally:
host.shutdown()
assert ("subscribers.flush_async",) not in relay.events
assert ("plugin.clear_async",) not in relay.events
def test_dynamic_activation_lifecycle_inside_running_event_loop(
tmp_path,
monkeypatch,
):
config = tmp_path / "plugins.toml"
config.write_text(
"""
[[plugins.dynamic]]
manifest = "relay-plugin.toml"
""".strip(),
encoding="utf-8",
)
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
relay = _FakeRelay()
relay.dynamic_plugin_specs = [{"plugin_id": "native.policy"}]
async def run_lifecycle() -> None:
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
assert host.managed_execution_enabled()
host.shutdown()
asyncio.run(run_lifecycle())
assert [event[0] for event in relay.events] == [
"plugin.load_dynamic_specs",
"plugin.initialize_dynamic",
"subscribers.flush_async",
"plugin.activation.close",
]
def test_shutdown_defers_dynamic_unload_until_async_operation_finishes(
tmp_path,
monkeypatch,
):
config = tmp_path / "plugins.toml"
config.write_text(
"""
[[plugins.dynamic]]
manifest = "relay-plugin.toml"
""".strip(),
encoding="utf-8",
)
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
relay = _FakeRelay()
relay.dynamic_plugin_specs = [{"plugin_id": "worker.policy"}]
async def run_lifecycle() -> None:
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
session = host.ensure_session({"session_id": "session"})
assert session is not None
started = asyncio.Event()
finish = asyncio.Event()
async def in_flight_call() -> None:
relay.events.append(("operation.start",))
started.set()
await finish.wait()
relay.events.append(("operation.end",))
operation = asyncio.create_task(
host.run_in_session_async(session, in_flight_call)
)
await started.wait()
host.shutdown()
assert host.ensure_session({"session_id": "late-session"}) is None
assert ("plugin.activation.close",) not in relay.events
finish.set()
await operation
assert await asyncio.to_thread(host._shutdown_complete.wait, 5)
asyncio.run(run_lifecycle())
assert relay.events.index(("operation.end",)) < relay.events.index(
("plugin.activation.close",)
)
def test_session_close_does_not_flush_during_concurrent_managed_publication(
explicit_static_config,
):
relay = _ConcurrentPublicationRelay()
completed = threading.Event()
errors: list[BaseException] = []
async def run_lifecycle() -> None:
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
closing_session = host.ensure_session({"session_id": "closing"})
active_session = host.ensure_session({"session_id": "active"})
assert closing_session is not None
assert active_session is not None
publication_started = asyncio.Event()
finish_publication = asyncio.Event()
async def managed_publication() -> None:
relay.events.append(("publication.start",))
publication_started.set()
await finish_publication.wait()
relay.events.append(("publication.end",))
relay.publication_finished.set()
publication = asyncio.create_task(
host.run_in_session_async(active_session, managed_publication)
)
await publication_started.wait()
host.close_session({"session_id": "closing"})
relay.events.append(("session.close.returned",))
finish_publication.set()
await publication
host.shutdown()
assert host._shutdown_complete.is_set()
def run_on_event_loop_thread() -> None:
try:
asyncio.run(run_lifecycle())
except BaseException as exc:
errors.append(exc)
finally:
completed.set()
event_loop_thread = threading.Thread(
target=run_on_event_loop_thread,
name="hermes-relay-session-close-regression",
daemon=True,
)
event_loop_thread.start()
if not completed.wait(3):
# Release a broken implementation so the test process can clean up
# after reporting the same deadlock guarded in production.
relay.publication_finished.set()
assert completed.wait(5)
pytest.fail("session close blocked the active asyncio event loop")
event_loop_thread.join()
assert errors == []
assert relay.events.count(("subscribers.flush_async",)) == 1
assert relay.events.index(("session.close.returned",)) < relay.events.index(
("publication.end",)
)
assert relay.events.index(("publication.end",)) < relay.events.index(
("subscribers.flush_async",)
)
assert relay.events[-1] == ("plugin.clear_async",)
def test_failed_dynamic_teardown_retains_activation_and_blocks_replacement(
tmp_path,
monkeypatch,
caplog,
):
config = tmp_path / "plugins.toml"
config.write_text(
"""
[[plugins.dynamic]]
manifest = "relay-plugin.toml"
""".strip(),
encoding="utf-8",
)
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
relay = _FakeRelay(activation_close_error=RuntimeError("worker still busy"))
relay.dynamic_plugin_specs = [
{
"plugin_id": "worker.policy",
"kind": "worker",
"manifest_ref": str(tmp_path / "relay-plugin.toml"),
"environment_ref": str(tmp_path / "environment"),
"config": {},
}
]
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
with caplog.at_level("WARNING"):
host.shutdown()
assert any(r.levelname == "WARNING" for r in caplog.records)
activation = relay_runtime._PLUGIN_CONFIGURATION._activation
assert activation is not None
with caplog.at_level("WARNING"):
replacement = relay_runtime.RelayRuntime(
relay=relay,
profile_key="replacement",
)
try:
assert not replacement.managed_execution_enabled()
assert relay_runtime._PLUGIN_CONFIGURATION._activation is activation
assert relay.events.count(
("plugin.initialize_dynamic", {}, relay.dynamic_plugin_specs)
) == 1
assert relay.events.count(("plugin.activation.close",)) == 2
assert any(r.levelname == "WARNING" for r in caplog.records)
finally:
replacement.shutdown()
# Relay treats a close failure as terminal; only reset the permissive
# fake so this process-global fixture cannot leak into later tests.
relay.activation_close_error = None
relay_runtime._PLUGIN_CONFIGURATION.reset_for_tests()
def test_standard_dynamic_records_use_relay_toml_loader(
tmp_path,
monkeypatch,
):
config = tmp_path / "plugins.toml"
config.write_text(
"""
[[plugins.dynamic]]
manifest = "relay-plugin.toml"
""".strip(),
encoding="utf-8",
)
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
relay = _FakeRelay()
relay.dynamic_plugin_specs = [
{
"plugin_id": "native.policy",
"kind": "rust_dynamic",
"manifest_ref": str(tmp_path / "relay-plugin.toml"),
"config": {},
}
]
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert host.managed_execution_enabled()
assert relay.events == [
("plugin.load_dynamic_specs", str(config)),
("plugin.initialize_dynamic", {}, relay.dynamic_plugin_specs),
]
finally:
host.shutdown()
def test_legacy_dynamic_records_are_rejected(
tmp_path,
monkeypatch,
caplog,
):
config = tmp_path / "plugins.toml"
config.write_text(
"""
version = 1
[[dynamic_plugins]]
plugin_id = "native.policy"
kind = "rust_dynamic"
manifest_ref = "relay-plugin.toml"
""".strip(),
encoding="utf-8",
)
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
relay = _FakeRelay()
with caplog.at_level("WARNING"):
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert not host.managed_execution_enabled()
assert (
host._plugin_configuration_state
is relay_runtime._RelayPluginConfigurationState.FAILED
)
assert relay.events == []
assert "Hermes [[dynamic_plugins]] records are unsupported" in caplog.text
assert "use Relay [[plugins.dynamic]] records" in caplog.text
finally:
host.shutdown()
def test_real_binding_loads_standard_dynamic_specs_from_explicit_toml(
tmp_path,
monkeypatch,
):
relay = pytest.importorskip("nemo_relay")
manifest = tmp_path / "plugins" / "relay-plugin.toml"
manifest.parent.mkdir()
manifest.write_text(
"""
manifest_version = 1
[plugin]
id = "fixture.native"
kind = "rust_dynamic"
""".strip(),
encoding="utf-8",
)
config = tmp_path / "plugins.toml"
config.write_text(
"""
version = 1
[[plugins.dynamic]]
manifest = "plugins/relay-plugin.toml"
[plugins.dynamic.config]
mode = "strict"
""".strip(),
encoding="utf-8",
)
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config))
plugin_config, specs = relay_runtime._configured_plugin_inputs(relay)
assert plugin_config == {"version": 1}
assert [spec.to_dict() for spec in specs] == [
{
"plugin_id": "fixture.native",
"kind": "rust_dynamic",
"manifest_ref": str(manifest.resolve()),
"config": {"mode": "strict"},
}
]
def test_real_binding_ignores_project_config_without_explicit_opt_in(
tmp_path,
monkeypatch,
):
relay = pytest.importorskip("nemo_relay")
if getattr(relay, "_native", None) is None:
pytest.skip("NeMo Relay native binding is unavailable on this platform")
project_root = tmp_path / "project"
working_directory = project_root / "workspace"
config_directory = project_root / ".nemo-relay"
atof_dir = tmp_path / "atof"
working_directory.mkdir(parents=True)
config_directory.mkdir()
(config_directory / "plugins.toml").write_text(
f"""
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 3
[components.config.atof]
enabled = true
[[components.config.atof.sinks]]
type = "file"
output_directory = "{atof_dir.as_posix()}"
filename = "events.jsonl"
mode = "overwrite"
""".strip(),
encoding="utf-8",
)
xdg_config_home = tmp_path / "xdg"
xdg_config_home.mkdir()
monkeypatch.chdir(working_directory)
monkeypatch.setenv("XDG_CONFIG_HOME", str(xdg_config_home))
monkeypatch.delenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, raising=False)
relay.plugin.clear()
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert not host.managed_execution_enabled()
host.ensure_session({"session_id": "native-no-plugins"})
finally:
host.shutdown()
relay_runtime._reset_for_tests()
assert not (atof_dir / "events.jsonl").exists()
def test_real_binding_ignores_project_config_with_explicit_opt_in(
tmp_path,
monkeypatch,
):
relay = pytest.importorskip("nemo_relay")
if getattr(relay, "_native", None) is None:
pytest.skip("NeMo Relay native binding is unavailable on this platform")
project_root = tmp_path / "project"
working_directory = project_root / "workspace"
config_directory = project_root / ".nemo-relay"
selected_directory = tmp_path / "selected-config"
project_atof_dir = tmp_path / "project-atof"
selected_atof_dir = tmp_path / "selected-atof"
working_directory.mkdir(parents=True)
config_directory.mkdir()
selected_directory.mkdir()
(config_directory / "plugins.toml").write_text(
f"""
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 3
[components.config.atof]
enabled = true
[[components.config.atof.sinks]]
type = "file"
output_directory = "{project_atof_dir.as_posix()}"
filename = "events.jsonl"
mode = "overwrite"
""".strip(),
encoding="utf-8",
)
selected_config = selected_directory / "plugins.toml"
selected_config.write_text(
f"""
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 4
[components.config.atof]
enabled = true
[[components.config.atof.sinks]]
type = "file"
output_directory = "{selected_atof_dir.as_posix()}"
filename = "events.jsonl"
mode = "overwrite"
""".strip(),
encoding="utf-8",
)
xdg_config_home = tmp_path / "xdg"
xdg_config_home.mkdir()
monkeypatch.chdir(working_directory)
monkeypatch.setenv("XDG_CONFIG_HOME", str(xdg_config_home))
monkeypatch.setenv(
relay_runtime.RELAY_PLUGINS_CONFIG_ENV,
str(selected_config),
)
relay.plugin.clear()
host = relay_runtime.RelayRuntime(relay=relay, profile_key="profile")
try:
assert host.managed_execution_enabled()
host.ensure_session({"session_id": "native-explicit-plugins"})
finally:
host.shutdown()
relay_runtime._reset_for_tests()
assert (selected_atof_dir / "events.jsonl").is_file()
assert not (project_atof_dir / "events.jsonl").exists()
def test_real_binding_keeps_two_profile_trajectories_separate_in_shared_exporters(
tmp_path,
monkeypatch,
):
relay = pytest.importorskip("nemo_relay")
if getattr(relay, "_native", None) is None:
pytest.skip("NeMo Relay native binding is unavailable on this platform")
from agent import relay_llm, relay_tools
working_directory = tmp_path / "project" / "workspace"
config_directory = tmp_path / "selected-config"
atof_dir = tmp_path / "atof"
atif_dir = tmp_path / "atif"
working_directory.mkdir(parents=True)
config_directory.mkdir()
config_path = config_directory / "plugins.toml"
config_path.write_text(
f"""
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config]
version = 3
[components.config.atof]
enabled = true
[[components.config.atof.sinks]]
type = "file"
output_directory = "{atof_dir.as_posix()}"
filename = "events.jsonl"
mode = "overwrite"
[components.config.atif]
enabled = true
output_directory = "{atif_dir.as_posix()}"
filename_template = "trajectory-{{session_id}}.json"
agent_name = "Hermes Native Test"
agent_version = "test"
""".strip(),
encoding="utf-8",
)
xdg_config_home = tmp_path / "xdg"
xdg_config_home.mkdir()
monkeypatch.chdir(working_directory)
monkeypatch.setenv("XDG_CONFIG_HOME", str(xdg_config_home))
monkeypatch.setenv(relay_runtime.RELAY_PLUGINS_CONFIG_ENV, str(config_path))
monkeypatch.setattr(relay_runtime, "_load_nemo_relay", lambda: relay)
relay.plugin.clear()
runtime_ids: dict[str, str] = {}
try:
for profile in ("profile-a", "profile-b"):
session_id = f"native-export-{profile}"
monkeypatch.setenv("HERMES_HOME", str(tmp_path / profile))
profile_key = relay_runtime.current_profile_key()
lease = relay_runtime.SESSION_COORDINATOR.acquire_conversation(
profile_key=profile_key,
session_id=session_id,
platform="cli",
model="test-model",
)
assert isinstance(lease.host, relay_runtime.RelayRuntime)
runtime_ids[profile] = lease.host.runtime_id
turn = relay_runtime.SESSION_COORDINATOR.begin_turn(
lease,
turn_id=f"turn-{profile}",
task_id=f"task-{profile}",
)
try:
assert lease.host.managed_execution_enabled()
relay_llm.execute(
{"model": "test-model", "messages": []},
lambda _request, profile=profile: {
"id": f"response-{profile}",
"model": "test-model",
"choices": [
{
"message": {
"role": "assistant",
"content": "ok",
},
"finish_reason": "stop",
}
],
},
session_id=session_id,
name="test-provider",
model_name="test-model",
metadata={
"api_mode": "chat_completions",
"api_request_id": f"request-{profile}",
},
)
relay_tools.execute(
"terminal",
{"command": "true"},
lambda _args: {"output": "ok"},
session_id=session_id,
metadata={"tool_call_id": f"tool-{profile}"},
)
finally:
relay_runtime.SESSION_COORDINATOR.end_turn(
turn,
outcome="success",
)
relay_runtime.SESSION_COORDINATOR.release_conversation(lease)
relay_runtime.SESSION_COORDINATOR.finalize_conversation(
profile_key=profile_key,
session_id=session_id,
)
finally:
relay_runtime._reset_for_tests()
assert (atof_dir / "events.jsonl").is_file()
atof_payload = (atof_dir / "events.jsonl").read_text(encoding="utf-8")
assert all(runtime_id in atof_payload for runtime_id in runtime_ids.values())
trajectories = list(atif_dir.glob("trajectory-*.json"))
assert len(trajectories) == 2
observed_runtime_ids: set[str] = set()
for trajectory_path in trajectories:
trajectory = json.loads(trajectory_path.read_text(encoding="utf-8"))
trajectory_payload = json.dumps(trajectory)
matching_runtime_ids = {
runtime_id
for runtime_id in runtime_ids.values()
if runtime_id in trajectory_payload
}
assert len(matching_runtime_ids) == 1
observed_runtime_ids.update(matching_runtime_ids)
observed_categories = {
event["category"]
for event in trajectory["extra"]["observed_events"]
if event["kind"] == "scope"
}
assert {"agent", "llm", "tool"} <= observed_categories
assert observed_runtime_ids == set(runtime_ids.values())