removed send_budget_alert_email, require provider param

This commit is contained in:
bryan
2026-02-11 11:26:23 -08:00
parent 976ae75fde
commit 9df147b450
6 changed files with 123 additions and 368 deletions
-20
View File
@@ -1,20 +0,0 @@
---
name: hive
description: Hive Agent Builder & Manager
mode: primary
tools:
agent-builder: true
tools: true
---
# Hive Agent
You are the Hive Agent Builder. Your goal is to help the user construct, configure, and deploy AI agents using the Hive framework.
## Capabilities
1. **Scaffold Agents:** Create new agent directories/configs.
2. **Manage Tools:** Add/remove tools via MCP.
3. **Debug:** Analyze agent workflows.
## Context
- You are an expert in the Hive framework architecture.
- Always use the `agent-builder` MCP server for filesystem operations.
+2 -2
View File
@@ -9,7 +9,7 @@ from .base import CredentialSpec
EMAIL_CREDENTIALS = {
"resend": CredentialSpec(
env_var="RESEND_API_KEY",
tools=["send_email", "send_budget_alert_email"],
tools=["send_email"],
node_types=[],
required=False,
startup_required=False,
@@ -35,7 +35,7 @@ EMAIL_CREDENTIALS = {
),
"google": CredentialSpec(
env_var="GOOGLE_ACCESS_TOKEN",
tools=["send_email", "send_budget_alert_email"],
tools=["send_email"],
node_types=[],
required=False,
startup_required=False,
+1 -2
View File
@@ -75,7 +75,7 @@ def register_all_tools(
# web_search supports multiple providers (Google, Brave) with auto-detection
register_web_search(mcp, credentials=credentials)
register_github(mcp, credentials=credentials)
# email supports multiple providers (Resend) with auto-detection
# email supports multiple providers (Gmail, Resend)
register_email(mcp, credentials=credentials)
register_hubspot(mcp, credentials=credentials)
register_apollo(mcp, credentials=credentials)
@@ -137,7 +137,6 @@ def register_all_tools(
"github_get_user_profile",
"github_get_user_emails",
"send_email",
"send_budget_alert_email",
"hubspot_search_contacts",
"hubspot_get_contact",
"hubspot_create_contact",
+25 -19
View File
@@ -1,6 +1,8 @@
# Email Tool
Send emails using multiple providers. Currently supports Resend.
Send emails using multiple providers. Supports Gmail (via Google OAuth2) and Resend.
The `provider` parameter is required — you must explicitly choose `"gmail"` or `"resend"`.
## Tools
@@ -9,36 +11,40 @@ Send a general-purpose email.
**Parameters:**
- `to` (str | list[str]) - Recipient email address(es)
- `subject` (str) - Email subject line
- `subject` (str) - Email subject line (1-998 chars per RFC 2822)
- `html` (str) - Email body as HTML
- `from_email` (str, optional) - Sender address. Falls back to `EMAIL_FROM` env var
- `provider` ("auto" | "resend", optional) - Provider to use (default: "auto")
### `send_budget_alert_email`
Send a formatted budget alert notification.
**Parameters:**
- `to` (str | list[str]) - Recipient email address(es)
- `budget_name` (str) - Name of the budget
- `current_spend` (float) - Current spending amount
- `budget_limit` (float) - Budget limit amount
- `currency` (str, optional) - Currency code (default: "USD")
- `from_email` (str, optional) - Sender address. Falls back to `EMAIL_FROM` env var
- `provider` ("auto" | "resend", optional) - Provider to use
- `provider` ("gmail" | "resend") - Provider to use. Required.
- `from_email` (str, optional) - Sender address. Falls back to `EMAIL_FROM` env var. Optional for Gmail (defaults to the authenticated user's address)
- `cc` (str | list[str], optional) - CC recipient(s)
- `bcc` (str | list[str], optional) - BCC recipient(s)
## Setup
### Gmail (via Aden OAuth2)
Connect Gmail through hive.adenhq.com. The `GOOGLE_ACCESS_TOKEN` is provided automatically at runtime via the `CredentialStoreAdapter`.
### Resend
```bash
export RESEND_API_KEY=re_your_api_key_here
export EMAIL_FROM=notifications@yourdomain.com
```
- `RESEND_API_KEY` - Get an API key at: https://resend.com/api-keys
- `EMAIL_FROM` - Default sender address. Must be from a domain verified in your email provider
- `EMAIL_FROM` - Default sender address. Must be from a domain verified in your email provider. Required for Resend, optional for Gmail.
### Testing override
Set `EMAIL_OVERRIDE_TO` to redirect all outbound mail to a single address. The original recipients are prepended to the subject line for traceability.
```bash
export EMAIL_OVERRIDE_TO=you@example.com
```
## Adding a New Provider
1. Add a `_send_via_<provider>` function in `email_tool.py`
2. Add the provider's credential to `credentials/email.py`
3. Extend the `provider` Literal type and auto-detection logic
2. Add the provider's credential key to `_get_credential()`
3. Extend the `provider` Literal type in `_send_email_impl()`
4. Add tests for the new provider
@@ -4,8 +4,6 @@ Email Tool - Send emails using multiple providers.
Supports:
- Gmail (GOOGLE_ACCESS_TOKEN, via Aden OAuth2)
- Resend (RESEND_API_KEY)
Auto-detection: If provider="auto", tries Gmail first, then Resend.
"""
from __future__ import annotations
@@ -116,17 +114,16 @@ def register_tools(
"subject": subject,
}
def _get_credentials() -> dict:
"""Get available email credentials."""
def _get_credential(provider: Literal["resend", "gmail"]) -> str | None:
"""Get the credential for the requested provider."""
if provider == "gmail":
if credentials is not None:
return credentials.get("google")
return os.getenv("GOOGLE_ACCESS_TOKEN")
# resend
if credentials is not None:
return {
"resend_api_key": credentials.get("resend"),
"gmail_access_token": credentials.get("google"), # Google OAuth for Gmail
}
return {
"resend_api_key": os.getenv("RESEND_API_KEY"),
"gmail_access_token": os.getenv("GOOGLE_ACCESS_TOKEN"),
}
return credentials.get("resend")
return os.getenv("RESEND_API_KEY")
def _resolve_from_email(from_email: str | None) -> str | None:
"""Resolve sender address: explicit param > EMAIL_FROM env var."""
@@ -149,8 +146,8 @@ def register_tools(
to: str | list[str],
subject: str,
html: str,
provider: Literal["resend", "gmail"],
from_email: str | None = None,
provider: Literal["auto", "resend", "gmail"] = "auto",
cc: str | list[str] | None = None,
bcc: str | list[str] | None = None,
) -> dict:
@@ -178,70 +175,34 @@ def register_tools(
bcc_list = None
subject = f"[TEST -> {', '.join(original_to)}] {subject}"
creds = _get_credentials()
gmail_available = bool(creds["gmail_access_token"])
resend_available = bool(creds["resend_api_key"])
# Gmail doesn't require from_email (defaults to authenticated user).
# Resend always requires it.
needs_from_email = provider == "resend" or (
provider == "auto" and not gmail_available and resend_available
)
if not from_email and needs_from_email:
# Resend always requires from_email; Gmail defaults to authenticated user.
if provider == "resend" and not from_email:
return {
"error": "Sender email is required",
"help": "Pass from_email or set EMAIL_FROM environment variable",
}
try:
credential = _get_credential(provider)
if not credential:
if provider == "gmail":
if not gmail_available:
return {
"error": "Gmail credentials not configured",
"help": "Connect Gmail via hive.adenhq.com",
}
return _send_via_gmail(
creds["gmail_access_token"],
to_list,
subject,
html,
from_email,
cc_list,
bcc_list,
)
if provider == "resend":
if not resend_available:
return {
"error": "Resend credentials not configured",
"help": "Set RESEND_API_KEY environment variable. "
"Get a key at https://resend.com/api-keys",
}
return _send_via_resend(
creds["resend_api_key"], to_list, subject, html, from_email, cc_list, bcc_list
)
# auto: Gmail first (user's own identity), then Resend
if gmail_available:
return _send_via_gmail(
creds["gmail_access_token"],
to_list,
subject,
html,
from_email,
cc_list,
bcc_list,
)
if resend_available:
return _send_via_resend(
creds["resend_api_key"], to_list, subject, html, from_email, cc_list, bcc_list
)
return {
"error": "Gmail credentials not configured",
"help": "Connect Gmail via hive.adenhq.com",
}
return {
"error": "No email credentials configured",
"help": "Connect Gmail via hive.adenhq.com or set RESEND_API_KEY",
"error": "Resend credentials not configured",
"help": "Set RESEND_API_KEY environment variable. "
"Get a key at https://resend.com/api-keys",
}
try:
if provider == "gmail":
return _send_via_gmail(
credential, to_list, subject, html, from_email, cc_list, bcc_list
)
return _send_via_resend(
credential, to_list, subject, html, from_email, cc_list, bcc_list
)
except Exception as e:
return {"error": f"Email send failed: {e}"}
@@ -250,8 +211,8 @@ def register_tools(
to: str | list[str],
subject: str,
html: str,
provider: Literal["resend", "gmail"],
from_email: str | None = None,
provider: Literal["auto", "resend", "gmail"] = "auto",
cc: str | list[str] | None = None,
bcc: str | list[str] | None = None,
) -> dict:
@@ -259,7 +220,6 @@ def register_tools(
Send an email.
Supports multiple email providers:
- "auto": Tries Gmail first, then Resend (default)
- "gmail": Use Gmail API (requires Gmail OAuth2 via Aden)
- "resend": Use Resend API (requires RESEND_API_KEY)
@@ -267,9 +227,9 @@ def register_tools(
to: Recipient email address(es). Single string or list of strings.
subject: Email subject line (1-998 chars per RFC 2822).
html: Email body as HTML string.
provider: Email provider to use ("gmail" or "resend"). Required.
from_email: Sender email address. Falls back to EMAIL_FROM env var if not provided.
Optional for Gmail (defaults to authenticated user's address).
provider: Email provider to use ("auto", "gmail", or "resend").
cc: CC recipient(s). Single string or list of strings. Optional.
bcc: BCC recipient(s). Single string or list of strings. Optional.
@@ -277,74 +237,4 @@ def register_tools(
Dict with send result including provider used and message ID,
or error dict with "error" and optional "help" keys.
"""
return _send_email_impl(to, subject, html, from_email, provider, cc, bcc)
@mcp.tool()
def send_budget_alert_email(
to: str | list[str],
budget_name: str,
current_spend: float,
budget_limit: float,
currency: str = "USD",
from_email: str | None = None,
provider: Literal["auto", "resend", "gmail"] = "auto",
cc: str | list[str] | None = None,
bcc: str | list[str] | None = None,
) -> dict:
"""
Send a budget alert email notification.
Generates a formatted HTML email for budget threshold alerts
and sends it via the configured email provider.
Args:
to: Recipient email address(es).
budget_name: Name of the budget (e.g., "Marketing Q1").
current_spend: Current spending amount.
budget_limit: Budget limit amount.
currency: Currency code (default: "USD").
from_email: Sender email address. Falls back to EMAIL_FROM env var if not provided.
Optional for Gmail (defaults to authenticated user's address).
provider: Email provider to use ("auto", "gmail", or "resend").
cc: CC recipient(s). Single string or list of strings. Optional.
bcc: BCC recipient(s). Single string or list of strings. Optional.
Returns:
Dict with send result or error dict.
"""
percentage = (current_spend / budget_limit * 100) if budget_limit > 0 else 0
if percentage >= 100:
severity = "EXCEEDED"
color = "#dc2626"
elif percentage >= 90:
severity = "CRITICAL"
color = "#ea580c"
elif percentage >= 75:
severity = "WARNING"
color = "#ca8a04"
else:
severity = "INFO"
color = "#2563eb"
subject = f"[{severity}] Budget Alert: {budget_name} at {percentage:.0f}%"
html = f"""
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: {color};">Budget Alert: {severity}</h2>
<p><strong>Budget:</strong> {budget_name}</p>
<p><strong>Current Spend:</strong> {currency} {current_spend:,.2f}</p>
<p><strong>Budget Limit:</strong> {currency} {budget_limit:,.2f}</p>
<p><strong>Usage:</strong>
<span style="color: {color}; font-weight: bold;">{percentage:.1f}%</span></p>
</div>
"""
return _send_email_impl(
to=to,
subject=subject,
html=html,
from_email=from_email,
provider=provider,
cc=cc,
bcc=bcc,
)
return _send_email_impl(to, subject, html, provider, from_email, cc, bcc)
+63 -183
View File
@@ -15,13 +15,6 @@ def send_email_fn(mcp: FastMCP):
return mcp._tool_manager._tools["send_email"].fn
@pytest.fixture
def send_budget_alert_fn(mcp: FastMCP):
"""Register and return the send_budget_alert_email tool function."""
register_tools(mcp)
return mcp._tool_manager._tools["send_budget_alert_email"].fn
class TestSendEmail:
"""Tests for send_email tool."""
@@ -31,10 +24,12 @@ class TestSendEmail:
monkeypatch.delenv("GOOGLE_ACCESS_TOKEN", raising=False)
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
result = send_email_fn(to="test@example.com", subject="Test", html="<p>Hi</p>")
result = send_email_fn(
to="test@example.com", subject="Test", html="<p>Hi</p>", provider="gmail"
)
assert "error" in result
assert "No email credentials configured" in result["error"]
assert "Gmail credentials not configured" in result["error"]
assert "help" in result
def test_resend_explicit_missing_key(self, send_email_fn, monkeypatch):
@@ -57,7 +52,9 @@ class TestSendEmail:
monkeypatch.delenv("GOOGLE_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("EMAIL_FROM", raising=False)
result = send_email_fn(to="test@example.com", subject="Test", html="<p>Hi</p>")
result = send_email_fn(
to="test@example.com", subject="Test", html="<p>Hi</p>", provider="resend"
)
assert "error" in result
assert "Sender email is required" in result["error"]
@@ -70,7 +67,9 @@ class TestSendEmail:
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_env"}
result = send_email_fn(to="test@example.com", subject="Test", html="<p>Hi</p>")
result = send_email_fn(
to="test@example.com", subject="Test", html="<p>Hi</p>", provider="resend"
)
assert result["success"] is True
call_args = mock_send.call_args[0][0]
@@ -88,6 +87,7 @@ class TestSendEmail:
subject="Test",
html="<p>Hi</p>",
from_email="custom@other.com",
provider="resend",
)
assert result["success"] is True
@@ -99,7 +99,7 @@ class TestSendEmail:
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
result = send_email_fn(to="", subject="Test", html="<p>Hi</p>")
result = send_email_fn(to="", subject="Test", html="<p>Hi</p>", provider="resend")
assert "error" in result
@@ -108,7 +108,7 @@ class TestSendEmail:
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
result = send_email_fn(to="test@example.com", subject="", html="<p>Hi</p>")
result = send_email_fn(to="test@example.com", subject="", html="<p>Hi</p>", provider="resend")
assert "error" in result
@@ -117,7 +117,9 @@ class TestSendEmail:
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
result = send_email_fn(to="test@example.com", subject="x" * 999, html="<p>Hi</p>")
result = send_email_fn(
to="test@example.com", subject="x" * 999, html="<p>Hi</p>", provider="resend"
)
assert "error" in result
@@ -126,7 +128,7 @@ class TestSendEmail:
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
result = send_email_fn(to="test@example.com", subject="Test", html="")
result = send_email_fn(to="test@example.com", subject="Test", html="", provider="resend")
assert "error" in result
@@ -137,7 +139,9 @@ class TestSendEmail:
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_123"}
result = send_email_fn(to="test@example.com", subject="Test", html="<p>Hi</p>")
result = send_email_fn(
to="test@example.com", subject="Test", html="<p>Hi</p>", provider="resend"
)
assert result["success"] is True
mock_send.assert_called_once()
@@ -153,6 +157,7 @@ class TestSendEmail:
to=["a@example.com", "b@example.com"],
subject="Test",
html="<p>Hi</p>",
provider="resend",
)
assert result["success"] is True
@@ -170,6 +175,7 @@ class TestSendEmail:
subject="Test",
html="<p>Hi</p>",
cc="cc@example.com",
provider="resend",
)
assert result["success"] is True
@@ -188,6 +194,7 @@ class TestSendEmail:
subject="Test",
html="<p>Hi</p>",
bcc="bcc@example.com",
provider="resend",
)
assert result["success"] is True
@@ -207,6 +214,7 @@ class TestSendEmail:
html="<p>Hi</p>",
cc=["cc1@example.com", "cc2@example.com"],
bcc=["bcc1@example.com"],
provider="resend",
)
assert result["success"] is True
@@ -221,7 +229,9 @@ class TestSendEmail:
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_no_cc"}
send_email_fn(to="test@example.com", subject="Test", html="<p>Hi</p>")
send_email_fn(
to="test@example.com", subject="Test", html="<p>Hi</p>", provider="resend"
)
call_args = mock_send.call_args[0][0]
assert "cc" not in call_args
@@ -234,7 +244,14 @@ class TestSendEmail:
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_empty_cc"}
send_email_fn(to="test@example.com", subject="Test", html="<p>Hi</p>", cc="", bcc="")
send_email_fn(
to="test@example.com",
subject="Test",
html="<p>Hi</p>",
cc="",
bcc="",
provider="resend",
)
call_args = mock_send.call_args[0][0]
assert "cc" not in call_args
@@ -247,7 +264,9 @@ class TestSendEmail:
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_ws_cc"}
send_email_fn(to="test@example.com", subject="Test", html="<p>Hi</p>", cc=" ")
send_email_fn(
to="test@example.com", subject="Test", html="<p>Hi</p>", cc=" ", provider="resend"
)
call_args = mock_send.call_args[0][0]
assert "cc" not in call_args
@@ -259,7 +278,14 @@ class TestSendEmail:
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_empty_list"}
send_email_fn(to="test@example.com", subject="Test", html="<p>Hi</p>", cc=[], bcc=[])
send_email_fn(
to="test@example.com",
subject="Test",
html="<p>Hi</p>",
cc=[],
bcc=[],
provider="resend",
)
call_args = mock_send.call_args[0][0]
assert "cc" not in call_args
@@ -277,6 +303,7 @@ class TestSendEmail:
subject="Test",
html="<p>Hi</p>",
cc=["", "valid@example.com", " "],
provider="resend",
)
call_args = mock_send.call_args[0][0]
@@ -295,6 +322,7 @@ class TestSendEmail:
html="<p>Hi</p>",
cc=["", " "],
bcc=[""],
provider="resend",
)
call_args = mock_send.call_args[0][0]
@@ -312,7 +340,9 @@ class TestResendProvider:
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_789"}
result = send_email_fn(to="test@example.com", subject="Test", html="<p>Hi</p>")
result = send_email_fn(
to="test@example.com", subject="Test", html="<p>Hi</p>", provider="resend"
)
assert result["success"] is True
assert result["provider"] == "resend"
@@ -325,131 +355,13 @@ class TestResendProvider:
with patch("resend.Emails.send") as mock_send:
mock_send.side_effect = Exception("API rate limit exceeded")
result = send_email_fn(to="test@example.com", subject="Test", html="<p>Hi</p>")
result = send_email_fn(
to="test@example.com", subject="Test", html="<p>Hi</p>", provider="resend"
)
assert "error" in result
class TestSendBudgetAlertEmail:
"""Tests for send_budget_alert_email tool."""
def test_no_credentials_returns_error(self, send_budget_alert_fn, monkeypatch):
"""Budget alert without credentials returns error."""
monkeypatch.delenv("RESEND_API_KEY", raising=False)
monkeypatch.delenv("GOOGLE_ACCESS_TOKEN", raising=False)
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
result = send_budget_alert_fn(
to="test@example.com",
budget_name="Marketing Q1",
current_spend=8000.0,
budget_limit=10000.0,
)
assert "error" in result
def test_exceeded_budget_severity(self, send_budget_alert_fn, monkeypatch):
"""Spend >= 100% generates EXCEEDED alert."""
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_budget_1"}
result = send_budget_alert_fn(
to="test@example.com",
budget_name="Marketing Q1",
current_spend=12000.0,
budget_limit=10000.0,
)
assert result["success"] is True
assert "EXCEEDED" in result["subject"]
def test_critical_budget_severity(self, send_budget_alert_fn, monkeypatch):
"""Spend 90-99% generates CRITICAL alert."""
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_budget_2"}
result = send_budget_alert_fn(
to="test@example.com",
budget_name="Dev Budget",
current_spend=9500.0,
budget_limit=10000.0,
)
assert result["success"] is True
assert "CRITICAL" in result["subject"]
def test_warning_budget_severity(self, send_budget_alert_fn, monkeypatch):
"""Spend 75-89% generates WARNING alert."""
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_budget_3"}
result = send_budget_alert_fn(
to="test@example.com",
budget_name="Ops Budget",
current_spend=8000.0,
budget_limit=10000.0,
)
assert result["success"] is True
assert "WARNING" in result["subject"]
def test_info_budget_severity(self, send_budget_alert_fn, monkeypatch):
"""Spend < 75% generates INFO alert."""
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_budget_4"}
result = send_budget_alert_fn(
to="test@example.com",
budget_name="Small Budget",
current_spend=3000.0,
budget_limit=10000.0,
)
assert result["success"] is True
assert "INFO" in result["subject"]
def test_custom_currency(self, send_budget_alert_fn, monkeypatch):
"""Custom currency is included in the alert."""
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_budget_5"}
result = send_budget_alert_fn(
to="test@example.com",
budget_name="EU Budget",
current_spend=5000.0,
budget_limit=10000.0,
currency="EUR",
)
assert result["success"] is True
def test_zero_budget_limit(self, send_budget_alert_fn, monkeypatch):
"""Zero budget limit does not cause division by zero."""
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "email_budget_6"}
result = send_budget_alert_fn(
to="test@example.com",
budget_name="Empty Budget",
current_spend=100.0,
budget_limit=0.0,
)
assert result["success"] is True
class TestGmailProvider:
"""Tests for Gmail email provider."""
@@ -545,47 +457,6 @@ class TestGmailProvider:
assert "expired" in result["error"].lower() or "invalid" in result["error"].lower()
assert "help" in result
def test_auto_prefers_gmail_over_resend(self, send_email_fn, monkeypatch):
"""Auto mode uses Gmail when both Gmail and Resend are available."""
monkeypatch.setenv("GOOGLE_ACCESS_TOKEN", "test_gmail_token")
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "user@gmail.com")
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"id": "gmail_auto_123"}
with (
patch("aden_tools.tools.email_tool.email_tool.httpx.post", return_value=mock_response),
patch("resend.Emails.send") as mock_resend,
):
result = send_email_fn(
to="test@example.com",
subject="Test",
html="<p>Hi</p>",
)
assert result["success"] is True
assert result["provider"] == "gmail"
mock_resend.assert_not_called()
def test_auto_falls_back_to_resend(self, send_email_fn, monkeypatch):
"""Auto mode falls back to Resend when Gmail is not available."""
monkeypatch.delenv("GOOGLE_ACCESS_TOKEN", raising=False)
monkeypatch.setenv("RESEND_API_KEY", "re_test_key")
monkeypatch.setenv("EMAIL_FROM", "test@example.com")
with patch("resend.Emails.send") as mock_send:
mock_send.return_value = {"id": "resend_fallback"}
result = send_email_fn(
to="test@example.com",
subject="Test",
html="<p>Hi</p>",
)
assert result["success"] is True
assert result["provider"] == "resend"
def test_gmail_no_from_email_ok(self, send_email_fn, monkeypatch):
"""Gmail works without from_email (defaults to authenticated user)."""
monkeypatch.setenv("GOOGLE_ACCESS_TOKEN", "test_gmail_token")
@@ -606,3 +477,12 @@ class TestGmailProvider:
assert result["success"] is True
assert result["provider"] == "gmail"
class TestProviderRequired:
"""Tests that provider is a required parameter."""
def test_missing_provider_raises_type_error(self, send_email_fn):
"""Calling send_email without provider raises TypeError."""
with pytest.raises(TypeError):
send_email_fn(to="test@example.com", subject="Test", html="<p>Hi</p>")