Files
kamidso-back/kamidso/engine/serve/connector/tests/test_service.py
T
javadandCursor edbaa2acec Add native Didar connectors so chats can query CRM data reliably.
Connector calls, semantic rules, and DuckDB memory keep long runs stable, and stream retry stops chats from stalling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 19:26:47 +03:30

1323 lines
46 KiB
Python

import json
from typing import Dict, List
from unittest.mock import AsyncMock, MagicMock
import pytest
from kamidso.engine.core.agent.resource.connector.credential import CredentialStore
from kamidso.engine.core.component import SystemApp
from kamidso.engine.core.storage.metadata import db
from kamidso.engine.serve.core.tests.conftest import config, system_app # noqa: F401
from ..config import ServeConfig
from ..models.models import ConnectorInstanceEntity
from ..service.service import (
ConnectorCreateRequest,
ConnectorService,
ConnectorUpdateRequest,
)
@pytest.fixture(autouse=True)
def setup_and_teardown():
db.init_db("sqlite:///:memory:")
db.create_all()
yield
@pytest.fixture
def service(system_app: SystemApp):
system_app.config.set("kamidso.engine.core.app.global.encrypt_key", "test_encrypt_key")
instance = ConnectorService(system_app, ServeConfig())
instance.init_app(system_app)
return instance
def test_create_connector_encrypts_credentials_before_persisting(
service: ConnectorService,
):
credentials = {"token": "secret-token"}
response = service.create_connector(
ConnectorCreateRequest(
connector_type="github",
display_name="GitHub Ops",
credentials=credentials,
)
)
with db.session() as session:
entity = (
session.query(ConnectorInstanceEntity)
.filter(ConnectorInstanceEntity.connector_id == response.connector_id)
.first()
)
assert entity is not None
assert entity.encrypted_credentials != json.dumps(credentials)
assert entity.encryption_salt
store = CredentialStore(system_app=service.system_app)
assert store.decrypt(entity.encrypted_credentials, entity.encryption_salt) == {
"token": "secret-token"
}
def test_update_connector_re_encrypts_credentials(service: ConnectorService):
response = service.create_connector(
ConnectorCreateRequest(
connector_type="github",
display_name="GitHub Ops",
credentials={"token": "secret-token"},
)
)
with db.session() as session:
original_entity = (
session.query(ConnectorInstanceEntity)
.filter(ConnectorInstanceEntity.connector_id == response.connector_id)
.first()
)
assert original_entity is not None
original_ciphertext = original_entity.encrypted_credentials
original_salt = original_entity.encryption_salt
service.update_connector(
response.connector_id,
ConnectorUpdateRequest(credentials={"token": "new-secret-token"}),
)
with db.session() as session:
entity = (
session.query(ConnectorInstanceEntity)
.filter(ConnectorInstanceEntity.connector_id == response.connector_id)
.first()
)
assert entity is not None
assert entity.encrypted_credentials != json.dumps({"token": "new-secret-token"})
assert entity.encrypted_credentials != original_ciphertext
assert entity.encryption_salt
assert entity.encryption_salt != original_salt
store = CredentialStore(system_app=service.system_app)
assert store.decrypt(entity.encrypted_credentials, entity.encryption_salt) == {
"token": "new-secret-token"
}
def test_create_connector_activates_external_connector_manager(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
fake_manager = MagicMock()
fake_manager.create_connector = AsyncMock(return_value="runtime-connector-id")
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
service.create_connector(
ConnectorCreateRequest(
connector_type="github",
display_name="GitHub Ops",
credentials={"token": "secret-token"},
config={"server_uri": "http://example.com/sse"},
)
)
fake_manager.create_connector.assert_awaited_once()
call_kwargs = fake_manager.create_connector.call_args.kwargs
assert call_kwargs["connector_type"] == "github"
assert call_kwargs["credentials"] == {"token": "secret-token"}
assert call_kwargs["name"] == "GitHub Ops"
assert call_kwargs["extra_config"] == {"server_uri": "http://example.com/sse"}
assert isinstance(call_kwargs["connector_id"], str)
assert call_kwargs["connector_id"]
def test_after_start_rehydrates_active_connectors_with_decrypted_credentials(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
credentials = {"token": "secret-token"}
store = CredentialStore(system_app=service.system_app)
encryption_salt = store.generate_salt()
encrypted_credentials = store.encrypt(credentials, encryption_salt)
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="connector-1",
connector_type="github",
display_name="GitHub Ops",
encrypted_credentials=encrypted_credentials,
encryption_salt=encryption_salt,
status="active",
config_json=json.dumps({"server_uri": "http://example.com/sse"}),
)
)
fake_manager = MagicMock()
fake_manager.create_connector = AsyncMock()
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
service.after_start()
fake_manager.create_connector.assert_awaited_once_with(
connector_type="github",
credentials=credentials,
name="GitHub Ops",
extra_config={"server_uri": "http://example.com/sse"},
connector_id="connector-1",
)
def test_after_start_marks_legacy_connector_needs_reactivation(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
"""Phase 1 era connectors with NULL config_json should be flagged as
needs_reactivation (not silently lost) after catalog downgrade."""
store = CredentialStore(system_app=service.system_app)
encryption_salt = store.generate_salt()
encrypted_credentials = store.encrypt({"token": "old-token"}, encryption_salt)
# Insert a legacy row: config_json is NULL (pre-1.5 era)
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="legacy-conn-1",
connector_type="github",
display_name="Legacy GitHub",
encrypted_credentials=encrypted_credentials,
encryption_salt=encryption_salt,
status="active",
config_json=None,
)
)
fake_manager = MagicMock()
fake_manager.create_connector = AsyncMock(
side_effect=ValueError(
"Built-in connector type 'github' requires extra_config.server_uri"
)
)
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
# Should not raise
service.after_start()
# manager.create_connector was called with extra_config=None (the legacy state)
fake_manager.create_connector.assert_awaited_once()
call_kwargs = fake_manager.create_connector.call_args.kwargs
assert call_kwargs["extra_config"] is None
# DB row should now be marked as needs_reactivation
with db.session() as session:
entity = (
session.query(ConnectorInstanceEntity)
.filter(ConnectorInstanceEntity.connector_id == "legacy-conn-1")
.first()
)
assert entity is not None
assert entity.status == "needs_reactivation"
# list_connectors must surface the needs_reactivation row (no silent filtering)
connectors = service.list_connectors()
assert any(c.status == "needs_reactivation" for c in connectors)
def test_create_connector_builtin_without_server_uri_propagates_value_error(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
"""Service-layer create with built-in type but no config.server_uri
should propagate the ValueError from manager."""
fake_manager = MagicMock()
fake_manager.create_connector = AsyncMock(
side_effect=ValueError(
"Built-in connector type 'github' requires extra_config.server_uri"
)
)
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
with pytest.raises(ValueError, match="requires extra_config.server_uri"):
service.create_connector(
ConnectorCreateRequest(
connector_type="github",
display_name="GitHub No URI",
credentials={"token": "secret"},
config=None,
)
)
def test_update_connector_merges_credentials_keeps_omitted_fields(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
"""P0-3: update with partial credentials merges into existing ones."""
fake_manager = MagicMock()
fake_manager.create_connector = AsyncMock()
fake_manager.remove_connector = AsyncMock()
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
response = service.create_connector(
ConnectorCreateRequest(
connector_type="github",
display_name="GitHub Ops",
credentials={"github_token": "ghp_xxx", "username": "alice"},
config={"server_uri": "http://example.com/sse"},
)
)
# Reset mock call counts from create
fake_manager.reset_mock()
service.update_connector(
response.connector_id,
ConnectorUpdateRequest(credentials={"username": "bob"}),
)
with db.session() as session:
entity = (
session.query(ConnectorInstanceEntity)
.filter(ConnectorInstanceEntity.connector_id == response.connector_id)
.first()
)
assert entity is not None
store = CredentialStore(system_app=service.system_app)
decrypted = store.decrypt(entity.encrypted_credentials, entity.encryption_salt)
assert decrypted == {"github_token": "ghp_xxx", "username": "bob"}
# credentials_changed is True, so re-activation should be triggered
fake_manager.remove_connector.assert_awaited_once_with(response.connector_id)
fake_manager.create_connector.assert_awaited_once()
def test_update_connector_resets_needs_reactivation_status(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
"""P0-1: updating config on a needs_reactivation connector resets status
to active and re-activates the manager."""
store = CredentialStore(system_app=service.system_app)
encryption_salt = store.generate_salt()
encrypted_credentials = store.encrypt({"token": "old-token"}, encryption_salt)
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="reactivate-conn-1",
connector_type="github",
display_name="Legacy GitHub",
encrypted_credentials=encrypted_credentials,
encryption_salt=encryption_salt,
status="needs_reactivation",
config_json=None,
)
)
fake_manager = MagicMock()
fake_manager.create_connector = AsyncMock()
fake_manager.remove_connector = AsyncMock()
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = service.update_connector(
"reactivate-conn-1",
ConnectorUpdateRequest(config={"server_uri": "http://new.example.com/sse"}),
)
assert result.status == "active"
fake_manager.remove_connector.assert_awaited_once_with("reactivate-conn-1")
fake_manager.create_connector.assert_awaited_once()
call_kwargs = fake_manager.create_connector.call_args.kwargs
assert call_kwargs["extra_config"] == {"server_uri": "http://new.example.com/sse"}
assert call_kwargs["connector_id"] == "reactivate-conn-1"
def test_update_connector_reactivation_failure_marks_error(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
"""When re-activation fails, the status should be set to 'error'."""
store = CredentialStore(system_app=service.system_app)
encryption_salt = store.generate_salt()
encrypted_credentials = store.encrypt({"token": "test-token"}, encryption_salt)
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="fail-conn-1",
connector_type="github",
display_name="Failing GitHub",
encrypted_credentials=encrypted_credentials,
encryption_salt=encryption_salt,
status="needs_reactivation",
config_json=None,
)
)
fake_manager = MagicMock()
fake_manager.remove_connector = AsyncMock()
fake_manager.create_connector = AsyncMock(
side_effect=RuntimeError("Connection refused")
)
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = service.update_connector(
"fail-conn-1",
ConnectorUpdateRequest(config={"server_uri": "http://bad.example.com/sse"}),
)
assert result.status == "error"
# ---------------------------------------------------------------------------
# Phase 1.5 — test_connection tests
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_test_connection_returns_success_when_pack_active(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
"""test_connection returns success=True when pack exists and preload succeeds."""
from kamidso.engine.core.agent.resource.tool.base import BaseTool
# Create a connector row in DB
response = service.create_connector(
ConnectorCreateRequest(
connector_type="github",
display_name="GitHub Ops",
credentials={"token": "secret-token"},
)
)
cid = response.connector_id
# Build mock pack with 3 tools
mock_tool = MagicMock(spec=BaseTool)
mock_pack = MagicMock()
mock_pack.preload_resource = AsyncMock()
mock_pack.sub_resources = [mock_tool, mock_tool, mock_tool]
fake_manager = MagicMock()
fake_manager.get_connector_tools = MagicMock(return_value=mock_pack)
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = await service.test_connection(cid)
assert result["success"] is True
assert "3 个工具" in result["message"]
mock_pack.preload_resource.assert_awaited_once()
@pytest.mark.asyncio
async def test_test_connection_returns_failure_when_pack_missing(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
"""connector_id in DB but manager._active_packs has no entry -> success=False."""
response = service.create_connector(
ConnectorCreateRequest(
connector_type="github",
display_name="GitHub Ops",
credentials={"token": "secret-token"},
)
)
cid = response.connector_id
fake_manager = MagicMock()
fake_manager.get_connector_tools = MagicMock(return_value=None)
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = await service.test_connection(cid)
assert result["success"] is False
assert "未激活" in result["message"]
@pytest.mark.asyncio
async def test_test_connection_returns_failure_when_preload_throws(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
"""preload_resource raises network error -> success=False + error message."""
response = service.create_connector(
ConnectorCreateRequest(
connector_type="github",
display_name="GitHub Ops",
credentials={"token": "secret-token"},
)
)
cid = response.connector_id
mock_pack = MagicMock()
mock_pack.preload_resource = AsyncMock(side_effect=ConnectionError("timeout"))
fake_manager = MagicMock()
fake_manager.get_connector_tools = MagicMock(return_value=mock_pack)
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = await service.test_connection(cid)
assert result["success"] is False
assert "连接失败" in result["message"]
@pytest.mark.asyncio
async def test_test_connection_success_heals_error_status_to_active(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
"""A successful test_connection on an 'error' row should heal status
back to 'active'.
Why: status is only written on create/update/after_start. Once a transient
re-activation failure stamps 'error', nothing else flips it back even after
the remote recovers. test_connection succeeding is proof the connector works
*now*, so we treat it as the self-heal trigger.
"""
from kamidso.engine.core.agent.resource.tool.base import BaseTool
store = CredentialStore(system_app=service.system_app)
encryption_salt = store.generate_salt()
encrypted_credentials = store.encrypt({"token": "tok"}, encryption_salt)
# Seed a row already in the 'error' state (e.g., from a past PUT failure).
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="heal-conn-1",
connector_type="custom_mcp",
display_name="ArXiv-Search",
encrypted_credentials=encrypted_credentials,
encryption_salt=encryption_salt,
status="error",
config_json=json.dumps({"server_uri": "http://example.com/sse"}),
)
)
mock_tool = MagicMock(spec=BaseTool)
mock_pack = MagicMock()
mock_pack.preload_resource = AsyncMock()
mock_pack.sub_resources = [mock_tool]
fake_manager = MagicMock()
fake_manager.get_connector_tools = MagicMock(return_value=mock_pack)
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = await service.test_connection("heal-conn-1")
assert result["success"] is True
with db.session() as session:
entity = (
session.query(ConnectorInstanceEntity)
.filter(ConnectorInstanceEntity.connector_id == "heal-conn-1")
.first()
)
assert entity is not None
assert entity.status == "active"
@pytest.mark.asyncio
async def test_test_connection_failure_does_not_overwrite_status(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
"""A failing test_connection must NOT mutate status — status only heals on
real success, and a failed test does not prove the connector is broken
(could be a transient network blip from the API server side)."""
store = CredentialStore(system_app=service.system_app)
encryption_salt = store.generate_salt()
encrypted_credentials = store.encrypt({"token": "tok"}, encryption_salt)
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="stable-conn-1",
connector_type="custom_mcp",
display_name="ArXiv-Search",
encrypted_credentials=encrypted_credentials,
encryption_salt=encryption_salt,
status="active",
config_json=json.dumps({"server_uri": "http://example.com/sse"}),
)
)
mock_pack = MagicMock()
mock_pack.preload_resource = AsyncMock(side_effect=ConnectionError("timeout"))
fake_manager = MagicMock()
fake_manager.get_connector_tools = MagicMock(return_value=mock_pack)
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = await service.test_connection("stable-conn-1")
assert result["success"] is False
with db.session() as session:
entity = (
session.query(ConnectorInstanceEntity)
.filter(ConnectorInstanceEntity.connector_id == "stable-conn-1")
.first()
)
assert entity is not None
assert entity.status == "active"
# ----------------------------------------------------------------------
# Tests added in v4: is_custom predicate, _tool_summary_for, list_tools
# ----------------------------------------------------------------------
def test_is_custom_connector_type_predicate():
"""The static predicate accepts only non-empty ``custom_<suffix>`` strings."""
from kamidso.engine.core.agent.resource.connector.manager import ConnectorManager
assert ConnectorManager.is_custom_connector_type("custom_mcp") is True
assert ConnectorManager.is_custom_connector_type("custom_arxiv") is True
assert ConnectorManager.is_custom_connector_type("custom_a") is True
assert ConnectorManager.is_custom_connector_type("custom_") is False
assert ConnectorManager.is_custom_connector_type("github") is False
assert ConnectorManager.is_custom_connector_type("slack") is False
assert ConnectorManager.is_custom_connector_type("") is False
assert ConnectorManager.is_custom_connector_type(None) is False # type: ignore[arg-type]
assert ConnectorManager.is_custom_connector_type(123) is False # type: ignore[arg-type]
def test_connector_response_is_custom_for_custom_mcp_entity(service: ConnectorService):
"""``ConnectorResponse.is_custom`` reflects the predicate on the type string."""
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="custom-1",
connector_type="custom_mcp",
display_name="Custom MCP",
status="active",
config_json=json.dumps({"server_uri": "http://example.com/sse"}),
)
)
response = service.get_connector("custom-1")
assert response is not None
assert response.is_custom is True
assert response.connector_type == "custom_mcp"
def test_connector_response_is_custom_false_for_builtin_type(service: ConnectorService):
"""Built-in connector types must report ``is_custom=False``."""
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="builtin-1",
connector_type="github",
display_name="GitHub",
status="active",
)
)
response = service.get_connector("builtin-1")
assert response is not None
assert response.is_custom is False
def test_tool_summary_for_returns_none_when_pack_absent():
"""``_tool_summary_for`` returns ``None`` when the connector_id is unknown."""
from kamidso.engine.core.agent.resource.connector.manager import ConnectorManager
manager = ConnectorManager()
assert manager._tool_summary_for("missing-id") is None
def test_tool_summary_for_shape_matches_list_active_entry():
"""The helper output shape is identical to one ``list_active`` entry's tools."""
from kamidso.engine.core.agent.resource.connector.manager import (
ConnectorManager,
ConnectorStatus,
)
from kamidso.engine.core.agent.resource.tool.base import BaseTool, ToolParameter
class _StubTool(BaseTool): # type: ignore[misc]
@property
def name(self) -> str:
return "stub_tool"
@property
def description(self) -> str:
return "stub tool"
@property
def args(self) -> Dict[str, ToolParameter]:
return {
"query": ToolParameter(
name="query",
type="string",
description="Search query",
required=True,
)
}
async def _arun(self, *args, **kwargs): # pragma: no cover - unused path
return None
def _run(self, *args, **kwargs): # pragma: no cover - unused path
return None
pack = MagicMock()
pack.name = "stub-pack"
pack.sub_resources = [_StubTool()]
manager = ConnectorManager()
manager._active_packs["c1"] = pack
manager._statuses["c1"] = ConnectorStatus.active
manager._connector_types["c1"] = "github"
summary = manager._tool_summary_for("c1")
list_active_entry = next(
e for e in manager.list_active() if e["connector_id"] == "c1"
)
assert summary is not None
assert summary == list_active_entry["tools"]
assert summary[0]["name"] == "stub_tool"
assert summary[0]["original_name"] == "stub_tool" # no routing prefix
assert summary[0]["args"]["query"]["required"] is True
assert summary[0]["args"]["query"]["type"] == "string"
def test_tool_summary_for_strips_routing_prefix():
"""Tools whose ``name`` carries the ``mcp__{prefix}__`` routing prefix
expose the bare MCP name via ``original_name``."""
from kamidso.engine.core.agent.resource.connector.manager import (
ConnectorManager,
ConnectorStatus,
)
from kamidso.engine.core.agent.resource.tool.base import BaseTool, ToolParameter
class _PrefixedTool(BaseTool): # type: ignore[misc]
@property
def name(self) -> str:
return "mcp__arxiv-search__search_papers"
@property
def description(self) -> str:
return "Search arXiv papers"
@property
def args(self) -> Dict[str, ToolParameter]:
return {}
async def _arun(self, *args, **kwargs): # pragma: no cover
return None
def _run(self, *args, **kwargs): # pragma: no cover
return None
pack = MagicMock()
pack.name = "arxiv-search"
pack.sub_resources = [_PrefixedTool()]
manager = ConnectorManager()
manager._active_packs["c-prefix"] = pack
manager._statuses["c-prefix"] = ConnectorStatus.active
manager._connector_types["c-prefix"] = "custom_mcp"
summary = manager._tool_summary_for("c-prefix")
assert summary is not None
assert summary[0]["name"] == "mcp__arxiv-search__search_papers"
assert summary[0]["original_name"] == "search_papers"
def test_tool_summary_for_dedups_duplicate_tools_by_original_name():
"""When ``_resources`` holds both prefixed and raw-name versions of the
same logical tool (a real-world symptom of ``MCPToolPack.preload_resource``
re-firing after ``_apply_tool_prefix``), the summary collapses them and
keeps the prefixed entry — the canonical routing name LLMs invoke."""
from kamidso.engine.core.agent.resource.connector.manager import (
ConnectorManager,
ConnectorStatus,
)
from kamidso.engine.core.agent.resource.tool.base import BaseTool, ToolParameter
def _tool(name: str, desc: str) -> BaseTool:
class _T(BaseTool): # type: ignore[misc]
@property
def name(self_inner) -> str:
return name
@property
def description(self_inner) -> str:
return desc
@property
def args(self_inner) -> Dict[str, ToolParameter]:
return {}
async def _arun(self_inner, *a, **kw): # pragma: no cover
return None
def _run(self_inner, *a, **kw): # pragma: no cover
return None
return _T()
pack = MagicMock()
pack.name = "arxiv-search"
# Two physical entries for the same logical tool — exactly what the
# production bug produced (one prefixed via _apply_tool_prefix, one raw).
pack.sub_resources = [
_tool("mcp__arxiv-search__search_papers", "search (via ArXiv-Search)"),
_tool("search_papers", "search"),
_tool("mcp__arxiv-search__list_authors", "list authors (via ArXiv-Search)"),
_tool("list_authors", "list authors"),
]
manager = ConnectorManager()
manager._active_packs["c-dup"] = pack
manager._statuses["c-dup"] = ConnectorStatus.active
manager._connector_types["c-dup"] = "custom_mcp"
summary = manager._tool_summary_for("c-dup")
assert summary is not None
# Dedup: two logical tools, not four physical entries.
assert len(summary) == 2
# The prefixed (canonical routing) version wins.
by_original = {t["original_name"]: t for t in summary}
assert by_original["search_papers"]["name"] == "mcp__arxiv-search__search_papers"
assert by_original["list_authors"]["name"] == "mcp__arxiv-search__list_authors"
def test_tool_summary_for_keeps_raw_when_only_raw_present():
"""If only the raw (unprefixed) version exists, it's still surfaced."""
from kamidso.engine.core.agent.resource.connector.manager import (
ConnectorManager,
ConnectorStatus,
)
from kamidso.engine.core.agent.resource.tool.base import BaseTool, ToolParameter
class _Raw(BaseTool): # type: ignore[misc]
@property
def name(self) -> str:
return "search_papers"
@property
def description(self) -> str:
return "raw"
@property
def args(self) -> Dict[str, ToolParameter]:
return {}
async def _arun(self, *a, **kw): # pragma: no cover
return None
def _run(self, *a, **kw): # pragma: no cover
return None
pack = MagicMock()
pack.name = "x"
pack.sub_resources = [_Raw()]
manager = ConnectorManager()
manager._active_packs["c-raw"] = pack
manager._statuses["c-raw"] = ConnectorStatus.active
manager._connector_types["c-raw"] = "custom_mcp"
summary = manager._tool_summary_for("c-raw")
assert summary == [
{
"name": "search_papers",
"original_name": "search_papers",
"description": "raw",
"args": {},
}
]
def test_strip_routing_prefix_unit():
"""The ``_strip_routing_prefix`` helper correctly peels ``mcp__{prefix}__``."""
from kamidso.engine.core.agent.resource.connector.manager import _strip_routing_prefix
assert _strip_routing_prefix("mcp__github__create_issue") == "create_issue"
assert _strip_routing_prefix("mcp__github-acme__create_issue") == "create_issue"
assert _strip_routing_prefix("mcp__my-arxiv__search_papers") == "search_papers"
# Tool name with underscores survives (split with maxsplit=2).
assert (
_strip_routing_prefix("mcp__svc__a_long_tool_name_with_underscores")
== "a_long_tool_name_with_underscores"
)
# No prefix → unchanged.
assert _strip_routing_prefix("search_papers") == "search_papers"
# Looks-like but malformed → unchanged.
assert _strip_routing_prefix("mcp__") == "mcp__"
assert _strip_routing_prefix("mcp__only_one_segment") == "mcp__only_one_segment"
def test_list_tools_unknown_connector_id_raises_404(service: ConnectorService):
"""Querying tools for a non-existent connector raises ``HTTPException(404)``."""
from fastapi import HTTPException
with pytest.raises(HTTPException) as exc_info:
service.list_tools("non-existent-id")
assert exc_info.value.status_code == 404
def test_list_tools_inactive_when_pack_not_in_manager(
service: ConnectorService, monkeypatch
):
"""Connector exists in DB but no live pack -> ``state="inactive"``, empty tools."""
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="inactive-1",
connector_type="github",
display_name="Inactive Conn",
status="disconnected",
)
)
fake_manager = MagicMock()
fake_manager._tool_summary_for = MagicMock(return_value=None)
fake_manager._statuses = {}
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = service.list_tools("inactive-1")
assert result.connector_id == "inactive-1"
assert result.state == "inactive"
assert result.tools == []
def test_list_tools_inactive_when_manager_unavailable(
service: ConnectorService, monkeypatch
):
"""Manager component missing -> still returns ``state="inactive"`` (no crash)."""
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="no-manager-1",
connector_type="github",
display_name="Conn",
status="active",
)
)
def _fake_get_component(name, component_type, default_component=None):
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = service.list_tools("no-manager-1")
assert result.state == "inactive"
assert result.tools == []
def test_list_tools_active_returns_tools_when_pack_present(
service: ConnectorService, monkeypatch
):
"""Active pack + active status -> ``state="active"`` with tool list."""
from kamidso.engine.core.agent.resource.connector.manager import ConnectorStatus
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="active-1",
connector_type="github",
display_name="Active Conn",
status="active",
)
)
fake_manager = MagicMock()
fake_manager._tool_summary_for = MagicMock(
return_value=[
{
"name": "github_search",
"description": "Search GitHub",
"args": {
"query": {
"type": "string",
"required": True,
"description": "search query",
}
},
}
]
)
fake_manager._statuses = {"active-1": ConnectorStatus.active}
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = service.list_tools("active-1")
assert result.state == "active"
assert len(result.tools) == 1
assert result.tools[0]["name"] == "github_search"
assert result.tools[0]["args"]["query"]["required"] is True
def test_list_tools_pack_present_but_status_not_active_is_inactive(
service: ConnectorService, monkeypatch
):
"""Pack registered but ``_statuses`` reports non-active -> ``state="inactive"``."""
from kamidso.engine.core.agent.resource.connector.manager import ConnectorStatus
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="degraded-1",
connector_type="github",
display_name="Degraded",
status="error",
)
)
fake_manager = MagicMock()
fake_manager._tool_summary_for = MagicMock(
return_value=[{"name": "x", "description": "y", "args": {}}]
)
fake_manager._statuses = {"degraded-1": ConnectorStatus.error}
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = service.list_tools("degraded-1")
# Pack is present so tools are returned, but status is not "active".
assert result.state == "inactive"
assert len(result.tools) == 1
def test_list_tools_truncates_args_over_8kb(service: ConnectorService, monkeypatch):
"""Tools whose args JSON exceeds 8KB are replaced with the truncation marker."""
from kamidso.engine.core.agent.resource.connector.manager import ConnectorStatus
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="huge-1",
connector_type="github",
display_name="Huge",
status="active",
)
)
huge_args = {
f"arg_{i}": {
"type": "string",
"required": False,
"description": "x" * 200,
}
for i in range(60)
}
fake_manager = MagicMock()
fake_manager._tool_summary_for = MagicMock(
return_value=[{"name": "huge_tool", "description": "huge", "args": huge_args}]
)
fake_manager._statuses = {"huge-1": ConnectorStatus.active}
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
result = service.list_tools("huge-1")
assert result.state == "active"
assert len(result.tools) == 1
args = result.tools[0]["args"]
assert args.get("_truncated") is True
assert args.get("byte_count", 0) > 8192
def test_list_tools_p95_latency_under_100ms_for_50_tools(
service: ConnectorService, monkeypatch
):
"""End-to-end p95 latency for a 50-tool synthetic pack stays under 100ms."""
import time
from kamidso.engine.core.agent.resource.connector.manager import ConnectorStatus
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="perf-1",
connector_type="github",
display_name="Perf",
status="active",
)
)
fifty_tools = [
{
"name": f"tool_{i}",
"description": f"description {i}",
"args": {
f"arg_{j}": {
"type": "string",
"required": j == 0,
"description": f"arg {j} of tool {i}",
}
for j in range(5)
},
}
for i in range(50)
]
fake_manager = MagicMock()
fake_manager._tool_summary_for = MagicMock(return_value=fifty_tools)
fake_manager._statuses = {"perf-1": ConnectorStatus.active}
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
samples_ms: List[float] = []
for _ in range(50):
# Fresh copy each iteration; the service mutates the args dict in place
# when applying the 8KB cap, so reusing the same list across calls would
# also work but copying keeps the test self-contained.
fake_manager._tool_summary_for = MagicMock(
return_value=[dict(t, args=dict(t["args"])) for t in fifty_tools]
)
start = time.perf_counter()
service.list_tools("perf-1")
samples_ms.append((time.perf_counter() - start) * 1000.0)
samples_ms.sort()
p95 = samples_ms[int(len(samples_ms) * 0.95) - 1]
assert p95 < 100.0, f"p95={p95:.2f}ms exceeds 100ms budget"
def _enable_didar(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KAMIDSO_NATIVE_CONNECTORS_V1", "1")
monkeypatch.setenv("KAMIDSO_DIDAR_CONNECTOR", "1")
from kamidso.native_connectors.registry import reset_registry
reset_registry()
def test_native_create_persists_without_mcp_and_encrypts_api_key(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
_enable_didar(monkeypatch)
from kamidso.native_connectors.executor import NativeConnectorExecutor
async def _probe(self, provider_id, credentials):
assert credentials["api_key"] == "didar-secret"
return {"success": True, "message": "ok"}
monkeypatch.setattr(NativeConnectorExecutor, "probe", _probe)
fake_manager = MagicMock()
fake_manager.create_connector = AsyncMock()
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
response = service.create_connector(
ConnectorCreateRequest(
connector_type="didar",
display_name="دیدار فروش",
credentials={"api_key": "didar-secret"},
user_name="usr_test",
)
)
fake_manager.create_connector.assert_not_called()
assert response.status == "active"
assert "didar-secret" not in response.model_dump_json()
with db.session() as session:
entity = (
session.query(ConnectorInstanceEntity)
.filter(ConnectorInstanceEntity.connector_id == response.connector_id)
.first()
)
assert entity is not None
assert entity.connector_type == "didar"
assert "didar-secret" not in (entity.encrypted_credentials or "")
store = CredentialStore(system_app=service.system_app)
assert store.decrypt(entity.encrypted_credentials, entity.encryption_salt) == {
"api_key": "didar-secret"
}
tools = service.list_tools(response.connector_id)
assert tools.state == "not_mcp"
assert tools.tools == []
functions = service.list_native_functions(response.connector_id)
assert functions["provider"] == "didar"
assert any(item["name"] == "search_deals" and item["enabled"] for item in functions["functions"])
assert "input_schema" not in json.dumps(functions)
def test_native_create_does_not_persist_when_health_check_fails(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
_enable_didar(monkeypatch)
from kamidso.native_connectors.executor import NativeConnectorExecutor
async def _probe(self, provider_id, credentials):
return {"success": False, "message": "invalid api key"}
monkeypatch.setattr(NativeConnectorExecutor, "probe", _probe)
with pytest.raises(ValueError, match="invalid api key"):
service.create_connector(
ConnectorCreateRequest(
connector_type="didar",
display_name="دیدار فروش",
credentials={"api_key": "bad"},
)
)
with db.session() as session:
assert session.query(ConnectorInstanceEntity).count() == 0
def test_native_create_requires_durable_key_in_production(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
_enable_didar(monkeypatch)
monkeypatch.setenv("KAMIDSO_ENV", "production")
monkeypatch.setattr(service._credential_store, "has_durable_key", lambda: False)
from kamidso.native_connectors.executor import NativeConnectorExecutor
called = {"probe": False}
async def _probe(self, provider_id, credentials):
called["probe"] = True
return {"success": True, "message": "ok"}
monkeypatch.setattr(NativeConnectorExecutor, "probe", _probe)
with pytest.raises(ValueError, match="ENCRYPT_KEY"):
service.create_connector(
ConnectorCreateRequest(
connector_type="didar",
display_name="دیدار فروش",
credentials={"api_key": "didar-secret"},
)
)
assert called["probe"] is False
def test_after_start_skips_native_connectors(
service: ConnectorService, monkeypatch: pytest.MonkeyPatch
):
_enable_didar(monkeypatch)
store = CredentialStore(system_app=service.system_app)
salt = store.generate_salt()
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="didar-1",
connector_type="didar",
display_name="دیدار فروش",
encrypted_credentials=store.encrypt({"api_key": "didar-secret"}, salt),
encryption_salt=salt,
status="active",
)
)
fake_manager = MagicMock()
fake_manager.create_connector = AsyncMock()
def _fake_get_component(name, component_type, default_component=None):
if name == "connector_manager":
return fake_manager
return default_component
monkeypatch.setattr(service.system_app, "get_component", _fake_get_component)
service.after_start()
fake_manager.create_connector.assert_not_called()
def test_retire_connector_keeps_the_row(service: ConnectorService):
with db.session() as session:
session.add(
ConnectorInstanceEntity(
connector_id="didar-keep",
connector_type="didar",
display_name="دیدار فروش",
status="active",
user_name="usr_1",
)
)
service.retire_connector("didar-keep")
kept = service.get_connector("didar-keep")
assert kept is not None
assert kept.status == "deleted"