Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44c53f8015 | ||
|
|
43acd5bfbf | ||
|
|
6dac5d225e |
@@ -0,0 +1,5 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.egg-info/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.pytest_cache/
|
||||||
@@ -1,3 +1,95 @@
|
|||||||
# trello-plugin
|
# trello-plugin
|
||||||
|
|
||||||
Hermes Agent plugin for Trello board integration
|
Hermes Agent plugin for Trello board integration.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Get Trello Credentials
|
||||||
|
|
||||||
|
1. Get your **API Key** from: https://trello.com/app-key
|
||||||
|
2. Generate a **Token** from the same page (click "Token" under API Key)
|
||||||
|
3. Set these as environment variables in your Hermes profile config (`.env` or `config.yaml`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export TRELLO_API_KEY="your-trello-api-key"
|
||||||
|
export TRELLO_TOKEN="your-trello-token"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd trello-plugin
|
||||||
|
pip install .
|
||||||
|
```
|
||||||
|
|
||||||
|
For development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Register with Hermes Agent
|
||||||
|
|
||||||
|
Add the plugin to your Hermes config (`~/.hermes/config.yaml` or profile):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugins:
|
||||||
|
- trello-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
Once configured, the agent has access to these tools:
|
||||||
|
|
||||||
|
### `trello_verify_credentials`
|
||||||
|
Verifies your Trello API key and token are valid by calling the Trello API.
|
||||||
|
|
||||||
|
```
|
||||||
|
✓ Trello credentials verified successfully (authenticated as your-username)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `trello_list_boards`
|
||||||
|
Lists all Trello boards accessible with your credentials.
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `id` | Trello board ID |
|
||||||
|
| `name` | Board name |
|
||||||
|
| `url` | Direct Trello URL |
|
||||||
|
| `closed` | Whether the board is archived |
|
||||||
|
| `starred` | Whether the board is starred |
|
||||||
|
|
||||||
|
### `trello_disconnect`
|
||||||
|
Clears Trello credentials from the in-memory client. Since credentials are stored in environment variables, the agent can reconnect automatically on the next tool call.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dev dependencies
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
PYTHONPATH=src python3 -m pytest tests/ -v
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
pip install ruff
|
||||||
|
ruff check src/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
trello-plugin/
|
||||||
|
├── src/
|
||||||
|
│ └── trello_plugin/
|
||||||
|
│ ├── __init__.py # Package exports
|
||||||
|
│ ├── client.py # TrelloClient — REST API wrapper
|
||||||
|
│ └── tools.py # Hermes tool definitions
|
||||||
|
├── tests/
|
||||||
|
│ └── test_auth.py # Test suite (18 tests)
|
||||||
|
├── docs/
|
||||||
|
│ └── backend/
|
||||||
|
│ └── trello-auth-spec.md
|
||||||
|
├── pyproject.toml
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Trello Plugin — Authentication & Connection
|
||||||
|
|
||||||
|
**Feature:** US: Authenticate and Connect to Trello Account
|
||||||
|
**Issue:** #1
|
||||||
|
**Branch:** `feature/trello-auth`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This feature provides the authentication layer for the Trello plugin. Users configure their Trello credentials via environment variables (`TRELLO_API_KEY` and `TRELLO_TOKEN`), and the plugin exposes tools to verify credentials, list accessible boards, and disconnect.
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
- **No interactive credential input** — The Hermes Agent has no UI for prompting users. Credentials are set in the profile's `.env` or `config.yaml` as environment variables, which is the standard Hermes pattern.
|
||||||
|
- **Trello uses API Key + Token auth** — Trello's API requires an API key (identifies the application) and a token (user-specific authorization). Both are required for any API call.
|
||||||
|
- **One client class** — All Trello API interactions go through a single `TrelloClient` class to centralize auth, base URL, and error handling. Future features (boards, lists, cards) will extend this same client.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `TRELLO_API_KEY` | Yes | Trello API application key |
|
||||||
|
| `TRELLO_TOKEN` | Yes | Trello user authorization token |
|
||||||
|
|
||||||
|
## Tools Exposed
|
||||||
|
|
||||||
|
### 1. `trello_verify_credentials`
|
||||||
|
|
||||||
|
Verifies that `TRELLO_API_KEY` and `TRELLO_TOKEN` are set and the Trello API responds successfully.
|
||||||
|
|
||||||
|
**Parameters:** None
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "✓ Trello credentials verified successfully (authenticated as user@example.com)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On failure:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "✗ Trello authentication failed: invalid key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `trello_list_boards`
|
||||||
|
|
||||||
|
Fetches and returns all Trello boards accessible to the authenticated user.
|
||||||
|
|
||||||
|
**Parameters:** None
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"boards": [
|
||||||
|
{"id": "abc123", "name": "My Project Board", "url": "https://trello.com/b/abc123"},
|
||||||
|
{"id": "def456", "name": "Personal Tasks", "url": "https://trello.com/b/def456"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. `trello_disconnect`
|
||||||
|
|
||||||
|
Clears the stored credentials from memory. Note: this does not revoke the Trello token — the user must invalidate it via Trello's settings if needed.
|
||||||
|
|
||||||
|
**Parameters:** None
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Trello credentials cleared. Set TRELLO_API_KEY and TRELLO_TOKEN again to reconnect."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Contract
|
||||||
|
|
||||||
|
### Internal: `TrelloClient`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TrelloClient:
|
||||||
|
def __init__(self, api_key: str | None = None, token: str | None = None)
|
||||||
|
|
||||||
|
def verify_credentials(self) -> dict
|
||||||
|
def list_boards(self) -> list[dict]
|
||||||
|
def disconnect(self) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trello API Endpoints Used
|
||||||
|
|
||||||
|
| Purpose | Method | Endpoint | Docs |
|
||||||
|
|---------|--------|----------|------|
|
||||||
|
| Verify credentials | GET | `/1/members/me` | https://developer.atlassian.com/cloud/trello/rest/api-group-members/ |
|
||||||
|
| List boards | GET | `/1/members/me/boards` | https://developer.atlassian.com/cloud/trello/rest/api-group-boards/ |
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Missing env vars → return `{"success": false, "message": "TRELLO_API_KEY and TRELLO_TOKEN must be set"}` with env var name
|
||||||
|
- Network errors → catch `requests.exceptions.RequestException` and return descriptive message
|
||||||
|
- Auth failures (401/403) → clear message about invalid credentials
|
||||||
|
- Rate limiting → Trello returns 429; surface the retry-after header
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Unit tests with mocked HTTP responses
|
||||||
|
- Test all error paths: missing creds, invalid creds, network error, rate limit
|
||||||
|
- Test successful verification via mock that returns member info
|
||||||
|
- Test board listing with mock JSON response
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=64"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "trello-plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Hermes Agent plugin for Trello board integration"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
dependencies = [
|
||||||
|
"requests>=2.31",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8",
|
||||||
|
"pytest-cov>=5",
|
||||||
|
"requests-mock>=1.12",
|
||||||
|
"ruff>=0.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py312"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "N", "W", "UP", "PL"]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
trello-plugin — Hermes Agent plugin for Trello board integration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from trello_plugin.tools import (
|
||||||
|
PLUGIN_DESCRIPTION,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
PLUGIN_TOOLS,
|
||||||
|
PLUGIN_VERSION,
|
||||||
|
check_requirements,
|
||||||
|
trello_disconnect,
|
||||||
|
trello_list_boards,
|
||||||
|
trello_verify_credentials,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"PLUGIN_NAME",
|
||||||
|
"PLUGIN_DESCRIPTION",
|
||||||
|
"PLUGIN_VERSION",
|
||||||
|
"PLUGIN_TOOLS",
|
||||||
|
"check_requirements",
|
||||||
|
"trello_verify_credentials",
|
||||||
|
"trello_list_boards",
|
||||||
|
"trello_disconnect",
|
||||||
|
]
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
"""
|
||||||
|
Trello Plugin — Hermes Agent plugin for Trello board integration.
|
||||||
|
|
||||||
|
Authentication & Connection module.
|
||||||
|
Provides the TrelloClient for interacting with the Trello REST API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Any, TypeGuard, TypedDict
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Base URL for the Trello REST API v1.
|
||||||
|
# See https://developer.atlassian.com/cloud/trello/rest
|
||||||
|
TRELLO_API_BASE = "https://api.trello.com/1"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Typed response contracts
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class SuccessResponse(TypedDict):
|
||||||
|
"""A successful API response carrying data."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
data: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorResponse(TypedDict):
|
||||||
|
"""An unsuccessful API response with a human-readable message."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
ApiResponse = SuccessResponse | ErrorResponse
|
||||||
|
|
||||||
|
|
||||||
|
def _is_success(resp: ApiResponse) -> TypeGuard[SuccessResponse]:
|
||||||
|
"""Narrow an ApiResponse union to its success branch."""
|
||||||
|
return bool(resp.get("success", False)) # noqa: FBT003
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Client
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TrelloClient:
|
||||||
|
"""Client for the Trello REST API.
|
||||||
|
|
||||||
|
Reads credentials from environment variables TRELLO_API_KEY and TRELLO_TOKEN
|
||||||
|
unless explicitly passed. All public methods return plain dicts suitable for
|
||||||
|
JSON serialization by Hermes tool handlers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
api_key: str
|
||||||
|
token: str
|
||||||
|
_session: requests.Session
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str | None = None,
|
||||||
|
token: str | None = None,
|
||||||
|
session: requests.Session | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.api_key = api_key or os.environ.get("TRELLO_API_KEY", "")
|
||||||
|
self.token = token or os.environ.get("TRELLO_TOKEN", "")
|
||||||
|
self._session = session or requests.Session()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Internal helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _auth_params(self) -> dict[str, str]:
|
||||||
|
"""Return query parameters common to every Trello API call."""
|
||||||
|
return {"key": self.api_key, "token": self.token}
|
||||||
|
|
||||||
|
def _check_credentials(self) -> ErrorResponse | None:
|
||||||
|
"""Return an ErrorResponse if credentials are missing, else None."""
|
||||||
|
if not self.api_key or not self.token:
|
||||||
|
return ErrorResponse(
|
||||||
|
success=False,
|
||||||
|
message="TRELLO_API_KEY and TRELLO_TOKEN must both be set as environment variables.",
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _handle_http_error(self, exc: requests.exceptions.HTTPError) -> ErrorResponse:
|
||||||
|
"""Map known HTTP status codes to user-facing error messages."""
|
||||||
|
status = exc.response.status_code if exc.response is not None else 0
|
||||||
|
messages: dict[int, str] = {
|
||||||
|
401: "Trello authentication failed (401). Check your TRELLO_API_KEY and TRELLO_TOKEN.",
|
||||||
|
403: "Trello access denied (403). Your token may not have the required scopes.",
|
||||||
|
429: "Trello rate limit exceeded. Try again later.",
|
||||||
|
}
|
||||||
|
message = messages.get(
|
||||||
|
status, f"Trello API error ({status}): {exc}"
|
||||||
|
)
|
||||||
|
return ErrorResponse(success=False, message=message)
|
||||||
|
|
||||||
|
def _request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
params: dict[str, Any] | None = None,
|
||||||
|
json_body: dict[str, Any] | None = None,
|
||||||
|
) -> ApiResponse:
|
||||||
|
"""Make an authenticated HTTP request and return the JSON response.
|
||||||
|
|
||||||
|
Handles credential checks, HTTP errors, network errors, and timeouts.
|
||||||
|
"""
|
||||||
|
cred_error = self._check_credentials()
|
||||||
|
if cred_error is not None:
|
||||||
|
return cred_error
|
||||||
|
|
||||||
|
url = f"{TRELLO_API_BASE}{path}"
|
||||||
|
merged_params = self._auth_params()
|
||||||
|
if params:
|
||||||
|
merged_params.update(params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = self._session.request(
|
||||||
|
method, url, params=merged_params, json=json_body, timeout=15
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data: dict[str, Any] = resp.json()
|
||||||
|
return SuccessResponse(success=True, data=data)
|
||||||
|
except requests.exceptions.HTTPError as exc:
|
||||||
|
return self._handle_http_error(exc)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return ErrorResponse(
|
||||||
|
success=False,
|
||||||
|
message="Could not connect to Trello API. Check your network connection.",
|
||||||
|
)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
return ErrorResponse(
|
||||||
|
success=False,
|
||||||
|
message="Trello API request timed out. Try again later.",
|
||||||
|
)
|
||||||
|
except requests.exceptions.RequestException as exc:
|
||||||
|
return ErrorResponse(
|
||||||
|
success=False, message=f"Trello API request failed: {exc}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get(
|
||||||
|
self, path: str, params: dict[str, Any] | None = None
|
||||||
|
) -> ApiResponse:
|
||||||
|
"""Make an authenticated GET request."""
|
||||||
|
return self._request("GET", path, params=params)
|
||||||
|
|
||||||
|
def _post(
|
||||||
|
self, path: str, json_body: dict[str, Any] | None = None
|
||||||
|
) -> ApiResponse:
|
||||||
|
"""Make an authenticated POST request."""
|
||||||
|
return self._request("POST", path, json_body=json_body)
|
||||||
|
|
||||||
|
def _put(
|
||||||
|
self, path: str, json_body: dict[str, Any] | None = None
|
||||||
|
) -> ApiResponse:
|
||||||
|
"""Make an authenticated PUT request."""
|
||||||
|
return self._request("PUT", path, json_body=json_body)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Public API — Auth & Connection
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def verify_credentials(self) -> dict[str, Any]:
|
||||||
|
"""Verify that the stored credentials are valid by fetching member info.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with ``success`` bool and ``message`` string. On success the
|
||||||
|
``member`` key contains the Trello member id and username.
|
||||||
|
"""
|
||||||
|
result = self._get("/members/me")
|
||||||
|
if _is_success(result):
|
||||||
|
member = result["data"]
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": (
|
||||||
|
f"✓ Trello credentials verified successfully "
|
||||||
|
f"(authenticated as {member.get('username', 'unknown')})"
|
||||||
|
),
|
||||||
|
"member": {
|
||||||
|
"id": member.get("id"),
|
||||||
|
"username": member.get("username"),
|
||||||
|
"full_name": member.get("fullName"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": result.get("message", "Trello credential verification failed."),
|
||||||
|
}
|
||||||
|
|
||||||
|
def list_boards(self) -> dict[str, Any]:
|
||||||
|
"""Fetch all boards accessible to the authenticated user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with ``success`` bool and a ``boards`` list (each containing
|
||||||
|
``id``, ``name``, ``url``, ``closed``, ``starred``).
|
||||||
|
"""
|
||||||
|
result = self._get(
|
||||||
|
"/members/me/boards", params={"fields": "id,name,url,closed,starred"}
|
||||||
|
)
|
||||||
|
if not _is_success(result):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": result.get("message", "Failed to list Trello boards."),
|
||||||
|
}
|
||||||
|
|
||||||
|
boards = [
|
||||||
|
{
|
||||||
|
"id": b.get("id"),
|
||||||
|
"name": b.get("name"),
|
||||||
|
"url": b.get("url"),
|
||||||
|
"closed": b.get("closed", False),
|
||||||
|
"starred": b.get("starred", False),
|
||||||
|
}
|
||||||
|
for b in result["data"]
|
||||||
|
]
|
||||||
|
return {"success": True, "boards": boards}
|
||||||
|
|
||||||
|
def disconnect(self) -> dict[str, Any]:
|
||||||
|
"""Clear credentials from the in-memory client.
|
||||||
|
|
||||||
|
Since credentials are stored in environment variables, the next tool call
|
||||||
|
will automatically re-read them and reconnect. To fully disconnect, also
|
||||||
|
unset TRELLO_API_KEY and TRELLO_TOKEN from the Hermes profile config.
|
||||||
|
|
||||||
|
Note: this does NOT revoke the Trello token. The user must invalidate
|
||||||
|
it via Trello's account settings if they want full revocation.
|
||||||
|
"""
|
||||||
|
self.api_key = ""
|
||||||
|
self.token = ""
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": (
|
||||||
|
"Trello credentials cleared from memory. "
|
||||||
|
"Next tool call will re-read TRELLO_API_KEY and TRELLO_TOKEN "
|
||||||
|
"from environment and reconnect automatically."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Public API — Board Management
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _resolve_board_id(self, board_id: str) -> dict[str, Any]:
|
||||||
|
"""Resolve a board name or ID to a Trello board ID.
|
||||||
|
|
||||||
|
Returns board info dict with at least an ``id`` key on success,
|
||||||
|
or an error dict with ``success: False``.
|
||||||
|
"""
|
||||||
|
# Try as ID first (Trello IDs are 8-24 char hex strings)
|
||||||
|
result = self._get(f"/boards/{board_id}", params={"fields": "id,name,url"})
|
||||||
|
if _is_success(result):
|
||||||
|
board = result["data"]
|
||||||
|
return {"id": board["id"], "name": board.get("name", ""), "url": board.get("url", "")}
|
||||||
|
|
||||||
|
# Try resolving by name
|
||||||
|
boards_result = self.list_boards()
|
||||||
|
if not _is_success(boards_result): # type: ignore[arg-type]
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": (
|
||||||
|
f"Board '{board_id}' not found. "
|
||||||
|
"Use trello_list_boards to see available boards."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
matches = [b for b in boards_result["boards"] if b["name"] == board_id] # type: ignore[typeddict-item]
|
||||||
|
if len(matches) == 1:
|
||||||
|
return dict(matches[0]) # already has id, name, url
|
||||||
|
|
||||||
|
if len(matches) > 1:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": (
|
||||||
|
f"Multiple boards named '{board_id}' found. "
|
||||||
|
"Use the board ID instead of the name."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": (
|
||||||
|
f"Board '{board_id}' not found. "
|
||||||
|
"Use trello_list_boards to see available boards."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_board(
|
||||||
|
self, name: str, default_lists: bool = True
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Create a new Trello board.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name for the new board.
|
||||||
|
default_lists: Whether to create the default lists (default: True).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with board details on success, or an error dict.
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {
|
||||||
|
"name": name,
|
||||||
|
"defaultLists": default_lists,
|
||||||
|
}
|
||||||
|
result = self._post("/boards", json_body=body)
|
||||||
|
if _is_success(result):
|
||||||
|
board = result["data"]
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"board": {
|
||||||
|
"id": board.get("id"),
|
||||||
|
"name": board.get("name"),
|
||||||
|
"url": board.get("url"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": result.get("message", "Failed to create board."),
|
||||||
|
}
|
||||||
|
|
||||||
|
def rename_board(self, board_id: str, name: str) -> dict[str, Any]:
|
||||||
|
"""Rename an existing Trello board.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: Board ID or name.
|
||||||
|
name: The new name for the board.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with board details on success, or an error dict.
|
||||||
|
"""
|
||||||
|
resolved = self._resolve_board_id(board_id)
|
||||||
|
if "success" in resolved and resolved["success"] is False:
|
||||||
|
return resolved # type: ignore[typeddict-item]
|
||||||
|
|
||||||
|
trello_id = resolved["id"]
|
||||||
|
result = self._put(
|
||||||
|
f"/boards/{trello_id}", json_body={"name": name}
|
||||||
|
)
|
||||||
|
if _is_success(result):
|
||||||
|
board = result["data"]
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"board": {
|
||||||
|
"id": board.get("id"),
|
||||||
|
"name": board.get("name"),
|
||||||
|
"url": board.get("url"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": result.get("message", "Failed to rename board."),
|
||||||
|
}
|
||||||
|
|
||||||
|
def archive_board(self, board_id: str) -> dict[str, Any]:
|
||||||
|
"""Close/archive a Trello board.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: Board ID or name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with success message or error.
|
||||||
|
"""
|
||||||
|
resolved = self._resolve_board_id(board_id)
|
||||||
|
if "success" in resolved and resolved["success"] is False:
|
||||||
|
return resolved # type: ignore[typeddict-item]
|
||||||
|
|
||||||
|
trello_id = resolved["id"]
|
||||||
|
board_name = resolved.get("name", trello_id)
|
||||||
|
result = self._put(
|
||||||
|
f"/boards/{trello_id}", json_body={"closed": True}
|
||||||
|
)
|
||||||
|
if _is_success(result):
|
||||||
|
return {"success": True, "message": f"Board '{board_name}' archived."}
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": result.get("message", "Failed to archive board."),
|
||||||
|
}
|
||||||
|
|
||||||
|
def open_board(self, board_id: str) -> dict[str, Any]:
|
||||||
|
"""Re-open a closed/archived Trello board.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: Board ID or name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with success message or error.
|
||||||
|
"""
|
||||||
|
resolved = self._resolve_board_id(board_id)
|
||||||
|
if "success" in resolved and resolved["success"] is False:
|
||||||
|
return resolved # type: ignore[typeddict-item]
|
||||||
|
|
||||||
|
trello_id = resolved["id"]
|
||||||
|
board_name = resolved.get("name", trello_id)
|
||||||
|
result = self._put(
|
||||||
|
f"/boards/{trello_id}", json_body={"closed": False}
|
||||||
|
)
|
||||||
|
if _is_success(result):
|
||||||
|
return {"success": True, "message": f"Board '{board_name}' opened."}
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": result.get("message", "Failed to open board."),
|
||||||
|
}
|
||||||
|
|
||||||
|
def board_details(self, board_id: str) -> dict[str, Any]:
|
||||||
|
"""Fetch detailed information about a Trello board.
|
||||||
|
|
||||||
|
Returns the board's lists, members, and a direct URL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: Board ID or name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with board details including lists and members on success.
|
||||||
|
"""
|
||||||
|
resolved = self._resolve_board_id(board_id)
|
||||||
|
if "success" in resolved and resolved["success"] is False:
|
||||||
|
return resolved # type: ignore[typeddict-item]
|
||||||
|
|
||||||
|
trello_id = resolved["id"]
|
||||||
|
result = self._get(
|
||||||
|
f"/boards/{trello_id}",
|
||||||
|
params={
|
||||||
|
"fields": "id,name,desc,url,closed,starred",
|
||||||
|
"lists": "all",
|
||||||
|
"members": "all",
|
||||||
|
"members_fields": "id,username,fullName",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not _is_success(result):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": result.get("message", "Failed to fetch board details."),
|
||||||
|
}
|
||||||
|
|
||||||
|
board = result["data"]
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"board": {
|
||||||
|
"id": board.get("id"),
|
||||||
|
"name": board.get("name"),
|
||||||
|
"url": board.get("url"),
|
||||||
|
"desc": board.get("desc", ""),
|
||||||
|
"closed": board.get("closed", False),
|
||||||
|
"starred": board.get("starred", False),
|
||||||
|
"lists": [
|
||||||
|
{"id": lst.get("id"), "name": lst.get("name")}
|
||||||
|
for lst in board.get("lists", [])
|
||||||
|
],
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"id": m.get("id"),
|
||||||
|
"username": m.get("username"),
|
||||||
|
"full_name": m.get("fullName"),
|
||||||
|
}
|
||||||
|
for m in board.get("members", [])
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_requirements() -> bool:
|
||||||
|
"""Check if the required environment variables are set."""
|
||||||
|
return bool(os.environ.get("TRELLO_API_KEY")) and bool(
|
||||||
|
os.environ.get("TRELLO_TOKEN")
|
||||||
|
)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""
|
||||||
|
Trello Plugin — Hermes Agent tool definitions.
|
||||||
|
|
||||||
|
Each function decorated with ``@tool`` registers itself as a Hermes tool
|
||||||
|
that the agent can invoke during a conversation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Module-level client (lazily initialised so env vars can be overridden)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_client: TrelloClient | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client() -> TrelloClient:
|
||||||
|
global _client # noqa: PLW0603
|
||||||
|
api_key = os.environ.get("TRELLO_API_KEY", "")
|
||||||
|
token = os.environ.get("TRELLO_TOKEN", "")
|
||||||
|
if _client is None or _client.api_key != api_key or _client.token != token:
|
||||||
|
_client = TrelloClient(api_key=api_key, token=token)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tool helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _respond(data: dict[str, Any]) -> str:
|
||||||
|
"""Wrap a result dict in a JSON string, the Hermes tool contract."""
|
||||||
|
return json.dumps(data, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tools
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def trello_verify_credentials() -> str:
|
||||||
|
"""Verify that the Trello API key and token are valid.
|
||||||
|
|
||||||
|
Checks the ``TRELLO_API_KEY`` and ``TRELLO_TOKEN`` environment variables
|
||||||
|
and attempts a test call to the Trello API. Returns the authenticated
|
||||||
|
user's Trello username on success.
|
||||||
|
"""
|
||||||
|
client = _get_client()
|
||||||
|
result = client.verify_credentials()
|
||||||
|
return _respond(result)
|
||||||
|
|
||||||
|
|
||||||
|
def trello_list_boards() -> str:
|
||||||
|
"""List all Trello boards accessible to the authenticated user.
|
||||||
|
|
||||||
|
Requires ``TRELLO_API_KEY`` and ``TRELLO_TOKEN`` to be set.
|
||||||
|
Returns board id, name, URL, and whether the board is closed/starred.
|
||||||
|
"""
|
||||||
|
client = _get_client()
|
||||||
|
result = client.list_boards()
|
||||||
|
return _respond(result)
|
||||||
|
|
||||||
|
|
||||||
|
def trello_disconnect() -> str:
|
||||||
|
"""Disconnect from Trello by clearing credentials from memory.
|
||||||
|
|
||||||
|
This does NOT revoke the Trello token — invalidate it via
|
||||||
|
Trello's account settings if full revocation is desired.
|
||||||
|
"""
|
||||||
|
global _client # noqa: PLW0603
|
||||||
|
_client = None
|
||||||
|
return _respond({"success": True, "message": "Trello credentials cleared from memory. Next tool call will re-read TRELLO_API_KEY and TRELLO_TOKEN from environment and reconnect automatically."})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Plugin metadata (used by Hermes plugin loader)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
PLUGIN_NAME = "trello-plugin"
|
||||||
|
PLUGIN_DESCRIPTION = "Hermes Agent plugin for Trello board integration"
|
||||||
|
PLUGIN_VERSION = "0.1.0"
|
||||||
|
PLUGIN_TOOLS = [
|
||||||
|
trello_verify_credentials,
|
||||||
|
trello_list_boards,
|
||||||
|
trello_disconnect,
|
||||||
|
]
|
||||||
|
PLUGIN_REQUIRES_ENV = ["TRELLO_API_KEY", "TRELLO_TOKEN"]
|
||||||
|
|
||||||
|
|
||||||
|
def check_requirements() -> bool:
|
||||||
|
"""Return True when both Trello env vars are present."""
|
||||||
|
return TrelloClient.check_requirements()
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
"""Tests for the Trello plugin — auth & connection feature."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from collections.abc import Generator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pytest import MonkeyPatch
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Tell pytest we plan to set env vars in tests
|
||||||
|
pytestmark = pytest.mark.usefixtures("clear_env")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fixtures
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def clear_env(monkeypatch: MonkeyPatch) -> Generator[None, None, None]:
|
||||||
|
"""Remove Trello env vars before each test so state is predictable."""
|
||||||
|
monkeypatch.delenv("TRELLO_API_KEY", raising=False)
|
||||||
|
monkeypatch.delenv("TRELLO_TOKEN", raising=False)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def _make_session(mocker: Any) -> requests.Session:
|
||||||
|
"""Build a requests.Session with a mocked adapter."""
|
||||||
|
session = requests.Session()
|
||||||
|
adapter = mocker.get_adapter()
|
||||||
|
session.mount("https://", adapter)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TrelloClient — verify_credentials
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestVerifyCredentials:
|
||||||
|
"""Tests for TrelloClient.verify_credentials()."""
|
||||||
|
|
||||||
|
def test_success(self, requests_mock: Any) -> None:
|
||||||
|
"""Happy path: valid creds return member info."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
requests_mock.get(
|
||||||
|
"https://api.trello.com/1/members/me",
|
||||||
|
json={"id": "member123", "username": "testuser", "fullName": "Test User"},
|
||||||
|
)
|
||||||
|
client = TrelloClient(api_key="key", token="tok")
|
||||||
|
result = client.verify_credentials()
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert "testuser" in result["message"]
|
||||||
|
assert result["member"]["id"] == "member123"
|
||||||
|
assert result["member"]["username"] == "testuser"
|
||||||
|
|
||||||
|
def test_missing_credentials(self) -> None:
|
||||||
|
"""Both API key and token must be set."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
client = TrelloClient(api_key="", token="")
|
||||||
|
result = client.verify_credentials()
|
||||||
|
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "TRELLO_API_KEY" in result["message"]
|
||||||
|
|
||||||
|
def test_auth_failure_401(self, requests_mock: Any) -> None:
|
||||||
|
"""401 response surfaces as auth failure."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
requests_mock.get(
|
||||||
|
"https://api.trello.com/1/members/me",
|
||||||
|
status_code=401,
|
||||||
|
)
|
||||||
|
client = TrelloClient(api_key="bad_key", token="bad_tok")
|
||||||
|
result = client.verify_credentials()
|
||||||
|
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "401" in result["message"]
|
||||||
|
|
||||||
|
def test_forbidden_403(self, requests_mock: Any) -> None:
|
||||||
|
"""403 surfaces as access denied."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
requests_mock.get(
|
||||||
|
"https://api.trello.com/1/members/me",
|
||||||
|
status_code=403,
|
||||||
|
)
|
||||||
|
client = TrelloClient(api_key="key", token="tok")
|
||||||
|
result = client.verify_credentials()
|
||||||
|
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "403" in result["message"]
|
||||||
|
|
||||||
|
def test_rate_limit_429(self, requests_mock: Any) -> None:
|
||||||
|
"""429 surfaces as rate limit error."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
requests_mock.get(
|
||||||
|
"https://api.trello.com/1/members/me",
|
||||||
|
status_code=429,
|
||||||
|
)
|
||||||
|
client = TrelloClient(api_key="key", token="tok")
|
||||||
|
result = client.verify_credentials()
|
||||||
|
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "rate limit" in result["message"].lower()
|
||||||
|
|
||||||
|
def test_network_error(self, requests_mock: Any) -> None:
|
||||||
|
"""Connection errors surface as network failure."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
requests_mock.get(
|
||||||
|
"https://api.trello.com/1/members/me",
|
||||||
|
exc=requests.exceptions.ConnectionError("connection refused"),
|
||||||
|
)
|
||||||
|
client = TrelloClient(api_key="key", token="tok")
|
||||||
|
result = client.verify_credentials()
|
||||||
|
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "connect" in result["message"].lower()
|
||||||
|
|
||||||
|
def test_timeout(self, requests_mock: Any) -> None:
|
||||||
|
"""Timeout surfaces gracefully."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
requests_mock.get(
|
||||||
|
"https://api.trello.com/1/members/me",
|
||||||
|
exc=requests.exceptions.Timeout("timed out"),
|
||||||
|
)
|
||||||
|
client = TrelloClient(api_key="key", token="tok")
|
||||||
|
result = client.verify_credentials()
|
||||||
|
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "timed out" in result["message"].lower()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TrelloClient — list_boards
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestListBoards:
|
||||||
|
"""Tests for TrelloClient.list_boards()."""
|
||||||
|
|
||||||
|
def test_success(self, requests_mock: Any) -> None:
|
||||||
|
"""Happy path: returns a list of boards."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
mock_boards = [
|
||||||
|
{"id": "b1", "name": "Board One", "url": "https://trello.com/b/b1", "closed": False, "starred": False},
|
||||||
|
{"id": "b2", "name": "Board Two", "url": "https://trello.com/b/b2", "closed": True, "starred": False},
|
||||||
|
]
|
||||||
|
requests_mock.get(
|
||||||
|
"https://api.trello.com/1/members/me/boards",
|
||||||
|
json=mock_boards,
|
||||||
|
)
|
||||||
|
client = TrelloClient(api_key="key", token="tok")
|
||||||
|
result = client.list_boards()
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert len(result["boards"]) == 2
|
||||||
|
assert result["boards"][0]["name"] == "Board One"
|
||||||
|
assert result["boards"][1]["closed"] is True
|
||||||
|
|
||||||
|
def test_missing_credentials(self) -> None:
|
||||||
|
"""Missing creds returns error before any network call."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
client = TrelloClient(api_key="", token="")
|
||||||
|
result = client.list_boards()
|
||||||
|
|
||||||
|
assert result["success"] is False
|
||||||
|
assert "TRELLO_API_KEY" in result["message"]
|
||||||
|
|
||||||
|
def test_empty_boards(self, requests_mock: Any) -> None:
|
||||||
|
"""No boards returns an empty list."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
requests_mock.get(
|
||||||
|
"https://api.trello.com/1/members/me/boards",
|
||||||
|
json=[],
|
||||||
|
)
|
||||||
|
client = TrelloClient(api_key="key", token="tok")
|
||||||
|
result = client.list_boards()
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["boards"] == []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TrelloClient — disconnect
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestDisconnect:
|
||||||
|
"""Tests for TrelloClient.disconnect()."""
|
||||||
|
|
||||||
|
def test_disconnect_clears_credentials(self) -> None:
|
||||||
|
"""Disconnect clears the in-memory credentials."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
client = TrelloClient(api_key="key", token="tok")
|
||||||
|
result = client.disconnect()
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert "cleared" in result["message"].lower()
|
||||||
|
|
||||||
|
# Subsequent calls should fail
|
||||||
|
verify_result = client.verify_credentials()
|
||||||
|
assert verify_result["success"] is False
|
||||||
|
assert "TRELLO_API_KEY" in verify_result["message"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# check_requirements
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestCheckRequirements:
|
||||||
|
"""Tests for TrelloClient.check_requirements()."""
|
||||||
|
|
||||||
|
def test_returns_false_when_missing(self) -> None:
|
||||||
|
"""Returns False when env vars are not set."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
assert TrelloClient.check_requirements() is False
|
||||||
|
|
||||||
|
def test_returns_true_when_set(self) -> None:
|
||||||
|
"""Returns True when both env vars are set."""
|
||||||
|
from trello_plugin.client import TrelloClient
|
||||||
|
|
||||||
|
os.environ["TRELLO_API_KEY"] = "test-key"
|
||||||
|
os.environ["TRELLO_TOKEN"] = "test-token"
|
||||||
|
assert TrelloClient.check_requirements() is True
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tool functions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestToolFunctions:
|
||||||
|
"""Tests for the Hermes tool wrapper functions."""
|
||||||
|
|
||||||
|
def test_trello_verify_credentials_tool(self, requests_mock: Any) -> None:
|
||||||
|
"""Tool returns valid JSON string on success."""
|
||||||
|
from trello_plugin.tools import trello_verify_credentials
|
||||||
|
|
||||||
|
os.environ["TRELLO_API_KEY"] = "test-key"
|
||||||
|
os.environ["TRELLO_TOKEN"] = "test-token"
|
||||||
|
requests_mock.get(
|
||||||
|
"https://api.trello.com/1/members/me",
|
||||||
|
json={"id": "m1", "username": "bot", "fullName": "Bot User"},
|
||||||
|
)
|
||||||
|
result = json.loads(trello_verify_credentials())
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["member"]["username"] == "bot"
|
||||||
|
|
||||||
|
def test_trello_list_boards_tool(self, requests_mock: Any) -> None:
|
||||||
|
"""Tool returns valid JSON string on success."""
|
||||||
|
from trello_plugin.tools import trello_list_boards
|
||||||
|
|
||||||
|
os.environ["TRELLO_API_KEY"] = "test-key"
|
||||||
|
os.environ["TRELLO_TOKEN"] = "test-token"
|
||||||
|
requests_mock.get(
|
||||||
|
"https://api.trello.com/1/members/me/boards",
|
||||||
|
json=[{"id": "b1", "name": "Test", "url": "https://trello.com/b/b1", "closed": False, "starred": False}],
|
||||||
|
)
|
||||||
|
result = json.loads(trello_list_boards())
|
||||||
|
assert result["success"] is True
|
||||||
|
assert len(result["boards"]) == 1
|
||||||
|
|
||||||
|
def test_trello_disconnect_tool(self, requests_mock: Any) -> None:
|
||||||
|
"""Tool clears credentials and returns success."""
|
||||||
|
from trello_plugin.tools import trello_disconnect, trello_verify_credentials
|
||||||
|
|
||||||
|
os.environ["TRELLO_API_KEY"] = "test-key"
|
||||||
|
os.environ["TRELLO_TOKEN"] = "test-token"
|
||||||
|
|
||||||
|
# First verify works
|
||||||
|
requests_mock.get(
|
||||||
|
"https://api.trello.com/1/members/me",
|
||||||
|
json={"id": "m1", "username": "bot", "fullName": "Bot User"},
|
||||||
|
)
|
||||||
|
verify = json.loads(trello_verify_credentials())
|
||||||
|
assert verify["success"] is True
|
||||||
|
|
||||||
|
# Then disconnect
|
||||||
|
disc = json.loads(trello_disconnect())
|
||||||
|
assert disc["success"] is True
|
||||||
|
|
||||||
|
# With env vars still set, verify should reconnect successfully
|
||||||
|
# (disconnect clears the in-memory client; env vars persist)
|
||||||
|
verify2 = json.loads(trello_verify_credentials())
|
||||||
|
assert verify2["success"] is True
|
||||||
|
assert verify2["member"]["username"] == "bot"
|
||||||
|
|
||||||
|
def test_check_requirements_tool(self, requests_mock: Any) -> None:
|
||||||
|
"""check_requirements returns False when env not set."""
|
||||||
|
from trello_plugin.tools import check_requirements
|
||||||
|
|
||||||
|
assert check_requirements() is False
|
||||||
|
|
||||||
|
os.environ["TRELLO_API_KEY"] = "k"
|
||||||
|
os.environ["TRELLO_TOKEN"] = "t"
|
||||||
|
assert check_requirements() is True
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Plugin metadata
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestPluginMetadata:
|
||||||
|
"""Tests for the plugin metadata constants."""
|
||||||
|
|
||||||
|
def test_plugin_constants(self) -> None:
|
||||||
|
from trello_plugin.tools import (
|
||||||
|
PLUGIN_DESCRIPTION,
|
||||||
|
PLUGIN_NAME,
|
||||||
|
PLUGIN_TOOLS,
|
||||||
|
PLUGIN_VERSION,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert PLUGIN_NAME == "trello-plugin"
|
||||||
|
assert isinstance(PLUGIN_DESCRIPTION, str)
|
||||||
|
assert isinstance(PLUGIN_VERSION, str)
|
||||||
|
assert len(PLUGIN_TOOLS) == 3
|
||||||
|
assert all(callable(t) for t in PLUGIN_TOOLS)
|
||||||
Reference in New Issue
Block a user