fix: v0.4.3 — inspect_image rendering polish (follow-up to #4)
Three live-NAS observations that the 0.4.2 implementation got wrong:
1. Header showed "?:?" — DSM Image/get returns image="" and tag=""
for name:tag lookups; the response fields are unreliable. The
header now echoes the user-supplied image_id, falling back to the
sha256 id only when image_id itself is empty.
2. Ports printed as raw Python dicts ({'port':'22','protocol':'tcp'}).
The ports array is a list of {port, protocol} objects — each entry
now renders as "22/tcp".
3. Environment printed as raw Python dicts ({'key':'PATH','value':...}).
The env array is a list of {key, value} objects — each entry now
renders as "PATH=/usr/local/...".
cmd, entrypoint, and volumes (plain string arrays) were already
correct and are untouched. Tests updated to match the live shape;
two new tests guard the header fallback. References #4 (already
closed by 0.4.2 — no need to reopen for a rendering polish).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -416,12 +416,17 @@ async def test_delete_image_api_error():
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
# Authoritative DSM SYNO.Docker.Image/get response shape (v1). Top-level
|
||||
# fields only — there is NO "layers" field on this endpoint, confirmed
|
||||
# via API capture against the live NAS.
|
||||
# Authoritative DSM SYNO.Docker.Image/get response shape (v1).
|
||||
# Live-captured peculiarities:
|
||||
# - `image` and `tag` come back as empty strings when the lookup is
|
||||
# by name:tag — the header therefore has to fall back to image_id.
|
||||
# - `ports` is an array of {"port", "protocol"} dicts, NOT strings.
|
||||
# - `env` is an array of {"key", "value"} dicts, NOT strings.
|
||||
# - `volumes` and `cmd`/`entrypoint` are plain string arrays.
|
||||
# - There is no `layers` field on this endpoint.
|
||||
SAMPLE_INSPECT = {
|
||||
"image": "nginx",
|
||||
"tag": "1.24",
|
||||
"image": "",
|
||||
"tag": "",
|
||||
"id": "sha256:aaaa1234567890abcdef",
|
||||
"digest": "sha256:digestabcdef",
|
||||
"size": 50 * 1024 * 1024,
|
||||
@@ -430,8 +435,14 @@ SAMPLE_INSPECT = {
|
||||
"docker_version": "20.10.0",
|
||||
"cmd": ["nginx", "-g", "daemon off;"],
|
||||
"entrypoint": ["/docker-entrypoint.sh"],
|
||||
"env": ["NGINX_VERSION=1.24", "PATH=/usr/local/sbin"],
|
||||
"ports": ["80/tcp", "443/tcp"],
|
||||
"env": [
|
||||
{"key": "NGINX_VERSION", "value": "1.24"},
|
||||
{"key": "PATH", "value": "/usr/local/sbin"},
|
||||
],
|
||||
"ports": [
|
||||
{"port": "80", "protocol": "tcp"},
|
||||
{"port": "443", "protocol": "tcp"},
|
||||
],
|
||||
"volumes": ["/var/cache/nginx"],
|
||||
}
|
||||
|
||||
@@ -467,10 +478,12 @@ async def test_inspect_image_by_name_tag():
|
||||
async def test_inspect_image_by_hash():
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
# Inspect data shaped for redis returned by hash lookup
|
||||
# Inspect data shaped for redis returned by hash lookup.
|
||||
# image/tag come back empty (matches live DSM behavior) — display
|
||||
# falls back to image_id, then to the id hash.
|
||||
redis_inspect = {
|
||||
"image": "redis",
|
||||
"tag": "7",
|
||||
"image": "",
|
||||
"tag": "",
|
||||
"id": "sha256:ccccredis",
|
||||
"digest": "sha256:rdigest",
|
||||
"size": 30 * 1024 * 1024,
|
||||
@@ -478,7 +491,7 @@ async def test_inspect_image_by_hash():
|
||||
"cmd": ["redis-server"],
|
||||
"entrypoint": [],
|
||||
"env": [],
|
||||
"ports": ["6379/tcp"],
|
||||
"ports": [{"port": "6379", "protocol": "tcp"}],
|
||||
"volumes": [],
|
||||
}
|
||||
client = _make_inspect_client(inspect_payload=redis_inspect)
|
||||
@@ -486,7 +499,39 @@ async def test_inspect_image_by_hash():
|
||||
register_images(mcp, make_config(), client)
|
||||
|
||||
result = await tools["inspect_image"](image_id="sha256:cccc")
|
||||
# The header echoes the user-supplied image_id; "redis" surfaces via the cmd line.
|
||||
assert "sha256:cccc" in result
|
||||
assert "redis" in result
|
||||
assert "6379/tcp" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inspect_image_header_uses_image_id_when_fields_empty():
|
||||
"""DSM Image/get returns image='' and tag='' for name:tag lookups — the header
|
||||
must fall back to the user-supplied image_id, NOT render '?:?'."""
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = _make_inspect_client() # SAMPLE_INSPECT has image='' / tag=''
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_images(mcp, make_config(), client)
|
||||
|
||||
result = await tools["inspect_image"](image_id="gitea/gitea:1.26.1")
|
||||
assert "Image: gitea/gitea:1.26.1" in result
|
||||
assert "?:?" not in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inspect_image_header_falls_back_to_id_when_image_id_empty():
|
||||
"""If image_id were empty (defensive — should not happen in practice), the
|
||||
header falls back to the sha256 id field."""
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
client = _make_inspect_client() # SAMPLE_INSPECT.id == sha256:aaaa1234567890abcdef
|
||||
mcp, tools = make_mock_mcp()
|
||||
register_images(mcp, make_config(), client)
|
||||
|
||||
result = await tools["inspect_image"](image_id="")
|
||||
assert "Image: sha256:aaaa1234567890abcdef" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -605,8 +650,9 @@ async def test_inspect_image_registry_prefixed():
|
||||
from mcp_synology_container.modules.images import register_images
|
||||
|
||||
registry_inspect = {
|
||||
"image": "ghcr.io/foo/bar",
|
||||
"tag": "v1",
|
||||
# image / tag come back empty for name:tag lookups (live DSM behavior).
|
||||
"image": "",
|
||||
"tag": "",
|
||||
"id": "sha256:dddd",
|
||||
"digest": "sha256:gdigest",
|
||||
"size": 100 * 1024 * 1024,
|
||||
|
||||
Reference in New Issue
Block a user