已合并
fix(sdk): clear cached sandbox state after successful deletion #191
ytluo创建于 9月2日
fix(sdk): clear cached sandbox state after successful deletion #191
已合并
ytluo创建于 9月2日
共 3 个文件变更+95-0
@@ -165,6 +165,8 @@ sandbox.delete(sandbox_id=None) -> bool
165 165 
166**返回:** 成功返回 `True`,失败抛出 `RuntimeError`166**返回:** 成功返回 `True`,失败抛出 `RuntimeError`
167 167 
168+成功删除当前实例对应的沙箱后,SDK 清空该对象缓存的身份、元数据和运行时信息,并关闭 Agent 客户端。删除失败或删除其他 ID 的沙箱时,不修改当前对象。
169+ 
168**静态方法:**170**静态方法:**
169```text171```text
170Sandbox.delete_sandbox(sandbox_id) -> bool172Sandbox.delete_sandbox(sandbox_id) -> bool
@@ -287,6 +289,8 @@ sandbox.get_info() -> SandboxInfo
287 289 
288获取当前实例保存的沙箱 ID、IP、源 Template Name 和实际 Template ID。290获取当前实例保存的沙箱 ID、IP、源 Template Name 和实际 Template ID。
289 291 
292+该方法只读取本地缓存,不请求 daemon。当前对象成功调用 `delete()` 后,返回 `SandboxInfo(sandbox_id="", ip="", template_name=None, template_id=None)`。通过其他对象、CLI 或进程删除沙箱,不会自动清空此对象的缓存。
293+ 
290**示例:**294**示例:**
291```python295```python
292info = sbx.get_info()296info = sbx.get_info()
@@ -833,11 +833,45 @@ class Sandbox:
833 833 
834 try:834 try:
835 self._delete_control_plane_requests(path)835 self._delete_control_plane_requests(path)
836+ if target_id == self.sandbox_id:
837+ self._clear_sandbox_state()
836 return True838 return True
837 839 
838 except requests.exceptions.RequestException as e:840 except requests.exceptions.RequestException as e:
839 raise RuntimeError(_request_exception_message(e))841 raise RuntimeError(_request_exception_message(e))
840 842 
843+ def _clear_sandbox_state(self) -> None:
844+ client = self._client
845+ self._client = None
846+ self.control_plane_only = False
847+ self.sandbox_id = ""
848+ self.ip = None
849+ self.agent_token = None
850+ self.template_name = None
851+ self.template_id = None
852+ self.vcpu_num = None
853+ self.vcpu_max = None
854+ self.ram_mb = None
855+ self.vmm_name = None
856+ self.image_name = None
857+ self.snapshot_id = None
858+ self.started_at = None
859+ self.end_at = None
860+ self.disk_size_mb = None
861+ self.conch_init_version = None
862+ self.alias = None
863+ self.volume_mounts = []
864+ self.env = None
865+ self.network = None
866+ self.metadata = {}
867+ self.lifecycle = {}
868+ if client is not None:
869+ try:
870+ client.close()
871+ except Exception:
872+ # Local connection cleanup must not undo a successful delete.
873+ pass
874+ 
841 @classmethod875 @classmethod
842 def service_health(cls) -> bool:876 def service_health(cls) -> bool:
843 try:877 try:
@@ -1,3 +1,5 @@
1+from unittest.mock import Mock
2+ 
1import pytest3import pytest
2import requests4import requests
3 5 
@@ -81,6 +83,61 @@ def test_control_plane_transport_failures(monkeypatch):
81 Sandbox.list()83 Sandbox.list()
82 84 
83 85 
86+@pytest.mark.parametrize("close_error", [None, RuntimeError("close failed")])
87+def test_delete_clears_local_sandbox_state(monkeypatch, close_error):
88+ monkeypatch.setattr(sandbox_module.requests_unixsocket, "Session", FakeSession)
89+ sandbox = Sandbox.get("sandbox-1")
90+ client = Mock()
91+ client.close.side_effect = close_error
92+ sandbox.client = client
93+ cached_fields = (
94+ "ip", "agent_token", "template_name", "template_id", "vcpu_num",
95+ "vcpu_max", "ram_mb", "vmm_name", "image_name", "snapshot_id",
96+ "started_at", "end_at", "disk_size_mb", "conch_init_version", "alias",
97+ )
98+ for field in cached_fields:
99+ setattr(sandbox, field, "cached")
100+ sandbox.volume_mounts = [{"path": "/data"}]
101+ sandbox.env = {"KEY": "value"}
102+ sandbox.lifecycle = {"state": "running"}
103+ 
104+ assert sandbox.delete() is True
105+ 
106+ assert sandbox.get_info() == sandbox_module.SandboxInfo("", "", None, None)
107+ assert all(getattr(sandbox, field) is None for field in cached_fields)
108+ assert sandbox.metadata == {}
109+ assert sandbox.lifecycle == {}
110+ assert sandbox.volume_mounts == []
111+ assert sandbox.env is None
112+ assert sandbox.network is None
113+ assert sandbox._client is None
114+ client.close.assert_called_once_with()
115+ with pytest.raises(RuntimeError, match="not initialized"):
116+ sandbox.health_check()
117+ 
118+ 
119+@pytest.mark.parametrize("delete_fails", [False, True])
120+def test_delete_preserves_state_on_failure_or_other_target(monkeypatch, delete_fails):
121+ monkeypatch.setattr(sandbox_module.requests_unixsocket, "Session", FakeSession)
122+ sandbox = Sandbox.get("sandbox-1")
123+ client = Mock()
124+ sandbox.client = client
125+ before = vars(sandbox).copy()
126+ delete = Mock(side_effect=requests.HTTPError("delete failed") if delete_fails else None)
127+ monkeypatch.setattr(sandbox._session, "delete", delete)
128+ 
129+ if delete_fails:
130+ with pytest.raises(RuntimeError, match="delete failed"):
131+ sandbox.delete()
132+ else:
133+ delete.return_value = FakeResponse(204)
134+ assert sandbox.delete("sandbox-2") is True
135+ assert delete.call_args.args[0].endswith("/sandboxes/sandbox-2")
136+ 
137+ assert vars(sandbox) == before
138+ client.close.assert_not_called()
139+ 
140+ 
84def test_control_plane_structured_error_uses_code_and_message():141def test_control_plane_structured_error_uses_code_and_message():
85 response = requests.Response()142 response = requests.Response()
86 response.status_code = 400143 response.status_code = 400