mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 15:01:34 +08:00
Four independent session-UX bugs reported by an external user (#16294). /save wrote hermes_conversation_<ts>.json to CWD — invisible to 'hermes sessions browse' and easy to lose. Snapshots now write under ~/.hermes/sessions/saved/ and the command prints the absolute path plus a 'hermes --resume <id>' hint for the live DB-indexed session. 'hermes sessions browse' default --limit raised from 50 to 500. With the old ceiling, users with moderately long histories saw only the most recent 50 rows and assumed older sessions had been lost. TUI session.list (`/resume` picker) switched from a hardcoded allow-list of 13 gateway source names to a deny-list of just { 'tool' }. Sessions tagged acp / webhook / user-defined HERMES_SESSION_SOURCE values and any newly-added platform now surface. Default limit 20 → 200. ollama-cloud provider setup passes force_refresh=True to fetch_ollama_cloud_models() so a user entering their API key sees the fresh catalog (e.g. deepseek v4 flash, kimi k2.6) immediately instead of waiting up to an hour for the disk cache TTL to expire. Closes #16294.
103 lines
3.5 KiB
Python
103 lines
3.5 KiB
Python
"""Tests for /save — the conversation snapshot slash command.
|
|
|
|
Regression: the old implementation wrote ``hermes_conversation_<ts>.json``
|
|
to the current working directory (CWD). Users who ran /save expected the
|
|
file to be discoverable via ``hermes sessions browse``, but CWD-resident
|
|
snapshots are not indexed in the state DB and are generally invisible.
|
|
The fix writes snapshots under ``~/.hermes/sessions/saved/`` and prints
|
|
the absolute path plus the resume hint for the live session.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def hermes_home(tmp_path, monkeypatch):
|
|
home = tmp_path / ".hermes"
|
|
home.mkdir()
|
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
# Clear any cached hermes_home computation
|
|
import hermes_constants
|
|
if hasattr(hermes_constants, "_hermes_home_cache"):
|
|
hermes_constants._hermes_home_cache = None
|
|
return home
|
|
|
|
|
|
def _make_stub_cli(history):
|
|
"""Build a minimal object exposing just what save_conversation uses."""
|
|
return SimpleNamespace(
|
|
conversation_history=history,
|
|
model="test-model",
|
|
session_id="20260101_120000_abc123",
|
|
session_start=datetime(2026, 1, 1, 12, 0, 0),
|
|
)
|
|
|
|
|
|
def test_save_conversation_writes_under_hermes_home(hermes_home, tmp_path, monkeypatch, capsys):
|
|
"""Snapshot must land under ~/.hermes/sessions/saved/, not CWD."""
|
|
# Change CWD to a different directory to prove the file does NOT go there.
|
|
work = tmp_path / "somewhere-else"
|
|
work.mkdir()
|
|
monkeypatch.chdir(work)
|
|
|
|
# Import fresh to pick up the HERMES_HOME fixture
|
|
for mod in [m for m in sys.modules if m.startswith("cli") or m == "hermes_constants"]:
|
|
sys.modules.pop(mod, None)
|
|
|
|
import cli # noqa: F401 (module under test)
|
|
|
|
stub = _make_stub_cli([
|
|
{"role": "user", "content": "hi"},
|
|
{"role": "assistant", "content": "hello"},
|
|
])
|
|
|
|
# Call the unbound method against our stub.
|
|
cli.HermesCLI.save_conversation(stub)
|
|
|
|
# File must NOT be in CWD
|
|
cwd_leak = list(work.glob("hermes_conversation_*.json"))
|
|
assert not cwd_leak, f"snapshot leaked to CWD: {cwd_leak}"
|
|
|
|
# File MUST be under ~/.hermes/sessions/saved/
|
|
saved_dir = hermes_home / "sessions" / "saved"
|
|
assert saved_dir.is_dir(), "expected saved/ subdirectory to be created"
|
|
files = list(saved_dir.glob("hermes_conversation_*.json"))
|
|
assert len(files) == 1, files
|
|
|
|
payload = json.loads(files[0].read_text())
|
|
assert payload["model"] == "test-model"
|
|
assert payload["session_id"] == "20260101_120000_abc123"
|
|
assert payload["messages"] == [
|
|
{"role": "user", "content": "hi"},
|
|
{"role": "assistant", "content": "hello"},
|
|
]
|
|
|
|
# User-facing message must include the absolute path AND the resume hint.
|
|
out = capsys.readouterr().out
|
|
assert str(files[0]) in out, out
|
|
assert "hermes --resume 20260101_120000_abc123" in out, out
|
|
|
|
|
|
def test_save_conversation_empty_history_does_nothing(hermes_home, capsys):
|
|
for mod in [m for m in sys.modules if m.startswith("cli") or m == "hermes_constants"]:
|
|
sys.modules.pop(mod, None)
|
|
import cli
|
|
|
|
stub = _make_stub_cli([])
|
|
cli.HermesCLI.save_conversation(stub)
|
|
|
|
saved_dir = hermes_home / "sessions" / "saved"
|
|
assert not saved_dir.exists() or not list(saved_dir.iterdir())
|
|
out = capsys.readouterr().out
|
|
assert "No conversation to save" in out
|