fix: get_thumbnail size limits, default=small, quality quirk docs (v0.3.5)

- Default size changed large → small (avoids MCP buffer overflows)
- Hard limit: return Error: when thumbnail exceeds ~2 MB base64 (1.5 MB raw)
- Soft limit: add "warning" field to JSON when thumbnail exceeds ~500 KB base64
  (375 KB raw), advising to use size='small'
- Constants _THUMB_ABORT_BYTES / _THUMB_WARN_BYTES moved to module level
- 6 new tests for size cap/warning/default/DSM-error paths (113 total)
- SPEC.md: document quality-ignored quirk, size ranges, soft+hard limits
- CLAUDE.md: DSM Quirks entry for Thumb quality/size behaviour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 06:39:57 +02:00
parent 4c6de3bfc7
commit 4430807b55
6 changed files with 157 additions and 12 deletions
+99
View File
@@ -1768,6 +1768,105 @@ async def test_get_md5_missing_hash_in_response(config: AppConfig) -> None:
assert "md5" in result.lower() or "hash" in result.lower()
# ──────────────────────────────────────────────────────────────────────────
# get_thumbnail
# ──────────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_get_thumbnail_success(config: AppConfig) -> None:
"""get_thumbnail returns JSON with filename, size_bytes, content_base64 for small images."""
import base64
client = MagicMock()
img_bytes = b"\xff\xd8\xff" + b"\x00" * 1000 # fake JPEG, well under limits
client.get_thumbnail_bytes = AsyncMock(return_value=img_bytes)
tools = _make_mcp_and_tools(config, client)
result = await tools["get_thumbnail"](path="/photo/test.jpg", size="small")
payload = json.loads(result)
assert payload["filename"] == "test.jpg"
assert payload["size_bytes"] == len(img_bytes)
assert base64.b64decode(payload["content_base64"]) == img_bytes
assert "warning" not in payload
@pytest.mark.asyncio
async def test_get_thumbnail_invalid_size(config: AppConfig) -> None:
"""get_thumbnail returns Error: for an unrecognised size value."""
client = MagicMock()
tools = _make_mcp_and_tools(config, client)
result = await tools["get_thumbnail"](path="/photo/test.jpg", size="tiny")
assert result.startswith("Error:")
assert "size" in result.lower()
@pytest.mark.asyncio
async def test_get_thumbnail_dsm_error(config: AppConfig) -> None:
"""get_thumbnail returns Error: when the client raises SynologyError."""
from mcp_synology_filestation.client import SynologyError
client = MagicMock()
client.get_thumbnail_bytes = AsyncMock(
side_effect=SynologyError("File not found: /photo/missing.jpg", code=404)
)
tools = _make_mcp_and_tools(config, client)
result = await tools["get_thumbnail"](path="/photo/missing.jpg")
assert result.startswith("Error:")
assert "not found" in result.lower()
@pytest.mark.asyncio
async def test_get_thumbnail_large_warning(config: AppConfig) -> None:
"""get_thumbnail includes a warning field when image exceeds the soft limit (~375 KB raw)."""
client = MagicMock()
# 400 000 bytes raw > 375 000 threshold but < 1 500 000 hard limit
img_bytes = b"\x00" * 400_000
client.get_thumbnail_bytes = AsyncMock(return_value=img_bytes)
tools = _make_mcp_and_tools(config, client)
result = await tools["get_thumbnail"](path="/photo/big.jpg", size="medium")
payload = json.loads(result)
assert "content_base64" in payload
assert "warning" in payload
assert "small" in payload["warning"].lower()
@pytest.mark.asyncio
async def test_get_thumbnail_too_large_aborts(config: AppConfig) -> None:
"""get_thumbnail returns Error: (no base64) when image exceeds the hard limit (~1.5 MB raw)."""
client = MagicMock()
# 2 000 000 bytes raw > 1 500 000 hard limit
img_bytes = b"\x00" * 2_000_000
client.get_thumbnail_bytes = AsyncMock(return_value=img_bytes)
tools = _make_mcp_and_tools(config, client)
result = await tools["get_thumbnail"](path="/photo/huge.jpg", size="original")
assert result.startswith("Error:")
assert "large" in result.lower() or "too large" in result.lower()
# Must NOT contain base64 payload
assert "content_base64" not in result
@pytest.mark.asyncio
async def test_get_thumbnail_default_size_is_small(config: AppConfig) -> None:
"""get_thumbnail defaults to size='small'."""
client = MagicMock()
client.get_thumbnail_bytes = AsyncMock(return_value=b"\xff\xd8" + b"\x00" * 100)
tools = _make_mcp_and_tools(config, client)
await tools["get_thumbnail"](path="/photo/test.jpg")
client.get_thumbnail_bytes.assert_called_once_with("/photo/test.jpg", "small")
# ──────────────────────────────────────────────────────────────────────────
# background_tasks
# ──────────────────────────────────────────────────────────────────────────