Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/tools/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,27 @@ async def llen(name: str) -> int:
return r.llen(name)
except RedisError as e:
return f"Error retrieving length of list '{name}': {str(e)}"

@mcp.tool()
async def lrem(name: str, count: int, element: FieldT) -> str:
"""Remove elements from a Redis list.

Args:
name: The name of the list
count: Number of elements to remove (0 = all, positive = from head, negative = from tail)
element: The element value to remove

Returns:
A string indicating the number of elements removed.
"""
try:
r = RedisConnectionManager.get_connection()
removed_count = r.lrem(name, count, element)

if removed_count == 0:
return f"Element '{element}' not found in list '{name}' or list does not exist."
else:
return f"Removed {removed_count} occurrence(s) of '{element}' from list '{name}'."

except RedisError as e:
return f"Error removing element from list '{name}': {str(e)}"
61 changes: 60 additions & 1 deletion tests/tools/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest
from redis.exceptions import RedisError

from src.tools.list import llen, lpop, lpush, lrange, rpop, rpush
from src.tools.list import llen, lpop, lpush, lrange, rpop, rpush, lrem


class TestListOperations:
Expand Down Expand Up @@ -278,3 +278,62 @@ async def test_push_operations_return_new_length(
# Results should indicate successful push regardless of return value
assert "pushed to the left of list" in result1
assert "pushed to the right of list" in result2

@pytest.mark.asyncio
async def test_lrem_success_single_removal(self, mock_redis_connection_manager):
"""Test successful removal of a single matching element."""
mock_redis = mock_redis_connection_manager
mock_redis.lrem.return_value = 1

result = await lrem("test_list", 1, "value1")

mock_redis.lrem.assert_called_once_with("test_list", 1, "value1")
assert "Removed 1 occurrence(s) of 'value1' from list 'test_list'" in result

@pytest.mark.asyncio
async def test_lrem_success_multiple_removal(self, mock_redis_connection_manager):
"""Test successful removal of multiple matching elements."""
mock_redis = mock_redis_connection_manager
mock_redis.lrem.return_value = 3

result = await lrem("test_list", 0, "value1")

mock_redis.lrem.assert_called_once_with("test_list", 0, "value1")
assert "Removed 3 occurrence(s) of 'value1' from list 'test_list'" in result

@pytest.mark.asyncio
async def test_lrem_no_elements_removed(self, mock_redis_connection_manager):
"""Test when no elements are removed because the element is not found."""
mock_redis = mock_redis_connection_manager
mock_redis.lrem.return_value = 0

result = await lrem("test_list", 2, "missing_value")

mock_redis.lrem.assert_called_once_with("test_list", 2, "missing_value")
assert (
"Element 'missing_value' not found in list 'test_list' or list does not exist."
in result
)

@pytest.mark.asyncio
async def test_lrem_negative_count(self, mock_redis_connection_manager):
"""Test removal with a negative count (from tail)."""
mock_redis = mock_redis_connection_manager
mock_redis.lrem.return_value = 2

result = await lrem("test_list", -2, "value_tail")

mock_redis.lrem.assert_called_once_with("test_list", -2, "value_tail")
assert "Removed 2 occurrence(s) of 'value_tail' from list 'test_list'" in result

@pytest.mark.asyncio
async def test_lrem_redis_error(self, mock_redis_connection_manager):
"""Test error handling during lrem operation."""
mock_redis = mock_redis_connection_manager
mock_redis.lrem.side_effect = RedisError("Connection failed")

result = await lrem("test_list", 1, "value1")

assert (
"Error removing element from list 'test_list': Connection failed" in result
)