mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-11 08:42:11 +00:00
When Hermes runs on an interactive coding surface (CLI, TUI, desktop app, ACP) inside a git repo, it now shifts into a coding posture: - Tool restriction: the toolset collapses to a new `coding` set (files, terminal, search, web docs, skills, todo, delegate, vision, browser) plus the user's enabled MCP servers. Messaging / TTS / image-gen / smart-home / music / cron / computer-use fall away. - Operating brief: a Cursor-style system block (gather context before editing, focused diffs, verify, never fabricate, git is the user's). - Live workspace snapshot: git root, branch + upstream (ahead/behind), worktree, dirty/staged counts, recent commits — built once per session (cache-safe; never re-probed per turn). Activation via `agent.coding_context` (auto|on|off, default auto). `auto` fires only on interactive surfaces in a git repo; messaging platforms are never affected. A `--toolsets` flag or `HERMES_TUI_TOOLSETS` pin always wins.
120 lines
5 KiB
Python
120 lines
5 KiB
Python
"""Tests for agent.coding_context — resolver, toolset substitution, git probe."""
|
|
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
from agent import coding_context as cc
|
|
|
|
|
|
def _git_init(path):
|
|
env = {
|
|
"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t",
|
|
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t",
|
|
}
|
|
for args in (
|
|
["init", "-q", "-b", "main"],
|
|
["commit", "-q", "--allow-empty", "-m", "init commit"],
|
|
):
|
|
subprocess.run(["git", "-C", str(path), *args], check=True, env={**env, "HOME": str(path)})
|
|
|
|
|
|
# ── resolver ──────────────────────────────────────────────────────────────
|
|
|
|
class TestIsCodingContext:
|
|
def test_off_never_activates(self, tmp_path):
|
|
_git_init(tmp_path)
|
|
cfg = {"agent": {"coding_context": "off"}}
|
|
assert cc.is_coding_context(platform="cli", cwd=tmp_path, config=cfg) is False
|
|
|
|
def test_on_forces_even_without_git(self, tmp_path):
|
|
cfg = {"agent": {"coding_context": "on"}}
|
|
assert cc.is_coding_context(platform="telegram", cwd=tmp_path, config=cfg) is True
|
|
|
|
def test_auto_requires_git_repo(self, tmp_path):
|
|
cfg = {"agent": {"coding_context": "auto"}}
|
|
assert cc.is_coding_context(platform="cli", cwd=tmp_path, config=cfg) is False
|
|
_git_init(tmp_path)
|
|
assert cc.is_coding_context(platform="cli", cwd=tmp_path, config=cfg) is True
|
|
|
|
def test_auto_skips_messaging_surfaces(self, tmp_path):
|
|
_git_init(tmp_path)
|
|
cfg = {"agent": {"coding_context": "auto"}}
|
|
assert cc.is_coding_context(platform="discord", cwd=tmp_path, config=cfg) is False
|
|
assert cc.is_coding_context(platform="tui", cwd=tmp_path, config=cfg) is True
|
|
|
|
def test_default_mode_is_auto(self, tmp_path):
|
|
# Unknown/missing value normalizes to auto.
|
|
_git_init(tmp_path)
|
|
assert cc.is_coding_context(platform="cli", cwd=tmp_path, config={}) is True
|
|
|
|
|
|
# ── toolset substitution ────────────────────────────────────────────────────
|
|
|
|
class TestCodingSelection:
|
|
def test_selects_coding_when_active(self, tmp_path):
|
|
_git_init(tmp_path)
|
|
cfg = {"agent": {"coding_context": "on"}}
|
|
out = cc.coding_selection(platform="cli", cwd=tmp_path, config=cfg)
|
|
assert out[0] == cc.CODING_TOOLSET
|
|
|
|
def test_none_when_inactive(self, tmp_path):
|
|
cfg = {"agent": {"coding_context": "off"}}
|
|
assert cc.coding_selection(platform="cli", cwd=tmp_path, config=cfg) is None
|
|
|
|
def test_coding_toolset_is_registered(self):
|
|
from toolsets import resolve_toolset
|
|
|
|
tools = resolve_toolset(cc.CODING_TOOLSET)
|
|
# Coding essentials present…
|
|
for t in ("read_file", "write_file", "patch", "search_files", "terminal", "todo"):
|
|
assert t in tools
|
|
# …and the noise is gone.
|
|
for t in ("send_message", "text_to_speech", "image_generate", "computer_use"):
|
|
assert t not in tools
|
|
|
|
|
|
# ── git/workspace probe ─────────────────────────────────────────────────────
|
|
|
|
class TestWorkspaceBlock:
|
|
def test_empty_outside_repo(self, tmp_path):
|
|
assert cc.build_coding_workspace_block(tmp_path) == ""
|
|
|
|
def test_reports_branch_and_clean_status(self, tmp_path):
|
|
_git_init(tmp_path)
|
|
block = cc.build_coding_workspace_block(tmp_path)
|
|
assert "Workspace" in block
|
|
assert f"Root: {tmp_path.resolve()}" in block or "Root:" in block
|
|
assert "Branch: main" in block
|
|
assert "Status: clean" in block
|
|
assert "init commit" in block
|
|
|
|
def test_reports_dirty_counts(self, tmp_path):
|
|
_git_init(tmp_path)
|
|
(tmp_path / "untracked.txt").write_text("hi")
|
|
block = cc.build_coding_workspace_block(tmp_path)
|
|
assert "untracked" in block
|
|
assert "clean" not in block.split("Status:")[1].splitlines()[0]
|
|
|
|
|
|
# ── prompt assembly integration ─────────────────────────────────────────────
|
|
|
|
class TestStatusParsing:
|
|
def test_parse_status_counts_and_branch(self):
|
|
porcelain = (
|
|
"# branch.head feature\n"
|
|
"# branch.upstream origin/feature\n"
|
|
"# branch.ab +2 -1\n"
|
|
"1 M. N... 100644 100644 100644 aaa bbb staged.py\n"
|
|
"1 .M N... 100644 100644 100644 ccc ddd modified.py\n"
|
|
"? new.py\n"
|
|
"u UU N... 1 2 3 abc def conflict.py\n"
|
|
)
|
|
branch, counts = cc._parse_status(porcelain)
|
|
assert branch["head"] == "feature"
|
|
assert branch["upstream"] == "origin/feature"
|
|
assert branch["ahead"] == "2" and branch["behind"] == "1"
|
|
assert counts["staged"] == 1
|
|
assert counts["modified"] == 1
|
|
assert counts["untracked"] == 1
|
|
assert counts["conflicts"] == 1
|