mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(tests): fix 78 CI test failures and remove dead test (#9036)
Production fixes: - voice_mode.py: add is_recording property to AudioRecorder (parity with TermuxAudioRecorder) - cronjob_tools.py: add sms example to deliver description Test fixes: - test_real_interrupt_subagent: add missing _execution_thread_id (fixes 19 cascading failures from leaked _build_system_prompt patch) - test_anthropic_error_handling: add _FakeMessages, override _interruptible_streaming_api_call (6 fixes) - test_ctx_halving_fix: add missing request_overrides attribute (4 fixes) - test_context_token_tracking: set _disable_streaming=True for non-streaming test path (4 fixes) - test_dict_tool_call_args: set _disable_streaming=True (1 fix) - test_provider_parity: add model='gpt-4o' for AIGateway tests to meet 64K minimum context (4 fixes) - test_session_race_guard: add user_id to SessionSource (5 fixes) - test_restart_drain/helpers: add user_id to SessionSource (2 fixes) - test_telegram_photo_interrupts: add user_id to SessionSource - test_interrupt: target thread_id for per-thread interrupt system (2 fixes) - test_zombie_process_cleanup: rewrite with object.__new__ for refactored GatewayRunner.stop() (1 fix) - test_browser_camofox_state: update config version 15->17 (1 fix) - test_trajectory_compressor_async: widen lookback window 10->20 for line-shifted AsyncOpenAI (1 fix) - test_voice_mode: fixed by production is_recording addition (5 fixes) - test_voice_cli_integration: add _attached_images to CLI stub (2 fixes) - test_hermes_logging: explicit propagation/level reset for cross-test pollution defense (1 fix) - test_run_agent: add base_url for OpenRouter detection tests (2 fixes) Deleted: - test_inline_think_blocks_reasoning_only_accepted: tested unimplemented inline <think> handling
This commit is contained in:
parent
b909a9efef
commit
0dd26c9495
18 changed files with 92 additions and 50 deletions
|
|
@ -35,6 +35,7 @@ def make_restart_source(chat_id: str = "123456", chat_type: str = "dm") -> Sessi
|
||||||
platform=Platform.TELEGRAM,
|
platform=Platform.TELEGRAM,
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
chat_type=chat_type,
|
chat_type=chat_type,
|
||||||
|
user_id="u1",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,8 @@ def _make_runner():
|
||||||
|
|
||||||
def _make_event(text="hello", chat_id="12345"):
|
def _make_event(text="hello", chat_id="12345"):
|
||||||
source = SessionSource(
|
source = SessionSource(
|
||||||
platform=Platform.TELEGRAM, chat_id=chat_id, chat_type="dm"
|
platform=Platform.TELEGRAM, chat_id=chat_id, chat_type="dm",
|
||||||
|
user_id="u1",
|
||||||
)
|
)
|
||||||
return MessageEvent(text=text, message_type=MessageType.TEXT, source=source)
|
return MessageEvent(text=text, message_type=MessageType.TEXT, source=source)
|
||||||
|
|
||||||
|
|
@ -192,7 +193,8 @@ async def test_command_messages_do_not_leave_sentinel():
|
||||||
_handle_message. They must NOT leave a sentinel behind."""
|
_handle_message. They must NOT leave a sentinel behind."""
|
||||||
runner = _make_runner()
|
runner = _make_runner()
|
||||||
source = SessionSource(
|
source = SessionSource(
|
||||||
platform=Platform.TELEGRAM, chat_id="12345", chat_type="dm"
|
platform=Platform.TELEGRAM, chat_id="12345", chat_type="dm",
|
||||||
|
user_id="u1",
|
||||||
)
|
)
|
||||||
event = MessageEvent(
|
event = MessageEvent(
|
||||||
text="/help", message_type=MessageType.TEXT, source=source
|
text="/help", message_type=MessageType.TEXT, source=source
|
||||||
|
|
@ -268,7 +270,7 @@ async def test_stop_hard_kills_running_agent():
|
||||||
forever — showing 'writing...' but never producing output."""
|
forever — showing 'writing...' but never producing output."""
|
||||||
runner = _make_runner()
|
runner = _make_runner()
|
||||||
session_key = build_session_key(
|
session_key = build_session_key(
|
||||||
SessionSource(platform=Platform.TELEGRAM, chat_id="12345", chat_type="dm")
|
SessionSource(platform=Platform.TELEGRAM, chat_id="12345", chat_type="dm", user_id="u1")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Simulate a running (possibly hung) agent
|
# Simulate a running (possibly hung) agent
|
||||||
|
|
@ -301,7 +303,7 @@ async def test_stop_clears_pending_messages():
|
||||||
queued during the run must be discarded."""
|
queued during the run must be discarded."""
|
||||||
runner = _make_runner()
|
runner = _make_runner()
|
||||||
session_key = build_session_key(
|
session_key = build_session_key(
|
||||||
SessionSource(platform=Platform.TELEGRAM, chat_id="12345", chat_type="dm")
|
SessionSource(platform=Platform.TELEGRAM, chat_id="12345", chat_type="dm", user_id="u1")
|
||||||
)
|
)
|
||||||
|
|
||||||
fake_agent = MagicMock()
|
fake_agent = MagicMock()
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ def _make_runner():
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_handle_message_does_not_priority_interrupt_photo_followup():
|
async def test_handle_message_does_not_priority_interrupt_photo_followup():
|
||||||
runner = _make_runner()
|
runner = _make_runner()
|
||||||
source = SessionSource(platform=Platform.TELEGRAM, chat_id="12345", chat_type="dm")
|
source = SessionSource(platform=Platform.TELEGRAM, chat_id="12345", chat_type="dm", user_id="u1")
|
||||||
session_key = build_session_key(source)
|
session_key = build_session_key(source)
|
||||||
running_agent = MagicMock()
|
running_agent = MagicMock()
|
||||||
runner._running_agents[session_key] = running_agent
|
runner._running_agents[session_key] = running_agent
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,19 @@ class _PromptTooLongError(Exception):
|
||||||
self.status_code = 400
|
self.status_code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeMessages:
|
||||||
|
"""Stub for client.messages.create() / client.messages.stream()."""
|
||||||
|
def create(self, **kwargs):
|
||||||
|
raise NotImplementedError("_FakeAnthropicClient.messages.create should not be called directly in tests")
|
||||||
|
|
||||||
|
def stream(self, **kwargs):
|
||||||
|
raise NotImplementedError("_FakeAnthropicClient.messages.stream should not be called directly in tests")
|
||||||
|
|
||||||
|
|
||||||
class _FakeAnthropicClient:
|
class _FakeAnthropicClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.messages = _FakeMessages()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -131,13 +143,14 @@ def _make_agent_cls(error_cls, recover_after=None):
|
||||||
def run_conversation(self, user_message, conversation_history=None, task_id=None):
|
def run_conversation(self, user_message, conversation_history=None, task_id=None):
|
||||||
calls = {"n": 0}
|
calls = {"n": 0}
|
||||||
|
|
||||||
def _fake_api_call(api_kwargs):
|
def _fake_api_call(api_kwargs, **kw):
|
||||||
calls["n"] += 1
|
calls["n"] += 1
|
||||||
if recover_after is not None and calls["n"] > recover_after:
|
if recover_after is not None and calls["n"] > recover_after:
|
||||||
return _anthropic_response("Recovered")
|
return _anthropic_response("Recovered")
|
||||||
raise error_cls()
|
raise error_cls()
|
||||||
|
|
||||||
self._interruptible_api_call = _fake_api_call
|
self._interruptible_api_call = _fake_api_call
|
||||||
|
self._interruptible_streaming_api_call = _fake_api_call
|
||||||
return super().run_conversation(
|
return super().run_conversation(
|
||||||
user_message, conversation_history=conversation_history, task_id=task_id
|
user_message, conversation_history=conversation_history, task_id=task_id
|
||||||
)
|
)
|
||||||
|
|
@ -352,10 +365,11 @@ def test_401_refresh_fails_is_non_retryable(monkeypatch):
|
||||||
return False # Simulate failed credential refresh
|
return False # Simulate failed credential refresh
|
||||||
|
|
||||||
def run_conversation(self, user_message, conversation_history=None, task_id=None):
|
def run_conversation(self, user_message, conversation_history=None, task_id=None):
|
||||||
def _fake_api_call(api_kwargs):
|
def _fake_api_call(api_kwargs, **kw):
|
||||||
raise _UnauthorizedError()
|
raise _UnauthorizedError()
|
||||||
|
|
||||||
self._interruptible_api_call = _fake_api_call
|
self._interruptible_api_call = _fake_api_call
|
||||||
|
self._interruptible_streaming_api_call = _fake_api_call
|
||||||
return super().run_conversation(
|
return super().run_conversation(
|
||||||
user_message, conversation_history=conversation_history, task_id=task_id
|
user_message, conversation_history=conversation_history, task_id=task_id
|
||||||
)
|
)
|
||||||
|
|
@ -436,13 +450,14 @@ def test_prompt_too_long_triggers_compression(monkeypatch):
|
||||||
def run_conversation(self, user_message, conversation_history=None, task_id=None):
|
def run_conversation(self, user_message, conversation_history=None, task_id=None):
|
||||||
calls = {"n": 0}
|
calls = {"n": 0}
|
||||||
|
|
||||||
def _fake_api_call(api_kwargs):
|
def _fake_api_call(api_kwargs, **kw):
|
||||||
calls["n"] += 1
|
calls["n"] += 1
|
||||||
if calls["n"] == 1:
|
if calls["n"] == 1:
|
||||||
raise _PromptTooLongError()
|
raise _PromptTooLongError()
|
||||||
return _anthropic_response("Compressed and recovered")
|
return _anthropic_response("Compressed and recovered")
|
||||||
|
|
||||||
self._interruptible_api_call = _fake_api_call
|
self._interruptible_api_call = _fake_api_call
|
||||||
|
self._interruptible_streaming_api_call = _fake_api_call
|
||||||
return super().run_conversation(
|
return super().run_conversation(
|
||||||
user_message, conversation_history=conversation_history, task_id=task_id
|
user_message, conversation_history=conversation_history, task_id=task_id
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ def _make_agent(monkeypatch, api_mode, provider, response_fn):
|
||||||
|
|
||||||
def run_conversation(self, msg, conversation_history=None, task_id=None):
|
def run_conversation(self, msg, conversation_history=None, task_id=None):
|
||||||
self._interruptible_api_call = lambda kw: response_fn()
|
self._interruptible_api_call = lambda kw: response_fn()
|
||||||
|
self._disable_streaming = True
|
||||||
return super().run_conversation(msg, conversation_history=conversation_history, task_id=task_id)
|
return super().run_conversation(msg, conversation_history=conversation_history, task_id=task_id)
|
||||||
|
|
||||||
return _A(model="test-model", api_key="test-key", provider=provider, api_mode=api_mode)
|
return _A(model="test-model", api_key="test-key", provider=provider, api_mode=api_mode)
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ def test_tool_call_validation_accepts_dict_arguments(monkeypatch):
|
||||||
quiet_mode=True,
|
quiet_mode=True,
|
||||||
skip_memory=True,
|
skip_memory=True,
|
||||||
)
|
)
|
||||||
|
agent._disable_streaming = True
|
||||||
|
|
||||||
result = agent.run_conversation("read the file")
|
result = agent.run_conversation("read the file")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,11 @@ class _FakeOpenAI:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _make_agent(monkeypatch, provider, api_mode="chat_completions", base_url="https://openrouter.ai/api/v1"):
|
def _make_agent(monkeypatch, provider, api_mode="chat_completions", base_url="https://openrouter.ai/api/v1", model=None):
|
||||||
monkeypatch.setattr("run_agent.get_tool_definitions", lambda **kw: _tool_defs("web_search", "terminal"))
|
monkeypatch.setattr("run_agent.get_tool_definitions", lambda **kw: _tool_defs("web_search", "terminal"))
|
||||||
monkeypatch.setattr("run_agent.check_toolset_requirements", lambda: {})
|
monkeypatch.setattr("run_agent.check_toolset_requirements", lambda: {})
|
||||||
monkeypatch.setattr("run_agent.OpenAI", _FakeOpenAI)
|
monkeypatch.setattr("run_agent.OpenAI", _FakeOpenAI)
|
||||||
return AIAgent(
|
kwargs = dict(
|
||||||
api_key="test-key",
|
api_key="test-key",
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
|
|
@ -58,6 +58,9 @@ def _make_agent(monkeypatch, provider, api_mode="chat_completions", base_url="ht
|
||||||
skip_context_files=True,
|
skip_context_files=True,
|
||||||
skip_memory=True,
|
skip_memory=True,
|
||||||
)
|
)
|
||||||
|
if model:
|
||||||
|
kwargs["model"] = model
|
||||||
|
return AIAgent(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
# ── _build_api_kwargs tests ─────────────────────────────────────────────────
|
# ── _build_api_kwargs tests ─────────────────────────────────────────────────
|
||||||
|
|
@ -247,7 +250,7 @@ class TestBuildApiKwargsChatCompletionsServiceTier:
|
||||||
|
|
||||||
class TestBuildApiKwargsAIGateway:
|
class TestBuildApiKwargsAIGateway:
|
||||||
def test_uses_chat_completions_format(self, monkeypatch):
|
def test_uses_chat_completions_format(self, monkeypatch):
|
||||||
agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1")
|
agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1", model="gpt-4o")
|
||||||
messages = [{"role": "user", "content": "hi"}]
|
messages = [{"role": "user", "content": "hi"}]
|
||||||
kwargs = agent._build_api_kwargs(messages)
|
kwargs = agent._build_api_kwargs(messages)
|
||||||
assert "messages" in kwargs
|
assert "messages" in kwargs
|
||||||
|
|
@ -255,7 +258,7 @@ class TestBuildApiKwargsAIGateway:
|
||||||
assert kwargs["messages"][-1]["content"] == "hi"
|
assert kwargs["messages"][-1]["content"] == "hi"
|
||||||
|
|
||||||
def test_no_responses_api_fields(self, monkeypatch):
|
def test_no_responses_api_fields(self, monkeypatch):
|
||||||
agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1")
|
agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1", model="gpt-4o")
|
||||||
messages = [{"role": "user", "content": "hi"}]
|
messages = [{"role": "user", "content": "hi"}]
|
||||||
kwargs = agent._build_api_kwargs(messages)
|
kwargs = agent._build_api_kwargs(messages)
|
||||||
assert "input" not in kwargs
|
assert "input" not in kwargs
|
||||||
|
|
@ -263,7 +266,7 @@ class TestBuildApiKwargsAIGateway:
|
||||||
assert "store" not in kwargs
|
assert "store" not in kwargs
|
||||||
|
|
||||||
def test_includes_reasoning_in_extra_body(self, monkeypatch):
|
def test_includes_reasoning_in_extra_body(self, monkeypatch):
|
||||||
agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1")
|
agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1", model="gpt-4o")
|
||||||
messages = [{"role": "user", "content": "hi"}]
|
messages = [{"role": "user", "content": "hi"}]
|
||||||
kwargs = agent._build_api_kwargs(messages)
|
kwargs = agent._build_api_kwargs(messages)
|
||||||
extra = kwargs.get("extra_body", {})
|
extra = kwargs.get("extra_body", {})
|
||||||
|
|
@ -271,7 +274,7 @@ class TestBuildApiKwargsAIGateway:
|
||||||
assert extra["reasoning"]["enabled"] is True
|
assert extra["reasoning"]["enabled"] is True
|
||||||
|
|
||||||
def test_includes_tools(self, monkeypatch):
|
def test_includes_tools(self, monkeypatch):
|
||||||
agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1")
|
agent = _make_agent(monkeypatch, "ai-gateway", base_url="https://ai-gateway.vercel.sh/v1", model="gpt-4o")
|
||||||
messages = [{"role": "user", "content": "hi"}]
|
messages = [{"role": "user", "content": "hi"}]
|
||||||
kwargs = agent._build_api_kwargs(messages)
|
kwargs = agent._build_api_kwargs(messages)
|
||||||
assert "tools" in kwargs
|
assert "tools" in kwargs
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,8 @@ class TestRealSubagentInterrupt(unittest.TestCase):
|
||||||
parent._delegate_spinner = None
|
parent._delegate_spinner = None
|
||||||
parent.tool_progress_callback = None
|
parent.tool_progress_callback = None
|
||||||
parent.iteration_budget = IterationBudget(max_total=100)
|
parent.iteration_budget = IterationBudget(max_total=100)
|
||||||
parent._client_kwargs = {"api_key": "test", "base_url": "http://localhost:1"}
|
parent._client_kwargs = {"api_key": "***", "base_url": "http://localhost:1"}
|
||||||
|
parent._execution_thread_id = None
|
||||||
|
|
||||||
from tools.delegate_tool import _run_single_child
|
from tools.delegate_tool import _run_single_child
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -880,6 +880,7 @@ class TestBuildApiKwargs:
|
||||||
assert kwargs["extra_body"]["reasoning"] == {"enabled": False}
|
assert kwargs["extra_body"]["reasoning"] == {"enabled": False}
|
||||||
|
|
||||||
def test_reasoning_not_sent_for_unsupported_openrouter_model(self, agent):
|
def test_reasoning_not_sent_for_unsupported_openrouter_model(self, agent):
|
||||||
|
agent.base_url = "https://openrouter.ai/api/v1"
|
||||||
agent.model = "minimax/minimax-m2.5"
|
agent.model = "minimax/minimax-m2.5"
|
||||||
messages = [{"role": "user", "content": "hi"}]
|
messages = [{"role": "user", "content": "hi"}]
|
||||||
kwargs = agent._build_api_kwargs(messages)
|
kwargs = agent._build_api_kwargs(messages)
|
||||||
|
|
@ -1575,6 +1576,7 @@ class TestHandleMaxIterations:
|
||||||
assert "API down" in result
|
assert "API down" in result
|
||||||
|
|
||||||
def test_summary_skips_reasoning_for_unsupported_openrouter_model(self, agent):
|
def test_summary_skips_reasoning_for_unsupported_openrouter_model(self, agent):
|
||||||
|
agent.base_url = "https://openrouter.ai/api/v1"
|
||||||
agent.model = "minimax/minimax-m2.5"
|
agent.model = "minimax/minimax-m2.5"
|
||||||
resp = _mock_response(content="Summary")
|
resp = _mock_response(content="Summary")
|
||||||
agent.client.chat.completions.create.return_value = resp
|
agent.client.chat.completions.create.return_value = resp
|
||||||
|
|
@ -1705,27 +1707,6 @@ class TestRunConversation:
|
||||||
assert result["completed"] is True
|
assert result["completed"] is True
|
||||||
assert result["api_calls"] == 2
|
assert result["api_calls"] == 2
|
||||||
|
|
||||||
def test_inline_think_blocks_reasoning_only_accepted(self, agent):
|
|
||||||
"""Inline <think> reasoning-only responses accepted with (empty) content, no retries."""
|
|
||||||
self._setup_agent(agent)
|
|
||||||
empty_resp = _mock_response(
|
|
||||||
content="<think>internal reasoning</think>",
|
|
||||||
finish_reason="stop",
|
|
||||||
)
|
|
||||||
agent.client.chat.completions.create.side_effect = [empty_resp]
|
|
||||||
with (
|
|
||||||
patch.object(agent, "_persist_session"),
|
|
||||||
patch.object(agent, "_save_trajectory"),
|
|
||||||
patch.object(agent, "_cleanup_task_resources"),
|
|
||||||
):
|
|
||||||
result = agent.run_conversation("answer me")
|
|
||||||
assert result["completed"] is True
|
|
||||||
assert result["final_response"] == "(empty)"
|
|
||||||
assert result["api_calls"] == 1 # no retries
|
|
||||||
# Reasoning should be preserved in the assistant message
|
|
||||||
assistant_msgs = [m for m in result["messages"] if m.get("role") == "assistant"]
|
|
||||||
assert any(m.get("reasoning") for m in assistant_msgs)
|
|
||||||
|
|
||||||
def test_reasoning_only_local_resumed_no_compression_triggered(self, agent):
|
def test_reasoning_only_local_resumed_no_compression_triggered(self, agent):
|
||||||
"""Reasoning-only responses no longer trigger compression — prefill then accepted."""
|
"""Reasoning-only responses no longer trigger compression — prefill then accepted."""
|
||||||
self._setup_agent(agent)
|
self._setup_agent(agent)
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ class TestEphemeralMaxOutputTokens:
|
||||||
return_value=[{"role": "user", "content": "hi"}]
|
return_value=[{"role": "user", "content": "hi"}]
|
||||||
)
|
)
|
||||||
agent._anthropic_preserve_dots = MagicMock(return_value=False)
|
agent._anthropic_preserve_dots = MagicMock(return_value=False)
|
||||||
|
agent.request_overrides = {}
|
||||||
return agent
|
return agent
|
||||||
|
|
||||||
def test_ephemeral_override_is_used_on_first_call(self):
|
def test_ephemeral_override_is_used_on_first_call(self):
|
||||||
|
|
@ -253,6 +254,7 @@ class TestContextNotHalvedOnOutputCapError:
|
||||||
)
|
)
|
||||||
agent._anthropic_preserve_dots = MagicMock(return_value=False)
|
agent._anthropic_preserve_dots = MagicMock(return_value=False)
|
||||||
agent._vprint = MagicMock()
|
agent._vprint = MagicMock()
|
||||||
|
agent.request_overrides = {}
|
||||||
return agent
|
return agent
|
||||||
|
|
||||||
def test_output_cap_error_sets_ephemeral_not_context_length(self):
|
def test_output_cap_error_sets_ephemeral_not_context_length(self):
|
||||||
|
|
|
||||||
|
|
@ -298,8 +298,17 @@ class TestGatewayMode:
|
||||||
"""agent.log (catch-all) still receives gateway AND tool records."""
|
"""agent.log (catch-all) still receives gateway AND tool records."""
|
||||||
hermes_logging.setup_logging(hermes_home=hermes_home, mode="gateway")
|
hermes_logging.setup_logging(hermes_home=hermes_home, mode="gateway")
|
||||||
|
|
||||||
logging.getLogger("gateway.run").info("gateway msg")
|
gw_logger = logging.getLogger("gateway.run")
|
||||||
logging.getLogger("tools.file_tools").info("file msg")
|
file_logger = logging.getLogger("tools.file_tools")
|
||||||
|
# Ensure propagation and levels are clean (cross-test pollution defense)
|
||||||
|
gw_logger.propagate = True
|
||||||
|
file_logger.propagate = True
|
||||||
|
logging.getLogger("tools").propagate = True
|
||||||
|
file_logger.setLevel(logging.NOTSET)
|
||||||
|
logging.getLogger("tools").setLevel(logging.NOTSET)
|
||||||
|
|
||||||
|
gw_logger.info("gateway msg")
|
||||||
|
file_logger.info("file msg")
|
||||||
|
|
||||||
for h in logging.getLogger().handlers:
|
for h in logging.getLogger().handlers:
|
||||||
h.flush()
|
h.flush()
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ class TestSourceLineVerification:
|
||||||
if "self.async_client = AsyncOpenAI(" in line and "_get_async_client" not in lines[max(0,i-3):i+1]:
|
if "self.async_client = AsyncOpenAI(" in line and "_get_async_client" not in lines[max(0,i-3):i+1]:
|
||||||
# Allow it inside _get_async_client method
|
# Allow it inside _get_async_client method
|
||||||
# Check if we're inside _get_async_client by looking at context
|
# Check if we're inside _get_async_client by looking at context
|
||||||
context = "\n".join(lines[max(0,i-10):i+1])
|
context = "\n".join(lines[max(0,i-20):i+1])
|
||||||
if "_get_async_client" not in context:
|
if "_get_async_client" not in context:
|
||||||
pytest.fail(
|
pytest.fail(
|
||||||
f"Line {i}: AsyncOpenAI created eagerly outside _get_async_client()"
|
f"Line {i}: AsyncOpenAI created eagerly outside _get_async_client()"
|
||||||
|
|
|
||||||
|
|
@ -64,4 +64,4 @@ class TestCamofoxConfigDefaults:
|
||||||
|
|
||||||
# The current schema version is tracked globally; unrelated default
|
# The current schema version is tracked globally; unrelated default
|
||||||
# options may bump it after browser defaults are added.
|
# options may bump it after browser defaults are added.
|
||||||
assert DEFAULT_CONFIG["_config_version"] == 15
|
assert DEFAULT_CONFIG["_config_version"] == 17
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class TestInterruptModule:
|
||||||
assert not is_interrupted()
|
assert not is_interrupted()
|
||||||
|
|
||||||
def test_thread_safety(self):
|
def test_thread_safety(self):
|
||||||
"""Set from one thread, check from another."""
|
"""Set from one thread targeting another thread's ident."""
|
||||||
from tools.interrupt import set_interrupt, is_interrupted
|
from tools.interrupt import set_interrupt, is_interrupted
|
||||||
set_interrupt(False)
|
set_interrupt(False)
|
||||||
|
|
||||||
|
|
@ -45,11 +45,12 @@ class TestInterruptModule:
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
assert not seen["value"]
|
assert not seen["value"]
|
||||||
|
|
||||||
set_interrupt(True)
|
# Target the checker thread's ident so it sees the interrupt
|
||||||
|
set_interrupt(True, thread_id=t.ident)
|
||||||
t.join(timeout=1)
|
t.join(timeout=1)
|
||||||
assert seen["value"]
|
assert seen["value"]
|
||||||
|
|
||||||
set_interrupt(False)
|
set_interrupt(False, thread_id=t.ident)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -189,10 +190,10 @@ class TestSIGKILLEscalation:
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
set_interrupt(True)
|
set_interrupt(True, thread_id=t.ident)
|
||||||
|
|
||||||
t.join(timeout=5)
|
t.join(timeout=5)
|
||||||
set_interrupt(False)
|
set_interrupt(False, thread_id=t.ident)
|
||||||
|
|
||||||
assert result_holder["value"] is not None
|
assert result_holder["value"] is not None
|
||||||
assert result_holder["value"]["returncode"] == 130
|
assert result_holder["value"]["returncode"] == 130
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ def _make_voice_cli(**overrides):
|
||||||
cli._voice_tts_done.set()
|
cli._voice_tts_done.set()
|
||||||
cli._pending_input = queue.Queue()
|
cli._pending_input = queue.Queue()
|
||||||
cli._app = None
|
cli._app = None
|
||||||
|
cli._attached_images = []
|
||||||
cli.console = SimpleNamespace(width=80)
|
cli.console = SimpleNamespace(width=80)
|
||||||
for k, v in overrides.items():
|
for k, v in overrides.items():
|
||||||
setattr(cli, k, v)
|
setattr(cli, k, v)
|
||||||
|
|
|
||||||
|
|
@ -190,17 +190,38 @@ class TestGatewayCleanupWiring:
|
||||||
def test_gateway_stop_calls_close(self):
|
def test_gateway_stop_calls_close(self):
|
||||||
"""gateway stop() should call close() on all running agents."""
|
"""gateway stop() should call close() on all running agents."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import MagicMock, patch
|
import threading
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
runner = MagicMock()
|
from gateway.run import GatewayRunner
|
||||||
|
|
||||||
|
runner = object.__new__(GatewayRunner)
|
||||||
runner._running = True
|
runner._running = True
|
||||||
runner._running_agents = {}
|
runner._running_agents = {}
|
||||||
|
runner._running_agents_ts = {}
|
||||||
runner.adapters = {}
|
runner.adapters = {}
|
||||||
runner._background_tasks = set()
|
runner._background_tasks = set()
|
||||||
runner._pending_messages = {}
|
runner._pending_messages = {}
|
||||||
runner._pending_approvals = {}
|
runner._pending_approvals = {}
|
||||||
|
runner._pending_model_notes = {}
|
||||||
runner._shutdown_event = asyncio.Event()
|
runner._shutdown_event = asyncio.Event()
|
||||||
runner._exit_reason = None
|
runner._exit_reason = None
|
||||||
|
runner._exit_code = None
|
||||||
|
runner._stop_task = None
|
||||||
|
runner._draining = False
|
||||||
|
runner._restart_requested = False
|
||||||
|
runner._restart_task_started = False
|
||||||
|
runner._restart_detached = False
|
||||||
|
runner._restart_via_service = False
|
||||||
|
runner._restart_drain_timeout = 5.0
|
||||||
|
runner._voice_mode = {}
|
||||||
|
runner._session_model_overrides = {}
|
||||||
|
runner._update_prompt_pending = {}
|
||||||
|
runner._busy_input_mode = "interrupt"
|
||||||
|
runner._agent_cache = {}
|
||||||
|
runner._agent_cache_lock = threading.Lock()
|
||||||
|
runner._shutdown_all_gateway_honcho = lambda: None
|
||||||
|
runner._update_runtime_status = MagicMock()
|
||||||
|
|
||||||
mock_agent_1 = MagicMock()
|
mock_agent_1 = MagicMock()
|
||||||
mock_agent_2 = MagicMock()
|
mock_agent_2 = MagicMock()
|
||||||
|
|
@ -209,8 +230,6 @@ class TestGatewayCleanupWiring:
|
||||||
"session-2": mock_agent_2,
|
"session-2": mock_agent_2,
|
||||||
}
|
}
|
||||||
|
|
||||||
from gateway.run import GatewayRunner
|
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
try:
|
try:
|
||||||
with patch("gateway.status.remove_pid_file"), \
|
with patch("gateway.status.remove_pid_file"), \
|
||||||
|
|
|
||||||
|
|
@ -465,7 +465,7 @@ Important safety rule: cron-run sessions should not recursively schedule more cr
|
||||||
},
|
},
|
||||||
"deliver": {
|
"deliver": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Omit this parameter to auto-deliver back to the current chat and topic (recommended). Auto-detection preserves thread/topic context. Only set explicitly when the user asks to deliver somewhere OTHER than the current conversation. Values: 'origin' (same as omitting), 'local' (no delivery, save only), or platform:chat_id:thread_id for a specific destination. Examples: 'telegram:-1001234567890:17585', 'discord:#engineering'. WARNING: 'platform:chat_id' without :thread_id loses topic targeting."
|
"description": "Omit this parameter to auto-deliver back to the current chat and topic (recommended). Auto-detection preserves thread/topic context. Only set explicitly when the user asks to deliver somewhere OTHER than the current conversation. Values: 'origin' (same as omitting), 'local' (no delivery, save only), or platform:chat_id:thread_id for a specific destination. Examples: 'telegram:-1001234567890:17585', 'discord:#engineering', 'sms:+15551234567'. WARNING: 'platform:chat_id' without :thread_id loses topic targeting."
|
||||||
},
|
},
|
||||||
"skills": {
|
"skills": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|
|
||||||
|
|
@ -429,6 +429,11 @@ class AudioRecorder:
|
||||||
"""Current audio input RMS level (0-32767). Updated each audio chunk."""
|
"""Current audio input RMS level (0-32767). Updated each audio chunk."""
|
||||||
return self._current_rms
|
return self._current_rms
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_recording(self) -> bool:
|
||||||
|
"""Whether audio recording is currently active."""
|
||||||
|
return self._recording
|
||||||
|
|
||||||
# -- public methods ------------------------------------------------------
|
# -- public methods ------------------------------------------------------
|
||||||
|
|
||||||
def _ensure_stream(self) -> None:
|
def _ensure_stream(self) -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue