Files
hermes-agent/tests/tools/test_connector_names.py
T
Siddharth Balyan d105376b21 Connector code lives in one package, tools/connectors/ (move only; NS-868 prep) (#110368)
* refactor(tools): discovery also scans tools/<pkg>/tool.py

A tool family that is a whole package had no way to register: discovery
globbed tools/*.py only and derived the module name from the filename.
Now the candidate list is tools/*.py plus tools/*/tool.py, merged and
sorted once so import order does not depend on depth (register() lets a
same-name duplicate overwrite silently), and the module name comes from
the path relative to tools/. Only tool.py is scanned inside a package,
so its siblings are libraries by construction. A package without an
__init__.py is skipped with a warning rather than registering from a
checkout and vanishing from the installed wheel.

The AST prefilter and the (mtime, size) disk cache are per absolute path
and work unchanged. The two hand-rolled tools/*.py enumerators in tests
now use the same candidate helper.

* refactor(connectors): one package for the connector domain, tools/connectors/

The connector code was spread across six flat files and a root-level
module that was a sibling of model_tools.py only by address:

  tools/connections_tool.py            -> tools/connectors/tool.py     (schema, register, dispatcher)
                                          tools/connectors/managed.py  (the managed leg, split out)
  tools/connections_tool_mcp.py        -> tools/connectors/mcp.py      (validation split out ->)
                                          tools/connectors/targets.py  (normalize_targets, validate_action)
  tools/connections_tool_operation.py  -> tools/connectors/operation.py
  tools/connector_search.py            -> tools/connectors/search.py
  model_tools_connectors.py            -> tools/connectors/dispatch.py
  tools/tool_gateway/                  -> tools/connectors/gateway/

Move only; every function body is unchanged. tools/connectors/__init__.py
is the door: nine names, the whole cross-package surface. model_tools and
tool_search deep-import a few helpers past it on purpose and the docstring
says so. The two split files make the import graph one-directional
(tool -> mcp -> targets, tool -> managed) where the old layout had
connections_tool importing validation out of the MCP file.

One behaviour-neutral seam change: the _connectors_available try/except
wrapper is gone. connectors_available() already fails closed, and both
the registry handler and the inline executor now read it as a module
attribute (gateway.config.connectors_available), so tests patch it in
one place instead of two. _default_client lives in managed.py, the only
module that calls it.

tools/managed_tool_gateway.py and tools/managed_gateway_auth.py stay:
they are gateway identity shared by tts, transcription, image and modal.

Test files follow their modules. No docs referenced the old paths; no
compat pointer is added (in-tree moves get none).

* ci: retrigger (zero-job dispatch on 142466de6b)
2026-09-15 00:41:13 +05:30

71 lines
1.7 KiB
Python

"""Invariant tests for model-facing connector names and wire-slug recovery."""
import pytest
from tools.connectors.gateway.names import (
format_connector_name,
parse_connector_name,
vendor_slug_candidates,
)
ENCODE_CASES = [
(
"gmail",
"GMAIL_GET_PROFILE",
"connectors__gmail__GET_PROFILE",
True,
),
(
"better_stack_mcp",
"BETTER_STACK_MCP_ACKNOWLEDGE_INCIDENT",
"connectors__better_stack_mcp__ACKNOWLEDGE_INCIDENT",
True,
),
(
"gmail",
"FETCH_PROFILE",
"connectors__gmail__FETCH_PROFILE",
False,
),
(
"gmail",
"GMAIL",
"connectors__gmail__GMAIL",
False,
),
(
"granola",
"GRANOLA_MCP_GET_MEETINGS",
"connectors__granola__MCP_GET_MEETINGS",
True,
),
]
@pytest.mark.parametrize("connector,vendor_slug,composed,_stripped", ENCODE_CASES)
def test_format_connector_name_strips_only_the_exact_toolkit_prefix(
connector, vendor_slug, composed, _stripped
):
assert format_connector_name(connector, vendor_slug) == composed
def test_vendor_slug_candidates_are_prefixed_then_literal():
assert vendor_slug_candidates("gmail", "GET_PROFILE") == (
"GMAIL_GET_PROFILE",
"GET_PROFILE",
)
@pytest.mark.parametrize("connector,vendor_slug,composed,stripped", ENCODE_CASES)
def test_every_encoded_vendor_slug_has_a_wire_recovery_candidate(
connector, vendor_slug, composed, stripped
):
parsed = parse_connector_name(composed)
assert parsed is not None
candidates = vendor_slug_candidates(parsed.connector, parsed.tool)
assert vendor_slug in candidates
if stripped:
assert candidates[0] == vendor_slug