removed send_budget_alert_email, require provider param
This commit is contained in:
@@ -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.
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>")
|
||||
|
||||
Reference in New Issue
Block a user