Files
hermes-agent/tests/gateway/test_yolo_command.py
Teknium b1a39a1b1b refactor(approval): one session /yolo contract for CLI, TUI/Desktop and the messaging gateway
The persist / restore / carry logic for session YOLO had grown three copies
(the CLI mixin, tui_gateway, the gateway runner), each with its own key choice,
ordering and error handling. tools/approval_yolo.py now owns it:

- toggle_session_yolo: flip or set, persist BEFORE the live flip (a racing
  restore never revives an OFF), stored-copy-aware so a toggle after a restart
  turns it OFF; a failed write logs a warning and never blocks the flip.
- restore_session_yolo: re-arm a persisted flag in a fresh process; skipped
  under the frozen process --yolo.
- transfer_session_yolo: move the live flag to a continuation id.
- with_session_yolo: a row being created records a live flag (lazy first row,
  compression child, /branch).

Each surface keeps only its storage: the session row for CLI/TUI/Desktop, the
routing entry for the gateway. Removed: HermesCLI._transfer_session_yolo /
_persist_session_yolo, GatewayRunner._restore_session_yolo, the TUI-only
restore helper, and the TUI's post-publish set_session_yolo carry (the
compression publish now writes the child row with the flag).

Closes a gap the unification exposed: a CLI manual /compress that rotated the
session dropped YOLO (the next dangerous command prompted, and a --resume of
the continuation came back with it off).
2026-10-08 00:49:32 -07:00

93 lines
3.2 KiB
Python

"""Tests for gateway /yolo session scoping."""
import os
import pytest
import gateway.run as gateway_run
from gateway.config import Platform
from gateway.platforms.event import MessageEvent
from gateway.session import SessionSource
from tools.approval import disable_session_yolo, is_session_yolo_enabled
@pytest.fixture(autouse=True)
def _clean_yolo_state(monkeypatch):
monkeypatch.delenv("HERMES_YOLO_MODE", raising=False)
disable_session_yolo("agent:main:telegram:dm:chat-a")
disable_session_yolo("agent:main:telegram:dm:chat-b")
yield
monkeypatch.delenv("HERMES_YOLO_MODE", raising=False)
disable_session_yolo("agent:main:telegram:dm:chat-a")
disable_session_yolo("agent:main:telegram:dm:chat-b")
def _make_runner():
runner = object.__new__(gateway_run.GatewayRunner)
runner.session_store = None
runner.config = None
return runner
def _make_event(chat_id: str) -> MessageEvent:
source = SessionSource(
platform=Platform.TELEGRAM,
user_id=f"user-{chat_id}",
chat_id=chat_id,
user_name="tester",
chat_type="dm",
)
return MessageEvent(text="/yolo", source=source)
@pytest.mark.asyncio
async def test_yolo_command_toggles_only_current_session(monkeypatch):
runner = _make_runner()
event_a = _make_event("chat-a")
session_a = runner._session_key_for_source(event_a.source)
session_b = runner._session_key_for_source(_make_event("chat-b").source)
await runner._handle_yolo_command(event_a)
assert is_session_yolo_enabled(session_a) is True
assert is_session_yolo_enabled(session_b) is False
assert os.environ.get("HERMES_YOLO_MODE") is None
await runner._handle_yolo_command(event_a)
assert is_session_yolo_enabled(session_a) is False
assert os.environ.get("HERMES_YOLO_MODE") is None
@pytest.mark.asyncio
async def test_yolo_survives_gateway_restart_and_dies_at_session_boundary(tmp_path):
"""/yolo is persisted on the routing entry: a fresh process re-arms it on the next turn, and a
conversation boundary (/new, /resume) clears both copies so a restart cannot revive it."""
from gateway.config import GatewayConfig
from gateway.session import SessionStore
from tools.approval_yolo import restore_session_yolo
def _runner():
runner = _make_runner()
runner.session_store = SessionStore(sessions_dir=tmp_path, config=GatewayConfig())
runner.session_store._db = None # JSON routing index: the restart reads it back from disk
return runner
event = _make_event("chat-a")
first = _runner()
key = first._session_key_for_source(event.source)
await first._handle_yolo_command(event)
assert is_session_yolo_enabled(key) is True
disable_session_yolo(key) # a new process starts with an empty in-memory approval set
second = _runner()
restore_session_yolo(key, second.session_store.get_or_create_session(event.source).yolo)
assert is_session_yolo_enabled(key) is True
second._clear_session_boundary_security_state(key)
assert is_session_yolo_enabled(key) is False
third = _runner()
restore_session_yolo(key, third.session_store.get_or_create_session(event.source).yolo)
assert is_session_yolo_enabled(key) is False