chore: uptick
This commit is contained in:
@@ -59,6 +59,28 @@ def test_write_json_returns_false_on_broken_pipe(monkeypatch):
|
||||
assert server.write_json({"ok": True}) is False
|
||||
|
||||
|
||||
def test_dispatch_rejects_non_object_request():
|
||||
resp = server.dispatch([])
|
||||
|
||||
assert resp == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": None,
|
||||
"error": {"code": -32600, "message": "invalid request: expected an object"},
|
||||
}
|
||||
|
||||
|
||||
def test_dispatch_rejects_non_object_params():
|
||||
resp = server.dispatch(
|
||||
{"id": "1", "method": "session.create", "params": []}
|
||||
)
|
||||
|
||||
assert resp == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "1",
|
||||
"error": {"code": -32602, "message": "invalid params: expected an object"},
|
||||
}
|
||||
|
||||
|
||||
def test_load_enabled_toolsets_prefers_tui_env(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_TUI_TOOLSETS", "web, terminal, ,memory")
|
||||
|
||||
@@ -115,7 +137,10 @@ def test_load_enabled_toolsets_rejects_disabled_mcp_env(monkeypatch, capsys):
|
||||
)
|
||||
monkeypatch.setattr(config_mod, "load_config", lambda: {"platform_toolsets": {"cli": ["memory"]}})
|
||||
|
||||
assert server._load_enabled_toolsets() == ["memory"]
|
||||
# Sorted: ["kanban", "memory"]. `kanban` is auto-recovered by
|
||||
# _get_platform_tools because it's a non-configurable platform toolset
|
||||
# whose tools live in hermes-cli's universe (see toolsets.py).
|
||||
assert server._load_enabled_toolsets() == ["kanban", "memory"]
|
||||
err = capsys.readouterr().err
|
||||
assert "ignoring disabled MCP servers" in err
|
||||
assert "mcp-off" in err
|
||||
@@ -134,7 +159,7 @@ def test_load_enabled_toolsets_falls_back_when_tui_env_invalid(monkeypatch, caps
|
||||
|
||||
monkeypatch.setattr(config_mod, "load_config", lambda: {"platform_toolsets": {"cli": ["memory"]}})
|
||||
|
||||
assert server._load_enabled_toolsets() == ["memory"]
|
||||
assert server._load_enabled_toolsets() == ["kanban", "memory"]
|
||||
assert "using configured CLI toolsets" in capsys.readouterr().err
|
||||
|
||||
|
||||
@@ -980,6 +1005,21 @@ def test_config_busy_get_and_set(monkeypatch):
|
||||
assert ("display.busy_input_mode", "interrupt") in writes
|
||||
|
||||
|
||||
def test_config_set_yolo_process_scope_treats_false_like_env_as_disabled(monkeypatch):
|
||||
monkeypatch.setenv("HERMES_YOLO_MODE", "false")
|
||||
|
||||
resp = server.handle_request(
|
||||
{
|
||||
"id": "1",
|
||||
"method": "config.set",
|
||||
"params": {"key": "yolo"},
|
||||
}
|
||||
)
|
||||
|
||||
assert resp["result"]["value"] == "1"
|
||||
assert os.environ.get("HERMES_YOLO_MODE") == "1"
|
||||
|
||||
|
||||
def test_config_get_statusbar_survives_non_dict_display(monkeypatch):
|
||||
monkeypatch.setattr(server, "_load_cfg", lambda: {"display": "broken"})
|
||||
|
||||
@@ -1898,6 +1938,55 @@ def test_input_detect_drop_attaches_image(monkeypatch):
|
||||
assert resp["result"]["text"] == "[User attached image: cat.png]"
|
||||
|
||||
|
||||
def test_input_detect_drop_path_with_spaces(tmp_path):
|
||||
"""input.detect_drop correctly handles image paths containing spaces."""
|
||||
# Create a minimal PNG file with a space in its name
|
||||
img = tmp_path / "screenshot with spaces.png"
|
||||
img.write_bytes(b"\x89PNG\r\n\x1a\n") # valid PNG header
|
||||
|
||||
server._sessions["sid"] = _session()
|
||||
|
||||
resp = server.handle_request(
|
||||
{
|
||||
"id": "2",
|
||||
"method": "input.detect_drop",
|
||||
"params": {"session_id": "sid", "text": str(img)},
|
||||
}
|
||||
)
|
||||
|
||||
assert resp["result"]["matched"] is True
|
||||
assert resp["result"]["is_image"] is True
|
||||
assert resp["result"]["path"] == str(img)
|
||||
assert resp["result"]["text"] == f"[User attached image: {img.name}]"
|
||||
# Verify attachment was recorded in the session
|
||||
assert len(server._sessions["sid"]["attached_images"]) == 1
|
||||
assert server._sessions["sid"]["attached_images"][0] == str(img)
|
||||
|
||||
|
||||
def test_input_detect_drop_path_with_spaces_and_remainder(tmp_path):
|
||||
"""input.detect_drop splits remainder when path contains spaces."""
|
||||
img = tmp_path / "photo with space.jpg"
|
||||
img.write_bytes(b"\xff\xd8\xff" + b"fakejpeg") # minimal-ish JPEG header
|
||||
|
||||
server._sessions["sid"] = _session()
|
||||
|
||||
user_input = f"{img} describe this image"
|
||||
resp = server.handle_request(
|
||||
{
|
||||
"id": "3",
|
||||
"method": "input.detect_drop",
|
||||
"params": {"session_id": "sid", "text": user_input},
|
||||
}
|
||||
)
|
||||
|
||||
assert resp["result"]["matched"] is True
|
||||
assert resp["result"]["is_image"] is True
|
||||
assert resp["result"]["path"] == str(img)
|
||||
# Remainder becomes the text sent to the model
|
||||
assert resp["result"]["text"] == "describe this image"
|
||||
assert server._sessions["sid"]["attached_images"][0] == str(img)
|
||||
|
||||
|
||||
def test_rollback_restore_resolves_number_and_file_path():
|
||||
calls = {}
|
||||
|
||||
@@ -2214,6 +2303,83 @@ def test_prompt_submit_history_version_match_persists_normally(monkeypatch):
|
||||
server._sessions.pop("sid", None)
|
||||
|
||||
|
||||
def test_prompt_submit_can_truncate_before_user_ordinal(monkeypatch):
|
||||
"""Desktop user-message edits should restart the turn from the edited user."""
|
||||
|
||||
seen = {}
|
||||
|
||||
class _Agent:
|
||||
def run_conversation(
|
||||
self, prompt, conversation_history=None, stream_callback=None
|
||||
):
|
||||
seen["prompt"] = prompt
|
||||
seen["history"] = conversation_history
|
||||
return {
|
||||
"final_response": "edited reply",
|
||||
"messages": [
|
||||
*(conversation_history or []),
|
||||
{"role": "user", "content": prompt},
|
||||
{"role": "assistant", "content": "edited reply"},
|
||||
],
|
||||
}
|
||||
|
||||
class _ImmediateThread:
|
||||
def __init__(self, target=None, daemon=None):
|
||||
self._target = target
|
||||
|
||||
def start(self):
|
||||
self._target()
|
||||
|
||||
original_history = [
|
||||
{"role": "user", "content": "first"},
|
||||
{"role": "assistant", "content": "first reply"},
|
||||
{"role": "user", "content": "second"},
|
||||
{"role": "assistant", "content": "second reply"},
|
||||
]
|
||||
server._sessions["sid"] = _session(agent=_Agent(), history=original_history)
|
||||
|
||||
class _StubDb:
|
||||
def __init__(self):
|
||||
self.replaced = []
|
||||
|
||||
def replace_messages(self, session_id, messages):
|
||||
self.replaced.append((session_id, list(messages)))
|
||||
|
||||
stub_db = _StubDb()
|
||||
|
||||
try:
|
||||
monkeypatch.setattr(server.threading, "Thread", _ImmediateThread)
|
||||
monkeypatch.setattr(server, "_get_usage", lambda _a: {})
|
||||
monkeypatch.setattr(server, "render_message", lambda _t, _c: "")
|
||||
monkeypatch.setattr(server, "_emit", lambda *a: None)
|
||||
monkeypatch.setattr(server, "_get_db", lambda: stub_db)
|
||||
|
||||
resp = server.handle_request(
|
||||
{
|
||||
"id": "1",
|
||||
"method": "prompt.submit",
|
||||
"params": {
|
||||
"session_id": "sid",
|
||||
"text": "edited second",
|
||||
"truncate_before_user_ordinal": 1,
|
||||
},
|
||||
}
|
||||
)
|
||||
assert resp.get("result"), f"got error: {resp.get('error')}"
|
||||
|
||||
assert seen["prompt"] == "edited second"
|
||||
assert seen["history"] == original_history[:2]
|
||||
assert server._sessions["sid"]["history"] == [
|
||||
*original_history[:2],
|
||||
{"role": "user", "content": "edited second"},
|
||||
{"role": "assistant", "content": "edited reply"},
|
||||
]
|
||||
assert server._sessions["sid"]["history_version"] == 2
|
||||
assert stub_db.replaced == [("session-key", original_history[:2])]
|
||||
finally:
|
||||
server._sessions.pop("sid", None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# session.interrupt must only cancel pending prompts owned by the calling
|
||||
# session — it must not blast-resolve clarify/sudo/secret prompts on
|
||||
|
||||
Reference in New Issue
Block a user