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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user