Files
arc53-machine 7b516ade82 Add a built-in GitHub connector for repository sync and read-only tools
One GitHub connection feeds both a Knowledge source and an agent tool.
Users connect with a personal access token, checked against GitHub and
named after the account, or, when an admin registers a GitHub App
(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_APP_SLUG), with Sign in
with GitHub. App tokens expire after eight hours and are refreshed before
each sync or tool call; tokens with no expiry are never treated as expired.

The catalog gains an optional second sign-in method (oauth_settings,
exposed as sign_in_methods) without changing any other connector.

Sync lists the repositories the connection can read (the token's own, or
the App installations') and ingests one with the connection's token. The
tool is GitHub's read-only MCP server (api.githubcopilot.com/mcp/readonly):
setup discovers its actions and creates it bound to the connection, and
the executor sends the connection's token only to that server.
2026-09-29 10:41:03 +01:00

130 lines
5.4 KiB
Python

"""Tests for the connector catalog."""
from __future__ import annotations
import pytest
from docsgpt.connectors import catalog
@pytest.fixture(autouse=True)
def _fresh_registry():
catalog.reset_registry_for_tests()
yield
catalog.reset_registry_for_tests()
class TestDefinitions:
def test_built_ins_are_registered(self):
keys = {d.key for d in catalog.all_definitions()}
assert {
"google_drive", "share_point", "confluence", "s3", "reddit",
"brave", "telegram", "ntfy", "postgres", "custom_mcp", "custom_openapi",
} <= keys
def test_every_definition_uses_known_values(self):
for definition in catalog.all_definitions():
assert definition.category in catalog.CATEGORIES, definition.key
assert definition.auth_kind in catalog.AUTH_KINDS, definition.key
assert definition.publisher in catalog.PUBLISHERS, definition.key
assert set(definition.capabilities) <= {"sync", "read", "write"}, definition.key
def test_tool_connectors_match_tool_config_requirements(self):
"""Credential fields must be the keys the tool reads its secrets from."""
import docsgpt.api.user # noqa: F401 (mcp_tool imports it; load it first)
from docsgpt.agents.tools.tool_manager import ToolManager
tools = ToolManager(config={}).tools
for definition in catalog.all_definitions():
if definition.publisher != "built_in":
continue
for tool_name in definition.tool_templates:
if tool_name == "mcp_tool":
# GitHub's MCP server gets the connection's token as a bearer token.
continue
requirements = tools[tool_name].get_config_requirements()
secret_keys = {k for k, spec in requirements.items() if spec.get("secret")}
field_keys = {f.key for f in definition.credential_fields}
assert secret_keys <= field_keys, definition.key
def test_to_dict_has_no_server_secrets(self):
payload = catalog.get_definition("google_drive").to_dict()
assert "required_settings" not in payload
assert payload["capabilities"] == ["sync"]
class TestAvailability:
def test_missing_settings_listed(self, monkeypatch):
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", None)
monkeypatch.setattr(settings, "GOOGLE_CLIENT_SECRET", "secret")
definition = catalog.get_definition("google_drive")
assert definition.missing_settings == ["GOOGLE_CLIENT_ID"]
assert not definition.configured
def test_configured_when_all_set(self, monkeypatch):
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", "id")
monkeypatch.setattr(settings, "GOOGLE_CLIENT_SECRET", "secret")
assert catalog.get_definition("google_drive").configured
def test_api_key_connectors_need_no_settings(self):
assert catalog.get_definition("telegram").configured
class TestRowMapping:
def test_oauth_provider_maps_to_key(self):
assert catalog.connector_key_for_row({"provider": "share_point"}) == "share_point"
def test_legacy_mcp_row_is_custom(self):
row = {"provider": "mcp:https://mcp.unknown.dev", "server_url": "https://mcp.unknown.dev"}
assert catalog.connector_key_for_row(row) == "custom_mcp"
def test_stored_key_wins(self):
assert catalog.connector_key_for_row({"provider": "telegram", "connector_key": "telegram"}) == "telegram"
def test_unknown_provider(self):
assert catalog.connector_key_for_row({"provider": "nope"}) is None
def test_definition_for_tool(self):
assert catalog.definition_for_tool("telegram").key == "telegram"
assert catalog.definition_for_tool("memory") is None
assert catalog.definition_for_tool("mcp_tool") is None
class TestPresets:
def test_presets_load_from_yaml(self, tmp_path, monkeypatch):
presets = tmp_path / "mcp.yaml"
presets.write_text(
"- key: mcp:example\n"
" name: Example\n"
" description: Example records.\n"
" icon: example\n"
" category: knowledge\n"
" mcp_url: https://mcp.example.com/mcp\n"
" auth_kind: mcp_oauth\n"
)
monkeypatch.setattr(catalog, "_PRESETS_FILE", presets)
definition = catalog.get_definition("mcp:example")
assert definition.publisher == "preset"
assert definition.mcp_base_url == "https://mcp.example.com"
assert catalog.preset_for_url("https://mcp.example.com/other").key == "mcp:example"
row = {"provider": "mcp:https://mcp.example.com", "server_url": "https://mcp.example.com"}
assert catalog.connector_key_for_row(row) == "mcp:example"
def test_base_url(self):
assert catalog.base_url("https://a.example.com:8443/x/y") == "https://a.example.com:8443"
assert catalog.base_url("not a url") == ""
def test_atlassian_preset_is_part_of_confluence():
"""One Confluence card: syncing pages and the Jira/Confluence agent actions."""
from docsgpt.connectors import catalog
atlassian = catalog.get_definition("mcp:atlassian")
assert atlassian.part_of == "confluence"
assert atlassian.to_dict()["part_of"] == "confluence"
assert catalog.get_definition("confluence").to_dict()["part_of"] is None