feat(deck): add response filters and archived stacks tool
Add filtering options to deck read tools to keep responses compact on boards with accumulated cards/comments, and expose archived stacks so agents can audit completed work that has been archived off the active board. - deck_get_board: include_acl, include_users, include_labels - deck_get_stacks/deck_get_stack: include_cards, include_archived_cards, description_max_length - deck_get_cards: include_archived, description_max_length - New deck_get_archived_stacks tool wrapping the existing client method Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
41d2286aab
commit
d8cd073e66
@@ -255,6 +255,37 @@ async def test_deck_get_stacks(mocker):
|
||||
mock_make_request.assert_called_once()
|
||||
|
||||
|
||||
async def test_deck_get_archived_stacks(mocker):
|
||||
"""Test that get_archived_stacks targets the archived endpoint and parses the response."""
|
||||
mock_response = create_mock_response(
|
||||
status_code=200,
|
||||
json_data=[
|
||||
{
|
||||
"id": 9,
|
||||
"title": "Archived Stack",
|
||||
"boardId": 123,
|
||||
"order": 1,
|
||||
"deletedAt": 0,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
mock_client = mocker.AsyncMock(spec=httpx.AsyncClient)
|
||||
mock_make_request = mocker.patch.object(
|
||||
DeckClient, "_make_request", return_value=mock_response
|
||||
)
|
||||
|
||||
client = DeckClient(mock_client, "testuser")
|
||||
stacks = await client.get_archived_stacks(board_id=123)
|
||||
|
||||
assert isinstance(stacks, list)
|
||||
assert len(stacks) == 1
|
||||
assert stacks[0].id == 9
|
||||
|
||||
mock_make_request.assert_called_once()
|
||||
assert "/boards/123/stacks/archived" in mock_make_request.call_args[0][1]
|
||||
|
||||
|
||||
# Card Tests
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import pytest
|
||||
|
||||
from nextcloud_mcp_server.models.deck import DeckCard
|
||||
from nextcloud_mcp_server.server.deck import _truncate_card_descriptions
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def _make_card(card_id: int, description: str | None) -> DeckCard:
|
||||
return DeckCard(
|
||||
id=card_id,
|
||||
title=f"Card {card_id}",
|
||||
stackId=1,
|
||||
type="plain",
|
||||
order=card_id,
|
||||
archived=False,
|
||||
owner="testuser",
|
||||
description=description,
|
||||
)
|
||||
|
||||
|
||||
def test_truncate_card_descriptions_no_op_when_limit_is_none():
|
||||
"""When description_max_length is None, descriptions are left untouched."""
|
||||
cards = [_make_card(1, "x" * 5000)]
|
||||
_truncate_card_descriptions(cards, None)
|
||||
assert cards[0].description is not None
|
||||
assert len(cards[0].description) == 5000
|
||||
|
||||
|
||||
def test_truncate_card_descriptions_truncates_long_descriptions():
|
||||
"""Descriptions over the limit are truncated and marked with an ellipsis."""
|
||||
cards = [_make_card(1, "x" * 5000), _make_card(2, "short")]
|
||||
_truncate_card_descriptions(cards, 100)
|
||||
assert cards[0].description is not None
|
||||
assert len(cards[0].description) == 101 # 100 chars + ellipsis
|
||||
assert cards[0].description.endswith("…")
|
||||
assert cards[1].description == "short"
|
||||
|
||||
|
||||
def test_truncate_card_descriptions_handles_none_description():
|
||||
"""Cards with no description are skipped without error."""
|
||||
cards = [_make_card(1, None)]
|
||||
_truncate_card_descriptions(cards, 100)
|
||||
assert cards[0].description is None
|
||||
|
||||
|
||||
def test_truncate_card_descriptions_at_exact_boundary():
|
||||
"""Descriptions at exactly the limit should not be truncated."""
|
||||
cards = [_make_card(1, "x" * 100)]
|
||||
_truncate_card_descriptions(cards, 100)
|
||||
assert cards[0].description == "x" * 100
|
||||
Reference in New Issue
Block a user