fix: return the created template from appliance install

POST /appliances/{id}/install replied 204 with an empty body, so the MCP
appliance_install tool crashed with 'Expecting value: line 1 column 1'
while the template had actually been created. The route now returns the
created template (201, response_model=schemas.Template), _create_template
propagates it, and the MCP handler parses the body defensively so an
empty reply degrades to a plain success message.
This commit is contained in:
YueGuobin committed 2026-08-25 23:39:38 +08:00
1 parent 73e5e27c7b
commit 888afdccbd
6 files changed
+44 -12

No files matched your search

+20 -1
View File
@@ -3,6 +3,8 @@ MCP handler unit tests with mocked Gns3Connector.
Tests that handlers correctly transform tool parameters into HTTP calls.
"""
import json
import pytest
from unittest.mock import MagicMock, patch
@@ -12,6 +14,7 @@ def _mock_conn(json_result=None):
conn = MagicMock()
conn.base_url = "http://192.168.1.3:3080/v3"
conn.http_call.return_value.json.return_value = json_result or {"status": "ok"}
conn.http_call.return_value.content = b"{}" # non-empty body by default
return conn
@@ -449,7 +452,7 @@ class TestAppliance:
def test_install_with_version(self, ctx):
from gns3server.agent.mcp.appliances import install_appliance_handler
with patch(f"{BASE}.{self.mod}._get_connector") as m:
conn = _mock_conn({"status": "installed"})
conn = _mock_conn({"template_id": "t1", "name": "FRR", "version": "8.2.2", "template_type": "docker"})
m.return_value = conn
result = install_appliance_handler({
"appliance_id": "a1", "version": "2.7.0.356",
@@ -458,6 +461,22 @@ class TestAppliance:
"post", "http://192.168.1.3:3080/v3/appliances/a1/install",
params={"version": "2.7.0.356"},
)
assert result["template"] == {
"template_id": "t1", "name": "FRR", "version": "8.2.2", "template_type": "docker",
}
def test_install_empty_body(self, ctx):
# a 204-style empty response must not blow up with a JSON decode error
# (the template is still created server-side)
from gns3server.agent.mcp.appliances import install_appliance_handler
with patch(f"{BASE}.{self.mod}._get_connector") as m:
conn = _mock_conn()
conn.http_call.return_value.content = b""
conn.http_call.return_value.json.side_effect = json.JSONDecodeError("Expecting value", "", 0)
m.return_value = conn
result = install_appliance_handler({"appliance_id": "a1"}, ctx)
assert "template" not in result
assert result["message"] == "Appliance a1 installed"
def test_install_missing_id(self, ctx):
from gns3server.agent.mcp.appliances import install_appliance_handler
@@ -53,7 +53,8 @@ class TestApplianceRoutes:
appliance_id = "fc520ae2-a4e5-48c3-9a13-516bb2e94668" # Alpine Linux appliance
response = await client.post(app.url_path_for("install_appliance", appliance_id=appliance_id))
assert response.status_code == status.HTTP_204_NO_CONTENT
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["name"] == "Alpine Linux"
async def test_docker_appliance_install_with_version(self, app: FastAPI, client: AsyncClient) -> None:
@@ -68,7 +69,9 @@ class TestApplianceRoutes:
appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance
params = {"version": "8G"}
response = await client.post(app.url_path_for("install_appliance", appliance_id=appliance_id), params=params)
assert response.status_code == status.HTTP_204_NO_CONTENT
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["name"] == "Empty VM"
assert response.json()["version"] == "8G"
async def test_qemu_appliance_install_without_version(self, app: FastAPI, client: AsyncClient, images_dir: str) -> None: