mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-02 16:57:36 +08:00
Merge upstream/main and address Copilot review feedback
Merge resolved conflicts in web/src/{i18n/{en,zh,types}.ts,lib/api.ts}
by keeping both this branch's `profiles` additions and upstream's new
`models` page additions.
Copilot review feedback:
- Implement POST /api/profiles/{name}/open-terminal endpoint (already
present); align Windows branch to `cmd.exe /c start "" <cmd>` so it
matches the new test and spawns a fresh window instead of /k reusing
the parent console.
- Move backslash escaping out of the macOS AppleScript f-string
expression (Python <3.12 disallows backslashes inside f-string
expression parts).
- Patch `_get_wrapper_dir` via monkeypatch in
test_profiles_create_creates_wrapper_alias_when_safe so the test no
longer writes to the real `~/.local/bin`.
- Extend test_dashboard_browser_safe_imports to scan `.ts` files in
addition to `.tsx`.
- Switch upstream's new ModelsPage.tsx away from the `@nous-research/ui`
root barrel onto per-component subpaths to satisfy the stricter scan.
- Fix NouiTypography `leading-1.4` -> `leading-[1.4]` so Tailwind
actually emits the line-height for the `sm` variant.
- Guard ProfilesPage.openSoulEditor against out-of-order responses by
tracking the latest requested profile via a ref.
- Replace ProfilesPage's hand-rolled setup command with a fetch to
`/api/profiles/{name}/setup-command` so the copied command always
matches what the backend would actually run (handles wrapper-alias
collisions and reserved names correctly).
- Wire SOUL.md textarea label `htmlFor` -> textarea `id` so screen
readers and clicking the label work as expected.
This commit is contained in:
@@ -29,7 +29,7 @@ class TestReloadEnv:
|
||||
"""reload_env() adds vars from .env that are not in os.environ."""
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text("TEST_RELOAD_VAR=hello123\n")
|
||||
with patch("hermes_cli.config.get_env_path", return_value=env_file):
|
||||
with patch.dict(reload_env.__globals__, {"get_env_path": lambda: env_file}):
|
||||
os.environ.pop("TEST_RELOAD_VAR", None)
|
||||
count = reload_env()
|
||||
assert count >= 1
|
||||
@@ -40,7 +40,7 @@ class TestReloadEnv:
|
||||
"""reload_env() updates vars whose value changed on disk."""
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text("TEST_RELOAD_VAR=old_value\n")
|
||||
with patch("hermes_cli.config.get_env_path", return_value=env_file):
|
||||
with patch.dict(reload_env.__globals__, {"get_env_path": lambda: env_file}):
|
||||
os.environ["TEST_RELOAD_VAR"] = "old_value"
|
||||
# Now change the file
|
||||
env_file.write_text("TEST_RELOAD_VAR=new_value\n")
|
||||
@@ -55,7 +55,7 @@ class TestReloadEnv:
|
||||
env_file.write_text("") # empty .env
|
||||
# Pick a known key from OPTIONAL_ENV_VARS
|
||||
known_key = next(iter(OPTIONAL_ENV_VARS.keys()))
|
||||
with patch("hermes_cli.config.get_env_path", return_value=env_file):
|
||||
with patch.dict(reload_env.__globals__, {"get_env_path": lambda: env_file}):
|
||||
os.environ[known_key] = "stale_value"
|
||||
count = reload_env()
|
||||
assert known_key not in os.environ
|
||||
@@ -65,7 +65,7 @@ class TestReloadEnv:
|
||||
"""reload_env() preserves non-Hermes env vars even when absent from .env."""
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text("")
|
||||
with patch("hermes_cli.config.get_env_path", return_value=env_file):
|
||||
with patch.dict(reload_env.__globals__, {"get_env_path": lambda: env_file}):
|
||||
os.environ["MY_CUSTOM_UNRELATED_VAR"] = "keep_me"
|
||||
reload_env()
|
||||
assert os.environ.get("MY_CUSTOM_UNRELATED_VAR") == "keep_me"
|
||||
@@ -371,6 +371,12 @@ class TestBuildSchemaFromConfig:
|
||||
assert entry["type"] == "select"
|
||||
assert "options" in entry
|
||||
assert "local" in entry["options"]
|
||||
assert "vercel_sandbox" in entry["options"]
|
||||
runtime_entry = CONFIG_SCHEMA["terminal.vercel_runtime"]
|
||||
assert runtime_entry["type"] == "select"
|
||||
assert "node24" in runtime_entry["options"]
|
||||
assert "python3.13" in runtime_entry["options"]
|
||||
assert len(runtime_entry["options"]) >= 3
|
||||
|
||||
def test_empty_prefix_produces_correct_keys(self):
|
||||
from hermes_cli.web_server import _build_schema_from_config
|
||||
@@ -671,8 +677,12 @@ class TestNewEndpoints:
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["command"] == "hermes setup"
|
||||
|
||||
def test_profiles_create_creates_wrapper_alias_when_safe(self):
|
||||
from pathlib import Path
|
||||
def test_profiles_create_creates_wrapper_alias_when_safe(self, monkeypatch, tmp_path):
|
||||
import hermes_cli.profiles as profiles_mod
|
||||
|
||||
wrapper_dir = tmp_path / "bin"
|
||||
wrapper_dir.mkdir()
|
||||
monkeypatch.setattr(profiles_mod, "_get_wrapper_dir", lambda: wrapper_dir)
|
||||
|
||||
resp = self.client.post(
|
||||
"/api/profiles",
|
||||
@@ -680,7 +690,7 @@ class TestNewEndpoints:
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
wrapper_path = Path.home() / ".local" / "bin" / "writer"
|
||||
wrapper_path = wrapper_dir / "writer"
|
||||
assert wrapper_path.exists()
|
||||
assert wrapper_path.read_text() == '#!/bin/sh\nexec hermes -p writer "$@"\n'
|
||||
|
||||
@@ -2057,14 +2067,24 @@ class TestPtyWebSocket:
|
||||
assert b"round-trip-payload" in buf
|
||||
|
||||
def test_resize_escape_is_forwarded(self, monkeypatch):
|
||||
# Resize escape gets intercepted and applied via TIOCSWINSZ,
|
||||
# then ``tput cols/lines`` reports the new dimensions back.
|
||||
# Resize escape gets intercepted and applied via TIOCSWINSZ, then the
|
||||
# child reads the TTY ioctl directly. Avoid tput because CI may not set
|
||||
# TERM for non-interactive shells.
|
||||
import sys
|
||||
|
||||
winsize_script = (
|
||||
"import fcntl, struct, termios, time; "
|
||||
"time.sleep(0.15); "
|
||||
"rows, cols, *_ = struct.unpack('HHHH', "
|
||||
"fcntl.ioctl(0, termios.TIOCGWINSZ, b'\\0' * 8)); "
|
||||
"print(cols); print(rows)"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
self.ws_module,
|
||||
"_resolve_chat_argv",
|
||||
# sleep gives the test time to push the resize before tput runs
|
||||
# sleep gives the test time to push the resize before the child reads the ioctl.
|
||||
lambda resume=None, sidecar_url=None: (
|
||||
["/bin/sh", "-c", "sleep 0.15; tput cols; tput lines"],
|
||||
[sys.executable, "-c", winsize_script],
|
||||
None,
|
||||
None,
|
||||
),
|
||||
@@ -2153,13 +2173,30 @@ class TestPtyWebSocket:
|
||||
def test_pub_broadcasts_to_events_subscribers(self, monkeypatch):
|
||||
"""Frame written to /api/pub is rebroadcast verbatim to every
|
||||
/api/events subscriber on the same channel."""
|
||||
import time
|
||||
from urllib.parse import urlencode
|
||||
from hermes_cli import web_server as ws_mod
|
||||
|
||||
qs = urlencode({"token": self.token, "channel": "broadcast-test"})
|
||||
pub_path = f"/api/pub?{qs}"
|
||||
sub_path = f"/api/events?{qs}"
|
||||
|
||||
with self.client.websocket_connect(sub_path) as sub:
|
||||
# Wait for the subscriber to be registered on the server side.
|
||||
# websocket_connect returns when ws.accept() completes, but the
|
||||
# server adds us to ``_event_channels`` in a follow-up await,
|
||||
# so a publish immediately after connect can race ahead of the
|
||||
# subscriber registration and the message is dropped.
|
||||
deadline = time.monotonic() + 5.0
|
||||
while time.monotonic() < deadline:
|
||||
if ws_mod._event_channels.get("broadcast-test"):
|
||||
break
|
||||
time.sleep(0.01)
|
||||
else:
|
||||
raise AssertionError(
|
||||
"subscriber did not register on channel within 5s"
|
||||
)
|
||||
|
||||
with self.client.websocket_connect(pub_path) as pub:
|
||||
pub.send_text('{"type":"tool.start","payload":{"tool_id":"t1"}}')
|
||||
received = sub.receive_text()
|
||||
|
||||
Reference in New Issue
Block a user