mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-10 14:08:52 +00:00
feat(sandbox): make E2B mount upload deadline configurable (#4876)
* feat(e2b-sandbox): make mount upload deadline configurable Replace the hardcoded 120-second mount upload deadline with a configurable `mount_upload_deadline_seconds` key read from SandboxConfig (extra=allow). The value is validated: zero and negative inputs are clamped to 1 second. Omitting the key preserves the existing 120-second default. This addresses the follow-up from PR #4842 review: operators with large mounts or slow networks can now size the deadline to their deployment without changing code. * fix(e2b-sandbox): address review feedback on configurable deadline - Remove import-time default capture from _mount_deadline_reason() and _MountUploadBudget.deadline_seconds to prevent silent drift. - Add warning log when mount_upload_deadline_seconds is clamped to 1 (was silent before). - Update AGENTS.md E2B Mount Uploads section: deadline is now configurable, not fixed 120. - Add mount_upload_deadline_seconds to YAML examples in provider docstring and __init__.py. - Add config-path test that exercises SandboxConfig -> _load_config -> _apply_mounts end-to-end. * fix(e2b-sandbox-provider): handle non-numeric mount_upload_deadline_seconds Guard _resolve_mount_upload_deadline against None, non-numeric strings, and other invalid values. None returns the default; non-numeric strings like '120s' or 'abc' log a warning and fall back to the 120-second default instead of crashing provider init with TypeError/ValueError. Extend the parametrized clamp test with None, suffix, and alpha cases, and add a warning assertion. Update CONFIGURATION.md with the new mount_upload_deadline_seconds key and its behavior. * fix(sandbox): handle infinite mount deadline
This commit is contained in:
parent
468eab4b5d
commit
0dd233afc4
@ -519,6 +519,7 @@ sandbox:
|
|||||||
home_dir: /home/user # /mnt/user-data is remapped under this directory
|
home_dir: /home/user # /mnt/user-data is remapped under this directory
|
||||||
idle_timeout: 600 # forwarded to e2b's server-side set_timeout()
|
idle_timeout: 600 # forwarded to e2b's server-side set_timeout()
|
||||||
replicas: 3 # max concurrent sandboxes per gateway process
|
replicas: 3 # max concurrent sandboxes per gateway process
|
||||||
|
mount_upload_deadline_seconds: 120 # per-sandbox time budget for mount uploads (seconds)
|
||||||
ownership: # use Redis when more than one gateway shares E2B
|
ownership: # use Redis when more than one gateway shares E2B
|
||||||
type: redis
|
type: redis
|
||||||
redis_url: $REDIS_URL
|
redis_url: $REDIS_URL
|
||||||
@ -564,6 +565,11 @@ Notes specific to `E2BSandboxProvider`:
|
|||||||
`/mnt/user-data/outputs/` (which is mapped to `home_dir/outputs/` inside the
|
`/mnt/user-data/outputs/` (which is mapped to `home_dir/outputs/` inside the
|
||||||
sandbox and surfaced through the standard artifact pipeline) to ship files
|
sandbox and surfaced through the standard artifact pipeline) to ship files
|
||||||
back to the gateway.
|
back to the gateway.
|
||||||
|
- `mount_upload_deadline_seconds` sets the per-sandbox time budget for mount
|
||||||
|
uploads. The provider checks it before each mount, during directory preflight,
|
||||||
|
and before each SDK write. The deadline does not interrupt active filesystem or
|
||||||
|
E2B SDK calls. Omitting the key preserves the 120-second default. Values below
|
||||||
|
1 are clamped to 1; non-numeric or null values fall back to the default.
|
||||||
|
|
||||||
**OpenSandbox Remote Sandbox** (runs code through an OpenSandbox deployment):
|
**OpenSandbox Remote Sandbox** (runs code through an OpenSandbox deployment):
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,8 @@ Each mount has these fixed limits:
|
|||||||
The full sandbox creation pass also allows 512 MiB and 2,000 files. Skill
|
The full sandbox creation pass also allows 512 MiB and 2,000 files. Skill
|
||||||
projections and configured mounts share this budget.
|
projections and configured mounts share this budget.
|
||||||
|
|
||||||
The pass has a cooperative 120-second deadline. The provider checks it before
|
The pass has a cooperative deadline controlled by
|
||||||
|
``mount_upload_deadline_seconds`` (default: 120 seconds). The provider checks it before
|
||||||
each mount, during directory preflight, and before each SDK write. The deadline
|
each mount, during directory preflight, and before each SDK write. The deadline
|
||||||
does not interrupt active filesystem or E2B SDK calls.
|
does not interrupt active filesystem or E2B SDK calls.
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ Configuration example (``config.yaml``)::
|
|||||||
reconciliation_max_pages: 10
|
reconciliation_max_pages: 10
|
||||||
reconciliation_max_items: 200
|
reconciliation_max_items: 200
|
||||||
reconciliation_max_seconds: 15
|
reconciliation_max_seconds: 15
|
||||||
|
mount_upload_deadline_seconds: 120 # mount upload pass deadline; default: 120
|
||||||
mounts: # one-shot upload of host files into the sandbox
|
mounts: # one-shot upload of host files into the sandbox
|
||||||
- host_path: /path/on/host
|
- host_path: /path/on/host
|
||||||
container_path: /path/in/sandbox
|
container_path: /path/in/sandbox
|
||||||
|
|||||||
@ -21,6 +21,7 @@ provider fields during startup.
|
|||||||
reconciliation_max_pages: 10
|
reconciliation_max_pages: 10
|
||||||
reconciliation_max_items: 200
|
reconciliation_max_items: 200
|
||||||
reconciliation_max_seconds: 15
|
reconciliation_max_seconds: 15
|
||||||
|
mount_upload_deadline_seconds: 120 # mount upload pass deadline; default: 120
|
||||||
ownership:
|
ownership:
|
||||||
type: redis # shares ownership and capacity across Gateways
|
type: redis # shares ownership and capacity across Gateways
|
||||||
redis_url: redis://redis:6379/0
|
redis_url: redis://redis:6379/0
|
||||||
@ -45,6 +46,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from collections.abc import Callable
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
@ -111,8 +113,8 @@ _MAX_MOUNT_PASS_FILES = 2000
|
|||||||
_MOUNT_PASS_DEADLINE_SECONDS = 120
|
_MOUNT_PASS_DEADLINE_SECONDS = 120
|
||||||
|
|
||||||
|
|
||||||
def _mount_deadline_reason() -> str:
|
def _mount_deadline_reason(deadline_seconds: int) -> str:
|
||||||
return f"time budget {_MOUNT_PASS_DEADLINE_SECONDS}s"
|
return f"time budget {deadline_seconds}s"
|
||||||
|
|
||||||
|
|
||||||
class _MountPassLimitExceeded(Exception):
|
class _MountPassLimitExceeded(Exception):
|
||||||
@ -122,6 +124,7 @@ class _MountPassLimitExceeded(Exception):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class _MountUploadBudget:
|
class _MountUploadBudget:
|
||||||
deadline: float
|
deadline: float
|
||||||
|
deadline_seconds: int
|
||||||
attempted_bytes: int = 0
|
attempted_bytes: int = 0
|
||||||
attempted_files: int = 0
|
attempted_files: int = 0
|
||||||
completed_bytes: int = 0
|
completed_bytes: int = 0
|
||||||
@ -133,7 +136,7 @@ class _MountUploadBudget:
|
|||||||
|
|
||||||
def check_deadline(self) -> None:
|
def check_deadline(self) -> None:
|
||||||
if self.expired:
|
if self.expired:
|
||||||
raise _MountPassLimitExceeded(_mount_deadline_reason())
|
raise _MountPassLimitExceeded(_mount_deadline_reason(self.deadline_seconds))
|
||||||
|
|
||||||
|
|
||||||
# Metadata keys we attach to every sandbox so we can discover ours via
|
# Metadata keys we attach to every sandbox so we can discover ours via
|
||||||
@ -151,6 +154,7 @@ E2B_EXTRA_CONFIG_KEYS = frozenset(
|
|||||||
"api_key",
|
"api_key",
|
||||||
"domain",
|
"domain",
|
||||||
"home_dir",
|
"home_dir",
|
||||||
|
"mount_upload_deadline_seconds",
|
||||||
"reconciliation_grace_seconds",
|
"reconciliation_grace_seconds",
|
||||||
"reconciliation_interval_seconds",
|
"reconciliation_interval_seconds",
|
||||||
"reconciliation_max_items",
|
"reconciliation_max_items",
|
||||||
@ -329,6 +333,7 @@ class E2BSandboxProvider(SandboxProvider):
|
|||||||
0.1,
|
0.1,
|
||||||
float(_opt("reconciliation_max_seconds", DEFAULT_RECONCILIATION_MAX_SECONDS)),
|
float(_opt("reconciliation_max_seconds", DEFAULT_RECONCILIATION_MAX_SECONDS)),
|
||||||
),
|
),
|
||||||
|
"mount_upload_deadline_seconds": self._resolve_mount_upload_deadline(_opt),
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -341,6 +346,28 @@ class E2BSandboxProvider(SandboxProvider):
|
|||||||
resolved[key] = "" if value is None else str(value)
|
resolved[key] = "" if value is None else str(value)
|
||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_mount_upload_deadline(_opt: Callable[[str, Any], Any]) -> int:
|
||||||
|
raw = _opt("mount_upload_deadline_seconds", _MOUNT_PASS_DEADLINE_SECONDS)
|
||||||
|
if raw is None:
|
||||||
|
return _MOUNT_PASS_DEADLINE_SECONDS
|
||||||
|
try:
|
||||||
|
value = int(raw)
|
||||||
|
except (TypeError, ValueError, OverflowError):
|
||||||
|
logger.warning(
|
||||||
|
"E2BSandboxProvider: non-numeric mount_upload_deadline_seconds=%r; falling back to %ds",
|
||||||
|
raw,
|
||||||
|
_MOUNT_PASS_DEADLINE_SECONDS,
|
||||||
|
)
|
||||||
|
return _MOUNT_PASS_DEADLINE_SECONDS
|
||||||
|
if value < 1:
|
||||||
|
logger.warning(
|
||||||
|
"E2BSandboxProvider: invalid mount_upload_deadline_seconds=%d; clamping to 1",
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
return value
|
||||||
|
|
||||||
def _get_sandbox_cls(self) -> type[E2BClientSandbox]:
|
def _get_sandbox_cls(self) -> type[E2BClientSandbox]:
|
||||||
"""Return the e2b SDK Sandbox class."""
|
"""Return the e2b SDK Sandbox class."""
|
||||||
return E2BClientSandbox
|
return E2BClientSandbox
|
||||||
@ -1798,7 +1825,11 @@ class E2BSandboxProvider(SandboxProvider):
|
|||||||
|
|
||||||
def _apply_mounts(self, client: E2BClientSandbox, *, user_id: str | None = None) -> None:
|
def _apply_mounts(self, client: E2BClientSandbox, *, user_id: str | None = None) -> None:
|
||||||
started_at = time.monotonic()
|
started_at = time.monotonic()
|
||||||
budget = _MountUploadBudget(deadline=started_at + _MOUNT_PASS_DEADLINE_SECONDS)
|
deadline_seconds = self._config.get("mount_upload_deadline_seconds", _MOUNT_PASS_DEADLINE_SECONDS)
|
||||||
|
budget = _MountUploadBudget(
|
||||||
|
deadline=started_at + deadline_seconds,
|
||||||
|
deadline_seconds=deadline_seconds,
|
||||||
|
)
|
||||||
|
|
||||||
def warn_pass_stopped(reason: str) -> None:
|
def warn_pass_stopped(reason: str) -> None:
|
||||||
elapsed_ms = int((time.monotonic() - started_at) * 1000)
|
elapsed_ms = int((time.monotonic() - started_at) * 1000)
|
||||||
@ -1835,7 +1866,7 @@ class E2BSandboxProvider(SandboxProvider):
|
|||||||
|
|
||||||
for host_path, container_path, read_only in mounts:
|
for host_path, container_path, read_only in mounts:
|
||||||
if budget.expired:
|
if budget.expired:
|
||||||
warn_pass_stopped(_mount_deadline_reason())
|
warn_pass_stopped(_mount_deadline_reason(deadline_seconds))
|
||||||
break
|
break
|
||||||
if not host_path.exists():
|
if not host_path.exists():
|
||||||
logger.warning("Skipping e2b mount: host_path %s does not exist", host_path)
|
logger.warning("Skipping e2b mount: host_path %s does not exist", host_path)
|
||||||
|
|||||||
@ -746,6 +746,209 @@ def test_apply_mounts_deadline_stops_before_next_mount_preflight(monkeypatch, tm
|
|||||||
assert "time budget 1s" in caplog.text
|
assert "time budget 1s" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_mounts_deadline_defaults_to_120_when_not_configured(monkeypatch, tmp_path, caplog):
|
||||||
|
mod = importlib.import_module("deerflow.community.e2b_sandbox.e2b_sandbox_provider")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
mod,
|
||||||
|
"get_app_config",
|
||||||
|
lambda: SimpleNamespace(skills=SimpleNamespace(container_path="/mnt/skills")),
|
||||||
|
)
|
||||||
|
clock = [0.0]
|
||||||
|
monkeypatch.setattr(mod.time, "monotonic", lambda: clock[0])
|
||||||
|
|
||||||
|
class DeadlineFilesAPI(FakeFilesAPI):
|
||||||
|
def write(self, path: str, content: Any) -> None:
|
||||||
|
super().write(path, content)
|
||||||
|
clock[0] = 121.0
|
||||||
|
|
||||||
|
source = tmp_path / "mount"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "first.txt").write_text("first", encoding="utf-8")
|
||||||
|
(source / "second.txt").write_text("second", encoding="utf-8")
|
||||||
|
provider = _make_provider()
|
||||||
|
assert "mount_upload_deadline_seconds" not in provider._config
|
||||||
|
monkeypatch.setattr(provider, "_skill_projection_mounts", lambda _user_id: [])
|
||||||
|
provider._config["mounts"] = [
|
||||||
|
SimpleNamespace(host_path=str(source), container_path="/mnt/data", read_only=False),
|
||||||
|
]
|
||||||
|
client = FakeClient(files=DeadlineFilesAPI())
|
||||||
|
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
provider._apply_mounts(client, user_id="user-1")
|
||||||
|
|
||||||
|
assert len(client.files.write_calls) == 1
|
||||||
|
assert "time budget 120s" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_mounts_deadline_uses_configured_value(monkeypatch, tmp_path, caplog):
|
||||||
|
mod = importlib.import_module("deerflow.community.e2b_sandbox.e2b_sandbox_provider")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
mod,
|
||||||
|
"get_app_config",
|
||||||
|
lambda: SimpleNamespace(skills=SimpleNamespace(container_path="/mnt/skills")),
|
||||||
|
)
|
||||||
|
clock = [0.0]
|
||||||
|
monkeypatch.setattr(mod.time, "monotonic", lambda: clock[0])
|
||||||
|
|
||||||
|
class DeadlineFilesAPI(FakeFilesAPI):
|
||||||
|
def write(self, path: str, content: Any) -> None:
|
||||||
|
super().write(path, content)
|
||||||
|
clock[0] = 61.0
|
||||||
|
|
||||||
|
source = tmp_path / "mount"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "first.txt").write_text("first", encoding="utf-8")
|
||||||
|
(source / "second.txt").write_text("second", encoding="utf-8")
|
||||||
|
provider = _make_provider()
|
||||||
|
provider._config["mount_upload_deadline_seconds"] = 60
|
||||||
|
monkeypatch.setattr(provider, "_skill_projection_mounts", lambda _user_id: [])
|
||||||
|
provider._config["mounts"] = [
|
||||||
|
SimpleNamespace(host_path=str(source), container_path="/mnt/data", read_only=False),
|
||||||
|
]
|
||||||
|
client = FakeClient(files=DeadlineFilesAPI())
|
||||||
|
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
provider._apply_mounts(client, user_id="user-1")
|
||||||
|
|
||||||
|
assert len(client.files.write_calls) == 1
|
||||||
|
assert "time budget 60s" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"raw,expected",
|
||||||
|
[
|
||||||
|
(0, 1),
|
||||||
|
(-5, 1),
|
||||||
|
(-100, 1),
|
||||||
|
(None, 120),
|
||||||
|
("120s", 120),
|
||||||
|
("abc", 120),
|
||||||
|
(float("inf"), 120),
|
||||||
|
],
|
||||||
|
ids=["zero", "negative", "large_negative", "none", "suffix", "alpha", "infinity"],
|
||||||
|
)
|
||||||
|
def test_load_config_clamps_invalid_mount_upload_deadline(monkeypatch, caplog, raw, expected):
|
||||||
|
mod = importlib.import_module("deerflow.community.e2b_sandbox.e2b_sandbox_provider")
|
||||||
|
|
||||||
|
class FakeConfig:
|
||||||
|
sandbox = SimpleNamespace(
|
||||||
|
model_extra={"mount_upload_deadline_seconds": raw},
|
||||||
|
api_key="test-key",
|
||||||
|
template=None,
|
||||||
|
image=None,
|
||||||
|
domain=None,
|
||||||
|
home_dir=None,
|
||||||
|
idle_timeout=None,
|
||||||
|
replicas=None,
|
||||||
|
overflow_policy=None,
|
||||||
|
acquire_timeout=None,
|
||||||
|
burst_limit=None,
|
||||||
|
mounts=[],
|
||||||
|
environment=None,
|
||||||
|
ownership=None,
|
||||||
|
mount_upload_deadline_seconds=raw,
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(mod, "get_app_config", lambda: FakeConfig())
|
||||||
|
provider = mod.E2BSandboxProvider.__new__(mod.E2BSandboxProvider)
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
config = provider._load_config()
|
||||||
|
assert config["mount_upload_deadline_seconds"] == expected
|
||||||
|
if raw is None:
|
||||||
|
assert "clamping" not in caplog.text
|
||||||
|
else:
|
||||||
|
assert "mount_upload_deadline_seconds" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_custom_mount_upload_deadline_flows_to_apply_mounts(monkeypatch, tmp_path, caplog):
|
||||||
|
mod = importlib.import_module("deerflow.community.e2b_sandbox.e2b_sandbox_provider")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
mod,
|
||||||
|
"get_app_config",
|
||||||
|
lambda: SimpleNamespace(skills=SimpleNamespace(container_path="/mnt/skills")),
|
||||||
|
)
|
||||||
|
clock = [0.0]
|
||||||
|
monkeypatch.setattr(mod.time, "monotonic", lambda: clock[0])
|
||||||
|
|
||||||
|
class DeadlineFilesAPI(FakeFilesAPI):
|
||||||
|
def write(self, path: str, content: Any) -> None:
|
||||||
|
super().write(path, content)
|
||||||
|
clock[0] = 61.0
|
||||||
|
|
||||||
|
source = tmp_path / "mount"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "first.txt").write_text("first", encoding="utf-8")
|
||||||
|
(source / "second.txt").write_text("second", encoding="utf-8")
|
||||||
|
|
||||||
|
class FakeConfig:
|
||||||
|
sandbox = SimpleNamespace(
|
||||||
|
model_extra={"mount_upload_deadline_seconds": 60},
|
||||||
|
api_key="test-key",
|
||||||
|
template=None,
|
||||||
|
image=None,
|
||||||
|
domain=None,
|
||||||
|
home_dir=None,
|
||||||
|
idle_timeout=None,
|
||||||
|
replicas=None,
|
||||||
|
overflow_policy=None,
|
||||||
|
acquire_timeout=None,
|
||||||
|
burst_limit=None,
|
||||||
|
mounts=[SimpleNamespace(host_path=str(source), container_path="/mnt/data", read_only=False)],
|
||||||
|
environment=None,
|
||||||
|
ownership=None,
|
||||||
|
mount_upload_deadline_seconds=60,
|
||||||
|
)
|
||||||
|
skills = SimpleNamespace(container_path="/mnt/skills")
|
||||||
|
|
||||||
|
monkeypatch.setattr(mod, "get_app_config", lambda: FakeConfig())
|
||||||
|
provider = mod.E2BSandboxProvider.__new__(mod.E2BSandboxProvider)
|
||||||
|
provider._config = provider._load_config()
|
||||||
|
monkeypatch.setattr(provider, "_skill_projection_mounts", lambda _user_id: [])
|
||||||
|
client = FakeClient(files=DeadlineFilesAPI())
|
||||||
|
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
provider._apply_mounts(client, user_id="user-1")
|
||||||
|
|
||||||
|
assert provider._config["mount_upload_deadline_seconds"] == 60
|
||||||
|
assert len(client.files.write_calls) == 1
|
||||||
|
assert "time budget 60s" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_mounts_deadline_reason_shows_configured_value(monkeypatch, tmp_path, caplog):
|
||||||
|
mod = importlib.import_module("deerflow.community.e2b_sandbox.e2b_sandbox_provider")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
mod,
|
||||||
|
"get_app_config",
|
||||||
|
lambda: SimpleNamespace(skills=SimpleNamespace(container_path="/mnt/skills")),
|
||||||
|
)
|
||||||
|
clock = [0.0]
|
||||||
|
monkeypatch.setattr(mod.time, "monotonic", lambda: clock[0])
|
||||||
|
|
||||||
|
class DeadlineFilesAPI(FakeFilesAPI):
|
||||||
|
def write(self, path: str, content: Any) -> None:
|
||||||
|
super().write(path, content)
|
||||||
|
clock[0] = 200.0
|
||||||
|
|
||||||
|
source = tmp_path / "mount"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "first.txt").write_text("first", encoding="utf-8")
|
||||||
|
(source / "second.txt").write_text("second", encoding="utf-8")
|
||||||
|
provider = _make_provider()
|
||||||
|
provider._config["mount_upload_deadline_seconds"] = 180
|
||||||
|
monkeypatch.setattr(provider, "_skill_projection_mounts", lambda _user_id: [])
|
||||||
|
provider._config["mounts"] = [
|
||||||
|
SimpleNamespace(host_path=str(source), container_path="/mnt/data", read_only=False),
|
||||||
|
]
|
||||||
|
client = FakeClient(files=DeadlineFilesAPI())
|
||||||
|
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
provider._apply_mounts(client, user_id="user-1")
|
||||||
|
|
||||||
|
assert len(client.files.write_calls) == 1
|
||||||
|
assert "time budget 180s" in caplog.text
|
||||||
|
assert "attempted_files=1" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_skill_projection_and_configured_mount_share_upload_budget(monkeypatch, tmp_path):
|
def test_skill_projection_and_configured_mount_share_upload_budget(monkeypatch, tmp_path):
|
||||||
mod = importlib.import_module("deerflow.community.e2b_sandbox.e2b_sandbox_provider")
|
mod = importlib.import_module("deerflow.community.e2b_sandbox.e2b_sandbox_provider")
|
||||||
monkeypatch.setattr(mod, "_MAX_MOUNT_PASS_FILES", 1)
|
monkeypatch.setattr(mod, "_MAX_MOUNT_PASS_FILES", 1)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user