fix: v0.3.3 — delete_container params (error 114) + delete_project orphan guard

Bug 1 — delete_container (DSM error 114):
SYNO.Docker.Container/delete requires three parameters: name
(JSON-encoded), force=false, and preserve_profile=false. Previously
only a bare `name` string was sent, causing DSM to reject the call
with error 114. Added the two missing fields and JSON-encode name to
match the DSM convention. The connector-side running-container guard
is unchanged; force stays hard-coded to false.

Bug 2 — delete_project orphan containers:
Production test revealed that DSM does NOT reject Project/delete on a
running project — it silently removes the registration and leaves the
containers running without any project context. The previous
implementation tried to handle this via a caught SynologyError that
never actually fires. Fix: check the project status from _find_project
connector-side before issuing any DSM call; if RUNNING, return an
error pointing at stop_project. The delete request is never sent for
a running project.

The corresponding unit test (test_delete_project_running_returns_stop_hint)
was a false positive — it mocked a DSM rejection that real DSM never
produces. Replaced with test_delete_project_running_blocked_connector_side
which asserts that client.request("delete") is never called when the
project is RUNNING.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 11:38:36 +02:00
parent 3f73ed0aef
commit 8adcf93b6a
7 changed files with 72 additions and 24 deletions
@@ -2,6 +2,7 @@
from __future__ import annotations
import json
import logging
import re
from typing import TYPE_CHECKING, Any
@@ -319,7 +320,11 @@ def register_containers(mcp: FastMCP, config: AppConfig, client: DsmClient) -> N
await client.request(
"SYNO.Docker.Container",
"delete",
params={"name": resolved_name},
params={
"name": json.dumps(resolved_name),
"force": "false",
"preserve_profile": "false",
},
)
return f"Deleted container '{display_name}'."
except Exception as e:
@@ -403,6 +403,15 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non
f"Call this tool again with confirmed=True to proceed."
)
# Guard: DSM does NOT reject a delete call on a running project — it
# removes the registration silently and leaves the containers running
# as orphans. Reject here so we never put the NAS into that state.
if status == "RUNNING":
return (
f"Error: project '{project_name}' is RUNNING. "
f"Stop it first with stop_project, then delete_project."
)
try:
await client.request(
"SYNO.Docker.Project",
@@ -411,14 +420,6 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non
params={"id": json.dumps(project_id)},
)
except SynologyError as e:
# DSM refuses to delete a running project. We deliberately do NOT
# auto-stop — that would be too destructive for a delete tool —
# but we tell the user how to proceed.
if status == "RUNNING":
return (
f"Cannot delete project '{project_name}' while it is running ({e}).\n"
f"Stop the project first with stop_project."
)
return f"Error deleting project '{project_name}': {e}"
return (