feat: add get_info tool using SYNO.FileStation.List::getinfo
SYNO.FileStation.Stat is absent from this NAS's API registry. SYNO.FileStation.List::getinfo returns identical data and is confirmed working. - tools/filestation.py: new get_info tool — accepts one or more comma-separated paths, calls getinfo with real_path/size/time/perm/ owner/type additional fields, returns a 9-column table - tests: 6 new tests covering single file, directory, multi-path, empty input, DSM error, and correct API method assertion - SPEC.md: remove SYNO.FileStation.Stat from API table, rewrite get_info tool section to reference getinfo, update list_dir note - CLAUDE.md: update Implemented Tools list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -251,3 +251,165 @@ async def test_list_dir_dsm_error(config: AppConfig) -> None:
|
||||
|
||||
assert result.startswith("Error:")
|
||||
assert "not found" in result.lower()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# get_info
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_single_file(config: AppConfig) -> None:
|
||||
"""get_info returns a table with metadata for a single file."""
|
||||
client = MagicMock()
|
||||
client.request = AsyncMock(
|
||||
return_value={
|
||||
"files": [
|
||||
{
|
||||
"path": "/dev/notes.txt",
|
||||
"name": "notes.txt",
|
||||
"isdir": False,
|
||||
"additional": {
|
||||
"real_path": "/volume1/dev/notes.txt",
|
||||
"size": 1024,
|
||||
"time": {"mtime": 1700000000, "crtime": 1690000000},
|
||||
"owner": {"user": "marcus", "group": "users"},
|
||||
"perm": {"posix": 0o644},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
tools = _make_mcp_and_tools(config, client)
|
||||
result = await tools["get_info"](path="/dev/notes.txt")
|
||||
|
||||
assert "/dev/notes.txt" in result
|
||||
assert "file" in result
|
||||
assert "1 KB" in result or "1024 B" in result
|
||||
assert "marcus" in result
|
||||
assert "users" in result
|
||||
assert "644" in result
|
||||
assert "/volume1/dev/notes.txt" in result
|
||||
assert "1 item(s)" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_directory(config: AppConfig) -> None:
|
||||
"""get_info shows '-' for size of a directory."""
|
||||
client = MagicMock()
|
||||
client.request = AsyncMock(
|
||||
return_value={
|
||||
"files": [
|
||||
{
|
||||
"path": "/dev",
|
||||
"name": "dev",
|
||||
"isdir": True,
|
||||
"additional": {
|
||||
"real_path": "/volume1/dev",
|
||||
"size": 0,
|
||||
"time": {"mtime": 1700000000, "crtime": 1690000000},
|
||||
"owner": {"user": "marcus", "group": "users"},
|
||||
"perm": {"posix": 0o755},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
tools = _make_mcp_and_tools(config, client)
|
||||
result = await tools["get_info"](path="/dev")
|
||||
|
||||
assert "dir" in result
|
||||
assert "755" in result
|
||||
# Size for directory should be "-"
|
||||
rows = [line for line in result.splitlines() if "/dev" in line and "|" in line]
|
||||
assert rows, "expected a data row containing /dev"
|
||||
size_col = rows[0].split("|")[3].strip()
|
||||
assert size_col == "-"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_multiple_paths(config: AppConfig) -> None:
|
||||
"""get_info handles comma-separated paths and returns one row per item."""
|
||||
client = MagicMock()
|
||||
client.request = AsyncMock(
|
||||
return_value={
|
||||
"files": [
|
||||
{
|
||||
"path": "/dev/a.txt",
|
||||
"name": "a.txt",
|
||||
"isdir": False,
|
||||
"additional": {
|
||||
"size": 100,
|
||||
"time": {"mtime": 1700000000, "crtime": 1690000000},
|
||||
"owner": {"user": "marcus", "group": "users"},
|
||||
"perm": {"posix": 0o644},
|
||||
},
|
||||
},
|
||||
{
|
||||
"path": "/data/b.txt",
|
||||
"name": "b.txt",
|
||||
"isdir": False,
|
||||
"additional": {
|
||||
"size": 200,
|
||||
"time": {"mtime": 1700000001, "crtime": 1690000001},
|
||||
"owner": {"user": "marcus", "group": "users"},
|
||||
"perm": {"posix": 0o644},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
tools = _make_mcp_and_tools(config, client)
|
||||
result = await tools["get_info"](path="/dev/a.txt,/data/b.txt")
|
||||
|
||||
assert "/dev/a.txt" in result
|
||||
assert "/data/b.txt" in result
|
||||
assert "2 item(s)" in result
|
||||
|
||||
# Verify the API received both paths as a comma-joined string
|
||||
call_params = client.request.call_args[1]["params"]
|
||||
assert call_params["path"] == "/dev/a.txt,/data/b.txt"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_empty_path(config: AppConfig) -> None:
|
||||
"""get_info returns Error: when path is empty."""
|
||||
client = MagicMock()
|
||||
client.request = AsyncMock()
|
||||
|
||||
tools = _make_mcp_and_tools(config, client)
|
||||
result = await tools["get_info"](path=" ")
|
||||
|
||||
assert result.startswith("Error:")
|
||||
client.request.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_dsm_error(config: AppConfig) -> None:
|
||||
"""get_info returns Error: on SynologyError."""
|
||||
client = MagicMock()
|
||||
client.request = AsyncMock(side_effect=SynologyError("File or folder not found", code=1800))
|
||||
|
||||
tools = _make_mcp_and_tools(config, client)
|
||||
result = await tools["get_info"](path="/dev/missing.txt")
|
||||
|
||||
assert result.startswith("Error:")
|
||||
assert "not found" in result.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_uses_getinfo_method(config: AppConfig) -> None:
|
||||
"""get_info calls SYNO.FileStation.List with method='getinfo'."""
|
||||
client = MagicMock()
|
||||
client.request = AsyncMock(return_value={"files": []})
|
||||
|
||||
tools = _make_mcp_and_tools(config, client)
|
||||
await tools["get_info"](path="/dev/file.txt")
|
||||
|
||||
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