mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
Remove unused imports (F401) and duplicate/shadowed import redefinitions (F811) across the codebase using ruff's safe autofixes. No behavioral changes -- imports only. - ~1400 safe autofixes applied across 644 files (net -1072 lines) - __init__.py re-exports preserved (excluded from F401 removal so public re-export surfaces stay intact) - Re-exports that are imported or monkeypatched by tests but look unused in their defining module are kept with explicit # noqa: F401 (gateway/run.py load_dotenv; run_agent re-exports from agent.message_sanitization, agent.context_compressor, agent.retry_utils, agent.prompt_builder, agent.process_bootstrap, agent.codex_responses_adapter) - Unsafe F841 (unused-variable) fixes deliberately skipped -- those can change behavior when the RHS has side effects - ruff lints remain disabled in pyproject.toml (only PLW1514 is selected); this is a one-time cleanup, not a config change Verification: - python -m compileall: clean - pytest --collect-only: all 27161 tests collect (zero import errors) - core entry points import clean (run_agent, model_tools, cli, toolsets, hermes_state, batch_runner, gateway) - static scan: every name any test imports directly from an edited module still resolves
179 lines
4.7 KiB
Python
179 lines
4.7 KiB
Python
"""Unit tests for hermes_cli.session_recap."""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
|
||
from hermes_cli.session_recap import build_recap
|
||
|
||
|
||
def _user(text):
|
||
return {"role": "user", "content": text}
|
||
|
||
|
||
def _assistant(text=None, tool_calls=None):
|
||
msg = {"role": "assistant", "content": text}
|
||
if tool_calls:
|
||
msg["tool_calls"] = tool_calls
|
||
return msg
|
||
|
||
|
||
def _tool_call(name, args):
|
||
return {
|
||
"id": f"call_{name}",
|
||
"type": "function",
|
||
"function": {"name": name, "arguments": json.dumps(args)},
|
||
}
|
||
|
||
|
||
def _tool_result(content="ok"):
|
||
return {"role": "tool", "content": content}
|
||
|
||
|
||
def test_empty_history():
|
||
out = build_recap([])
|
||
assert "Session recap" in out
|
||
assert "nothing to recap" in out
|
||
|
||
|
||
def test_header_shows_title_when_provided():
|
||
out = build_recap([_user("hello")], session_title="Refactor the adapter")
|
||
assert "Refactor the adapter" in out.splitlines()[0]
|
||
|
||
|
||
def test_header_shows_short_id_when_no_title():
|
||
out = build_recap([_user("hello")], session_id="abcdef1234567890")
|
||
assert "abcdef12" in out.splitlines()[0]
|
||
|
||
|
||
def test_counts_recent_turns():
|
||
msgs = [
|
||
_user("one"),
|
||
_assistant("first reply"),
|
||
_user("two"),
|
||
_assistant("second reply"),
|
||
]
|
||
out = build_recap(msgs)
|
||
assert "2 user turn" in out
|
||
assert "assistant repl" in out
|
||
|
||
|
||
def test_last_ask_and_reply_are_surfaced():
|
||
msgs = [
|
||
_user("old question"),
|
||
_assistant("old answer"),
|
||
_user("summarise the docs"),
|
||
_assistant("here is the summary of the docs you asked for"),
|
||
]
|
||
out = build_recap(msgs)
|
||
assert "summarise the docs" in out
|
||
assert "summary of the docs" in out
|
||
|
||
|
||
def test_tool_counts_and_files():
|
||
msgs = [
|
||
_user("edit the readme and run tests"),
|
||
_assistant(
|
||
tool_calls=[
|
||
_tool_call("read_file", {"path": "README.md"}),
|
||
_tool_call("patch", {"path": "README.md"}),
|
||
]
|
||
),
|
||
_tool_result(),
|
||
_tool_result(),
|
||
_assistant(
|
||
tool_calls=[
|
||
_tool_call("terminal", {"command": "pytest"}),
|
||
]
|
||
),
|
||
_tool_result("tests ok"),
|
||
_assistant("All green."),
|
||
]
|
||
out = build_recap(msgs)
|
||
assert "patch×1" in out
|
||
assert "terminal×1" in out
|
||
assert "read_file×1" in out
|
||
# README.md should appear (may include cwd-relative prefix stripping).
|
||
assert "README.md" in out
|
||
|
||
|
||
def test_tool_preview_length_truncates_long_user_prompt():
|
||
long = "x " * 500
|
||
out = build_recap([_user(long)])
|
||
ask_line = [l for l in out.splitlines() if "Last ask" in l][0]
|
||
assert len(ask_line) < 300 # truncated with ellipsis
|
||
assert "…" in ask_line
|
||
|
||
|
||
def test_respects_recent_window():
|
||
# 30 turns of user+assistant; only the most recent 20 should be summarised.
|
||
msgs = []
|
||
for i in range(30):
|
||
msgs.append(_user(f"question {i}"))
|
||
msgs.append(_assistant(f"answer {i}"))
|
||
out = build_recap(msgs)
|
||
# We scoped to the 20-turn window but show "of 30/30 total".
|
||
assert "of 30/30 total" in out
|
||
|
||
|
||
def test_multimodal_content_blocks_flattened():
|
||
msgs = [
|
||
{
|
||
"role": "user",
|
||
"content": [
|
||
{"type": "text", "text": "check this file"},
|
||
{"type": "image_url", "image_url": {"url": "..."}},
|
||
],
|
||
},
|
||
_assistant("Looked at your image."),
|
||
]
|
||
out = build_recap(msgs)
|
||
assert "check this file" in out
|
||
assert "Looked at your image" in out
|
||
|
||
|
||
def test_handles_arguments_as_dict_not_string():
|
||
# Some providers return arguments already as a dict.
|
||
msgs = [
|
||
_user("go"),
|
||
{
|
||
"role": "assistant",
|
||
"content": None,
|
||
"tool_calls": [
|
||
{
|
||
"type": "function",
|
||
"function": {
|
||
"name": "patch",
|
||
"arguments": {"path": "foo.py"},
|
||
},
|
||
}
|
||
],
|
||
},
|
||
]
|
||
out = build_recap(msgs)
|
||
assert "patch×1" in out
|
||
assert "foo.py" in out
|
||
|
||
|
||
def test_no_assistant_activity_hint():
|
||
out = build_recap([_user("just sent my first message")])
|
||
assert "no assistant activity" in out or "Last ask" in out
|
||
|
||
|
||
def test_tool_message_count_reported():
|
||
msgs = [
|
||
_user("go"),
|
||
_assistant(tool_calls=[_tool_call("read_file", {"path": "a"})]),
|
||
_tool_result(),
|
||
_tool_result(),
|
||
_assistant("done"),
|
||
]
|
||
out = build_recap(msgs)
|
||
assert "2 tool result" in out
|
||
|
||
|
||
def test_ignores_non_mapping_entries_gracefully():
|
||
msgs = [None, "stray", _user("hi"), _assistant("hello")]
|
||
# Should not raise.
|
||
out = build_recap(msgs)
|
||
assert "Session recap" in out
|