Author SHA1 Message Date
crisleo94 20d31cd0bd Merge pull request 'docs: rename spec docs to migration-style numbering' (#9) from feature/docs-migration-numbering into main
Reviewed-on: #9
Reviewed-by: crisleo94 <cristhian.reinoso@codescripters.org>
2026-05-27 04:28:10 +00:00
Marko (Hermes Implementer) eec481c528 docs: rename spec docs to migration-style numbering
Rename docs/backend/*.md to Django-migration-style naming:
  trello-auth-spec.md    → 001_trello_auth_spec.md    (Issue #1)
  manage-boards-spec.md  → 002_manage_boards_spec.md  (Issue #2)
  manage-lists-spec.md   → 003_manage_lists_spec.md   (Issue #3)
  manage-cards-spec.md   → 004_manage_cards_spec.md   (Issue #4)

Each doc now includes a **Doc:** field with its sequence number.
2026-05-26 23:12:25 +00:00
crisleo94 f6dca202aa Merge pull request 'Implement: US: Manage Trello Cards' (#8) from feature/manage-cards into main
Reviewed-on: #8
Reviewed-by: crisleo94 <cristhian.reinoso@codescripters.org>
2026-05-26 23:01:12 +00:00
crisleo94 7fad3881bd Merge branch 'main' into feature/manage-cards 2026-05-26 23:00:46 +00:00
crisleo94 3dce15d87d Merge pull request 'Implement: US: Manage Trello Lists' (#7) from feature/manage-lists into main
Reviewed-on: #7
Reviewed-by: crisleo94 <cristhian.reinoso@codescripters.org>
2026-05-26 23:00:39 +00:00
crisleo94 8c05e999b3 Merge branch 'main' into feature/manage-lists 2026-05-26 22:59:22 +00:00
crisleo94 9496ef45ea Merge pull request 'Implement: US: Manage Trello Boards' (#6) from feature/manage-boards into main
Reviewed-on: #6
Reviewed-by: crisleo94 <cristhian.reinoso@codescripters.org>
2026-05-26 22:59:10 +00:00
crisleo94 5705b067c8 Merge branch 'main' into feature/manage-boards 2026-05-26 22:59:03 +00:00
crisleo94 44c53f8015 Merge pull request 'Implement: US: Authenticate and Connect to Trello Account' (#5) from feature/trello-auth into main
Reviewed-on: #5
Reviewed-by: crisleo94 <cristhian.reinoso@codescripters.org>
2026-05-26 22:46:26 +00:00
Marko (Hermes Implementer) 034ed2c928 feat: implement Trello card management
Add 12 card management tools:
- trello_create_card — create a card on a list
- trello_card_details — view card with members, checklists, comments
- trello_update_card — update title, description, due date
- trello_move_card — move card to a different list
- trello_archive_card — archive a card
- trello_assign_member / trello_remove_member — member management
- trello_add_comment / trello_delete_comment — comments
- trello_add_checklist_item / trello_toggle_checklist_item / trello_delete_checklist_item

Also adds _delete() helper on TrelloClient for DELETE verbs.

73 total tests — all passing.

Issue: #4
2026-05-26 22:42:50 +00:00
Marko (Hermes Implementer) f2d65f8914 feat: implement Trello list management
Add 4 list management tools extending the Trello client:
- trello_create_list — create a new list on a board
- trello_rename_list — rename an existing list
- trello_archive_list — archive a list
- trello_move_list — move a list to a new position

Also update the auth spec doc to reflect the PR #5 review fixes
(TypedDict contracts, updated disconnect message).

Issue: #3
2026-05-26 22:37:02 +00:00
Marko (Hermes Implementer) f4bff86b70 feat: implement Trello board management
Add 5 board management tools extending the Trello client:
- trello_create_board — create a new board
- trello_rename_board — rename an existing board
- trello_archive_board — close/archive a board
- trello_open_board — re-open a closed board
- trello_board_details — view board with lists and members

Includes board name-to-ID resolution, 16 new tests, and spec doc.

Issue: #2
2026-05-26 22:28:49 +00:00
11 changed files with 2329 additions and 3 deletions
+9 -2
View File
@@ -65,7 +65,9 @@ Fetches and returns all Trello boards accessible to the authenticated user.
### 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.
Clears the stored credentials from memory. 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 settings if needed.
**Parameters:** None
@@ -73,7 +75,7 @@ Clears the stored credentials from memory. Note: this does not revoke the Trello
```json
{
"success": true,
"message": "Trello credentials cleared. Set TRELLO_API_KEY and TRELLO_TOKEN again to reconnect."
"message": "Trello credentials cleared from memory. Next tool call will re-read TRELLO_API_KEY and TRELLO_TOKEN from environment and reconnect automatically."
}
```
@@ -83,6 +85,9 @@ Clears the stored credentials from memory. Note: this does not revoke the Trello
```python
class TrelloClient:
api_key: str
token: str
def __init__(self, api_key: str | None = None, token: str | None = None)
def verify_credentials(self) -> dict
@@ -90,6 +95,8 @@ class TrelloClient:
def disconnect(self) -> dict
```
The client uses `TypedDict` response contracts (`SuccessResponse`/`ErrorResponse`) with a `TypeGuard` helper `_is_success()` for type-safe narrowing. Internal helpers include `_request()`, `_get()`, `_post()`, `_put()`, `_check_credentials()`, and `_handle_http_error()`.
### Trello API Endpoints Used
| Purpose | Method | Endpoint | Docs |
+117
View File
@@ -0,0 +1,117 @@
# Trello Plugin — Manage Trello Boards
**Feature:** US: Manage Trello Boards
**Issue:** #2
**Branch:** `feature/manage-boards`
**Doc:** 002
## Overview
Extends the Trello plugin with board management capabilities: create, rename, close/archive, open, and view details of Trello boards.
## Design Decisions
- **Extends existing TrelloClient** — All board operations go through the same `TrelloClient` class from the auth feature.
- **Board selection by ID** — The Trello API identifies boards by ID. The tool accepts both ID and name (client resolves name to ID).
- **Resolves board name to ID** — When a user provides a board name instead of ID, the client fetches all boards and matches by name.
## Tools
### `trello_create_board`
Create a new Trello board.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Board name |
| `default_lists` | bool | No | Whether to create the default lists (default: true) |
**Returns:**
```json
{
"success": true,
"board": {"id": "abc123", "name": "My Board", "url": "https://trello.com/b/abc123"}
}
```
### `trello_rename_board`
Rename an existing board.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `board_id` | string | Yes | Board ID or name |
| `name` | string | Yes | New board name |
**Returns:**
```json
{"success": true, "board": {"id": "abc123", "name": "New Name"}}
```
### `trello_archive_board`
Close/archive a board.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `board_id` | string | Yes | Board ID or name |
**Returns:**
```json
{"success": true, "message": "Board 'My Board' archived."}
```
### `trello_open_board`
Re-open a closed/archived board.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `board_id` | string | Yes | Board ID or name |
**Returns:**
```json
{"success": true, "message": "Board 'My Board' opened."}
```
### `trello_board_details`
View details of a specific board, including its lists and members.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `board_id` | string | Yes | Board ID or name |
**Returns:**
```json
{
"success": true,
"board": {
"id": "abc123",
"name": "My Board",
"url": "https://trello.com/b/abc123",
"desc": "",
"closed": false,
"starred": false,
"lists": [{"id": "l1", "name": "To Do"}, {"id": "l2", "name": "In Progress"}],
"members": [{"id": "m1", "username": "user1", "full_name": "User One"}]
}
}
```
## Trello API Endpoints
| Purpose | Method | Endpoint |
|---------|--------|----------|
| Create board | POST | `/1/boards/` |
| Update board | PUT | `/1/boards/{id}` |
| Get board details | GET | `/1/boards/{id}` (with lists and members fields) |
## Error Handling
- Board not found → clear message suggesting `trello_list_boards` to find the ID
- Validation errors → surfaced directly from Trello API
+89
View File
@@ -0,0 +1,89 @@
# Trello Plugin — Manage Trello Lists
**Feature:** US: Manage Trello Lists
**Issue:** #3
**Branch:** `feature/manage-lists`
**Doc:** 003
## Overview
Extends the Trello plugin with list management capabilities: create, rename, archive, and reposition lists on a Trello board.
## Design Decisions
- **Lists require a board context** — Creating a list needs a board ID. Other operations (rename, archive, move) use the list's Trello ID which is globally unique.
- **Position parameter** — Uses Trello's `pos` field which accepts `"top"`, `"bottom"`, or a positive number.
## Tools
### `trello_create_list`
Create a new list on a board.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | List name |
| `board_id` | string | Yes | Board ID or name |
| `pos` | string | No | Position: `"top"`, `"bottom"`, or number (default: `"bottom"`) |
**Returns:**
```json
{"success": true, "list": {"id": "l1", "name": "My List", "id_board": "b1"}}
```
### `trello_rename_list`
Rename an existing list.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `list_id` | string | Yes | List ID |
| `name` | string | Yes | New name |
**Returns:**
```json
{"success": true, "list": {"id": "l1", "name": "Renamed List"}}
```
### `trello_archive_list`
Archive a list.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `list_id` | string | Yes | List ID |
**Returns:**
```json
{"success": true, "message": "List 'My List' archived."}
```
### `trello_move_list`
Move a list to a different position.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `list_id` | string | Yes | List ID |
| `pos` | string | Yes | Position: `"top"`, `"bottom"`, or a number |
**Returns:**
```json
{"success": true, "message": "List 'My List' moved."}
```
## Trello API Endpoints
| Purpose | Method | Endpoint |
|---------|--------|----------|
| Create list | POST | `/1/lists` |
| Update list (rename, archive, move) | PUT | `/1/lists/{id}` |
## Error Handling
- List not found → clear error message
- Board not found when creating → propagate board lookup error
+163
View File
@@ -0,0 +1,163 @@
# Trello Plugin — Manage Trello Cards
**Feature:** US: Manage Trello Cards
**Issue:** #4
**Branch:** `feature/manage-cards`
**Doc:** 004
## Overview
Extends the Trello plugin with full card management capabilities: create, view, update, move, archive, assign members, comments, and checklist management.
## Tools
### `trello_create_card`
Create a new card on a selected list.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Card title |
| `list_id` | string | Yes | List ID |
| `desc` | string | No | Card description |
| `due` | string | No | Due date (ISO 8601) |
**Returns:** Card ID, name, URL, and short URL.
### `trello_card_details`
View detailed information about a card.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card ID |
**Returns:** Card details including title, description, due date, members, checklists, comments, URL.
### `trello_update_card`
Update a card's core attributes (title, description, due date).
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card ID |
| `name` | string | No | New title |
| `desc` | string | No | New description |
| `due` | string | No | New due date (ISO 8601) |
**Returns:** Updated card details.
### `trello_move_card`
Move a card to a different list.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card ID |
| `list_id` | string | Yes | Target list ID |
**Returns:** Success message with new list location.
### `trello_archive_card`
Archive a card.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card ID |
**Returns:** Success message.
### `trello_assign_member`
Add a member to a card.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card ID |
| `member_id` | string | Yes | Trello member ID |
**Returns:** Success message.
### `trello_remove_member`
Remove a member from a card.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card ID |
| `member_id` | string | Yes | Trello member ID |
**Returns:** Success message.
### `trello_add_comment`
Add a comment to a card.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card ID |
| `text` | string | Yes | Comment text |
**Returns:** Comment ID and success.
### `trello_delete_comment`
Delete a comment from a card.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card ID |
| `comment_id` | string | Yes | Comment/action ID |
**Returns:** Success message.
### `trello_add_checklist_item`
Add a checklist item to a card.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card ID |
| `name` | string | Yes | Checklist item text |
| `checklist_id` | string | No | Specific checklist ID (if card has multiple) |
**Returns:** Checklist item details.
### `trello_toggle_checklist_item`
Mark a checklist item as complete or incomplete.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card ID |
| `item_id` | string | Yes | Checklist item ID |
| `checked` | bool | Yes | True = complete, False = incomplete |
**Returns:** Updated item state.
### `trello_delete_checklist_item`
Remove a checklist item.
**Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `card_id` | string | Yes | Card ID |
| `item_id` | string | Yes | Checklist item ID |
**Returns:** Success message.
## Trello API Endpoints
| Purpose | Method | Endpoint |
|---------|--------|----------|
| Create card | POST | `/1/cards` |
| Get card | GET | `/1/cards/{id}` |
| Update card | PUT | `/1/cards/{id}` |
| Add member | POST | `/1/cards/{id}/idMembers` |
| Remove member | DELETE | `/1/cards/{id}/idMembers/{memberId}` |
| Add comment | POST | `/1/cards/{id}/actions/comments` |
| Delete comment | DELETE | `/1/cards/{id}/actions/{commentId}/comments` |
| Add checklist item | POST | `/1/cards/{id}/checklistItems` |
| Update checklist item | PUT | `/1/cards/{id}/checklistItem/{itemId}/state` |
| Delete checklist item | DELETE | `/1/cards/{id}/checklistItems/{itemId}`
+42
View File
@@ -8,8 +8,29 @@ from trello_plugin.tools import (
PLUGIN_TOOLS,
PLUGIN_VERSION,
check_requirements,
trello_add_checklist_item,
trello_add_comment,
trello_archive_board,
trello_archive_card,
trello_archive_list,
trello_assign_member,
trello_board_details,
trello_card_details,
trello_create_board,
trello_create_card,
trello_create_list,
trello_delete_checklist_item,
trello_delete_comment,
trello_disconnect,
trello_list_boards,
trello_move_card,
trello_move_list,
trello_open_board,
trello_remove_member,
trello_rename_board,
trello_rename_list,
trello_toggle_checklist_item,
trello_update_card,
trello_verify_credentials,
)
@@ -22,4 +43,25 @@ __all__ = [
"trello_verify_credentials",
"trello_list_boards",
"trello_disconnect",
"trello_create_board",
"trello_rename_board",
"trello_archive_board",
"trello_open_board",
"trello_board_details",
"trello_create_list",
"trello_rename_list",
"trello_archive_list",
"trello_move_list",
"trello_create_card",
"trello_card_details",
"trello_update_card",
"trello_move_card",
"trello_archive_card",
"trello_assign_member",
"trello_remove_member",
"trello_add_comment",
"trello_delete_comment",
"trello_add_checklist_item",
"trello_toggle_checklist_item",
"trello_delete_checklist_item",
]
+505
View File
@@ -163,6 +163,10 @@ class TrelloClient:
"""Make an authenticated PUT request."""
return self._request("PUT", path, json_body=json_body)
def _delete(self, path: str) -> ApiResponse:
"""Make an authenticated DELETE request."""
return self._request("DELETE", path)
# ------------------------------------------------------------------
# Public API — Auth & Connection
# ------------------------------------------------------------------
@@ -259,6 +263,11 @@ class TrelloClient:
board = result["data"]
return {"id": board["id"], "name": board.get("name", ""), "url": board.get("url", "")}
# If the error is about missing credentials, propagate that directly
error_msg = result.get("message", "")
if "TRELLO_API_KEY" in error_msg:
return {"success": False, "message": error_msg}
# Try resolving by name
boards_result = self.list_boards()
if not _is_success(boards_result): # type: ignore[arg-type]
@@ -462,6 +471,502 @@ class TrelloClient:
},
}
# ------------------------------------------------------------------
# Public API — List Management
# ------------------------------------------------------------------
def create_list(self, name: str, board_id: str, pos: str = "bottom") -> dict[str, Any]:
"""Create a new list on a Trello board.
Args:
name: The name for the new list.
board_id: Board ID or name.
pos: Position — ``"top"``, ``"bottom"``, or a number.
Returns:
dict with list 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]
body: dict[str, Any] = {
"name": name,
"idBoard": resolved["id"],
"pos": pos,
}
result = self._post("/lists", json_body=body)
if _is_success(result):
lst = result["data"]
return {
"success": True,
"list": {
"id": lst.get("id"),
"name": lst.get("name"),
"id_board": lst.get("idBoard"),
},
}
return {
"success": False,
"message": result.get("message", "Failed to create list."),
}
def rename_list(self, list_id: str, name: str) -> dict[str, Any]:
"""Rename an existing list.
Args:
list_id: Trello list ID.
name: The new name.
Returns:
dict with list details on success, or an error dict.
"""
result = self._put(f"/lists/{list_id}", json_body={"name": name})
if _is_success(result):
lst = result["data"]
return {
"success": True,
"list": {
"id": lst.get("id"),
"name": lst.get("name"),
},
}
return {
"success": False,
"message": result.get("message", "Failed to rename list."),
}
def archive_list(self, list_id: str) -> dict[str, Any]:
"""Archive a list.
Args:
list_id: Trello list ID.
Returns:
dict with success message or error.
"""
result = self._put(f"/lists/{list_id}", json_body={"closed": True})
if _is_success(result):
lst = result["data"]
list_name = lst.get("name", list_id)
return {"success": True, "message": f"List '{list_name}' archived."}
return {
"success": False,
"message": result.get("message", "Failed to archive list."),
}
def move_list(self, list_id: str, pos: str = "bottom") -> dict[str, Any]:
"""Move a list to a different position on the board.
Args:
list_id: Trello list ID.
pos: Position — ``"top"``, ``"bottom"``, or a number.
Returns:
dict with success message or error.
"""
result = self._put(f"/lists/{list_id}", json_body={"pos": pos})
if _is_success(result):
lst = result["data"]
list_name = lst.get("name", list_id)
return {"success": True, "message": f"List '{list_name}' moved."}
return {
"success": False,
"message": result.get("message", "Failed to move list."),
}
# ------------------------------------------------------------------
# Public API — Card Management
# ------------------------------------------------------------------
def create_card(
self,
name: str,
list_id: str,
desc: str = "",
due: str | None = None,
) -> dict[str, Any]:
"""Create a new card on a Trello list.
Args:
name: Card title.
list_id: Trello list ID.
desc: Card description.
due: Due date in ISO 8601 format.
Returns:
dict with card details on success.
"""
body: dict[str, Any] = {"name": name, "idList": list_id}
if desc:
body["desc"] = desc
if due:
body["due"] = due
result = self._post("/cards", json_body=body)
if _is_success(result):
card = result["data"]
return {
"success": True,
"card": {
"id": card.get("id"),
"name": card.get("name"),
"url": card.get("url"),
"short_url": card.get("shortUrl"),
"id_list": card.get("idList"),
},
}
return {
"success": False,
"message": result.get("message", "Failed to create card."),
}
def card_details(self, card_id: str) -> dict[str, Any]:
"""Fetch detailed information about a card.
Returns card attributes, members, checklists, and comments.
Args:
card_id: Trello card ID.
Returns:
dict with full card details.
"""
result = self._get(
f"/cards/{card_id}",
params={
"fields": "id,name,desc,due,dueComplete,url,shortUrl,idList,closed",
"members": "true",
"member_fields": "id,username,fullName",
"checklists": "all",
"actions": "commentCard",
"actions_limit": "50",
"list": "true",
},
)
if not _is_success(result):
return {
"success": False,
"message": result.get("message", "Failed to fetch card details."),
}
card = result["data"]
return {
"success": True,
"card": {
"id": card.get("id"),
"name": card.get("name"),
"desc": card.get("desc", ""),
"due": card.get("due"),
"due_complete": card.get("dueComplete", False),
"url": card.get("url"),
"short_url": card.get("shortUrl"),
"closed": card.get("closed", False),
"list": {"id": card.get("idList"), "name": (card.get("list") or {}).get("name", "")},
"members": [
{"id": m.get("id"), "username": m.get("username"), "full_name": m.get("fullName")}
for m in card.get("members", [])
],
"checklists": [
{
"id": cl.get("id"),
"name": cl.get("name"),
"items": [
{
"id": item.get("id"),
"name": item.get("name"),
"state": item.get("state"),
}
for item in cl.get("checkItems", [])
],
}
for cl in card.get("checklists", [])
],
"comments": [
{
"id": a.get("id"),
"text": a.get("data", {}).get("text", ""),
"member": a.get("memberCreator", {}).get("username", "unknown"),
"date": a.get("date"),
}
for a in card.get("actions", [])
if a.get("type") == "commentCard"
],
},
}
def update_card(
self,
card_id: str,
name: str | None = None,
desc: str | None = None,
due: str | None = None,
) -> dict[str, Any]:
"""Update a card's core attributes.
Args:
card_id: Trello card ID.
name: New title (omit to keep current).
desc: New description (omit to keep current).
due: New due date ISO 8601, or empty string to clear.
Returns:
dict with updated card details.
"""
body: dict[str, Any] = {}
if name is not None:
body["name"] = name
if desc is not None:
body["desc"] = desc
if due is not None:
body["due"] = due
if not body:
return {
"success": True,
"message": "No changes specified.",
}
result = self._put(f"/cards/{card_id}", json_body=body)
if _is_success(result):
card = result["data"]
return {
"success": True,
"card": {
"id": card.get("id"),
"name": card.get("name"),
"desc": card.get("desc", ""),
"due": card.get("due"),
},
}
return {
"success": False,
"message": result.get("message", "Failed to update card."),
}
def move_card(self, card_id: str, list_id: str) -> dict[str, Any]:
"""Move a card to a different list.
Args:
card_id: Trello card ID.
list_id: Target list ID.
Returns:
dict with success message.
"""
result = self._put(f"/cards/{card_id}", json_body={"idList": list_id})
if _is_success(result):
card = result["data"]
return {
"success": True,
"message": f"Card '{card.get('name', card_id)}' moved to list '{card.get('idList', list_id)}'.",
}
return {
"success": False,
"message": result.get("message", "Failed to move card."),
}
def archive_card(self, card_id: str) -> dict[str, Any]:
"""Archive a card.
Args:
card_id: Trello card ID.
Returns:
dict with success message.
"""
result = self._put(f"/cards/{card_id}", json_body={"closed": True})
if _is_success(result):
card = result["data"]
return {"success": True, "message": f"Card '{card.get('name', card_id)}' archived."}
return {
"success": False,
"message": result.get("message", "Failed to archive card."),
}
def assign_member(self, card_id: str, member_id: str) -> dict[str, Any]:
"""Add a member to a card.
Args:
card_id: Trello card ID.
member_id: Trello member ID.
Returns:
dict with success message.
"""
result = self._post(f"/cards/{card_id}/idMembers", json_body={"value": member_id})
if _is_success(result):
return {"success": True, "message": f"Member {member_id} assigned to card."}
return {
"success": False,
"message": result.get("message", "Failed to assign member."),
}
def remove_member(self, card_id: str, member_id: str) -> dict[str, Any]:
"""Remove a member from a card.
Args:
card_id: Trello card ID.
member_id: Trello member ID.
Returns:
dict with success message.
"""
result = self._delete(f"/cards/{card_id}/idMembers/{member_id}")
if _is_success(result):
return {"success": True, "message": f"Member {member_id} removed from card."}
return {
"success": False,
"message": result.get("message", "Failed to remove member."),
}
def add_comment(self, card_id: str, text: str) -> dict[str, Any]:
"""Add a comment to a card.
Args:
card_id: Trello card ID.
text: Comment text.
Returns:
dict with comment details.
"""
result = self._post(
f"/cards/{card_id}/actions/comments",
json_body={"text": text},
)
if _is_success(result):
action = result["data"]
return {
"success": True,
"comment": {
"id": action.get("id"),
"text": action.get("data", {}).get("text", text),
},
}
return {
"success": False,
"message": result.get("message", "Failed to add comment."),
}
def delete_comment(self, card_id: str, comment_id: str) -> dict[str, Any]:
"""Delete a comment from a card.
Args:
card_id: Trello card ID.
comment_id: Action/comment ID.
Returns:
dict with success message.
"""
result = self._delete(f"/cards/{card_id}/actions/{comment_id}/comments")
if _is_success(result):
return {"success": True, "message": "Comment deleted."}
return {
"success": False,
"message": result.get("message", "Failed to delete comment."),
}
def add_checklist_item(self, card_id: str, name: str, checklist_id: str | None = None) -> dict[str, Any]:
"""Add a checklist item to a card.
Args:
card_id: Trello card ID.
name: Checklist item text.
checklist_id: Specific checklist (fetches first if omitted).
Returns:
dict with item details.
"""
resolved_checklist_id = checklist_id
if not resolved_checklist_id:
# Fetch checklists to find the first one
details = self.card_details(card_id)
if not details.get("success"):
return {
"success": False,
"message": "Card has no checklists. Add one via Trello web first.",
}
checklists = details["card"].get("checklists", [])
if not checklists:
return {
"success": False,
"message": "Card has no checklists. Add one via Trello web first.",
}
resolved_checklist_id = checklists[0]["id"]
result = self._post(
f"/cards/{card_id}/checklistItems",
json_body={
"idChecklist": resolved_checklist_id,
"name": name,
"checked": False,
},
)
if _is_success(result):
item = result["data"]
return {
"success": True,
"item": {
"id": item.get("id"),
"name": item.get("name"),
"state": item.get("state", "incomplete"),
"id_checklist": item.get("idChecklist"),
},
}
return {
"success": False,
"message": result.get("message", "Failed to add checklist item."),
}
def toggle_checklist_item(self, card_id: str, item_id: str, checked: bool) -> dict[str, Any]: # noqa: FBT001
"""Mark a checklist item as complete or incomplete.
Args:
card_id: Trello card ID.
item_id: Checklist item ID.
checked: True for complete, False for incomplete.
Returns:
dict with updated item state.
"""
state = "complete" if checked else "incomplete"
result = self._put(
f"/cards/{card_id}/checklistItem/{item_id}",
json_body={"state": state},
)
if _is_success(result):
item = result["data"]
return {
"success": True,
"item": {
"id": item.get("id"),
"name": item.get("name"),
"state": item.get("state"),
},
}
return {
"success": False,
"message": result.get("message", "Failed to update checklist item."),
}
def delete_checklist_item(self, card_id: str, item_id: str) -> dict[str, Any]:
"""Remove a checklist item.
Args:
card_id: Trello card ID.
item_id: Checklist item ID.
Returns:
dict with success message.
"""
result = self._delete(f"/cards/{card_id}/checklistItems/{item_id}")
if _is_success(result):
return {"success": True, "message": "Checklist item deleted."}
return {
"success": False,
"message": result.get("message", "Failed to delete checklist item."),
}
@staticmethod
def check_requirements() -> bool:
"""Check if the required environment variables are set."""
+458
View File
@@ -76,6 +76,443 @@ def trello_disconnect() -> str:
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."})
# ---------------------------------------------------------------------------
# Tools — Board Management
# ---------------------------------------------------------------------------
def trello_create_board(name: str, default_lists: bool = True) -> str: # noqa: FBT001, FBT002
"""Create a new Trello board.
Parameters
----------
name : str
The name for the new board.
default_lists : bool, optional
Whether to create default lists (default: True).
Returns
-------
str
JSON with board id, name, and URL on success.
"""
client = _get_client()
result = client.create_board(name=name, default_lists=default_lists)
return _respond(result)
def trello_rename_board(board_id: str, name: str) -> str:
"""Rename an existing Trello board.
Parameters
----------
board_id : str
Board ID or name.
name : str
The new name for the board.
Returns
-------
str
JSON with updated board details on success.
"""
client = _get_client()
result = client.rename_board(board_id=board_id, name=name)
return _respond(result)
def trello_archive_board(board_id: str) -> str:
"""Close/archive a Trello board.
Parameters
----------
board_id : str
Board ID or name.
Returns
-------
str
JSON with success message.
"""
client = _get_client()
result = client.archive_board(board_id=board_id)
return _respond(result)
def trello_open_board(board_id: str) -> str:
"""Re-open a closed/archived Trello board.
Parameters
----------
board_id : str
Board ID or name.
Returns
-------
str
JSON with success message.
"""
client = _get_client()
result = client.open_board(board_id=board_id)
return _respond(result)
def trello_board_details(board_id: str) -> str:
"""View details of a Trello board, including its lists and members.
Parameters
----------
board_id : str
Board ID or name.
Returns
-------
str
JSON with board details including lists and members.
"""
client = _get_client()
result = client.board_details(board_id=board_id)
return _respond(result)
# ---------------------------------------------------------------------------
# Tools — List Management
# ---------------------------------------------------------------------------
def trello_create_list(name: str, board_id: str, pos: str = "bottom") -> str:
"""Create a new list on a Trello board.
Parameters
----------
name : str
The name for the new list.
board_id : str
Board ID or name.
pos : str, optional
Position: ``"top"``, ``"bottom"``, or a number (default: ``"bottom"``).
Returns
-------
str
JSON with list details on success.
"""
client = _get_client()
result = client.create_list(name=name, board_id=board_id, pos=pos)
return _respond(result)
def trello_rename_list(list_id: str, name: str) -> str:
"""Rename an existing list.
Parameters
----------
list_id : str
Trello list ID.
name : str
The new name.
Returns
-------
str
JSON with updated list details on success.
"""
client = _get_client()
result = client.rename_list(list_id=list_id, name=name)
return _respond(result)
def trello_archive_list(list_id: str) -> str:
"""Archive a list.
Parameters
----------
list_id : str
Trello list ID.
Returns
-------
str
JSON with success message.
"""
client = _get_client()
result = client.archive_list(list_id=list_id)
return _respond(result)
def trello_move_list(list_id: str, pos: str = "bottom") -> str:
"""Move a list to a different position on the board.
Parameters
----------
list_id : str
Trello list ID.
pos : str, optional
Position: ``"top"``, ``"bottom"``, or a number (default: ``"bottom"``).
Returns
-------
str
JSON with success message.
"""
client = _get_client()
result = client.move_list(list_id=list_id, pos=pos)
return _respond(result)
# ---------------------------------------------------------------------------
# Tools — Card Management
# ---------------------------------------------------------------------------
def trello_create_card(name: str, list_id: str, desc: str = "", due: str | None = None) -> str:
"""Create a new card on a Trello list.
Parameters
----------
name : str
Card title.
list_id : str
List ID.
desc : str, optional
Card description.
due : str, optional
Due date in ISO 8601 format.
Returns
-------
str
JSON with card details.
"""
client = _get_client()
result = client.create_card(name=name, list_id=list_id, desc=desc, due=due)
return _respond(result)
def trello_card_details(card_id: str) -> str:
"""View detailed information about a Trello card.
Parameters
----------
card_id : str
Card ID.
Returns
-------
str
JSON with full card details including members, checklists, and comments.
"""
client = _get_client()
result = client.card_details(card_id=card_id)
return _respond(result)
def trello_update_card(card_id: str, name: str | None = None, desc: str | None = None, due: str | None = None) -> str:
"""Update a card's core attributes (title, description, due date).
Parameters
----------
card_id : str
Card ID.
name : str, optional
New title.
desc : str, optional
New description.
due : str, optional
New due date ISO 8601.
Returns
-------
str
JSON with updated card details.
"""
client = _get_client()
result = client.update_card(card_id=card_id, name=name, desc=desc, due=due)
return _respond(result)
def trello_move_card(card_id: str, list_id: str) -> str:
"""Move a card to a different list.
Parameters
----------
card_id : str
Card ID.
list_id : str
Target list ID.
Returns
-------
str
JSON with success message.
"""
client = _get_client()
result = client.move_card(card_id=card_id, list_id=list_id)
return _respond(result)
def trello_archive_card(card_id: str) -> str:
"""Archive a Trello card.
Parameters
----------
card_id : str
Card ID.
Returns
-------
str
JSON with success message.
"""
client = _get_client()
result = client.archive_card(card_id=card_id)
return _respond(result)
def trello_assign_member(card_id: str, member_id: str) -> str:
"""Add a member to a Trello card.
Parameters
----------
card_id : str
Card ID.
member_id : str
Trello member ID.
Returns
-------
str
JSON with success message.
"""
client = _get_client()
result = client.assign_member(card_id=card_id, member_id=member_id)
return _respond(result)
def trello_remove_member(card_id: str, member_id: str) -> str:
"""Remove a member from a Trello card.
Parameters
----------
card_id : str
Card ID.
member_id : str
Trello member ID.
Returns
-------
str
JSON with success message.
"""
client = _get_client()
result = client.remove_member(card_id=card_id, member_id=member_id)
return _respond(result)
def trello_add_comment(card_id: str, text: str) -> str:
"""Add a comment to a Trello card.
Parameters
----------
card_id : str
Card ID.
text : str
Comment text.
Returns
-------
str
JSON with comment ID.
"""
client = _get_client()
result = client.add_comment(card_id=card_id, text=text)
return _respond(result)
def trello_delete_comment(card_id: str, comment_id: str) -> str:
"""Delete a comment from a Trello card.
Parameters
----------
card_id : str
Card ID.
comment_id : str
Comment/action ID.
Returns
-------
str
JSON with success message.
"""
client = _get_client()
result = client.delete_comment(card_id=card_id, comment_id=comment_id)
return _respond(result)
def trello_add_checklist_item(card_id: str, name: str, checklist_id: str | None = None) -> str:
"""Add a checklist item to a Trello card.
Parameters
----------
card_id : str
Card ID.
name : str
Checklist item text.
checklist_id : str, optional
Specific checklist ID (auto-discovers the first if omitted).
Returns
-------
str
JSON with item details.
"""
client = _get_client()
result = client.add_checklist_item(card_id=card_id, name=name, checklist_id=checklist_id)
return _respond(result)
def trello_toggle_checklist_item(card_id: str, item_id: str, checked: bool) -> str: # noqa: FBT001
"""Mark a checklist item as complete or incomplete.
Parameters
----------
card_id : str
Card ID.
item_id : str
Checklist item ID.
checked : bool
True = complete, False = incomplete.
Returns
-------
str
JSON with updated item state.
"""
client = _get_client()
result = client.toggle_checklist_item(card_id=card_id, item_id=item_id, checked=checked)
return _respond(result)
def trello_delete_checklist_item(card_id: str, item_id: str) -> str:
"""Remove a checklist item from a Trello card.
Parameters
----------
card_id : str
Card ID.
item_id : str
Checklist item ID.
Returns
-------
str
JSON with success message.
"""
client = _get_client()
result = client.delete_checklist_item(card_id=card_id, item_id=item_id)
return _respond(result)
# ---------------------------------------------------------------------------
# Plugin metadata (used by Hermes plugin loader)
# ---------------------------------------------------------------------------
@@ -87,6 +524,27 @@ PLUGIN_TOOLS = [
trello_verify_credentials,
trello_list_boards,
trello_disconnect,
trello_create_board,
trello_rename_board,
trello_archive_board,
trello_open_board,
trello_board_details,
trello_create_list,
trello_rename_list,
trello_archive_list,
trello_move_list,
trello_create_card,
trello_card_details,
trello_update_card,
trello_move_card,
trello_archive_card,
trello_assign_member,
trello_remove_member,
trello_add_comment,
trello_delete_comment,
trello_add_checklist_item,
trello_toggle_checklist_item,
trello_delete_checklist_item,
]
PLUGIN_REQUIRES_ENV = ["TRELLO_API_KEY", "TRELLO_TOKEN"]
+1 -1
View File
@@ -325,5 +325,5 @@ class TestPluginMetadata:
assert PLUGIN_NAME == "trello-plugin"
assert isinstance(PLUGIN_DESCRIPTION, str)
assert isinstance(PLUGIN_VERSION, str)
assert len(PLUGIN_TOOLS) == 3
assert len(PLUGIN_TOOLS) == 24
assert all(callable(t) for t in PLUGIN_TOOLS)
+406
View File
@@ -0,0 +1,406 @@
"""Tests for the Trello plugin — board management feature."""
from __future__ import annotations
import json
import os
from typing import Any
import pytest
import requests
pytestmark = pytest.mark.usefixtures("clear_env")
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def clear_env(monkeypatch: pytest.MonkeyPatch) -> Any:
"""Remove Trello env vars before each test so state is predictable."""
monkeypatch.delenv("TRELLO_API_KEY", raising=False)
monkeypatch.delenv("TRELLO_TOKEN", raising=False)
# ---------------------------------------------------------------------------
# TrelloClient — create_board
# ---------------------------------------------------------------------------
class TestCreateBoard:
"""Tests for TrelloClient.create_board()."""
def test_success(self, requests_mock: Any) -> None:
"""Happy path: creates a board and returns details."""
from trello_plugin.client import TrelloClient
requests_mock.post(
"https://api.trello.com/1/boards",
json={"id": "b1", "name": "New Board", "url": "https://trello.com/b/b1"},
status_code=200,
)
client = TrelloClient(api_key="key", token="tok")
result = client.create_board(name="New Board")
assert result["success"] is True
assert result["board"]["name"] == "New Board"
assert result["board"]["id"] == "b1"
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.create_board(name="Board")
assert result["success"] is False
assert "TRELLO_API_KEY" in result["message"]
# ---------------------------------------------------------------------------
# TrelloClient — rename_board
# ---------------------------------------------------------------------------
class TestRenameBoard:
"""Tests for TrelloClient.rename_board()."""
def test_success_by_id(self, requests_mock: Any) -> None:
"""Renaming a board by ID works."""
from trello_plugin.client import TrelloClient
# Resolve board
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Old Name", "url": "https://trello.com/b/b1"},
)
# Rename
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "New Name", "url": "https://trello.com/b/b1"},
)
client = TrelloClient(api_key="key", token="tok")
result = client.rename_board(board_id="b1", name="New Name")
assert result["success"] is True
assert result["board"]["name"] == "New Name"
def test_board_not_found(self, requests_mock: Any) -> None:
"""Non-existent board returns a clear error."""
from trello_plugin.client import TrelloClient
# Board lookup fails
requests_mock.get(
"https://api.trello.com/1/boards/nonexistent",
status_code=404,
)
# List boards returns empty
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=[],
)
client = TrelloClient(api_key="key", token="tok")
result = client.rename_board(board_id="nonexistent", name="New")
assert result["success"] is False
assert "not found" in result["message"].lower()
# ---------------------------------------------------------------------------
# TrelloClient — archive_board
# ---------------------------------------------------------------------------
class TestArchiveBoard:
"""Tests for TrelloClient.archive_board()."""
def test_success(self, requests_mock: Any) -> None:
"""Archiving a board returns success message."""
from trello_plugin.client import TrelloClient
# Resolve
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
)
# Archive
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "closed": True},
)
client = TrelloClient(api_key="key", token="tok")
result = client.archive_board(board_id="b1")
assert result["success"] is True
assert "archived" in result["message"].lower()
# ---------------------------------------------------------------------------
# TrelloClient — open_board
# ---------------------------------------------------------------------------
class TestOpenBoard:
"""Tests for TrelloClient.open_board()."""
def test_success(self, requests_mock: Any) -> None:
"""Opening a closed board returns success message."""
from trello_plugin.client import TrelloClient
# Resolve
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
)
# Open
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "closed": False},
)
client = TrelloClient(api_key="key", token="tok")
result = client.open_board(board_id="b1")
assert result["success"] is True
assert "opened" in result["message"].lower()
# ---------------------------------------------------------------------------
# TrelloClient — board_details
# ---------------------------------------------------------------------------
class TestBoardDetails:
"""Tests for TrelloClient.board_details()."""
def test_success(self, requests_mock: Any) -> None:
"""Board details return lists and members."""
from trello_plugin.client import TrelloClient
# Resolve
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
)
# Details fetch
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={
"id": "b1",
"name": "My Board",
"url": "https://trello.com/b/b1",
"desc": "A test board",
"closed": False,
"starred": False,
"lists": [
{"id": "l1", "name": "To Do"},
{"id": "l2", "name": "Done"},
],
"members": [
{"id": "m1", "username": "user1", "fullName": "User One"},
],
},
)
client = TrelloClient(api_key="key", token="tok")
result = client.board_details(board_id="b1")
assert result["success"] is True
assert len(result["board"]["lists"]) == 2
assert len(result["board"]["members"]) == 1
assert result["board"]["lists"][0]["name"] == "To Do"
def test_not_found(self, requests_mock: Any) -> None:
"""Non-existent board returns error."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/boards/nonexistent",
status_code=404,
)
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=[],
)
client = TrelloClient(api_key="key", token="tok")
result = client.board_details(board_id="nonexistent")
assert result["success"] is False
assert "not found" in result["message"].lower()
# ---------------------------------------------------------------------------
# TrelloClient — _resolve_board_id
# ---------------------------------------------------------------------------
class TestResolveBoardId:
"""Tests for TrelloClient._resolve_board_id()."""
def test_resolves_by_id(self, requests_mock: Any) -> None:
"""Resolves a valid ID directly."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
)
client = TrelloClient(api_key="key", token="tok")
result = client._resolve_board_id("b1")
assert result["id"] == "b1"
def test_resolves_by_name(self, requests_mock: Any) -> None:
"""Resolves a board name to its ID."""
from trello_plugin.client import TrelloClient
# ID lookup fails
requests_mock.get(
"https://api.trello.com/1/boards/My%20Board",
status_code=404,
)
# List boards for name matching
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=[
{"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1", "closed": False, "starred": False},
{"id": "b2", "name": "Other Board", "url": "https://trello.com/b/b2", "closed": False, "starred": False},
],
)
client = TrelloClient(api_key="key", token="tok")
result = client._resolve_board_id("My Board")
assert result["id"] == "b1"
def test_duplicate_name_error(self, requests_mock: Any) -> None:
"""Multiple boards with same name returns error."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/boards/Duplicate",
status_code=404,
)
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=[
{"id": "b1", "name": "Duplicate", "url": "", "closed": False, "starred": False},
{"id": "b2", "name": "Duplicate", "url": "", "closed": False, "starred": False},
],
)
client = TrelloClient(api_key="key", token="tok")
result = client._resolve_board_id("Duplicate")
assert "success" in result and result["success"] is False
assert "multiple" in result["message"].lower()
# ---------------------------------------------------------------------------
# Tool functions
# ---------------------------------------------------------------------------
class TestBoardToolFunctions:
"""Tests for the Hermes board management tool wrappers."""
def test_trello_create_board_tool(self, requests_mock: Any) -> None:
"""Create board tool returns valid JSON."""
from trello_plugin.tools import trello_create_board
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.post(
"https://api.trello.com/1/boards",
json={"id": "b1", "name": "New Board", "url": "https://trello.com/b/b1"},
)
result = json.loads(trello_create_board(name="New Board"))
assert result["success"] is True
assert result["board"]["name"] == "New Board"
def test_trello_rename_board_tool(self, requests_mock: Any) -> None:
"""Rename board tool returns valid JSON."""
from trello_plugin.tools import trello_rename_board
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Old", "url": ""},
)
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Renamed", "url": ""},
)
result = json.loads(trello_rename_board(board_id="b1", name="Renamed"))
assert result["success"] is True
assert result["board"]["name"] == "Renamed"
def test_trello_archive_board_tool(self, requests_mock: Any) -> None:
"""Archive board tool returns valid JSON."""
from trello_plugin.tools import trello_archive_board
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Test Board", "url": ""},
)
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Test Board", "closed": True},
)
result = json.loads(trello_archive_board(board_id="b1"))
assert result["success"] is True
assert "archived" in result["message"].lower()
def test_trello_open_board_tool(self, requests_mock: Any) -> None:
"""Open board tool returns valid JSON."""
from trello_plugin.tools import trello_open_board
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Test Board", "url": ""},
)
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Test Board", "closed": False},
)
result = json.loads(trello_open_board(board_id="b1"))
assert result["success"] is True
assert "opened" in result["message"].lower()
def test_trello_board_details_tool(self, requests_mock: Any) -> None:
"""Board details tool returns valid JSON."""
from trello_plugin.tools import trello_board_details
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Test", "url": ""},
)
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={
"id": "b1",
"name": "Test",
"url": "",
"desc": "",
"closed": False,
"starred": False,
"lists": [{"id": "l1", "name": "To Do"}],
"members": [],
},
)
result = json.loads(trello_board_details(board_id="b1"))
assert result["success"] is True
assert len(result["board"]["lists"]) == 1
+313
View File
@@ -0,0 +1,313 @@
"""Tests for the Trello plugin — card management feature."""
from __future__ import annotations
import json
import os
from typing import Any
import pytest
pytestmark = pytest.mark.usefixtures("clear_env")
@pytest.fixture(autouse=True)
def clear_env(monkeypatch: pytest.MonkeyPatch) -> Any:
monkeypatch.delenv("TRELLO_API_KEY", raising=False)
monkeypatch.delenv("TRELLO_TOKEN", raising=False)
# ---------------------------------------------------------------------------
# TrelloClient — create_card
# ---------------------------------------------------------------------------
class TestCreateCard:
def test_success(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.post("https://api.trello.com/1/cards", json={"id": "c1", "name": "My Card", "url": "https://trello.com/c/c1", "shortUrl": "https://trello.com/c/abc", "idList": "l1"})
client = TrelloClient(api_key="k", token="t")
result = client.create_card(name="My Card", list_id="l1")
assert result["success"]
assert result["card"]["name"] == "My Card"
def test_with_description_and_due(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.post("https://api.trello.com/1/cards", json={"id": "c1", "name": "Card", "url": "", "shortUrl": "", "idList": "l1"})
client = TrelloClient(api_key="k", token="t")
result = client.create_card(name="Card", list_id="l1", desc="Desc", due="2026-06-01T00:00:00Z")
assert result["success"]
def test_missing_credentials(self) -> None:
from trello_plugin.client import TrelloClient
client = TrelloClient(api_key="", token="")
result = client.create_card(name="Card", list_id="l1")
assert not result["success"]
assert "TRELLO_API_KEY" in result["message"]
# ---------------------------------------------------------------------------
# TrelloClient — card_details
# ---------------------------------------------------------------------------
class TestCardDetails:
def test_success(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/cards/c1",
json={
"id": "c1", "name": "My Card", "desc": "Description", "due": None,
"dueComplete": False, "url": "https://trello.com/c/c1", "shortUrl": "https://trello.com/c/abc",
"idList": "l1", "closed": False, "list": {"id": "l1", "name": "To Do"},
"members": [{"id": "m1", "username": "user1", "fullName": "User One"}],
"checklists": [{"id": "cl1", "name": "Checklist", "checkItems": [{"id": "ci1", "name": "Item", "state": "incomplete"}]}],
"actions": [{"id": "a1", "type": "commentCard", "data": {"text": "Nice card"}, "memberCreator": {"username": "user1"}, "date": "2026-05-01T00:00:00Z"}],
},
)
client = TrelloClient(api_key="k", token="t")
result = client.card_details(card_id="c1")
assert result["success"]
assert result["card"]["name"] == "My Card"
assert len(result["card"]["members"]) == 1
assert len(result["card"]["checklists"]) == 1
assert len(result["card"]["comments"]) == 1
# ---------------------------------------------------------------------------
# TrelloClient — update_card
# ---------------------------------------------------------------------------
class TestUpdateCard:
def test_update_name(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Updated", "desc": "", "due": None})
client = TrelloClient(api_key="k", token="t")
result = client.update_card(card_id="c1", name="Updated")
assert result["success"]
assert result["card"]["name"] == "Updated"
def test_no_changes(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
client = TrelloClient(api_key="k", token="t")
result = client.update_card(card_id="c1")
assert result["success"]
assert "No changes" in result["message"]
# ---------------------------------------------------------------------------
# TrelloClient — move_card, archive_card
# ---------------------------------------------------------------------------
class TestMoveCard:
def test_success(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "idList": "l2"})
client = TrelloClient(api_key="k", token="t")
result = client.move_card(card_id="c1", list_id="l2")
assert result["success"]
class TestArchiveCard:
def test_success(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "closed": True})
client = TrelloClient(api_key="k", token="t")
result = client.archive_card(card_id="c1")
assert result["success"]
# ---------------------------------------------------------------------------
# TrelloClient — member operations
# ---------------------------------------------------------------------------
class TestMemberOps:
def test_assign_member(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.post("https://api.trello.com/1/cards/c1/idMembers", json={"id": "c1"}, status_code=200)
client = TrelloClient(api_key="k", token="t")
result = client.assign_member(card_id="c1", member_id="m1")
assert result["success"]
def test_remove_member(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.delete("https://api.trello.com/1/cards/c1/idMembers/m1", status_code=200, json={})
client = TrelloClient(api_key="k", token="t")
result = client.remove_member(card_id="c1", member_id="m1")
assert result["success"]
# ---------------------------------------------------------------------------
# TrelloClient — comments
# ---------------------------------------------------------------------------
class TestComments:
def test_add_comment(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.post("https://api.trello.com/1/cards/c1/actions/comments", json={"id": "a1", "data": {"text": "Great!"}})
client = TrelloClient(api_key="k", token="t")
result = client.add_comment(card_id="c1", text="Great!")
assert result["success"]
assert result["comment"]["id"] == "a1"
def test_delete_comment(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.delete("https://api.trello.com/1/cards/c1/actions/a1/comments", status_code=200, json={})
client = TrelloClient(api_key="k", token="t")
result = client.delete_comment(card_id="c1", comment_id="a1")
assert result["success"]
# ---------------------------------------------------------------------------
# TrelloClient — checklists
# ---------------------------------------------------------------------------
class TestChecklistItems:
def test_add_item_with_checklist_id(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.post("https://api.trello.com/1/cards/c1/checklistItems", json={"id": "ci1", "name": "Task", "state": "incomplete", "idChecklist": "cl1"})
client = TrelloClient(api_key="k", token="t")
result = client.add_checklist_item(card_id="c1", name="Task", checklist_id="cl1")
assert result["success"]
assert result["item"]["name"] == "Task"
def test_add_item_auto_discover(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
# card_details call for auto-discover
requests_mock.get(
"https://api.trello.com/1/cards/c1",
json={
"id": "c1", "name": "Card", "desc": "", "due": None,
"dueComplete": False, "url": "", "shortUrl": "", "idList": "l1", "closed": False,
"list": {"id": "l1", "name": "List"},
"members": [], "checklists": [{"id": "cl1", "name": "Checklist", "checkItems": []}],
"actions": [],
},
)
requests_mock.post("https://api.trello.com/1/cards/c1/checklistItems", json={"id": "ci1", "name": "Auto", "state": "incomplete", "idChecklist": "cl1"})
client = TrelloClient(api_key="k", token="t")
result = client.add_checklist_item(card_id="c1", name="Auto")
assert result["success"]
def test_toggle_complete(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.put("https://api.trello.com/1/cards/c1/checklistItem/ci1", json={"id": "ci1", "name": "Task", "state": "complete"})
client = TrelloClient(api_key="k", token="t")
result = client.toggle_checklist_item(card_id="c1", item_id="ci1", checked=True)
assert result["success"]
assert result["item"]["state"] == "complete"
def test_toggle_incomplete(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.put("https://api.trello.com/1/cards/c1/checklistItem/ci1", json={"id": "ci1", "name": "Task", "state": "incomplete"})
client = TrelloClient(api_key="k", token="t")
result = client.toggle_checklist_item(card_id="c1", item_id="ci1", checked=False)
assert result["success"]
assert result["item"]["state"] == "incomplete"
def test_delete_item(self, requests_mock: Any) -> None:
from trello_plugin.client import TrelloClient
requests_mock.delete("https://api.trello.com/1/cards/c1/checklistItems/ci1", status_code=200, json={})
client = TrelloClient(api_key="k", token="t")
result = client.delete_checklist_item(card_id="c1", item_id="ci1")
assert result["success"]
# ---------------------------------------------------------------------------
# Tool functions
# ---------------------------------------------------------------------------
class TestCardToolFunctions:
def test_trello_create_card_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_create_card
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.post("https://api.trello.com/1/cards", json={"id": "c1", "name": "Card", "url": "", "shortUrl": "", "idList": "l1"})
result = json.loads(trello_create_card(name="Card", list_id="l1"))
assert result["success"]
def test_trello_card_details_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_card_details
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.get("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "desc": "", "due": None, "dueComplete": False, "url": "", "shortUrl": "", "idList": "l1", "closed": False, "list": {"id": "l1", "name": "List"}, "members": [], "checklists": [], "actions": []})
result = json.loads(trello_card_details(card_id="c1"))
assert result["success"]
def test_trello_update_card_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_update_card
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "New", "desc": "", "due": None})
result = json.loads(trello_update_card(card_id="c1", name="New"))
assert result["success"]
def test_trello_move_card_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_move_card
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "idList": "l2"})
result = json.loads(trello_move_card(card_id="c1", list_id="l2"))
assert result["success"]
def test_trello_archive_card_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_archive_card
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "closed": True})
result = json.loads(trello_archive_card(card_id="c1"))
assert result["success"]
def test_trello_assign_member_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_assign_member
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.post("https://api.trello.com/1/cards/c1/idMembers", json={}, status_code=200)
result = json.loads(trello_assign_member(card_id="c1", member_id="m1"))
assert result["success"]
def test_trello_remove_member_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_remove_member
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.delete("https://api.trello.com/1/cards/c1/idMembers/m1", status_code=200, json={})
result = json.loads(trello_remove_member(card_id="c1", member_id="m1"))
assert result["success"]
def test_trello_add_comment_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_add_comment
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.post("https://api.trello.com/1/cards/c1/actions/comments", json={"id": "a1", "data": {"text": "Nice"}})
result = json.loads(trello_add_comment(card_id="c1", text="Nice"))
assert result["success"]
def test_trello_delete_comment_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_delete_comment
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.delete("https://api.trello.com/1/cards/c1/actions/a1/comments", status_code=200, json={})
result = json.loads(trello_delete_comment(card_id="c1", comment_id="a1"))
assert result["success"]
def test_trello_add_checklist_item_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_add_checklist_item
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.post("https://api.trello.com/1/cards/c1/checklistItems", json={"id": "ci1", "name": "Task", "state": "incomplete", "idChecklist": "cl1"})
result = json.loads(trello_add_checklist_item(card_id="c1", name="Task", checklist_id="cl1"))
assert result["success"]
def test_trello_toggle_checklist_item_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_toggle_checklist_item
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.put("https://api.trello.com/1/cards/c1/checklistItem/ci1", json={"id": "ci1", "name": "Task", "state": "complete"})
result = json.loads(trello_toggle_checklist_item(card_id="c1", item_id="ci1", checked=True))
assert result["success"]
def test_trello_delete_checklist_item_tool(self, requests_mock: Any) -> None:
from trello_plugin.tools import trello_delete_checklist_item
os.environ["TRELLO_API_KEY"] = "k"
os.environ["TRELLO_TOKEN"] = "t"
requests_mock.delete("https://api.trello.com/1/cards/c1/checklistItems/ci1", status_code=200, json={})
result = json.loads(trello_delete_checklist_item(card_id="c1", item_id="ci1"))
assert result["success"]
+226
View File
@@ -0,0 +1,226 @@
"""Tests for the Trello plugin — list management feature."""
from __future__ import annotations
import json
import os
from typing import Any
import pytest
pytestmark = pytest.mark.usefixtures("clear_env")
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def clear_env(monkeypatch: pytest.MonkeyPatch) -> Any:
"""Remove Trello env vars before each test so state is predictable."""
monkeypatch.delenv("TRELLO_API_KEY", raising=False)
monkeypatch.delenv("TRELLO_TOKEN", raising=False)
# ---------------------------------------------------------------------------
# TrelloClient — create_list
# ---------------------------------------------------------------------------
class TestCreateList:
"""Tests for TrelloClient.create_list()."""
def test_success(self, requests_mock: Any) -> None:
"""Happy path: creates a list and returns details."""
from trello_plugin.client import TrelloClient
# Resolve board
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "url": ""},
)
# Create list
requests_mock.post(
"https://api.trello.com/1/lists",
json={"id": "l1", "name": "To Do", "idBoard": "b1"},
)
client = TrelloClient(api_key="key", token="tok")
result = client.create_list(name="To Do", board_id="b1")
assert result["success"] is True
assert result["list"]["name"] == "To Do"
assert result["list"]["id_board"] == "b1"
def test_board_not_found(self, requests_mock: Any) -> None:
"""Non-existent board returns error."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/boards/nonexistent",
status_code=404,
)
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=[],
)
client = TrelloClient(api_key="key", token="tok")
result = client.create_list(name="List", board_id="nonexistent")
assert result["success"] is False
assert "not found" in result["message"].lower()
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.create_list(name="List", board_id="b1")
assert result["success"] is False
assert "TRELLO_API_KEY" in result["message"]
# ---------------------------------------------------------------------------
# TrelloClient — rename_list
# ---------------------------------------------------------------------------
class TestRenameList:
"""Tests for TrelloClient.rename_list()."""
def test_success(self, requests_mock: Any) -> None:
"""Renaming a list works."""
from trello_plugin.client import TrelloClient
requests_mock.put(
"https://api.trello.com/1/lists/l1",
json={"id": "l1", "name": "Renamed List"},
)
client = TrelloClient(api_key="key", token="tok")
result = client.rename_list(list_id="l1", name="Renamed List")
assert result["success"] is True
assert result["list"]["name"] == "Renamed List"
# ---------------------------------------------------------------------------
# TrelloClient — archive_list
# ---------------------------------------------------------------------------
class TestArchiveList:
"""Tests for TrelloClient.archive_list()."""
def test_success(self, requests_mock: Any) -> None:
"""Archiving a list returns success message."""
from trello_plugin.client import TrelloClient
requests_mock.put(
"https://api.trello.com/1/lists/l1",
json={"id": "l1", "name": "My List", "closed": True},
)
client = TrelloClient(api_key="key", token="tok")
result = client.archive_list(list_id="l1")
assert result["success"] is True
assert "archived" in result["message"].lower()
# ---------------------------------------------------------------------------
# TrelloClient — move_list
# ---------------------------------------------------------------------------
class TestMoveList:
"""Tests for TrelloClient.move_list()."""
def test_success(self, requests_mock: Any) -> None:
"""Moving a list returns success message."""
from trello_plugin.client import TrelloClient
requests_mock.put(
"https://api.trello.com/1/lists/l1",
json={"id": "l1", "name": "My List", "pos": 1},
)
client = TrelloClient(api_key="key", token="tok")
result = client.move_list(list_id="l1", pos="top")
assert result["success"] is True
assert "moved" in result["message"].lower()
# ---------------------------------------------------------------------------
# Tool functions
# ---------------------------------------------------------------------------
class TestListToolFunctions:
"""Tests for the Hermes list management tool wrappers."""
def test_trello_create_list_tool(self, requests_mock: Any) -> None:
"""Create list tool returns valid JSON."""
from trello_plugin.tools import trello_create_list
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Board", "url": ""},
)
requests_mock.post(
"https://api.trello.com/1/lists",
json={"id": "l1", "name": "New List", "idBoard": "b1"},
)
result = json.loads(trello_create_list(name="New List", board_id="b1"))
assert result["success"] is True
assert result["list"]["name"] == "New List"
def test_trello_rename_list_tool(self, requests_mock: Any) -> None:
"""Rename list tool returns valid JSON."""
from trello_plugin.tools import trello_rename_list
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.put(
"https://api.trello.com/1/lists/l1",
json={"id": "l1", "name": "Renamed"},
)
result = json.loads(trello_rename_list(list_id="l1", name="Renamed"))
assert result["success"] is True
assert result["list"]["name"] == "Renamed"
def test_trello_archive_list_tool(self, requests_mock: Any) -> None:
"""Archive list tool returns valid JSON."""
from trello_plugin.tools import trello_archive_list
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.put(
"https://api.trello.com/1/lists/l1",
json={"id": "l1", "name": "My List", "closed": True},
)
result = json.loads(trello_archive_list(list_id="l1"))
assert result["success"] is True
assert "archived" in result["message"].lower()
def test_trello_move_list_tool(self, requests_mock: Any) -> None:
"""Move list tool returns valid JSON."""
from trello_plugin.tools import trello_move_list
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.put(
"https://api.trello.com/1/lists/l1",
json={"id": "l1", "name": "My List", "pos": 1},
)
result = json.loads(trello_move_list(list_id="l1", pos="top"))
assert result["success"] is True
assert "moved" in result["message"].lower()