fix: tool tests

This commit is contained in:
bryan
2026-03-04 09:22:34 -08:00
parent 873af04c6e
commit 67d094f51a
4 changed files with 309 additions and 60 deletions
+107 -17
View File
@@ -39,6 +39,7 @@ logger = logging.getLogger(__name__)
CLAUDE_CREDENTIALS_FILE = Path.home() / ".claude" / ".credentials.json"
CLAUDE_OAUTH_TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"
CLAUDE_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials"
# Buffer in seconds before token expiry to trigger a proactive refresh
_TOKEN_REFRESH_BUFFER_SECS = 300 # 5 minutes
@@ -51,6 +52,96 @@ CODEX_KEYCHAIN_SERVICE = "Codex Auth"
_CODEX_TOKEN_LIFETIME_SECS = 3600 # 1 hour (no explicit expiry field)
def _read_claude_keychain() -> dict | None:
"""Read Claude Code credentials from macOS Keychain.
Returns the parsed JSON dict, or None if not on macOS or entry missing.
"""
import getpass
import platform
import subprocess
if platform.system() != "Darwin":
return None
try:
account = getpass.getuser()
result = subprocess.run(
[
"security",
"find-generic-password",
"-s",
CLAUDE_KEYCHAIN_SERVICE,
"-a",
account,
"-w",
],
capture_output=True,
encoding="utf-8",
timeout=5,
)
if result.returncode != 0:
return None
raw = result.stdout.strip()
if not raw:
return None
return json.loads(raw)
except (subprocess.TimeoutExpired, json.JSONDecodeError, OSError) as exc:
logger.debug("Claude keychain read failed: %s", exc)
return None
def _save_claude_keychain(creds: dict) -> bool:
"""Write Claude Code credentials to macOS Keychain. Returns True on success."""
import getpass
import platform
import subprocess
if platform.system() != "Darwin":
return False
try:
account = getpass.getuser()
data = json.dumps(creds)
result = subprocess.run(
[
"security",
"add-generic-password",
"-U",
"-s",
CLAUDE_KEYCHAIN_SERVICE,
"-a",
account,
"-w",
data,
],
capture_output=True,
timeout=5,
)
return result.returncode == 0
except (subprocess.TimeoutExpired, OSError) as exc:
logger.debug("Claude keychain write failed: %s", exc)
return False
def _read_claude_credentials() -> dict | None:
"""Read Claude Code credentials from Keychain (macOS) or file (Linux/Windows)."""
# Try macOS Keychain first
creds = _read_claude_keychain()
if creds:
return creds
# Fall back to file
if not CLAUDE_CREDENTIALS_FILE.exists():
return None
try:
with open(CLAUDE_CREDENTIALS_FILE, encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return None
def _refresh_claude_code_token(refresh_token: str) -> dict | None:
"""Refresh the Claude Code OAuth token using the refresh token.
@@ -89,16 +180,14 @@ def _refresh_claude_code_token(refresh_token: str) -> dict | None:
def _save_refreshed_credentials(token_data: dict) -> None:
"""Write refreshed token data back to ~/.claude/.credentials.json."""
"""Write refreshed token data back to Keychain (macOS) or credentials file."""
import time
if not CLAUDE_CREDENTIALS_FILE.exists():
creds = _read_claude_credentials()
if not creds:
return
try:
with open(CLAUDE_CREDENTIALS_FILE, encoding="utf-8") as f:
creds = json.load(f)
oauth = creds.get("claudeAiOauth", {})
oauth["accessToken"] = token_data["access_token"]
if "refresh_token" in token_data:
@@ -107,9 +196,15 @@ def _save_refreshed_credentials(token_data: dict) -> None:
oauth["expiresAt"] = int((time.time() + token_data["expires_in"]) * 1000)
creds["claudeAiOauth"] = oauth
with open(CLAUDE_CREDENTIALS_FILE, "w", encoding="utf-8") as f:
json.dump(creds, f, indent=2)
logger.debug("Claude Code credentials refreshed successfully")
# Try Keychain first (macOS), fall back to file
if _save_claude_keychain(creds):
logger.debug("Claude Code credentials refreshed in Keychain")
return
if CLAUDE_CREDENTIALS_FILE.exists():
with open(CLAUDE_CREDENTIALS_FILE, "w", encoding="utf-8") as f:
json.dump(creds, f, indent=2)
logger.debug("Claude Code credentials refreshed in file")
except (json.JSONDecodeError, OSError, KeyError) as exc:
logger.debug("Failed to save refreshed credentials: %s", exc)
@@ -117,8 +212,8 @@ def _save_refreshed_credentials(token_data: dict) -> None:
def get_claude_code_token() -> str | None:
"""Get the OAuth token from Claude Code subscription with auto-refresh.
Reads from ~/.claude/.credentials.json which is created by the
Claude Code CLI when users authenticate with their subscription.
Reads from macOS Keychain (on Darwin) or ~/.claude/.credentials.json
(on Linux/Windows), as created by the Claude Code CLI.
If the token is expired or close to expiry, attempts an automatic
refresh using the stored refresh token.
@@ -128,13 +223,8 @@ def get_claude_code_token() -> str | None:
"""
import time
if not CLAUDE_CREDENTIALS_FILE.exists():
return None
try:
with open(CLAUDE_CREDENTIALS_FILE, encoding="utf-8") as f:
creds = json.load(f)
except (json.JSONDecodeError, OSError):
creds = _read_claude_credentials()
if not creds:
return None
oauth = creds.get("claudeAiOauth", {})
+156 -22
View File
@@ -1260,33 +1260,167 @@ class IntercomHealthChecker(OAuthBearerHealthChecker):
)
# --- Simple Bearer-auth checkers ---
class ApifyHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://api.apify.com/v2/users/me"
SERVICE_NAME = "Apify"
class AsanaHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://app.asana.com/api/1.0/users/me"
SERVICE_NAME = "Asana"
class AttioHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://api.attio.com/v2/workspace_members"
SERVICE_NAME = "Attio"
class DockerHubHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://hub.docker.com/v2/user/login"
SERVICE_NAME = "Docker Hub"
class GoogleSearchConsoleHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://www.googleapis.com/webmasters/v3/sites"
SERVICE_NAME = "Google Search Console"
class HuggingFaceHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://huggingface.co/api/whoami-v2"
SERVICE_NAME = "Hugging Face"
class LinearHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://api.linear.app/graphql"
SERVICE_NAME = "Linear"
class MicrosoftGraphHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://graph.microsoft.com/v1.0/me"
SERVICE_NAME = "Microsoft Graph"
class PineconeHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://api.pinecone.io/indexes"
SERVICE_NAME = "Pinecone"
class VercelHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://api.vercel.com/v2/user"
SERVICE_NAME = "Vercel"
# --- Custom-header auth checkers ---
class GitLabHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://gitlab.com/api/v4/user"
SERVICE_NAME = "GitLab"
AUTH_TYPE = BaseHttpHealthChecker.AUTH_HEADER
AUTH_HEADER_NAME = "PRIVATE-TOKEN"
AUTH_HEADER_TEMPLATE = "{token}"
class NotionHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://api.notion.com/v1/users/me"
SERVICE_NAME = "Notion"
def _build_headers(self, credential_value: str) -> dict[str, str]:
headers = super()._build_headers(credential_value)
headers["Notion-Version"] = "2022-06-28"
return headers
# --- Basic-auth checkers ---
class GreenhouseHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://harvest.greenhouse.io/v1/jobs?per_page=1"
SERVICE_NAME = "Greenhouse"
AUTH_TYPE = BaseHttpHealthChecker.AUTH_BASIC
# --- Query-param auth checkers ---
class PipedriveHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://api.pipedrive.com/v1/users/me"
SERVICE_NAME = "Pipedrive"
AUTH_TYPE = BaseHttpHealthChecker.AUTH_QUERY
AUTH_QUERY_PARAM_NAME = "api_token"
class TrelloKeyHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://api.trello.com/1/members/me"
SERVICE_NAME = "Trello"
AUTH_TYPE = BaseHttpHealthChecker.AUTH_QUERY
AUTH_QUERY_PARAM_NAME = "key"
class TrelloTokenHealthChecker(BaseHttpHealthChecker):
ENDPOINT = "https://api.trello.com/1/members/me"
SERVICE_NAME = "Trello"
AUTH_TYPE = BaseHttpHealthChecker.AUTH_QUERY
AUTH_QUERY_PARAM_NAME = "token"
class YouTubeHealthChecker(BaseHttpHealthChecker):
ENDPOINT = (
"https://www.googleapis.com/youtube/v3/videoCategories"
"?part=snippet&regionCode=US"
)
SERVICE_NAME = "YouTube"
AUTH_TYPE = BaseHttpHealthChecker.AUTH_QUERY
AUTH_QUERY_PARAM_NAME = "key"
# Registry of health checkers
HEALTH_CHECKERS: dict[str, CredentialHealthChecker] = {
"discord": DiscordHealthChecker(),
"hubspot": HubSpotHealthChecker(),
"zoho_crm": ZohoCRMHealthChecker(),
"brave_search": BraveSearchHealthChecker(),
"google_calendar_oauth": GoogleCalendarHealthChecker(),
"google": GoogleGmailHealthChecker(),
"slack": SlackHealthChecker(),
"calendly_pat": CalendlyHealthChecker(),
"google_search": GoogleSearchHealthChecker(),
"google_maps": GoogleMapsHealthChecker(),
"anthropic": AnthropicHealthChecker(),
"github": GitHubHealthChecker(),
"intercom": IntercomHealthChecker(),
"resend": ResendHealthChecker(),
"lusha_api_key": LushaHealthChecker(),
"stripe": StripeHealthChecker(),
"exa_search": ExaSearchHealthChecker(),
"google_docs": GoogleDocsHealthChecker(),
"calcom": CalcomHealthChecker(),
"serpapi": SerpApiHealthChecker(),
"apify": ApifyHealthChecker(),
"apollo": ApolloHealthChecker(),
"telegram": TelegramHealthChecker(),
"newsdata": NewsdataHealthChecker(),
"finlight": FinlightHealthChecker(),
"asana": AsanaHealthChecker(),
"attio": AttioHealthChecker(),
"brave_search": BraveSearchHealthChecker(),
"brevo": BrevoHealthChecker(),
"calcom": CalcomHealthChecker(),
"calendly_pat": CalendlyHealthChecker(),
"discord": DiscordHealthChecker(),
"docker_hub": DockerHubHealthChecker(),
"exa_search": ExaSearchHealthChecker(),
"finlight": FinlightHealthChecker(),
"github": GitHubHealthChecker(),
"gitlab_token": GitLabHealthChecker(),
"google": GoogleGmailHealthChecker(),
"google_calendar_oauth": GoogleCalendarHealthChecker(),
"google_docs": GoogleDocsHealthChecker(),
"google_maps": GoogleMapsHealthChecker(),
"google_search": GoogleSearchHealthChecker(),
"google_search_console": GoogleSearchConsoleHealthChecker(),
"greenhouse_token": GreenhouseHealthChecker(),
"hubspot": HubSpotHealthChecker(),
"huggingface": HuggingFaceHealthChecker(),
"intercom": IntercomHealthChecker(),
"linear": LinearHealthChecker(),
"lusha_api_key": LushaHealthChecker(),
"microsoft_graph": MicrosoftGraphHealthChecker(),
"newsdata": NewsdataHealthChecker(),
"notion_token": NotionHealthChecker(),
"pinecone": PineconeHealthChecker(),
"pipedrive": PipedriveHealthChecker(),
"resend": ResendHealthChecker(),
"serpapi": SerpApiHealthChecker(),
"slack": SlackHealthChecker(),
"stripe": StripeHealthChecker(),
"telegram": TelegramHealthChecker(),
"trello_key": TrelloKeyHealthChecker(),
"trello_token": TrelloTokenHealthChecker(),
"vercel": VercelHealthChecker(),
"youtube": YouTubeHealthChecker(),
"zoho_crm": ZohoCRMHealthChecker(),
}
+9 -1
View File
@@ -20,7 +20,15 @@ class TestRegistryCompleteness:
# - google_cse: shares google_search checker (same credential_group)
# - razorpay/razorpay_secret: requires HTTP Basic auth with TWO credentials,
# which the single-value health check dispatcher can't support
KNOWN_EXCEPTIONS = {"google_cse", "razorpay", "razorpay_secret"}
# - plaid_client_id/plaid_secret: requires POST with both client_id and
# secret in JSON body, can't validate with a single credential value
KNOWN_EXCEPTIONS = {
"google_cse",
"razorpay",
"razorpay_secret",
"plaid_client_id",
"plaid_secret",
}
def test_specs_with_endpoint_have_checkers(self):
"""Every CredentialSpec with health_check_endpoint has a HEALTH_CHECKERS entry."""
+37 -20
View File
@@ -74,30 +74,47 @@ class TestHealthCheckerRegistry:
def test_all_expected_checkers_registered(self):
"""All expected health checkers are in the registry."""
expected = {
"hubspot",
"brave_search",
"google_search",
"google_maps",
"anthropic",
"github",
"intercom",
"resend",
"google_calendar_oauth",
"google",
"slack",
"lusha_api_key",
"discord",
"stripe",
"exa_search",
"google_docs",
"calcom",
"serpapi",
"apify",
"apollo",
"telegram",
"newsdata",
"finlight",
"asana",
"attio",
"brave_search",
"brevo",
"calcom",
"calendly_pat",
"discord",
"docker_hub",
"exa_search",
"finlight",
"github",
"gitlab_token",
"google",
"google_calendar_oauth",
"google_docs",
"google_maps",
"google_search",
"google_search_console",
"greenhouse_token",
"hubspot",
"huggingface",
"intercom",
"linear",
"lusha_api_key",
"microsoft_graph",
"newsdata",
"notion_token",
"pinecone",
"pipedrive",
"resend",
"serpapi",
"slack",
"stripe",
"telegram",
"trello_key",
"trello_token",
"vercel",
"youtube",
"zoho_crm",
}
assert set(HEALTH_CHECKERS.keys()) == expected