Fix delete_image: use rpartition for name:tag splitting
partition(":") split at the first colon, so registry-prefixed images
like "ghcr.io/open-webui/open-webui:v0.8.10" produced name="ghcr"
instead of the correct repository name, causing DSM error 114.
rpartition(":") always splits at the last colon, correctly handling:
- plain images: "nginx:1.24" → name="nginx", tag="1.24"
- namespaced: "nouchka/sqlite3:latest" → correct split
- registry URLs: "ghcr.io/foo/bar:v1" → name="ghcr.io/foo/bar", tag="v1"
- no tag: "nginx" → name="nginx", tag="latest" (fallback)
Added regression test verifying correct params sent to DSM delete API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -166,9 +166,13 @@ def register_images(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None:
|
||||
confirmed: Must be True to actually delete. Default False shows
|
||||
a preview only.
|
||||
"""
|
||||
# Parse name:tag
|
||||
name, _, tag = image_id.partition(":")
|
||||
if not tag:
|
||||
# Parse name and tag using the last ":" as separator so that
|
||||
# registry-prefixed images (e.g. "ghcr.io/foo/bar:v1") are handled
|
||||
# correctly. rpartition returns ("", "", original) when ":" is absent.
|
||||
name, sep, tag = image_id.rpartition(":")
|
||||
if not sep:
|
||||
# No ":" found — bare name without explicit tag
|
||||
name = image_id
|
||||
tag = "latest"
|
||||
|
||||
# Fetch the local image list for size reporting and in-use detection
|
||||
|
||||
@@ -291,6 +291,52 @@ async def test_delete_image_by_hash():
|
||||
assert "redis" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_image_registry_prefixed_name():
|
||||
"""Registry-prefixed image names (e.g. ghcr.io/foo/bar:v1) must split at last ':'."""
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
registry_images = {
|
||||
"images": [
|
||||
{
|
||||
"id": "sha256:dddd",
|
||||
"repository": "ghcr.io/open-webui/open-webui",
|
||||
"tags": ["v0.8.10"],
|
||||
"size": 100 * 1024 * 1024,
|
||||
"created": 1700000000,
|
||||
"upgradable": False,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
client = AsyncMock()
|
||||
|
||||
async def mock_request(api, method, **kwargs):
|
||||
if api == "SYNO.Docker.Image" and method == "list":
|
||||
return registry_images
|
||||
if api == "SYNO.Docker.Container":
|
||||
return {"containers": []}
|
||||
if api == "SYNO.Docker.Image" and method == "delete":
|
||||
return {}
|
||||
return {}
|
||||
|
||||
client.request.side_effect = mock_request
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_images(mcp, make_config(), client)
|
||||
|
||||
result = await tools["delete_image"](
|
||||
image_id="ghcr.io/open-webui/open-webui:v0.8.10", confirmed=True
|
||||
)
|
||||
assert "Deleted" in result
|
||||
assert "open-webui" in result
|
||||
|
||||
# Verify delete was called with correct split params (not "ghcr" as name)
|
||||
delete_call = next(c for c in client.request.call_args_list if c.args[1] == "delete")
|
||||
params = delete_call.kwargs.get("params") or {}
|
||||
assert params.get("name") == "ghcr.io/open-webui/open-webui"
|
||||
assert params.get("tag") == "v0.8.10"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_image_api_error():
|
||||
from mcp_synology_container.dsm_client import SynologyError
|
||||
|
||||
Reference in New Issue
Block a user