Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui
# Conflicts: # cli.py # hermes_cli/main.py # run_agent.py # tests/hermes_cli/test_cmd_update.py # tools/mcp_tool.py # web/src/lib/gatewayClient.ts
This commit is contained in:
@@ -269,7 +269,7 @@ def _scan_for_plugin_adapter_antipattern(source: str) -> list[str]:
|
||||
and isinstance(func.value.value, ast.Name)
|
||||
and func.value.value.id == "sys"
|
||||
and func.value.attr == "path"
|
||||
and func.attr in ("insert", "append", "extend")
|
||||
and func.attr in {"insert", "append", "extend"}
|
||||
):
|
||||
target_name = f"sys.path.{func.attr}"
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ def _would_warn():
|
||||
"MATRIX_ALLOWED_USERS", "DINGTALK_ALLOWED_USERS", "FEISHU_ALLOWED_USERS", "WECOM_ALLOWED_USERS",
|
||||
"GATEWAY_ALLOWED_USERS")
|
||||
)
|
||||
_allow_all = os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in ("true", "1", "yes") or any(
|
||||
os.getenv(v, "").lower() in ("true", "1", "yes")
|
||||
_allow_all = os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"} or any(
|
||||
os.getenv(v, "").lower() in {"true", "1", "yes"}
|
||||
for v in ("TELEGRAM_ALLOW_ALL_USERS", "DISCORD_ALLOW_ALL_USERS",
|
||||
"WHATSAPP_ALLOW_ALL_USERS", "SLACK_ALLOW_ALL_USERS",
|
||||
"SIGNAL_ALLOW_ALL_USERS", "EMAIL_ALLOW_ALL_USERS",
|
||||
|
||||
@@ -445,7 +445,12 @@ class TestHealthEndpoint:
|
||||
async with TestClient(TestServer(app)) as cli:
|
||||
resp = await cli.get("/health")
|
||||
assert resp.status == 200
|
||||
assert resp.headers.get("Content-Security-Policy") == "default-src 'none'; frame-ancestors 'none'"
|
||||
assert resp.headers.get("Permissions-Policy") == "camera=(), microphone=(), geolocation=()"
|
||||
assert resp.headers.get("Strict-Transport-Security") == "max-age=31536000; includeSubDomains"
|
||||
assert resp.headers.get("X-Content-Type-Options") == "nosniff"
|
||||
assert resp.headers.get("X-Frame-Options") == "DENY"
|
||||
assert resp.headers.get("X-XSS-Protection") == "0"
|
||||
assert resp.headers.get("Referrer-Policy") == "no-referrer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -704,6 +709,37 @@ class TestChatCompletionsEndpoint:
|
||||
assert "[DONE]" in body
|
||||
assert "Hello!" in body
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_string_false_returns_json_completion(self, adapter):
|
||||
"""Quoted false must not route chat completions into SSE mode."""
|
||||
mock_result = {
|
||||
"final_response": "Hello! How can I help you today?",
|
||||
"messages": [],
|
||||
"api_calls": 1,
|
||||
}
|
||||
|
||||
app = _create_app(adapter)
|
||||
async with TestClient(TestServer(app)) as cli:
|
||||
with patch.object(adapter, "_run_agent", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (
|
||||
mock_result,
|
||||
{"input_tokens": 10, "output_tokens": 5, "total_tokens": 15},
|
||||
)
|
||||
resp = await cli.post(
|
||||
"/v1/chat/completions",
|
||||
json={
|
||||
"model": "hermes-agent",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"stream": "false",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status == 200
|
||||
assert "text/event-stream" not in resp.headers.get("Content-Type", "")
|
||||
data = await resp.json()
|
||||
assert data["object"] == "chat.completion"
|
||||
assert data["choices"][0]["message"]["content"] == mock_result["final_response"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_task_done_callback_enqueues_eos_for_chat_completions(self, adapter):
|
||||
"""Regression guard for #24451: completion callback must signal SSE EOS."""
|
||||
@@ -1655,6 +1691,31 @@ class TestResponsesEndpoint:
|
||||
# The response has an ID but it shouldn't be retrievable
|
||||
assert adapter._response_store.get(data["id"]) is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_store_string_false_does_not_store(self, adapter):
|
||||
"""Quoted false must preserve ephemeral store=false semantics."""
|
||||
mock_result = {"final_response": "OK", "messages": [], "api_calls": 1}
|
||||
|
||||
app = _create_app(adapter)
|
||||
async with TestClient(TestServer(app)) as cli:
|
||||
with patch.object(adapter, "_run_agent", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (
|
||||
mock_result,
|
||||
{"input_tokens": 0, "output_tokens": 0, "total_tokens": 0},
|
||||
)
|
||||
resp = await cli.post(
|
||||
"/v1/responses",
|
||||
json={
|
||||
"model": "hermes-agent",
|
||||
"input": "Hello",
|
||||
"store": "false",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
assert adapter._response_store.get(data["id"]) is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_instructions_inherited_from_previous(self, adapter):
|
||||
"""If no instructions provided, carry forward from previous response."""
|
||||
@@ -1749,6 +1810,37 @@ class TestResponsesStreaming:
|
||||
assert "Hello" in body
|
||||
assert " world" in body
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_string_false_returns_json_response(self, adapter):
|
||||
"""Quoted false must not route Responses API requests into SSE mode."""
|
||||
mock_result = {
|
||||
"final_response": "Paris is the capital of France.",
|
||||
"messages": [],
|
||||
"api_calls": 1,
|
||||
}
|
||||
|
||||
app = _create_app(adapter)
|
||||
async with TestClient(TestServer(app)) as cli:
|
||||
with patch.object(adapter, "_run_agent", new_callable=AsyncMock) as mock_run:
|
||||
mock_run.return_value = (
|
||||
mock_result,
|
||||
{"input_tokens": 0, "output_tokens": 0, "total_tokens": 0},
|
||||
)
|
||||
resp = await cli.post(
|
||||
"/v1/responses",
|
||||
json={
|
||||
"model": "hermes-agent",
|
||||
"input": "What is the capital of France?",
|
||||
"stream": "false",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status == 200
|
||||
assert "text/event-stream" not in resp.headers.get("Content-Type", "")
|
||||
data = await resp.json()
|
||||
assert data["object"] == "response"
|
||||
assert data["output"][0]["content"][0]["text"] == mock_result["final_response"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_task_done_callback_enqueues_eos_for_responses(self, adapter):
|
||||
"""Regression guard for #24451 on /v1/responses streaming path."""
|
||||
|
||||
@@ -335,6 +335,28 @@ class TestRunEvents:
|
||||
"approval_not_pending",
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_approval_string_false_does_not_resolve_all(self, adapter):
|
||||
"""Quoted false must not fan out approval resolution across the queue."""
|
||||
app = _create_runs_app(adapter)
|
||||
run_id = "run_bool_parse"
|
||||
adapter._run_statuses[run_id] = {"run_id": run_id, "status": "running"}
|
||||
adapter._run_approval_sessions[run_id] = "session-123"
|
||||
|
||||
async with TestClient(TestServer(app)) as cli:
|
||||
with patch("tools.approval.resolve_gateway_approval", return_value=1) as mock_resolve:
|
||||
approval_resp = await cli.post(
|
||||
f"/v1/runs/{run_id}/approval",
|
||||
json={"choice": "once", "all": "false"},
|
||||
)
|
||||
|
||||
assert approval_resp.status == 200
|
||||
mock_resolve.assert_called_once_with(
|
||||
"session-123",
|
||||
"once",
|
||||
resolve_all=False,
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_events_not_found_returns_404(self, adapter):
|
||||
app = _create_runs_app(adapter)
|
||||
|
||||
@@ -316,6 +316,7 @@ class TestRunBackgroundTask:
|
||||
assert mock_adapter.send.call_args.kwargs["metadata"] == {
|
||||
"thread_id": "20197",
|
||||
"telegram_dm_topic_reply_fallback": True,
|
||||
"direct_messages_topic_id": "20197",
|
||||
"telegram_reply_to_message_id": "463",
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,11 @@ class TestBlueBubblesHelpers:
|
||||
adapter = _make_adapter(monkeypatch)
|
||||
assert adapter.format_message("**Hello** `world`") == "Hello world"
|
||||
|
||||
def test_format_message_preserves_underscores_in_identifiers(self, monkeypatch):
|
||||
adapter = _make_adapter(monkeypatch)
|
||||
text = "Use /api_v2 with FEATURE_FLAG_NAME and config_file.json"
|
||||
assert adapter.format_message(text) == text
|
||||
|
||||
def test_strip_markdown_headers(self, monkeypatch):
|
||||
adapter = _make_adapter(monkeypatch)
|
||||
assert adapter.format_message("## Heading\ntext") == "Heading\ntext"
|
||||
|
||||
@@ -44,7 +44,7 @@ def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None):
|
||||
val = terminal_cfg[cfg_key]
|
||||
# Skip cwd placeholder values — don't overwrite already-resolved
|
||||
# TERMINAL_CWD. Mirrors the fix in gateway/run.py.
|
||||
if cfg_key == "cwd" and str(val) in (".", "auto", "cwd"):
|
||||
if cfg_key == "cwd" and str(val) in {".", "auto", "cwd"}:
|
||||
continue
|
||||
# Expand shell tilde so subprocess.Popen never receives a literal
|
||||
# "~/" which the kernel rejects.
|
||||
@@ -70,7 +70,7 @@ def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None):
|
||||
|
||||
# --- Replicate lines 144-147: MESSAGING_CWD fallback ---
|
||||
configured_cwd = env.get("TERMINAL_CWD", "")
|
||||
if not configured_cwd or configured_cwd in (".", "auto", "cwd"):
|
||||
if not configured_cwd or configured_cwd in {".", "auto", "cwd"}:
|
||||
messaging_cwd = env.get("MESSAGING_CWD") or "/root" # Path.home() for root
|
||||
env["TERMINAL_CWD"] = messaging_cwd
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class TestDiscordSystemMessageFilter(unittest.TestCase):
|
||||
return False
|
||||
|
||||
# System message filter (the fix being tested)
|
||||
if message.type not in (discord.MessageType.default, discord.MessageType.reply):
|
||||
if message.type not in {discord.MessageType.default, discord.MessageType.reply}:
|
||||
return False
|
||||
|
||||
return True # message accepted
|
||||
|
||||
@@ -2740,7 +2740,7 @@ class _FakeAiohttpSession:
|
||||
|
||||
def _install_fake_aiohttp(monkeypatch, session):
|
||||
fake_aiohttp = types.SimpleNamespace(
|
||||
ClientSession=lambda timeout=None: session,
|
||||
ClientSession=lambda timeout=None, **kwargs: session,
|
||||
ClientTimeout=lambda total=None: None,
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "aiohttp", fake_aiohttp)
|
||||
|
||||
@@ -2257,6 +2257,210 @@ class TestMatrixOnRoomMessageFilter:
|
||||
ev = self._mk_event(sender="@alice:example.org", body="hello bot")
|
||||
await self.adapter._on_room_message(ev)
|
||||
self.adapter._handle_text_message.assert_awaited_once()
|
||||
|
||||
|
||||
class TestMatrixClockSkewWarning:
|
||||
"""Clock-skew detector for #12614.
|
||||
|
||||
Reporter's host clock was set ~2 hours ahead of real time. The grace
|
||||
filter `event_ts < startup_ts - 5` then drops every live event because
|
||||
server timestamps look "older than startup". When this happens well
|
||||
after startup (>30s), the adapter logs a one-shot WARNING pointing the
|
||||
user at NTP instead of failing silently.
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
self.adapter = _make_adapter()
|
||||
self.adapter._user_id = "@bot:example.org"
|
||||
self.adapter._handle_text_message = AsyncMock()
|
||||
self.adapter._handle_media_message = AsyncMock()
|
||||
|
||||
@staticmethod
|
||||
def _mk_event(sender, ts_ms, event_id=None):
|
||||
ev = MagicMock()
|
||||
ev.room_id = "!room:example.org"
|
||||
ev.sender = sender
|
||||
ev.event_id = event_id or f"$evt-{sender}-{ts_ms}"
|
||||
ev.timestamp = ts_ms
|
||||
ev.server_timestamp = ts_ms
|
||||
ev.content = {"msgtype": "m.text", "body": "hi"}
|
||||
return ev
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_late_drops_emit_one_shot_clock_skew_warning(self, caplog):
|
||||
import logging
|
||||
import time as _t
|
||||
|
||||
# Simulate the reporter's environment: host clock is ~2 hours ahead
|
||||
# of server time. Startup happened "in the future" relative to the
|
||||
# real-world events we're now receiving.
|
||||
now = _t.time()
|
||||
self.adapter._startup_ts = now - 60 # bot started 60s ago (wall clock)
|
||||
# Server events are dated 2h before startup_ts (skewed clock).
|
||||
skewed_event_ts_ms = int((self.adapter._startup_ts - 7200) * 1000)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="gateway.platforms.matrix"):
|
||||
for i in range(5):
|
||||
ev = self._mk_event(
|
||||
sender=f"@alice{i}:example.org", ts_ms=skewed_event_ts_ms
|
||||
)
|
||||
await self.adapter._on_room_message(ev)
|
||||
|
||||
# Handler should never be invoked — all events failed the grace check.
|
||||
self.adapter._handle_text_message.assert_not_called()
|
||||
# Exactly one WARNING from THIS logger should be emitted. Filter by
|
||||
# logger name so unrelated stdlib/library warnings can't satisfy the
|
||||
# assertion.
|
||||
skew_warnings = [
|
||||
r for r in caplog.records
|
||||
if r.name == "gateway.platforms.matrix"
|
||||
and r.levelname == "WARNING"
|
||||
and "set-ntp" in r.getMessage()
|
||||
]
|
||||
assert len(skew_warnings) == 1, (
|
||||
f"expected exactly 1 clock-skew warning, got {len(skew_warnings)}"
|
||||
)
|
||||
msg = skew_warnings[0].getMessage()
|
||||
assert "7200" in msg, f"skew value missing from message: {msg!r}"
|
||||
# Pin the counter so a regression in the gating logic (e.g. warning
|
||||
# at threshold 1 or 5, or not stopping after warn) is caught.
|
||||
assert self.adapter._late_grace_drops == 3
|
||||
assert self.adapter._clock_skew_warned is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initial_sync_drops_do_not_warn(self, caplog):
|
||||
"""During the first 30s after startup, old events are normal backfill."""
|
||||
import logging
|
||||
import time as _t
|
||||
|
||||
now = _t.time()
|
||||
# Startup was 1s ago — we're still in the initial-sync window.
|
||||
self.adapter._startup_ts = now - 1
|
||||
old_ts_ms = int((self.adapter._startup_ts - 3600) * 1000)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="gateway.platforms.matrix"):
|
||||
for i in range(5):
|
||||
ev = self._mk_event(
|
||||
sender=f"@alice{i}:example.org", ts_ms=old_ts_ms
|
||||
)
|
||||
await self.adapter._on_room_message(ev)
|
||||
|
||||
# Backfill drops are silent — no clock-skew warning fired.
|
||||
assert self.adapter._clock_skew_warned is False
|
||||
skew_warnings = [
|
||||
r for r in caplog.records
|
||||
if r.name == "gateway.platforms.matrix"
|
||||
and "set-ntp" in r.getMessage()
|
||||
]
|
||||
assert skew_warnings == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fewer_than_three_late_drops_do_not_warn(self, caplog):
|
||||
"""A single delayed backfill event after 30s shouldn't trigger NTP advice."""
|
||||
import logging
|
||||
import time as _t
|
||||
|
||||
now = _t.time()
|
||||
self.adapter._startup_ts = now - 120 # extra slack vs the 30s gate
|
||||
old_ts_ms = int((self.adapter._startup_ts - 3600) * 1000)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="gateway.platforms.matrix"):
|
||||
for i in range(2): # only 2 late drops — under the threshold
|
||||
ev = self._mk_event(
|
||||
sender=f"@alice{i}:example.org", ts_ms=old_ts_ms
|
||||
)
|
||||
await self.adapter._on_room_message(ev)
|
||||
|
||||
assert self.adapter._late_grace_drops == 2
|
||||
assert self.adapter._clock_skew_warned is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_varied_backfill_skews_do_not_warn(self, caplog):
|
||||
"""Backfill from a freshly-invited room delivers events of varied age.
|
||||
|
||||
A genuine clock-skew bug produces drops with a *constant* offset
|
||||
(every event is ~X seconds older than wall clock). Joining an old
|
||||
room post-startup delivers events spanning hours-to-days; those
|
||||
skews vary wildly and must NOT trigger the NTP warning.
|
||||
"""
|
||||
import logging
|
||||
import time as _t
|
||||
|
||||
now = _t.time()
|
||||
self.adapter._startup_ts = now - 120
|
||||
# Each event has a different age, ranging from 1h to 30d ago.
|
||||
ages_in_hours = [1, 24, 168, 720, 4] # 1h, 1d, 1w, 30d, 4h
|
||||
with caplog.at_level(logging.WARNING, logger="gateway.platforms.matrix"):
|
||||
for i, hrs in enumerate(ages_in_hours):
|
||||
ts_ms = int((self.adapter._startup_ts - hrs * 3600) * 1000)
|
||||
ev = self._mk_event(
|
||||
sender=f"@alice{i}:example.org", ts_ms=ts_ms
|
||||
)
|
||||
await self.adapter._on_room_message(ev)
|
||||
|
||||
# The varied-skew guard should keep the counter from reaching 3.
|
||||
assert self.adapter._late_grace_drops < 3
|
||||
assert self.adapter._clock_skew_warned is False
|
||||
skew_warnings = [
|
||||
r for r in caplog.records
|
||||
if r.name == "gateway.platforms.matrix"
|
||||
and "set-ntp" in r.getMessage()
|
||||
]
|
||||
assert skew_warnings == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_reset_allows_warning_to_fire_again(self, caplog):
|
||||
"""After the reset block at top of connect() runs, the warning is rearmed.
|
||||
|
||||
Reconnect lifecycle: the user fixes NTP, restarts the bot, and the
|
||||
new connect() call resets _late_grace_drops / _clock_skew_warned at
|
||||
the top. This test exercises the rearm path by:
|
||||
1. Tripping the warning once (state: warned=True).
|
||||
2. Running the same reset block connect() runs.
|
||||
3. Tripping the warning a second time — the second warning should
|
||||
fire because the state was cleared.
|
||||
"""
|
||||
import logging
|
||||
import time as _t
|
||||
|
||||
now = _t.time()
|
||||
self.adapter._startup_ts = now - 60
|
||||
skewed_ms = int((self.adapter._startup_ts - 7200) * 1000)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="gateway.platforms.matrix"):
|
||||
for i in range(3):
|
||||
ev = self._mk_event(
|
||||
sender=f"@alice{i}:example.org", ts_ms=skewed_ms,
|
||||
event_id=f"$first-{i}",
|
||||
)
|
||||
await self.adapter._on_room_message(ev)
|
||||
assert self.adapter._clock_skew_warned is True
|
||||
|
||||
# Mirror the reset block in connect() (matrix.py around line 855).
|
||||
self.adapter._startup_ts = _t.time() - 60
|
||||
self.adapter._late_grace_drops = 0
|
||||
self.adapter._late_grace_skew = 0.0
|
||||
self.adapter._clock_skew_warned = False
|
||||
|
||||
# Same skewed-clock scenario should warn AGAIN after reset.
|
||||
skewed_ms2 = int((self.adapter._startup_ts - 7200) * 1000)
|
||||
for i in range(3):
|
||||
ev = self._mk_event(
|
||||
sender=f"@bob{i}:example.org", ts_ms=skewed_ms2,
|
||||
event_id=f"$second-{i}",
|
||||
)
|
||||
await self.adapter._on_room_message(ev)
|
||||
|
||||
skew_warnings = [
|
||||
r for r in caplog.records
|
||||
if r.name == "gateway.platforms.matrix"
|
||||
and "set-ntp" in r.getMessage()
|
||||
]
|
||||
assert len(skew_warnings) == 2, (
|
||||
f"expected 2 warnings (one per connect cycle), got {len(skew_warnings)}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DM auto-thread
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -76,12 +76,12 @@ def test_checker_returns_true_when_configured(platform, checker, monkeypatch):
|
||||
elif platform == Platform.SMS:
|
||||
monkeypatch.setenv("TWILIO_ACCOUNT_SID", "ACtest")
|
||||
mock_config.extra = {}
|
||||
elif platform in (
|
||||
elif platform in {
|
||||
Platform.API_SERVER,
|
||||
Platform.WEBHOOK,
|
||||
Platform.MSGRAPH_WEBHOOK,
|
||||
Platform.WHATSAPP,
|
||||
):
|
||||
}:
|
||||
mock_config.extra = {}
|
||||
elif platform == Platform.FEISHU:
|
||||
mock_config.extra = {"app_id": "app"}
|
||||
|
||||
@@ -1076,7 +1076,7 @@ class TestBuildApprovalKeyboard:
|
||||
parsed = parse_approval_button_data(btn.action.data)
|
||||
assert parsed is not None
|
||||
assert parsed[0] == session_key
|
||||
assert parsed[1] in ("allow-once", "allow-always", "deny")
|
||||
assert parsed[1] in {"allow-once", "allow-always", "deny"}
|
||||
|
||||
|
||||
class TestBuildUpdatePromptKeyboard:
|
||||
|
||||
@@ -33,7 +33,16 @@ async def test_restart_command_while_busy_requests_drain_without_interrupt(monke
|
||||
|
||||
result = await runner._handle_message(event)
|
||||
|
||||
assert result == t("gateway.draining", count=1)
|
||||
expected = t("gateway.draining", count=1)
|
||||
assert result == expected
|
||||
# Guard against the silent-degradation regression in #22266: if the i18n
|
||||
# catalog cannot be resolved (e.g. xdist workers losing the locales path)
|
||||
# then ``t("gateway.draining", count=1)`` returns the bare key
|
||||
# ``"gateway.draining"`` instead of the formatted English string, and both
|
||||
# sides of the equality above would still match. Assert on the catalog
|
||||
# output explicitly so a broken locale resolution fails loudly here.
|
||||
assert expected != "gateway.draining"
|
||||
assert "Draining" in expected and "1" in expected
|
||||
running_agent.interrupt.assert_not_called()
|
||||
runner.request_restart.assert_called_once_with(detached=True, via_service=False)
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ def _build_agent_history(history: list) -> list:
|
||||
agent_history: list = []
|
||||
for msg in history:
|
||||
role = msg.get("role")
|
||||
if not role or role in ("session_meta", "system"):
|
||||
if not role or role in {"session_meta", "system"}:
|
||||
continue
|
||||
has_tool_calls = "tool_calls" in msg
|
||||
has_tool_call_id = "tool_call_id" in msg
|
||||
|
||||
@@ -108,7 +108,7 @@ async def test_finalize_before_reset(mock_invoke_hook):
|
||||
await runner._handle_reset_command(_make_event("/new"))
|
||||
|
||||
calls = [c for c in mock_invoke_hook.call_args_list
|
||||
if c[0][0] in ("on_session_finalize", "on_session_reset")]
|
||||
if c[0][0] in {"on_session_finalize", "on_session_reset"}]
|
||||
hook_names = [c[0][0] for c in calls]
|
||||
assert hook_names == ["on_session_finalize", "on_session_reset"]
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ fallback_providers:
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
|
||||
|
||||
def fake_resolve_runtime_provider(*, requested=None, explicit_base_url=None, explicit_api_key=None):
|
||||
if requested in (None, "", "openai-codex"):
|
||||
if requested in {None, "", "openai-codex"}:
|
||||
from hermes_cli.auth import AuthError
|
||||
raise AuthError("No Codex credentials stored. Run `hermes auth` to authenticate.")
|
||||
assert requested == "openrouter"
|
||||
|
||||
@@ -763,7 +763,7 @@ def _install_fake_aiohttp(monkeypatch, session):
|
||||
"""Replace ``aiohttp`` in ``sys.modules`` so ``import aiohttp as _aiohttp``
|
||||
inside ``_standalone_send`` picks up our fake."""
|
||||
fake_aiohttp = types.SimpleNamespace(
|
||||
ClientSession=lambda timeout=None: session,
|
||||
ClientSession=lambda timeout=None, **kwargs: session,
|
||||
ClientTimeout=lambda total=None: None,
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "aiohttp", fake_aiohttp)
|
||||
|
||||
@@ -407,6 +407,7 @@ async def test_gateway_runner_busy_ack_replies_to_triggering_message_for_telegra
|
||||
assert adapter.calls[0]["metadata"] == {
|
||||
"thread_id": "20197",
|
||||
"telegram_dm_topic_reply_fallback": True,
|
||||
"direct_messages_topic_id": "20197",
|
||||
"telegram_reply_to_message_id": "463",
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ def _filter_history(history: list) -> list:
|
||||
role = msg.get("role")
|
||||
if not role:
|
||||
continue
|
||||
if role in ("session_meta",):
|
||||
if role in {"session_meta",}:
|
||||
continue
|
||||
if role == "system":
|
||||
continue
|
||||
|
||||
@@ -237,6 +237,8 @@ class TestUpdateCommandGatewayFlag:
|
||||
cmd_string = call_args[-1] if isinstance(call_args, list) else str(call_args)
|
||||
assert "--gateway" in cmd_string
|
||||
assert "PYTHONUNBUFFERED" in cmd_string
|
||||
assert "rc=$?" in cmd_string
|
||||
assert "status=$?" not in cmd_string
|
||||
assert "stream progress" in result
|
||||
|
||||
|
||||
|
||||
@@ -461,6 +461,7 @@ class TestSendVoiceReply:
|
||||
assert call_kwargs["metadata"] == {
|
||||
"thread_id": "20197",
|
||||
"telegram_dm_topic_reply_fallback": True,
|
||||
"direct_messages_topic_id": "20197",
|
||||
"telegram_reply_to_message_id": "462",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user