mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
Merge pull request #21012 from stephenschoettler/fix/ci-pr-check-unblock
fix(ci): unblock shared PR checks
This commit is contained in:
commit
cd64bed55e
10 changed files with 194 additions and 70 deletions
|
|
@ -12,12 +12,24 @@ Covers:
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from types import SimpleNamespace
|
from contextlib import contextmanager
|
||||||
|
from types import ModuleType, SimpleNamespace
|
||||||
from unittest.mock import MagicMock, patch, PropertyMock
|
from unittest.mock import MagicMock, patch, PropertyMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _mock_botocore_session(*, return_value=None, side_effect=None):
|
||||||
|
"""Patch botocore.session even when botocore is not installed."""
|
||||||
|
botocore_mod = ModuleType("botocore")
|
||||||
|
session_mod = ModuleType("botocore.session")
|
||||||
|
session_mod.get_session = MagicMock(return_value=return_value, side_effect=side_effect)
|
||||||
|
botocore_mod.session = session_mod
|
||||||
|
with patch.dict("sys.modules", {"botocore": botocore_mod, "botocore.session": session_mod}):
|
||||||
|
yield session_mod.get_session
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# AWS credential detection
|
# AWS credential detection
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -120,7 +132,7 @@ class TestResolveBedrocRegion:
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
mock_session.get_config_variable.return_value = None
|
mock_session.get_config_variable.return_value = None
|
||||||
with patch("botocore.session.get_session", return_value=mock_session):
|
with _mock_botocore_session(return_value=mock_session):
|
||||||
assert resolve_bedrock_region({}) == "us-east-1"
|
assert resolve_bedrock_region({}) == "us-east-1"
|
||||||
|
|
||||||
def test_falls_back_to_botocore_profile_region(self):
|
def test_falls_back_to_botocore_profile_region(self):
|
||||||
|
|
@ -128,13 +140,13 @@ class TestResolveBedrocRegion:
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
mock_session.get_config_variable.return_value = "eu-central-1"
|
mock_session.get_config_variable.return_value = "eu-central-1"
|
||||||
with patch("botocore.session.get_session", return_value=mock_session):
|
with _mock_botocore_session(return_value=mock_session):
|
||||||
assert resolve_bedrock_region({}) == "eu-central-1"
|
assert resolve_bedrock_region({}) == "eu-central-1"
|
||||||
|
|
||||||
def test_botocore_failure_falls_back_to_us_east_1(self):
|
def test_botocore_failure_falls_back_to_us_east_1(self):
|
||||||
from agent.bedrock_adapter import resolve_bedrock_region
|
from agent.bedrock_adapter import resolve_bedrock_region
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
with patch("botocore.session.get_session", side_effect=Exception("no botocore")):
|
with _mock_botocore_session(side_effect=Exception("no botocore")):
|
||||||
assert resolve_bedrock_region({}) == "us-east-1"
|
assert resolve_bedrock_region({}) == "us-east-1"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -253,20 +253,24 @@ class TestErrorClassifierBedrock:
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class TestPackaging:
|
class TestPackaging:
|
||||||
"""Verify bedrock optional dependency is declared."""
|
"""Verify Bedrock remains a declared lazy optional dependency."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _optional_dependencies():
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
content = (Path(__file__).parent.parent.parent / "pyproject.toml").read_text()
|
||||||
|
return tomllib.loads(content)["project"]["optional-dependencies"]
|
||||||
|
|
||||||
def test_bedrock_extra_exists(self):
|
def test_bedrock_extra_exists(self):
|
||||||
import configparser
|
extras = self._optional_dependencies()
|
||||||
from pathlib import Path
|
assert "bedrock" in extras
|
||||||
# Read pyproject.toml to verify [bedrock] extra
|
assert any(dep.startswith("boto3==") for dep in extras["bedrock"])
|
||||||
toml_path = Path(__file__).parent.parent.parent / "pyproject.toml"
|
|
||||||
content = toml_path.read_text()
|
|
||||||
assert 'bedrock = ["boto3' in content
|
|
||||||
|
|
||||||
def test_bedrock_in_all_extra(self):
|
def test_bedrock_is_not_eager_installed_by_all_extra(self):
|
||||||
from pathlib import Path
|
extras = self._optional_dependencies()
|
||||||
content = (Path(__file__).parent.parent.parent / "pyproject.toml").read_text()
|
assert "hermes-agent[bedrock]" not in extras["all"]
|
||||||
assert '"hermes-agent[bedrock]"' in content
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,80 @@ import pytest
|
||||||
from gateway.config import Platform, PlatformConfig
|
from gateway.config import Platform, PlatformConfig
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeDingTalkModel:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeChatbotMessage(SimpleNamespace):
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data):
|
||||||
|
data = data or {}
|
||||||
|
return cls(
|
||||||
|
message_id=data.get("msgId") or data.get("messageId") or data.get("message_id") or "",
|
||||||
|
conversation_id=data.get("conversationId") or data.get("conversation_id") or "",
|
||||||
|
conversation_type=str(data.get("conversationType") or data.get("conversation_type") or "1"),
|
||||||
|
sender_id=data.get("senderId") or data.get("sender_id") or "",
|
||||||
|
sender_staff_id=data.get("senderStaffId") or data.get("sender_staff_id") or data.get("senderId") or "",
|
||||||
|
sender_nick=data.get("senderNick") or data.get("sender_nick") or "",
|
||||||
|
text=data.get("text") or "",
|
||||||
|
rich_text=data.get("richText") or data.get("rich_text"),
|
||||||
|
rich_text_content=data.get("richTextContent") or data.get("rich_text_content"),
|
||||||
|
session_webhook=data.get("sessionWebhook") or data.get("session_webhook") or "",
|
||||||
|
session_webhook_expired_time=data.get("sessionWebhookExpiredTime") or data.get("session_webhook_expired_time") or 0,
|
||||||
|
create_at=data.get("createAt") or data.get("create_at") or 0,
|
||||||
|
at_users=data.get("atUsers") or data.get("at_users") or [],
|
||||||
|
is_in_at_list=bool(data.get("isInAtList") or data.get("is_in_at_list")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _fake_dingtalk_optional_sdks(monkeypatch):
|
||||||
|
"""Keep DingTalk adapter tests hermetic when optional SDKs are absent."""
|
||||||
|
from gateway.platforms import dingtalk as dt
|
||||||
|
|
||||||
|
card_models = SimpleNamespace(**{
|
||||||
|
name: _FakeDingTalkModel
|
||||||
|
for name in (
|
||||||
|
"CreateCardRequest",
|
||||||
|
"CreateCardRequestCardData",
|
||||||
|
"CreateCardRequestImGroupOpenSpaceModel",
|
||||||
|
"CreateCardRequestImRobotOpenSpaceModel",
|
||||||
|
"CreateCardHeaders",
|
||||||
|
"DeliverCardRequest",
|
||||||
|
"DeliverCardRequestImGroupOpenDeliverModel",
|
||||||
|
"DeliverCardRequestImRobotOpenDeliverModel",
|
||||||
|
"DeliverCardHeaders",
|
||||||
|
"StreamingUpdateRequest",
|
||||||
|
"StreamingUpdateHeaders",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
robot_models = SimpleNamespace(**{
|
||||||
|
name: _FakeDingTalkModel
|
||||||
|
for name in (
|
||||||
|
"RobotReplyEmotionRequestTextEmotion",
|
||||||
|
"RobotReplyEmotionRequest",
|
||||||
|
"RobotReplyEmotionHeaders",
|
||||||
|
"RobotRecallEmotionRequestTextEmotion",
|
||||||
|
"RobotRecallEmotionRequest",
|
||||||
|
"RobotRecallEmotionHeaders",
|
||||||
|
"RobotMessageFileDownloadRequest",
|
||||||
|
"RobotMessageFileDownloadHeaders",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
monkeypatch.setattr(dt, "ChatbotMessage", _FakeChatbotMessage, raising=False)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
dt,
|
||||||
|
"AckMessage",
|
||||||
|
SimpleNamespace(STATUS_OK=200, STATUS_SYSTEM_EXCEPTION=500),
|
||||||
|
raising=False,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(dt, "tea_util_models", SimpleNamespace(RuntimeOptions=_FakeDingTalkModel), raising=False)
|
||||||
|
monkeypatch.setattr(dt, "dingtalk_card_models", card_models, raising=False)
|
||||||
|
monkeypatch.setattr(dt, "dingtalk_robot_models", robot_models, raising=False)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Requirements check
|
# Requirements check
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -18,7 +92,8 @@ from gateway.config import Platform, PlatformConfig
|
||||||
class TestDingTalkRequirements:
|
class TestDingTalkRequirements:
|
||||||
|
|
||||||
def test_returns_false_when_sdk_missing(self, monkeypatch):
|
def test_returns_false_when_sdk_missing(self, monkeypatch):
|
||||||
with patch.dict("sys.modules", {"dingtalk_stream": None}):
|
with patch.dict("sys.modules", {"dingtalk_stream": None}), \
|
||||||
|
patch("tools.lazy_deps.ensure", side_effect=ImportError("dingtalk_stream unavailable")):
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"gateway.platforms.dingtalk.DINGTALK_STREAM_AVAILABLE", False
|
"gateway.platforms.dingtalk.DINGTALK_STREAM_AVAILABLE", False
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,36 @@ def test_admit_per_group_require_mention_overrides_global():
|
||||||
def test_hydrate_bot_identity_populates_self_ids_from_bot_v3_info(monkeypatch):
|
def test_hydrate_bot_identity_populates_self_ids_from_bot_v3_info(monkeypatch):
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from gateway.platforms.feishu import FeishuAdapter
|
from gateway.platforms import feishu as feishu_mod
|
||||||
|
FeishuAdapter = feishu_mod.FeishuAdapter
|
||||||
|
|
||||||
|
class _FakeBaseRequestBuilder:
|
||||||
|
def __init__(self):
|
||||||
|
self._request = SimpleNamespace()
|
||||||
|
|
||||||
|
def http_method(self, value):
|
||||||
|
self._request.http_method = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def uri(self, value):
|
||||||
|
self._request.uri = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def token_types(self, value):
|
||||||
|
self._request.token_types = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
return self._request
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
feishu_mod,
|
||||||
|
"BaseRequest",
|
||||||
|
SimpleNamespace(builder=lambda: _FakeBaseRequestBuilder()),
|
||||||
|
raising=False,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(feishu_mod, "HttpMethod", SimpleNamespace(GET="GET"), raising=False)
|
||||||
|
monkeypatch.setattr(feishu_mod, "AccessTokenType", SimpleNamespace(TENANT="TENANT"), raising=False)
|
||||||
|
|
||||||
adapter = object.__new__(FeishuAdapter)
|
adapter = object.__new__(FeishuAdapter)
|
||||||
adapter._bot_open_id = ""
|
adapter._bot_open_id = ""
|
||||||
|
|
|
||||||
|
|
@ -716,8 +716,10 @@ class TestMatrixModuleImport:
|
||||||
"sys.meta_path.insert(0, _Blocker())\n"
|
"sys.meta_path.insert(0, _Blocker())\n"
|
||||||
"for k in list(sys.modules):\n"
|
"for k in list(sys.modules):\n"
|
||||||
" if k.startswith('mautrix'): del sys.modules[k]\n"
|
" if k.startswith('mautrix'): del sys.modules[k]\n"
|
||||||
|
"from unittest.mock import patch\n"
|
||||||
"from gateway.platforms.matrix import check_matrix_requirements\n"
|
"from gateway.platforms.matrix import check_matrix_requirements\n"
|
||||||
"assert not check_matrix_requirements()\n"
|
"with patch('tools.lazy_deps.ensure', side_effect=ImportError('blocked')):\n"
|
||||||
|
" assert not check_matrix_requirements()\n"
|
||||||
"print('OK')\n"
|
"print('OK')\n"
|
||||||
)],
|
)],
|
||||||
capture_output=True, text=True, timeout=10,
|
capture_output=True, text=True, timeout=10,
|
||||||
|
|
@ -737,7 +739,8 @@ class TestMatrixRequirements:
|
||||||
import mautrix # noqa: F401
|
import mautrix # noqa: F401
|
||||||
assert check_matrix_requirements() is True
|
assert check_matrix_requirements() is True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
assert check_matrix_requirements() is False
|
with patch("tools.lazy_deps.ensure", side_effect=ImportError("mautrix unavailable")):
|
||||||
|
assert check_matrix_requirements() is False
|
||||||
|
|
||||||
def test_check_requirements_without_creds(self, monkeypatch):
|
def test_check_requirements_without_creds(self, monkeypatch):
|
||||||
monkeypatch.delenv("MATRIX_ACCESS_TOKEN", raising=False)
|
monkeypatch.delenv("MATRIX_ACCESS_TOKEN", raising=False)
|
||||||
|
|
@ -759,7 +762,8 @@ class TestMatrixRequirements:
|
||||||
monkeypatch.setenv("MATRIX_ENCRYPTION", "true")
|
monkeypatch.setenv("MATRIX_ENCRYPTION", "true")
|
||||||
|
|
||||||
from gateway.platforms import matrix as matrix_mod
|
from gateway.platforms import matrix as matrix_mod
|
||||||
with patch.object(matrix_mod, "_check_e2ee_deps", return_value=False):
|
with patch.object(matrix_mod, "_check_e2ee_deps", return_value=False), \
|
||||||
|
patch("tools.lazy_deps.ensure", side_effect=ImportError("mautrix unavailable")):
|
||||||
assert matrix_mod.check_matrix_requirements() is False
|
assert matrix_mod.check_matrix_requirements() is False
|
||||||
|
|
||||||
def test_check_requirements_encryption_false_no_e2ee_deps_ok(self, monkeypatch):
|
def test_check_requirements_encryption_false_no_e2ee_deps_ok(self, monkeypatch):
|
||||||
|
|
@ -775,7 +779,8 @@ class TestMatrixRequirements:
|
||||||
import mautrix # noqa: F401
|
import mautrix # noqa: F401
|
||||||
assert matrix_mod.check_matrix_requirements() is True
|
assert matrix_mod.check_matrix_requirements() is True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
assert matrix_mod.check_matrix_requirements() is False
|
with patch("tools.lazy_deps.ensure", side_effect=ImportError("mautrix unavailable")):
|
||||||
|
assert matrix_mod.check_matrix_requirements() is False
|
||||||
|
|
||||||
def test_check_requirements_encryption_true_with_e2ee_deps(self, monkeypatch):
|
def test_check_requirements_encryption_true_with_e2ee_deps(self, monkeypatch):
|
||||||
"""MATRIX_ENCRYPTION=true should pass if E2EE deps are available."""
|
"""MATRIX_ENCRYPTION=true should pass if E2EE deps are available."""
|
||||||
|
|
@ -789,7 +794,8 @@ class TestMatrixRequirements:
|
||||||
import mautrix # noqa: F401
|
import mautrix # noqa: F401
|
||||||
assert matrix_mod.check_matrix_requirements() is True
|
assert matrix_mod.check_matrix_requirements() is True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
assert matrix_mod.check_matrix_requirements() is False
|
with patch("tools.lazy_deps.ensure", side_effect=ImportError("mautrix unavailable")):
|
||||||
|
assert matrix_mod.check_matrix_requirements() is False
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ All Bedrock API calls are mocked — no real AWS credentials needed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from types import ModuleType
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -26,6 +28,19 @@ import pytest
|
||||||
# Shared helpers / fixtures
|
# Shared helpers / fixtures
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _mock_botocore_session(*, return_value=None):
|
||||||
|
"""Patch botocore.session even when botocore is not installed."""
|
||||||
|
botocore_mod = ModuleType("botocore")
|
||||||
|
session_mod = ModuleType("botocore.session")
|
||||||
|
session_mod.get_session = MagicMock(return_value=return_value)
|
||||||
|
botocore_mod.session = session_mod
|
||||||
|
with patch.dict("sys.modules", {"botocore": botocore_mod, "botocore.session": session_mod}):
|
||||||
|
yield session_mod.get_session
|
||||||
|
|
||||||
|
|
||||||
_EU_MODELS = [
|
_EU_MODELS = [
|
||||||
{"id": "eu.anthropic.claude-sonnet-4-6-20250514-v1:0", "name": "Claude Sonnet 4.6 (EU)", "provider": "inference-profile"},
|
{"id": "eu.anthropic.claude-sonnet-4-6-20250514-v1:0", "name": "Claude Sonnet 4.6 (EU)", "provider": "inference-profile"},
|
||||||
{"id": "eu.anthropic.claude-haiku-4-5-20251015-v1:0", "name": "Claude Haiku 4.5 (EU)", "provider": "inference-profile"},
|
{"id": "eu.anthropic.claude-haiku-4-5-20251015-v1:0", "name": "Claude Haiku 4.5 (EU)", "provider": "inference-profile"},
|
||||||
|
|
@ -276,7 +291,7 @@ class TestBedrockRegionRouting:
|
||||||
|
|
||||||
with patch("agent.bedrock_adapter.has_aws_credentials", return_value=True), \
|
with patch("agent.bedrock_adapter.has_aws_credentials", return_value=True), \
|
||||||
patch("agent.bedrock_adapter.discover_bedrock_models", side_effect=_mock_discover), \
|
patch("agent.bedrock_adapter.discover_bedrock_models", side_effect=_mock_discover), \
|
||||||
patch("botocore.session.get_session", return_value=mock_session):
|
_mock_botocore_session(return_value=mock_session):
|
||||||
providers = list_authenticated_providers(current_provider="bedrock")
|
providers = list_authenticated_providers(current_provider="bedrock")
|
||||||
|
|
||||||
bedrock = next((p for p in providers if p["slug"] == "bedrock"), None)
|
bedrock = next((p for p in providers if p["slug"] == "bedrock"), None)
|
||||||
|
|
@ -310,7 +325,7 @@ class TestBedrockRegionRouting:
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
mock_session.get_config_variable.return_value = "eu-central-1"
|
mock_session.get_config_variable.return_value = "eu-central-1"
|
||||||
|
|
||||||
with patch("botocore.session.get_session", return_value=mock_session):
|
with _mock_botocore_session(return_value=mock_session):
|
||||||
region = resolve_bedrock_region()
|
region = resolve_bedrock_region()
|
||||||
|
|
||||||
assert region == "us-west-2", "env var should override botocore profile"
|
assert region == "us-west-2", "env var should override botocore profile"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
"""Tests that switch_model preserves config_context_length."""
|
"""Tests that switch_model does not inherit stale context_length overrides."""
|
||||||
|
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ def _make_agent_with_compressor(config_context_length=None) -> AIAgent:
|
||||||
agent.client = MagicMock()
|
agent.client = MagicMock()
|
||||||
agent.quiet_mode = True
|
agent.quiet_mode = True
|
||||||
|
|
||||||
# Store config_context_length for later use in switch_model
|
# Store the initial config_context_length override used at agent construction.
|
||||||
agent._config_context_length = config_context_length
|
agent._config_context_length = config_context_length
|
||||||
|
|
||||||
# Context compressor with primary model values
|
# Context compressor with primary model values
|
||||||
|
|
@ -41,8 +41,8 @@ def _make_agent_with_compressor(config_context_length=None) -> AIAgent:
|
||||||
|
|
||||||
|
|
||||||
@patch("agent.model_metadata.get_model_context_length", return_value=131_072)
|
@patch("agent.model_metadata.get_model_context_length", return_value=131_072)
|
||||||
def test_switch_model_preserves_config_context_length(mock_ctx_len):
|
def test_switch_model_clears_previous_config_context_length(mock_ctx_len):
|
||||||
"""When switching models, config_context_length should be passed to get_model_context_length."""
|
"""Switching models must not reuse the previous model.context_length override."""
|
||||||
agent = _make_agent_with_compressor(config_context_length=32_768)
|
agent = _make_agent_with_compressor(config_context_length=32_768)
|
||||||
|
|
||||||
assert agent.context_compressor.model == "primary-model"
|
assert agent.context_compressor.model == "primary-model"
|
||||||
|
|
@ -51,13 +51,14 @@ def test_switch_model_preserves_config_context_length(mock_ctx_len):
|
||||||
# Switch model
|
# Switch model
|
||||||
agent.switch_model("new-model", "openrouter", api_key="sk-new", base_url="https://openrouter.ai/api/v1")
|
agent.switch_model("new-model", "openrouter", api_key="sk-new", base_url="https://openrouter.ai/api/v1")
|
||||||
|
|
||||||
# Verify get_model_context_length was called with config_context_length
|
# Verify the old config override is not passed to the new model.
|
||||||
mock_ctx_len.assert_called_once()
|
mock_ctx_len.assert_called_once()
|
||||||
call_kwargs = mock_ctx_len.call_args.kwargs
|
call_kwargs = mock_ctx_len.call_args.kwargs
|
||||||
assert call_kwargs.get("config_context_length") == 32_768
|
assert call_kwargs.get("config_context_length") is None
|
||||||
|
|
||||||
# Verify compressor was updated
|
# Verify compressor was updated from the newly resolved model metadata.
|
||||||
assert agent.context_compressor.model == "new-model"
|
assert agent.context_compressor.model == "new-model"
|
||||||
|
assert agent.context_compressor.context_length == 131_072
|
||||||
|
|
||||||
|
|
||||||
def test_switch_model_without_config_context_length():
|
def test_switch_model_without_config_context_length():
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from tools.registry import ToolRegistry, discover_builtin_tools
|
from tools.registry import ToolRegistry, _module_registers_tools, discover_builtin_tools
|
||||||
|
|
||||||
|
|
||||||
def _dummy_handler(args, **kwargs):
|
def _dummy_handler(args, **kwargs):
|
||||||
|
|
@ -289,43 +289,19 @@ class TestCheckFnExceptionHandling:
|
||||||
|
|
||||||
|
|
||||||
class TestBuiltinDiscovery:
|
class TestBuiltinDiscovery:
|
||||||
def test_matches_previous_manual_builtin_tool_set(self):
|
def test_discovers_all_real_self_registering_builtin_tool_modules(self):
|
||||||
expected = {
|
tools_dir = Path(__file__).resolve().parents[2] / "tools"
|
||||||
"tools.browser_cdp_tool",
|
expected = [
|
||||||
"tools.browser_dialog_tool",
|
f"tools.{path.stem}"
|
||||||
"tools.browser_tool",
|
for path in sorted(tools_dir.glob("*.py"))
|
||||||
"tools.clarify_tool",
|
if path.name not in {"__init__.py", "registry.py", "mcp_tool.py"}
|
||||||
"tools.code_execution_tool",
|
and _module_registers_tools(path)
|
||||||
"tools.computer_use_tool",
|
]
|
||||||
"tools.cronjob_tools",
|
|
||||||
"tools.delegate_tool",
|
|
||||||
"tools.discord_tool",
|
|
||||||
"tools.feishu_doc_tool",
|
|
||||||
"tools.feishu_drive_tool",
|
|
||||||
"tools.file_tools",
|
|
||||||
"tools.homeassistant_tool",
|
|
||||||
"tools.image_generation_tool",
|
|
||||||
"tools.kanban_tools",
|
|
||||||
"tools.memory_tool",
|
|
||||||
"tools.mixture_of_agents_tool",
|
|
||||||
"tools.process_registry",
|
|
||||||
"tools.rl_training_tool",
|
|
||||||
"tools.send_message_tool",
|
|
||||||
"tools.session_search_tool",
|
|
||||||
"tools.skill_manager_tool",
|
|
||||||
"tools.skills_tool",
|
|
||||||
"tools.terminal_tool",
|
|
||||||
"tools.todo_tool",
|
|
||||||
"tools.tts_tool",
|
|
||||||
"tools.vision_tools",
|
|
||||||
"tools.web_tools",
|
|
||||||
"tools.yuanbao_tools",
|
|
||||||
}
|
|
||||||
|
|
||||||
with patch("tools.registry.importlib.import_module"):
|
with patch("tools.registry.importlib.import_module"):
|
||||||
imported = discover_builtin_tools(Path(__file__).resolve().parents[2] / "tools")
|
imported = discover_builtin_tools(tools_dir)
|
||||||
|
|
||||||
assert set(imported) == expected
|
assert imported == expected
|
||||||
|
|
||||||
def test_imports_only_self_registering_modules(self, tmp_path):
|
def test_imports_only_self_registering_modules(self, tmp_path):
|
||||||
tools_dir = tmp_path / "tools"
|
tools_dir = tmp_path / "tools"
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,16 @@ import json
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
from unittest.mock import MagicMock, patch, mock_open
|
from unittest.mock import MagicMock, patch, mock_open
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_faster_whisper_module(mock_model):
|
||||||
|
return SimpleNamespace(WhisperModel=MagicMock(return_value=mock_model))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Provider selection
|
# Provider selection
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -137,8 +142,9 @@ class TestTranscribeLocal:
|
||||||
mock_model = MagicMock()
|
mock_model = MagicMock()
|
||||||
mock_model.transcribe.return_value = ([mock_segment], mock_info)
|
mock_model.transcribe.return_value = ([mock_segment], mock_info)
|
||||||
|
|
||||||
|
fake_fw = _fake_faster_whisper_module(mock_model)
|
||||||
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True), \
|
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True), \
|
||||||
patch("faster_whisper.WhisperModel", return_value=mock_model), \
|
patch.dict("sys.modules", {"faster_whisper": fake_fw}), \
|
||||||
patch("tools.transcription_tools._local_model", None):
|
patch("tools.transcription_tools._local_model", None):
|
||||||
from tools.transcription_tools import _transcribe_local
|
from tools.transcription_tools import _transcribe_local
|
||||||
result = _transcribe_local(str(audio_file), "base")
|
result = _transcribe_local(str(audio_file), "base")
|
||||||
|
|
@ -300,7 +306,8 @@ class TestNormalizeLocalModel:
|
||||||
}), \
|
}), \
|
||||||
patch("tools.transcription_tools._local_model", None), \
|
patch("tools.transcription_tools._local_model", None), \
|
||||||
patch("tools.transcription_tools._local_model_name", None), \
|
patch("tools.transcription_tools._local_model_name", None), \
|
||||||
patch("faster_whisper.WhisperModel", return_value=mock_model) as mock_cls:
|
patch.dict("sys.modules", {"faster_whisper": _fake_faster_whisper_module(mock_model)}):
|
||||||
|
mock_cls = __import__("faster_whisper").WhisperModel
|
||||||
from tools.transcription_tools import transcribe_audio
|
from tools.transcription_tools import transcribe_audio
|
||||||
transcribe_audio(audio_file)
|
transcribe_audio(audio_file)
|
||||||
# WhisperModel must NOT have been called with "whisper-1"
|
# WhisperModel must NOT have been called with "whisper-1"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import json
|
import json
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -27,7 +26,7 @@ def mock_kittentts_module():
|
||||||
"""Inject a fake kittentts + soundfile module that return stub objects."""
|
"""Inject a fake kittentts + soundfile module that return stub objects."""
|
||||||
fake_model = MagicMock()
|
fake_model = MagicMock()
|
||||||
# 24kHz float32 PCM at ~2s of silence
|
# 24kHz float32 PCM at ~2s of silence
|
||||||
fake_model.generate.return_value = np.zeros(48000, dtype=np.float32)
|
fake_model.generate.return_value = [0.0] * 48000
|
||||||
fake_cls = MagicMock(return_value=fake_model)
|
fake_cls = MagicMock(return_value=fake_model)
|
||||||
fake_kittentts = MagicMock()
|
fake_kittentts = MagicMock()
|
||||||
fake_kittentts.KittenTTS = fake_cls
|
fake_kittentts.KittenTTS = fake_cls
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue