generated from coulomb/repo-seed
feat: cloud adapters E2B/Modal and billing export (SAND-WP-0010)
Add credentialed E2B and Modal extensions, burst routing fallback, fin-hub meter export hook, BYOK docs, and 77 tests.
This commit is contained in:
50
tests/test_billing_export.py
Normal file
50
tests/test_billing_export.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""fin-hub billing export tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from sandboxer.models import ActorType, Consumer, MeterRecord, SandboxState, SandboxStatus
|
||||
from sandboxer.payments.billing_export import export_meter_usage
|
||||
|
||||
|
||||
def test_export_skipped_when_url_unset() -> None:
|
||||
now = datetime.now(UTC)
|
||||
status = SandboxStatus(
|
||||
sandbox_id="s1",
|
||||
profile_id="profile.e2b-burst",
|
||||
extension_id="ext.e2b",
|
||||
state=SandboxState.DESTROYED,
|
||||
consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"),
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
meter = MeterRecord(pricing_model="metered", actual_usd=0.5, duration_s=100.0)
|
||||
assert export_meter_usage(status, extension_id="ext.e2b", meter=meter) is None
|
||||
|
||||
|
||||
def test_export_posts_when_configured(monkeypatch) -> None:
|
||||
monkeypatch.setenv("SANDBOXER_FIN_HUB_URL", "http://fin-hub.test")
|
||||
now = datetime.now(UTC)
|
||||
status = SandboxStatus(
|
||||
sandbox_id="s1",
|
||||
profile_id="profile.e2b-burst",
|
||||
extension_id="ext.e2b",
|
||||
state=SandboxState.DESTROYED,
|
||||
consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"),
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
meter = MeterRecord(pricing_model="metered", actual_usd=0.5, duration_s=100.0)
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"ok": True}
|
||||
with patch("sandboxer.payments.billing_export.httpx.post", return_value=mock_response) as post:
|
||||
result = export_meter_usage(status, extension_id="ext.e2b", meter=meter)
|
||||
|
||||
assert result == {"ok": True}
|
||||
post.assert_called_once()
|
||||
payload = post.call_args.kwargs["json"]
|
||||
assert payload["sandbox_id"] == "s1"
|
||||
assert payload["actual_usd"] == 0.5
|
||||
72
tests/test_e2b.py
Normal file
72
tests/test_e2b.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""E2B cloud adapter tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from sandboxer.extensions.credentials import credentials_available, resolve_api_key
|
||||
from sandboxer.extensions.e2b import E2BExtension
|
||||
from sandboxer.models import Profile
|
||||
|
||||
|
||||
def _profile() -> Profile:
|
||||
return Profile.model_validate(
|
||||
{
|
||||
"id": "profile.e2b-burst",
|
||||
"version": "1.0.0",
|
||||
"extension": "ext.e2b",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_credentials_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("E2B_API_KEY", "test-key")
|
||||
config = {"api_key_env": "E2B_API_KEY", "secret_ref": "e2b-api-key"}
|
||||
assert resolve_api_key(config, extension_id="ext.e2b") == "test-key"
|
||||
assert credentials_available("ext.e2b", config)
|
||||
|
||||
|
||||
def test_provision_and_teardown_with_mock_client(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("E2B_API_KEY", "test-key")
|
||||
ext = E2BExtension(
|
||||
{
|
||||
"api_base": "https://api.e2b.dev",
|
||||
"api_key_env": "E2B_API_KEY",
|
||||
"provider": "e2b",
|
||||
"template_id": "base",
|
||||
}
|
||||
)
|
||||
|
||||
mock_client = MagicMock()
|
||||
create_resp = MagicMock()
|
||||
create_resp.status_code = 200
|
||||
create_resp.json.return_value = {
|
||||
"sandboxID": "e2b-prov-1",
|
||||
"sandboxURL": "https://e2b-prov-1.e2b.dev",
|
||||
}
|
||||
ready_resp = MagicMock()
|
||||
ready_resp.status_code = 200
|
||||
delete_resp = MagicMock()
|
||||
delete_resp.status_code = 204
|
||||
mock_client.post.return_value = create_resp
|
||||
mock_client.get.return_value = ready_resp
|
||||
mock_client.delete.return_value = delete_resp
|
||||
mock_client.__enter__.return_value = mock_client
|
||||
mock_client.__exit__.return_value = None
|
||||
|
||||
with patch.object(ext, "_client_factory", return_value=mock_client):
|
||||
handle = ext.provision(_profile(), {}, "e2b")
|
||||
assert handle["provider_sandbox_id"] == "e2b-prov-1"
|
||||
reach = ext.wait_ready(handle)
|
||||
assert "e2b-prov-1" in reach["endpoint"]
|
||||
report = ext.teardown(handle)
|
||||
assert report["provider_removed"] == "true"
|
||||
|
||||
|
||||
def test_estimate_cost() -> None:
|
||||
ext = E2BExtension({"rate_usd_per_hour": 0.15, "session_fee_usd": 0.02})
|
||||
quote = ext.estimate_cost(_profile(), {}, duration_s=3600)
|
||||
assert quote.extension_id == "ext.e2b"
|
||||
assert quote.estimated_usd > 0
|
||||
63
tests/test_modal.py
Normal file
63
tests/test_modal.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Modal cloud adapter tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from sandboxer.extensions.modal import ModalExtension
|
||||
from sandboxer.models import Profile
|
||||
|
||||
|
||||
def _profile() -> Profile:
|
||||
return Profile.model_validate(
|
||||
{
|
||||
"id": "profile.modal-gpu",
|
||||
"version": "1.0.0",
|
||||
"extension": "ext.modal",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_provision_and_teardown_with_mock_client(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("MODAL_TOKEN_ID", "modal-token")
|
||||
ext = ModalExtension(
|
||||
{
|
||||
"api_base": "https://api.modal.com",
|
||||
"api_key_env": "MODAL_TOKEN_ID",
|
||||
"provider": "modal",
|
||||
}
|
||||
)
|
||||
|
||||
mock_client = MagicMock()
|
||||
create_resp = MagicMock()
|
||||
create_resp.status_code = 200
|
||||
create_resp.json.return_value = {
|
||||
"sandbox_id": "modal-prov-1",
|
||||
"url": "https://modal.run/sandbox/modal-prov-1",
|
||||
"status": "ready",
|
||||
}
|
||||
ready_resp = MagicMock()
|
||||
ready_resp.status_code = 200
|
||||
ready_resp.json.return_value = {"status": "ready"}
|
||||
delete_resp = MagicMock()
|
||||
delete_resp.status_code = 200
|
||||
mock_client.post.return_value = create_resp
|
||||
mock_client.get.return_value = ready_resp
|
||||
mock_client.delete.return_value = delete_resp
|
||||
mock_client.__enter__.return_value = mock_client
|
||||
mock_client.__exit__.return_value = None
|
||||
|
||||
with patch.object(ext, "_client_factory", return_value=mock_client):
|
||||
handle = ext.provision(_profile(), {}, "modal")
|
||||
assert handle["provider_sandbox_id"] == "modal-prov-1"
|
||||
ext.wait_ready(handle)
|
||||
report = ext.teardown(handle)
|
||||
assert report["provider_removed"] == "true"
|
||||
|
||||
|
||||
def test_provision_without_credentials_raises() -> None:
|
||||
ext = ModalExtension({"api_key_env": "MODAL_TOKEN_ID"})
|
||||
with pytest.raises(RuntimeError, match="API key"):
|
||||
ext.provision(_profile(), {}, "modal")
|
||||
@@ -16,7 +16,12 @@ def _burst_profile() -> Profile:
|
||||
"extension": "ext.compose-ssh",
|
||||
"route": {
|
||||
"strategy": "prefer-self-hosted",
|
||||
"extensions": ["ext.compose-ssh", "ext.saas-stub"],
|
||||
"extensions": [
|
||||
"ext.compose-ssh",
|
||||
"ext.e2b",
|
||||
"ext.modal",
|
||||
"ext.saas-stub",
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -42,12 +47,23 @@ def test_prefer_self_hosted_when_host_set(monkeypatch: pytest.MonkeyPatch) -> No
|
||||
|
||||
def test_prefer_self_hosted_falls_back_to_saas(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("SANDBOXER_HOST", raising=False)
|
||||
monkeypatch.delenv("E2B_API_KEY", raising=False)
|
||||
monkeypatch.delenv("MODAL_TOKEN_ID", raising=False)
|
||||
monkeypatch.setenv("SANDBOXER_FORCE_SAAS", "1")
|
||||
ext = resolve_extension(_burst_profile(), {}, host_override=None)
|
||||
assert ext.id == "ext.saas-stub"
|
||||
assert ext.capabilities.pricing_model == "metered"
|
||||
|
||||
|
||||
def test_prefer_self_hosted_falls_back_to_e2b_when_credentialed(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("SANDBOXER_FORCE_SAAS", "1")
|
||||
monkeypatch.setenv("E2B_API_KEY", "test-key")
|
||||
ext = resolve_extension(_burst_profile(), {}, host_override=None)
|
||||
assert ext.id == "ext.e2b"
|
||||
|
||||
|
||||
def test_lowest_cost_picks_metered_when_forced(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
profile = _burst_profile()
|
||||
profile.route = RouteSpec(
|
||||
|
||||
Reference in New Issue
Block a user