Skip to content

Commit 2b928d6

Browse files
committed
fix(sandbox): preserve registry runtime state
1 parent b6e8ee5 commit 2b928d6

5 files changed

Lines changed: 107 additions & 28 deletions

File tree

astrbot/core/computer/sandbox_manager.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ def _sandbox_boot_lock(self, sandbox_id: str) -> asyncio.Lock:
4949
def drop_boot_lock(self, sandbox_id: str) -> None:
5050
self.boot_locks.pop(sandbox_id, None)
5151

52-
def get_idle_timeout(self, config: dict, provider_id: str) -> float:
53-
_ = config, provider_id
54-
return 0.0
55-
5652
def build_record_payload(
5753
self,
5854
*,

astrbot/core/computer/sandbox_models.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,23 @@ class SandboxRecord:
4040
labels: dict[str, Any] = field(default_factory=dict)
4141
notes: str | None = None
4242

43+
@staticmethod
44+
def _required_string(data: dict[str, Any], field_name: str) -> str:
45+
value = data[field_name]
46+
if not isinstance(value, str):
47+
raise ValueError(f"{field_name} must be a non-empty string")
48+
value = value.strip()
49+
if not value:
50+
raise ValueError(f"{field_name} must be a non-empty string")
51+
return value
52+
4353
@classmethod
4454
def from_dict(cls, data: dict[str, Any]) -> SandboxRecord:
4555
return cls(
46-
sandbox_id=str(data["sandbox_id"]),
47-
sandbox_name=str(data["sandbox_name"]),
48-
booter_type=str(data["booter_type"]),
49-
provider=str(data["provider"]),
56+
sandbox_id=cls._required_string(data, "sandbox_id"),
57+
sandbox_name=cls._required_string(data, "sandbox_name"),
58+
booter_type=cls._required_string(data, "booter_type"),
59+
provider=cls._required_string(data, "provider"),
5060
managed=bool(data["managed"]),
5161
created_by_astrbot=bool(data["created_by_astrbot"]),
5262
is_default=bool(data.get("is_default", False)),

astrbot/core/computer/sandbox_registry.py

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,16 @@ def upsert_sandbox(
6969
owner_session_id: str | None,
7070
connect_info: dict[str, Any],
7171
is_default: bool = False,
72-
status: str = "running",
73-
idle_timeout: int | float | None = None,
74-
expires_at: float | None = None,
75-
retention_policy: str = "temporary",
76-
last_used_at: float | None = None,
77-
controller_user_id: str | None = None,
78-
controller_session_id: str | None = None,
79-
lease_expires_at: float | None = None,
80-
labels: dict[str, Any] | None = None,
81-
notes: str | None = None,
72+
status: str | object = _UNSET,
73+
idle_timeout: int | float | None | object = _UNSET,
74+
expires_at: float | None | object = _UNSET,
75+
retention_policy: str | object = _UNSET,
76+
last_used_at: float | None | object = _UNSET,
77+
controller_user_id: str | None | object = _UNSET,
78+
controller_session_id: str | None | object = _UNSET,
79+
lease_expires_at: float | None | object = _UNSET,
80+
labels: dict[str, Any] | None | object = _UNSET,
81+
notes: str | None | object = _UNSET,
8282
) -> dict[str, Any]:
8383
record = self._payload["sandboxes"].get(sandbox_id, {})
8484
record.update(
@@ -92,19 +92,39 @@ def upsert_sandbox(
9292
"is_default": is_default,
9393
"owner_user_id": owner_user_id,
9494
"owner_session_id": owner_session_id,
95-
"controller_user_id": controller_user_id,
96-
"controller_session_id": controller_session_id,
97-
"lease_expires_at": lease_expires_at,
98-
"last_used_at": last_used_at,
99-
"idle_timeout": idle_timeout,
100-
"expires_at": expires_at,
101-
"retention_policy": retention_policy,
102-
"status": status,
10395
"connect_info": deepcopy(connect_info),
104-
"labels": deepcopy(labels) if labels is not None else {},
105-
"notes": notes,
10696
}
10797
)
98+
defaults = {
99+
"controller_user_id": None,
100+
"controller_session_id": None,
101+
"lease_expires_at": None,
102+
"last_used_at": None,
103+
"idle_timeout": None,
104+
"expires_at": None,
105+
"retention_policy": "temporary",
106+
"status": "running",
107+
"labels": {},
108+
"notes": None,
109+
}
110+
updates = {
111+
"controller_user_id": controller_user_id,
112+
"controller_session_id": controller_session_id,
113+
"lease_expires_at": lease_expires_at,
114+
"last_used_at": last_used_at,
115+
"idle_timeout": idle_timeout,
116+
"expires_at": expires_at,
117+
"retention_policy": retention_policy,
118+
"status": status,
119+
"labels": deepcopy(labels) if labels is not _UNSET else _UNSET,
120+
"notes": notes,
121+
}
122+
for field_name, default_value in defaults.items():
123+
value = updates[field_name]
124+
if value is _UNSET:
125+
record.setdefault(field_name, deepcopy(default_value))
126+
else:
127+
record[field_name] = value
108128
record = SandboxRecord.from_dict(record).to_dict()
109129
self._payload["sandboxes"][sandbox_id] = record
110130
if is_default or (managed and self._payload["default_sandbox_id"] is None):

tests/unit/test_sandbox_models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pytest
2+
13
from astrbot.core.computer.sandbox_models import (
24
SandboxRecord,
35
SandboxRetentionPolicy,
@@ -81,3 +83,28 @@ def test_sandbox_record_detects_active_control_lease():
8183
assert record.is_controlled_by("session-a", now=100.0) is True
8284
assert record.is_controlled_by("session-b", now=100.0) is False
8385
assert record.has_active_lease(now=201.0) is False
86+
87+
88+
@pytest.mark.parametrize(
89+
"field,value",
90+
[
91+
("sandbox_id", None),
92+
("sandbox_id", ""),
93+
("sandbox_name", " "),
94+
("booter_type", None),
95+
("provider", ""),
96+
],
97+
)
98+
def test_sandbox_record_rejects_empty_required_string_fields(field, value):
99+
payload = {
100+
"sandbox_id": "sb-1",
101+
"sandbox_name": "worker",
102+
"booter_type": "cua",
103+
"provider": "cua",
104+
"managed": True,
105+
"created_by_astrbot": True,
106+
}
107+
payload[field] = value
108+
109+
with pytest.raises(ValueError, match=field):
110+
SandboxRecord.from_dict(payload)

tests/unit/test_sandbox_registry.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,29 @@ def test_sandbox_registry_updates_config_and_status(tmp_path):
146146
assert record["expires_at"] == 200
147147
assert record["retention_policy"] == "persistent"
148148
assert record["status"] == "stopped"
149+
150+
151+
def test_sandbox_registry_upsert_preserves_runtime_fields_when_omitted(tmp_path):
152+
registry = SandboxRegistry(storage_path=tmp_path / "sandbox_registry.json")
153+
_upsert(
154+
registry,
155+
"sb-1",
156+
controller_user_id="user-a",
157+
controller_session_id="session-a",
158+
lease_expires_at=200,
159+
last_used_at=100,
160+
idle_timeout=30,
161+
status="unknown",
162+
)
163+
164+
_upsert(registry, "sb-1", sandbox_name="renamed", connect_info={"name": "renamed"})
165+
166+
record = registry.get_sandbox("sb-1")
167+
assert record["sandbox_name"] == "renamed"
168+
assert record["connect_info"] == {"name": "renamed"}
169+
assert record["controller_user_id"] == "user-a"
170+
assert record["controller_session_id"] == "session-a"
171+
assert record["lease_expires_at"] == 200
172+
assert record["last_used_at"] == 100
173+
assert record["idle_timeout"] == 30
174+
assert record["status"] == "unknown"

0 commit comments

Comments
 (0)