fix(deck): include archived cards in list tools for status=all/archived

The active Deck listing endpoints (StackService::findAll /
CardMapper::findAllForStacks and StackService::find / CardMapper::findAll)
filter out archived cards at the SQL level — only the /stacks/archived
endpoint returns them. The client-side status="all"/"archived" filters in
deck_get_cards, deck_get_stacks, deck_get_stack and deck_get_board_overview
therefore operated on a list the server had already stripped of archived
cards, so they could never surface one. deck_get_card (by ID) bypasses the
filter, which is why it appeared to work. Fixes #842.

When status is "all" or "archived", also fetch /stacks/archived
(client.deck.get_archived_stacks) and merge those cards back in per stack —
concurrently with the active fetch where applicable. status="open"/"done"
are unchanged and cost no extra call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-07 18:03:28 +02:00
co-authored by Claude Opus 4.8
parent 792c536802
commit 961449be30
4 changed files with 273 additions and 9 deletions
+120
View File
@@ -421,3 +421,123 @@ async def test_deck_get_board_overview_mcp(
assert stack["card_count"] == len(stack["cards"])
card_ids = [c["id"] for c in stack["cards"]]
assert card_data["id"] in card_ids
# Archived-card visibility in the active list tools (issue #842)
async def _create_and_archive_card(
nc_mcp_client: ClientSession, board_id: int, stack_id: int
) -> int:
"""Create a card in the given stack, archive it, and return its id.
The card is cleaned up when the temporary stack is deleted by the
fixture teardown (deleting a stack removes its cards)."""
create_result = await nc_mcp_client.call_tool(
"deck_create_card",
{
"board_id": board_id,
"stack_id": stack_id,
"title": f"Archived Card {uuid.uuid4().hex[:8]}",
},
)
assert create_result.isError is False, f"create failed: {create_result.content}"
archived_id = json.loads(create_result.content[0].text)["id"]
archive_result = await nc_mcp_client.call_tool(
"deck_archive_card",
{"board_id": board_id, "stack_id": stack_id, "card_id": archived_id},
)
assert archive_result.isError is False, f"archive failed: {archive_result.content}"
return archived_id
async def test_deck_get_cards_status_includes_archived_mcp(
nc_mcp_client: ClientSession, temporary_board_with_card: tuple
):
"""deck_get_cards: archived cards appear under status="all"/"archived" and
are excluded under the default "open" — the regression behind issue #842."""
board_data, stack_data, card_data = temporary_board_with_card
board_id = board_data["id"]
stack_id = stack_data["id"]
open_id = card_data["id"]
archived_id = await _create_and_archive_card(nc_mcp_client, board_id, stack_id)
async def card_ids(status: str) -> list[int]:
result = await nc_mcp_client.call_tool(
"deck_get_cards",
{"board_id": board_id, "stack_id": stack_id, "status": status},
)
assert result.isError is False, f"deck_get_cards({status}) failed"
return [c["id"] for c in json.loads(result.content[0].text)["cards"]]
open_ids = await card_ids("open")
assert open_id in open_ids
assert archived_id not in open_ids, "archived card must not show under 'open'"
all_ids = await card_ids("all")
assert open_id in all_ids and archived_id in all_ids, (
"status='all' must include both open and archived cards"
)
archived_only = await card_ids("archived")
assert archived_only == [archived_id], (
f"status='archived' should return only the archived card, got {archived_only}"
)
async def test_deck_get_stack_status_includes_archived_mcp(
nc_mcp_client: ClientSession, temporary_board_with_card: tuple
):
"""deck_get_stack (single stack) honours archived cards for status
"all"/"archived" too, mirroring deck_get_cards (issue #842)."""
board_data, stack_data, card_data = temporary_board_with_card
board_id = board_data["id"]
stack_id = stack_data["id"]
archived_id = await _create_and_archive_card(nc_mcp_client, board_id, stack_id)
async def stack_card_ids(status: str) -> list[int]:
result = await nc_mcp_client.call_tool(
"deck_get_stack",
{"board_id": board_id, "stack_id": stack_id, "status": status},
)
assert result.isError is False, f"deck_get_stack({status}) failed"
payload = json.loads(result.content[0].text)
return [c["id"] for c in (payload.get("cards") or [])]
assert archived_id not in await stack_card_ids("open")
assert archived_id in await stack_card_ids("all")
assert await stack_card_ids("archived") == [archived_id]
async def test_deck_get_stacks_and_overview_include_archived_mcp(
nc_mcp_client: ClientSession, temporary_board_with_card: tuple
):
"""deck_get_stacks and deck_get_board_overview surface archived cards when
status="all" (issue #842), keyed onto the correct stack."""
board_data, stack_data, card_data = temporary_board_with_card
board_id = board_data["id"]
stack_id = stack_data["id"]
archived_id = await _create_and_archive_card(nc_mcp_client, board_id, stack_id)
# deck_get_stacks(status="all")
stacks_result = await nc_mcp_client.call_tool(
"deck_get_stacks", {"board_id": board_id, "status": "all"}
)
assert stacks_result.isError is False, "deck_get_stacks(all) failed"
stacks_payload = json.loads(stacks_result.content[0].text)
stack = next(s for s in stacks_payload["stacks"] if s["id"] == stack_id)
stack_card_ids = [c["id"] for c in (stack.get("cards") or [])]
assert card_data["id"] in stack_card_ids
assert archived_id in stack_card_ids, "archived card missing from deck_get_stacks"
# deck_get_board_overview(status="all")
overview_result = await nc_mcp_client.call_tool(
"deck_get_board_overview", {"board_id": board_id, "status": "all"}
)
assert overview_result.isError is False, "board overview failed"
overview_payload = json.loads(overview_result.content[0].text)
ov_stack = next(s for s in overview_payload["stacks"] if s["id"] == stack_id)
ov_card_ids = [c["id"] for c in ov_stack["cards"]]
assert archived_id in ov_card_ids, "archived card missing from board overview"
assert ov_stack["card_count"] == len(ov_stack["cards"])