Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions agentrun/sandbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,11 +728,20 @@ async def delete_sandbox_async(

# 判断返回结果是否成功
if result.get("code") != "SUCCESS":
# 数据面报告 sandbox 不存在时,视为幂等删除成功
# When the data plane reports sandbox not found, treat as
# idempotent success (control plane may still list TERMINATED
# instances after the data plane has already removed them)
message = result.get("message", "")
if "sandbox not found" in message.lower():
return Sandbox.model_validate(
{"sandboxId": sandbox_id}, by_alias=True
)
raise ClientError(
status_code=0,
message=(
"Failed to stop sandbox:"
f" {result.get('message', 'Unknown error')}"
f" {message or 'Unknown error'}"
),
)

Expand Down Expand Up @@ -768,11 +777,20 @@ def delete_sandbox(

# 判断返回结果是否成功
if result.get("code") != "SUCCESS":
# 数据面报告 sandbox 不存在时,视为幂等删除成功
# When the data plane reports sandbox not found, treat as
# idempotent success (control plane may still list TERMINATED
# instances after the data plane has already removed them)
message = result.get("message", "")
if "sandbox not found" in message.lower():
return Sandbox.model_validate(
{"sandboxId": sandbox_id}, by_alias=True
)
raise ClientError(
status_code=0,
message=(
"Failed to stop sandbox:"
f" {result.get('message', 'Unknown error')}"
f" {message or 'Unknown error'}"
),
)

Expand Down
44 changes: 44 additions & 0 deletions tests/unittests/sandbox/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,50 @@ def test_delete_sandbox_not_exist(
with pytest.raises(ResourceNotExistError):
client.delete_sandbox("nonexistent")

@patch("agentrun.sandbox.client.SandboxControlAPI")
@patch("agentrun.sandbox.client.SandboxDataAPI")
def test_delete_sandbox_not_found_in_response_is_idempotent(
self, mock_data_api_class, mock_control_api_class
):
"""数据面返回 not found 时,delete_sandbox 应幂等成功

When the data plane returns a non-SUCCESS response whose message
contains "not found", the SDK should treat the delete as a success
rather than raising an error. This handles the case where the
control-plane list API still shows a TERMINATED sandbox, but the
data plane has already removed it.
"""
mock_data_api = MagicMock()
mock_data_api.delete_sandbox.return_value = {
"code": "FAILED",
"message": "sandbox not found",
}
mock_data_api_class.return_value = mock_data_api

client = SandboxClient()
result = client.delete_sandbox("sandbox-123")
assert result.sandbox_id == "sandbox-123"

@patch("agentrun.sandbox.client.SandboxControlAPI")
@patch("agentrun.sandbox.client.SandboxDataAPI")
@pytest.mark.asyncio
async def test_delete_sandbox_async_not_found_in_response_is_idempotent(
self, mock_data_api_class, mock_control_api_class
):
"""数据面返回 not found 时,delete_sandbox_async 应幂等成功"""
mock_data_api = MagicMock()
mock_data_api.delete_sandbox_async = AsyncMock(
return_value={
"code": "FAILED",
"message": "sandbox not found",
}
)
mock_data_api_class.return_value = mock_data_api

client = SandboxClient()
result = await client.delete_sandbox_async("sandbox-123")
assert result.sandbox_id == "sandbox-123"

@patch("agentrun.sandbox.client.SandboxControlAPI")
@patch("agentrun.sandbox.client.SandboxDataAPI")
@pytest.mark.asyncio
Expand Down