mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-14 15:07:45 +00:00
246 lines
8.1 KiB
Python
246 lines
8.1 KiB
Python
import json
|
|
import re
|
|
import sqlite3
|
|
import subprocess
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
|
COMPACT_UI = re.sub(r"\s+", "", UI_JS)
|
|
|
|
|
|
def test_cli_tool_result_diff_snippet_is_not_cut_to_200_chars():
|
|
"""Diff-like CLI tool results should reach the existing tool-card expander."""
|
|
assert "function _cliToolResultSnippet" in UI_JS
|
|
assert "function _cliLooksLikePatchDiff" in UI_JS
|
|
assert r"\*\*\* Begin Patch" in UI_JS
|
|
assert "diff --git" in UI_JS
|
|
assert (
|
|
"if(_cliLooksLikePatchDiff(fullText))return_clipCliToolSnippet(fullText);"
|
|
in COMPACT_UI
|
|
)
|
|
assert "returnString(fullText||'').slice(0,200);" in COMPACT_UI
|
|
|
|
|
|
def test_cli_tool_fallback_promotes_apply_patch_args_to_tool_card_snippet():
|
|
"""A successful apply_patch result may only say 'Success'; keep the patch visible."""
|
|
assert "function _cliPatchSnippetFromArgs" in UI_JS
|
|
assert "toolName==='apply_patch'" in COMPACT_UI
|
|
assert "'old_string'" in UI_JS
|
|
assert "'new_string'" in UI_JS
|
|
assert "constpatchSnippet=_cliPatchSnippetFromArgs(name,args);" in COMPACT_UI
|
|
assert "snippet:_cliToolCardSnippet(resultSnippet,patchSnippet)" in COMPACT_UI
|
|
assert "is_diff:_cliToolCardHasDiffSnippet(resultSnippet,patchSnippet)" in COMPACT_UI
|
|
|
|
|
|
def test_diff_tool_cards_use_show_diff_expander_label():
|
|
assert "const moreLabel=tc.is_diff?'Show diff':'Show more';" in UI_JS
|
|
assert "const lessLabel=tc.is_diff?'Hide diff':'Show less';" in UI_JS
|
|
assert 'data-more-label="${esc(moreLabel)}"' in UI_JS
|
|
|
|
|
|
def _function_source(src: str, name: str) -> str:
|
|
match = re.search(rf"function\s+{re.escape(name)}\s*\(", src)
|
|
assert match, f"{name}() not found"
|
|
brace = src.find("{", match.end())
|
|
assert brace != -1, f"{name}() has no body"
|
|
depth = 1
|
|
i = brace + 1
|
|
in_string = None
|
|
escaped = False
|
|
in_line_comment = False
|
|
in_block_comment = False
|
|
while i < len(src) and depth:
|
|
ch = src[i]
|
|
nxt = src[i + 1] if i + 1 < len(src) else ""
|
|
if in_line_comment:
|
|
if ch == "\n":
|
|
in_line_comment = False
|
|
i += 1
|
|
continue
|
|
if in_block_comment:
|
|
if ch == "*" and nxt == "/":
|
|
in_block_comment = False
|
|
i += 2
|
|
continue
|
|
i += 1
|
|
continue
|
|
if in_string:
|
|
if escaped:
|
|
escaped = False
|
|
elif ch == "\\":
|
|
escaped = True
|
|
elif ch == in_string:
|
|
in_string = None
|
|
i += 1
|
|
continue
|
|
if ch == "/" and nxt == "/":
|
|
in_line_comment = True
|
|
i += 2
|
|
continue
|
|
if ch == "/" and nxt == "*":
|
|
in_block_comment = True
|
|
i += 2
|
|
continue
|
|
if ch in "'\"`":
|
|
in_string = ch
|
|
i += 1
|
|
continue
|
|
if ch == "{":
|
|
depth += 1
|
|
elif ch == "}":
|
|
depth -= 1
|
|
i += 1
|
|
assert depth == 0, f"{name}() body did not close"
|
|
return src[match.start() : i]
|
|
|
|
|
|
def test_rendered_apply_patch_tool_card_html_contains_diff_lines():
|
|
"""Drive the actual snippet helpers and buildToolCard() through Node."""
|
|
function_names = [
|
|
"_clipCliToolSnippet",
|
|
"_cliToolResultText",
|
|
"_cliLooksLikePatchDiff",
|
|
"_cliToolResultSnippet",
|
|
"_prefixedCliDiffLines",
|
|
"_firstOwnedValue",
|
|
"_cliPatchSnippetFromArgs",
|
|
"_cliToolCardSnippet",
|
|
"_cliToolCardHasDiffSnippet",
|
|
"buildToolCard",
|
|
]
|
|
functions = "\n".join(_function_source(UI_JS, name) for name in function_names)
|
|
script = textwrap.dedent(
|
|
f"""
|
|
function esc(s){{return String(s||'').replace(/[&<>]/g,c=>({{'&':'&','<':'<','>':'>'}}[c]));}}
|
|
function li(){{return '';}}
|
|
function toolIcon(){{return '';}}
|
|
function _toolDisplayName(tc){{return tc.name||'tool';}}
|
|
const document={{
|
|
createElement(){{return {{className:'', innerHTML:''}};}}
|
|
}};
|
|
{functions}
|
|
|
|
const longPatch = [
|
|
'*** Begin Patch',
|
|
'*** Update File: app.py',
|
|
'@@',
|
|
'-old',
|
|
'+new',
|
|
...Array.from({{length: 150}}, (_, i) => '+line ' + i),
|
|
'*** End Patch'
|
|
].join('\\n');
|
|
const resultSnippet = _cliToolResultSnippet(JSON.stringify({{output:'Success'}}));
|
|
const patchSnippet = _cliPatchSnippetFromArgs('apply_patch', {{patch: longPatch}});
|
|
const row = buildToolCard({{
|
|
name: 'apply_patch',
|
|
snippet: _cliToolCardSnippet(resultSnippet, patchSnippet),
|
|
is_diff: _cliToolCardHasDiffSnippet(resultSnippet, patchSnippet),
|
|
args: {{patch: '(shown in diff)'}},
|
|
done: true
|
|
}});
|
|
const errorSnippet = _cliToolCardSnippet('Patch failed: context not found', patchSnippet);
|
|
process.stdout.write(JSON.stringify({{html: row.innerHTML, errorSnippet}}));
|
|
"""
|
|
)
|
|
proc = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
|
payload = json.loads(proc.stdout)
|
|
html = payload["html"]
|
|
assert "-old" in html
|
|
assert "+new" in html
|
|
assert "Show diff" in html
|
|
assert "Patch failed: context not found" in payload["errorSnippet"]
|
|
assert "-old" in payload["errorSnippet"]
|
|
|
|
|
|
def _make_state_db(path: Path) -> None:
|
|
patch = "\n".join(
|
|
[
|
|
"*** Begin Patch",
|
|
"*** Update File: app.py",
|
|
"@@",
|
|
"-old",
|
|
"+new",
|
|
"*** End Patch",
|
|
]
|
|
)
|
|
tool_calls = [
|
|
{
|
|
"id": "call_patch",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "apply_patch",
|
|
"arguments": json.dumps({"patch": patch}),
|
|
},
|
|
}
|
|
]
|
|
conn = sqlite3.Connection(str(path))
|
|
try:
|
|
conn.executescript(
|
|
"""
|
|
CREATE TABLE messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT,
|
|
role TEXT,
|
|
content TEXT,
|
|
timestamp TEXT,
|
|
tool_call_id TEXT,
|
|
tool_calls TEXT,
|
|
tool_name TEXT
|
|
);
|
|
"""
|
|
)
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO messages (session_id, role, content, timestamp, tool_calls)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
""",
|
|
("issue1824", "assistant", "", "2026-01-01T00:00:01Z", json.dumps(tool_calls)),
|
|
)
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO messages (session_id, role, content, timestamp, tool_call_id, tool_name)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
"issue1824",
|
|
"tool",
|
|
json.dumps({"output": "Success"}),
|
|
"2026-01-01T00:00:02Z",
|
|
"call_patch",
|
|
"apply_patch",
|
|
),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_cli_session_reader_preserves_apply_patch_metadata(tmp_path, monkeypatch):
|
|
"""The API payload should keep tool_calls/tool rows for the UI renderer."""
|
|
_make_state_db(tmp_path / "state.db")
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
import api.profiles
|
|
from api.models import get_cli_session_messages
|
|
|
|
monkeypatch.setattr(api.profiles, "get_active_hermes_home", lambda: str(tmp_path))
|
|
|
|
messages = get_cli_session_messages("issue1824")
|
|
assert [m["role"] for m in messages] == ["assistant", "tool"]
|
|
|
|
assistant = messages[0]
|
|
assert assistant["tool_calls"][0]["function"]["name"] == "apply_patch"
|
|
args = json.loads(assistant["tool_calls"][0]["function"]["arguments"])
|
|
assert "*** Begin Patch" in args["patch"]
|
|
assert "-old" in args["patch"]
|
|
assert "+new" in args["patch"]
|
|
|
|
tool = messages[1]
|
|
assert tool["tool_call_id"] == "call_patch"
|
|
assert tool["tool_name"] == "apply_patch"
|
|
assert tool["name"] == "apply_patch"
|
|
assert json.loads(tool["content"])["output"] == "Success"
|