feat: add check_exist tool

SYNO.FileStation.CheckExist returns error 400 for every parameter format on
this DSM firmware, so the tool falls back to SYNO.FileStation.List::getinfo.
DSM returns an entry per requested path with name=None for non-existent paths,
which provides a reliable exists/not-exists signal.

Accepts a single path or a comma-separated list; returns a table of
Path | Exists (Yes/No) with a count footer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 10:49:50 +02:00
parent 80ac894165
commit dbab842738
2 changed files with 179 additions and 0 deletions
+122
View File
@@ -1075,3 +1075,125 @@ async def test_upload_dsm_error(config: AppConfig) -> None:
assert result.startswith("Error:")
assert "permission" in result.lower()
# ──────────────────────────────────────────────────────────────────────────
# check_exist
# ──────────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_check_exist_single_existing(config: AppConfig) -> None:
"""check_exist returns Yes for a path that exists."""
client = MagicMock()
client.request = AsyncMock(
return_value={
"files": [
{"path": "/docker", "name": "docker", "isdir": True, "additional": {}},
]
}
)
tools = _make_mcp_and_tools(config, client)
result = await tools["check_exist"](path="/docker")
assert "/docker" in result
assert "Yes" in result
assert "No" not in result
assert "1 path(s) checked" in result
@pytest.mark.asyncio
async def test_check_exist_single_missing(config: AppConfig) -> None:
"""check_exist returns No for a path that does not exist (name=None from DSM)."""
client = MagicMock()
client.request = AsyncMock(
return_value={
"files": [
{"path": "/no-such-path", "name": None, "isdir": None, "additional": None},
]
}
)
tools = _make_mcp_and_tools(config, client)
result = await tools["check_exist"](path="/no-such-path")
assert "/no-such-path" in result
assert "No" in result
assert "1 path(s) checked" in result
@pytest.mark.asyncio
async def test_check_exist_multi_path(config: AppConfig) -> None:
"""check_exist handles comma-separated paths and reports each correctly."""
client = MagicMock()
client.request = AsyncMock(
return_value={
"files": [
{"path": "/docker", "name": "docker", "isdir": True, "additional": {}},
{"path": "/ghost", "name": None, "isdir": None, "additional": None},
]
}
)
tools = _make_mcp_and_tools(config, client)
result = await tools["check_exist"](path="/docker,/ghost")
assert "/docker" in result
assert "/ghost" in result
assert "Yes" in result
assert "No" in result
assert "2 path(s) checked" in result
# Verify DSM was called with both paths as a JSON array
call_params = client.request.call_args[1]["params"]
requested_paths = json.loads(call_params["path"])
assert "/docker" in requested_paths
assert "/ghost" in requested_paths
@pytest.mark.asyncio
async def test_check_exist_empty_path(config: AppConfig) -> None:
"""check_exist returns Error: when no path is given."""
client = MagicMock()
client.request = AsyncMock()
tools = _make_mcp_and_tools(config, client)
result = await tools["check_exist"](path=" ")
assert result.startswith("Error:")
client.request.assert_not_called()
@pytest.mark.asyncio
async def test_check_exist_dsm_error(config: AppConfig) -> None:
"""check_exist propagates DSM errors as Error: messages."""
client = MagicMock()
client.request = AsyncMock(side_effect=SynologyError("Permission denied", code=105))
tools = _make_mcp_and_tools(config, client)
result = await tools["check_exist"](path="/docker")
assert result.startswith("Error:")
assert "Permission denied" in result
@pytest.mark.asyncio
async def test_check_exist_uses_getinfo(config: AppConfig) -> None:
"""check_exist uses SYNO.FileStation.List::getinfo as its DSM backend."""
client = MagicMock()
client.request = AsyncMock(
return_value={
"files": [
{"path": "/docker", "name": "docker", "isdir": True, "additional": {}},
]
}
)
tools = _make_mcp_and_tools(config, client)
await tools["check_exist"](path="/docker")
client.request.assert_called_once()
call_args = client.request.call_args
assert call_args[0][0] == "SYNO.FileStation.List"
assert call_args[0][1] == "getinfo"