chore: import upstream snapshot with attribution
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:33 +08:00
commit e0e362d700
1949 changed files with 375388 additions and 0 deletions
@@ -0,0 +1,159 @@
/*
* Copyright 2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.opensandbox.e2e;
import static org.junit.jupiter.api.Assertions.*;
import com.alibaba.opensandbox.sandbox.config.ConnectionConfig;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.*;
import org.junit.jupiter.api.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Base class for all E2E tests providing common setup and teardown functionality. */
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class BaseE2ETest {
protected static final Logger logger = LoggerFactory.getLogger(BaseE2ETest.class);
// ==========================================
// Configuration Keys
// ==========================================
private static final String PROP_API_KEY = "opensandbox.test.api.key";
private static final String PROP_DOMAIN = "opensandbox.test.domain";
private static final String PROP_PROTOCOL = "opensandbox.test.protocol";
private static final String PROP_IMG_DEFAULT = "opensandbox.sandbox.default.image";
private static final String ENV_API_KEY = "OPENSANDBOX_TEST_API_KEY";
private static final String ENV_DOMAIN = "OPENSANDBOX_TEST_DOMAIN";
private static final String ENV_PROTOCOL = "OPENSANDBOX_TEST_PROTOCOL";
private static final String ENV_IMG_DEFAULT = "OPENSANDBOX_SANDBOX_DEFAULT_IMAGE";
private static final String ENV_USE_SERVER_PROXY = "OPENSANDBOX_TEST_USE_SERVER_PROXY";
// ==========================================
// Shared State (Static)
// ==========================================
protected static final Properties testProperties = new Properties();
protected static ConnectionConfig sharedConnectionConfig;
static {
loadTestProperties();
initializeSharedConfig();
}
protected static String getSandboxImage() {
return configValue(PROP_IMG_DEFAULT, ENV_IMG_DEFAULT, "opensandbox/code-interpreter:latest");
}
protected static ConnectionConfig createConnectionConfig(boolean useServerProxy) {
String protocol = configValue(PROP_PROTOCOL, ENV_PROTOCOL, "http");
return ConnectionConfig.builder()
.apiKey(configValue(PROP_API_KEY, ENV_API_KEY, "e2e-test"))
.domain(configValue(PROP_DOMAIN, ENV_DOMAIN, "localhost:8080"))
.requestTimeout(Duration.ofMinutes(3))
.protocol(protocol)
.useServerProxy(useServerProxy || shouldUseServerProxy())
.build();
}
private static void loadTestProperties() {
try (InputStream input =
BaseE2ETest.class.getClassLoader().getResourceAsStream("test.properties")) {
if (input != null) {
testProperties.load(input);
} else {
logger.warn("test.properties file not found, using default values.");
}
} catch (IOException e) {
throw new RuntimeException("Failed to load test properties", e);
}
}
private static void initializeSharedConfig() {
String protocol = configValue(PROP_PROTOCOL, ENV_PROTOCOL, "http");
sharedConnectionConfig =
ConnectionConfig.builder()
.apiKey(configValue(PROP_API_KEY, ENV_API_KEY, "e2e-test"))
.domain(configValue(PROP_DOMAIN, ENV_DOMAIN, "localhost:8080"))
.requestTimeout(Duration.ofMinutes(3))
.protocol(protocol)
.useServerProxy(shouldUseServerProxy())
.build();
}
private static String configValue(String propertyKey, String envKey, String defaultValue) {
String envValue = System.getenv(envKey);
if (envValue != null && !envValue.isBlank()) {
return envValue;
}
return testProperties.getProperty(propertyKey, defaultValue);
}
private static boolean shouldUseServerProxy() {
String envValue = System.getenv(ENV_USE_SERVER_PROXY);
return envValue != null && Boolean.parseBoolean(envValue);
}
@BeforeEach
void beforeEach(TestInfo testInfo) {
logger.info("=== Starting test: {} ===", testInfo.getDisplayName());
}
// ==========================================
// Shared assertion helpers (ported from python e2e style)
// ==========================================
protected static long nowMs() {
return System.currentTimeMillis();
}
protected static void assertRecentTimestampMs(long ts, long toleranceMs) {
assertTrue(ts > 0, "timestamp must be > 0");
long delta = Math.abs(nowMs() - ts);
assertTrue(
delta <= toleranceMs,
"timestamp too far from now: delta=" + delta + "ms (ts=" + ts + ")");
}
protected static void assertEndpointHasPort(String endpoint, int expectedPort) {
assertNotNull(endpoint);
assertFalse(endpoint.contains("://"), "unexpected scheme in endpoint: " + endpoint);
if (endpoint.contains("/")) {
assertTrue(
endpoint.endsWith("/" + expectedPort),
"endpoint route must end with /" + expectedPort + ": " + endpoint);
String prefix = endpoint.split("/", 2)[0];
assertFalse(prefix.isBlank(), "missing domain in endpoint: " + endpoint);
return;
}
int idx = endpoint.lastIndexOf(':');
assertTrue(idx > 0, "missing host:port in endpoint: " + endpoint);
String host = endpoint.substring(0, idx);
String port = endpoint.substring(idx + 1);
assertFalse(host.isBlank(), "missing host in endpoint: " + endpoint);
assertTrue(port.matches("\\d+"), "non-numeric port in endpoint: " + endpoint);
assertEquals(expectedPort, Integer.parseInt(port), "endpoint port mismatch: " + endpoint);
}
protected static void assertTimesClose(
OffsetDateTime createdAt, OffsetDateTime modifiedAt, long toleranceSeconds) {
long delta = Math.abs(Duration.between(createdAt, modifiedAt).getSeconds());
assertTrue(delta <= toleranceSeconds, "created/modified skew too large: " + delta + "s");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,536 @@
/*
* Copyright 2026 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.opensandbox.e2e;
import static org.junit.jupiter.api.Assertions.*;
import com.alibaba.opensandbox.sandbox.Sandbox;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.Execution;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.OutputMessage;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.RunCommandRequest;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.Credential;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialAuth;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialBinding;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialBindingMetadata;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialBindingMutationSet;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialMatch;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialMetadata;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialMutationSet;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialProxyConfig;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialSubstitution;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialVaultCreateRequest;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialVaultPatchRequest;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CredentialVaultState;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.CustomHeaderEntry;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.NetworkPolicy;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.NetworkRule;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
@Tag("e2e")
@DisplayName("Credential Vault E2E Tests (JVM/Kotlin SDK)")
class CredentialVaultE2ETest extends BaseE2ETest {
private static final String DEFAULT_TARGET_HOST = "credential-vault-e2e.opensandbox.test";
private static final Map<String, String> SECRET_VALUES =
Map.of(
"bearer-token", "vault-bearer-token",
"basic-token", "dXNlcjpwYXNz",
"api-key-token", "vault-api-key-token",
"client-id", "vault-client-id",
"client-secret", "vault-client-secret",
"query-secret", "vault-query-secret",
"path-secret", "vault-path-secret",
"body-secret", "vault-body-secret",
"runtime-token", "vault-runtime-token",
"runtime-token-replaced", "vault-runtime-token-replaced");
@Test
@Timeout(value = 5, unit = TimeUnit.MINUTES)
void credentialVaultInjectsAllAuthTypes() {
String targetIp = credentialVaultTargetIp();
Sandbox sandbox = createCredentialVaultSandbox(targetIp);
try {
CredentialVaultState state =
sandbox.credentialVault()
.create(
CredentialVaultCreateRequest.builder()
.credentials(
credentials(
"bearer-token",
"basic-token",
"api-key-token",
"client-id",
"client-secret",
"runtime-token",
"runtime-token-replaced"))
.bindings(
List.of(
binding(
"bearer",
"/bearer",
CredentialAuth.bearer(
"bearer-token")),
binding(
"basic",
"/basic",
CredentialAuth.basic(
"basic-token")),
binding(
"api-key",
"/api-key",
CredentialAuth.apiKey(
"X-Api-Key",
"api-key-token")),
binding(
"custom-headers",
"/custom-headers",
CredentialAuth.customHeaders(
List.of(
CustomHeaderEntry
.builder()
.name(
"X-Client-Id")
.credential(
"client-id")
.build(),
CustomHeaderEntry
.builder()
.name(
"X-Client-Secret")
.credential(
"client-secret")
.build())))))
.build());
Set<String> authTypes =
state.getBindings().stream()
.map(CredentialBindingMetadata::getAuth)
.filter(auth -> auth != null)
.map(auth -> auth.getType())
.collect(Collectors.toSet());
assertEquals(Set.of("bearer", "basic", "apiKey", "customHeaders"), authTypes);
for (String path : List.of("/bearer", "/basic", "/api-key", "/custom-headers")) {
String response = curlJson(sandbox, targetIp, path, true);
assertJsonCase(response, path.substring(1), true, "[]");
}
} finally {
killSandbox(sandbox);
}
}
@Test
@Timeout(value = 5, unit = TimeUnit.MINUTES)
void credentialVaultSubstitutesPlaceholdersInQueryPathAndBody() {
String targetIp = credentialVaultTargetIp();
Sandbox sandbox = createCredentialVaultSandbox(targetIp);
try {
sandbox.credentialVault()
.create(
CredentialVaultCreateRequest.builder()
.credentials(
credentials(
"query-secret", "path-secret", "body-secret"))
.bindings(
List.of(
binding(
"query-substitution",
"/query-substitution",
CredentialAuth.passthrough(
List.of(
substitution(
"query-secret",
"__query_secret__",
CredentialSubstitution
.Surface
.QUERY)))),
binding(
"path-substitution",
"/tenant/*",
CredentialAuth.passthrough(
List.of(
substitution(
"path-secret",
"__path_secret__",
CredentialSubstitution
.Surface
.PATH)))),
binding(
"body-substitution",
"/body-substitution",
CredentialAuth.passthrough(
List.of(
substitution(
"body-secret",
"__body_secret__",
CredentialSubstitution
.Surface
.BODY))),
List.of("POST"))))
.build());
String response =
curlJson(
sandbox,
targetIp,
"/query-substitution?api_key=__query_secret__",
true);
assertJsonCase(response, "query-substitution", true, "[]");
response =
curlJson(
sandbox,
targetIp,
"/tenant/__path_secret__/resource?tenant=__path_secret__",
true);
assertJsonCase(response, "path-substitution", true, "[]");
assertTrue(response.contains("\"queryStillPlaceholder\":true"), response);
response =
curlJson(
sandbox,
targetIp,
"/body-substitution",
true,
"POST",
List.of("content-type: application/json"),
"{\"client_secret\":\"__body_secret__\"}");
assertJsonCase(response, "body-substitution", true, "[]");
} finally {
killSandbox(sandbox);
}
}
@Test
@Timeout(value = 5, unit = TimeUnit.MINUTES)
void credentialVaultRuntimeMutationAddsReplacesAndDeletesBinding() {
String targetIp = credentialVaultTargetIp();
Sandbox sandbox = createCredentialVaultSandbox(targetIp);
try {
CredentialVaultState state =
sandbox.credentialVault()
.create(
CredentialVaultCreateRequest.builder()
.credentials(List.of())
.bindings(List.of())
.build());
assertEquals(1, state.getRevision());
assertTrue(state.getCredentials().isEmpty());
assertTrue(state.getBindings().isEmpty());
state =
sandbox.credentialVault()
.patch(
CredentialVaultPatchRequest.builder()
.expectedRevision(state.getRevision())
.credentials(
CredentialMutationSet.builder()
.add(
List.of(
credential(
"runtime-token",
"runtime-token")))
.build())
.bindings(
CredentialBindingMutationSet.builder()
.add(
List.of(
binding(
"runtime-added",
"/runtime-added",
CredentialAuth
.apiKey(
"X-Runtime-Token",
"runtime-token"))))
.build())
.build());
assertEquals(2, state.getRevision());
assertEquals(List.of("runtime-token"), credentialNames(state.getCredentials()));
assertEquals(List.of("runtime-added"), bindingNames(state.getBindings()));
String response = curlJson(sandbox, targetIp, "/runtime-added", true);
assertJsonCase(response, "runtime-added", true, "[]");
state =
sandbox.credentialVault()
.patch(
CredentialVaultPatchRequest.builder()
.expectedRevision(state.getRevision())
.bindings(
CredentialBindingMutationSet.builder()
.delete("runtime-added")
.build())
.build());
assertEquals(3, state.getRevision());
assertTrue(state.getBindings().isEmpty());
state =
sandbox.credentialVault()
.patch(
CredentialVaultPatchRequest.builder()
.expectedRevision(state.getRevision())
.credentials(
CredentialMutationSet.builder()
.replace(
List.of(
credential(
"runtime-token",
"runtime-token-replaced")))
.build())
.bindings(
CredentialBindingMutationSet.builder()
.add(
List.of(
binding(
"runtime-replaced",
"/runtime-replaced",
CredentialAuth
.apiKey(
"X-Runtime-Token",
"runtime-token"))))
.build())
.build());
assertEquals(4, state.getRevision());
assertEquals(List.of("runtime-token"), credentialNames(state.getCredentials()));
assertEquals(List.of("runtime-replaced"), bindingNames(state.getBindings()));
response = curlJson(sandbox, targetIp, "/runtime-replaced", true);
assertJsonCase(response, "runtime-replaced", true, "[]");
response = curlJson(sandbox, targetIp, "/runtime-added", false);
assertJsonCase(response, "runtime-added", false, "[\"x-runtime-token\"]");
state =
sandbox.credentialVault()
.patch(
CredentialVaultPatchRequest.builder()
.expectedRevision(state.getRevision())
.bindings(
CredentialBindingMutationSet.builder()
.delete("runtime-replaced")
.build())
.build());
assertEquals(5, state.getRevision());
assertTrue(state.getBindings().isEmpty());
state =
sandbox.credentialVault()
.patch(
CredentialVaultPatchRequest.builder()
.expectedRevision(state.getRevision())
.credentials(
CredentialMutationSet.builder()
.delete("runtime-token")
.build())
.build());
assertEquals(6, state.getRevision());
assertTrue(state.getCredentials().isEmpty());
} finally {
killSandbox(sandbox);
}
}
private static Sandbox createCredentialVaultSandbox(String targetIp) {
String image =
envOrDefault("OPENSANDBOX_CREDENTIAL_VAULT_E2E_SANDBOX_IMAGE", getSandboxImage());
Map<String, String> resource = new HashMap<>();
resource.put("cpu", envOrDefault("OPENSANDBOX_E2E_SANDBOX_CPU", "1"));
resource.put("memory", envOrDefault("OPENSANDBOX_E2E_SANDBOX_MEMORY", "2Gi"));
return Sandbox.builder()
.connectionConfig(createConnectionConfig(false))
.image(image)
.resource(resource)
.timeout(Duration.ofMinutes(5))
.readyTimeout(Duration.ofSeconds(90))
.networkPolicy(
NetworkPolicy.builder()
.defaultAction(NetworkPolicy.DefaultAction.DENY)
.addEgress(
NetworkRule.builder()
.action(NetworkRule.Action.ALLOW)
.target(credentialVaultTargetHost())
.build())
.addEgress(
NetworkRule.builder()
.action(NetworkRule.Action.ALLOW)
.target(targetIp)
.build())
.build())
.credentialProxy(CredentialProxyConfig.enabled())
.metadata(
Map.of(
envOrDefault(
"OPENSANDBOX_CREDENTIAL_VAULT_E2E_LABEL_KEY",
"opensandbox.e2e"),
envOrDefault(
"OPENSANDBOX_CREDENTIAL_VAULT_E2E_LABEL_VALUE",
"credential-vault")))
.build();
}
private static List<Credential> credentials(String... names) {
return java.util.Arrays.stream(names)
.map(name -> credential(name, name))
.collect(Collectors.toList());
}
private static Credential credential(String name, String valueName) {
return Credential.builder().name(name).inlineSource(SECRET_VALUES.get(valueName)).build();
}
private static CredentialBinding binding(String name, String path, CredentialAuth auth) {
return binding(name, path, auth, List.of("GET"));
}
private static CredentialBinding binding(
String name, String path, CredentialAuth auth, List<String> methods) {
return CredentialBinding.builder()
.name(name)
.match(
CredentialMatch.builder()
.schemes(CredentialMatch.Scheme.HTTP)
.hosts(credentialVaultTargetHost())
.methods(methods)
.paths(path)
.build())
.auth(auth)
.build();
}
private static CredentialSubstitution substitution(
String credential, String placeholder, CredentialSubstitution.Surface surface) {
return CredentialSubstitution.builder()
.credential(credential)
.placeholder(placeholder)
.surfaces(surface)
.build();
}
private static String curlJson(
Sandbox sandbox, String targetIp, String path, boolean failOnHttpError) {
return curlJson(sandbox, targetIp, path, failOnHttpError, null, List.of(), null);
}
private static String curlJson(
Sandbox sandbox,
String targetIp,
String path,
boolean failOnHttpError,
String method,
List<String> headers,
String data) {
String failFlag = failOnHttpError ? "--fail " : "";
String methodFlag = method == null ? "" : "--request " + method + " ";
String headerFlags =
headers.stream()
.map(header -> "--header " + shellQuote(header) + " ")
.collect(Collectors.joining());
String dataFlag = data == null ? "" : "--data " + shellQuote(data) + " ";
String command =
"curl "
+ failFlag
+ "--silent --show-error --connect-timeout 5 --max-time 20 "
+ methodFlag
+ headerFlags
+ dataFlag
+ "--resolve "
+ credentialVaultTargetHost()
+ ":80:"
+ targetIp
+ " http://"
+ credentialVaultTargetHost()
+ path;
for (String secret : SECRET_VALUES.values()) {
assertFalse(command.contains(secret), "command must not contain secret material");
}
Execution execution =
sandbox.commands().run(RunCommandRequest.builder().command(command).build());
assertNull(execution.getError(), "curl command failed");
assertEquals(0, execution.getExitCode());
String stdout =
execution.getLogs().getStdout().stream()
.map(OutputMessage::getText)
.collect(Collectors.joining());
assertFalse(stdout.isBlank(), "curl response must not be blank");
return stdout;
}
private static String shellQuote(String value) {
return "'" + value.replace("'", "'\\''") + "'";
}
private static void assertJsonCase(
String payload, String expectedCase, boolean expectedOk, String expectedMissing) {
assertTrue(payload.contains("\"ok\":" + expectedOk), payload);
assertTrue(payload.contains("\"case\":\"" + expectedCase + "\""), payload);
assertTrue(payload.contains("\"missingOrInvalid\":" + expectedMissing), payload);
}
private static List<String> credentialNames(List<CredentialMetadata> credentials) {
return credentials.stream().map(CredentialMetadata::getName).collect(Collectors.toList());
}
private static List<String> bindingNames(List<CredentialBindingMetadata> bindings) {
return bindings.stream()
.map(CredentialBindingMetadata::getName)
.collect(Collectors.toList());
}
private static String credentialVaultTargetHost() {
return envOrDefault("OPENSANDBOX_CREDENTIAL_VAULT_E2E_TARGET_HOST", DEFAULT_TARGET_HOST);
}
private static String credentialVaultTargetIp() {
String targetIp = System.getenv("OPENSANDBOX_CREDENTIAL_VAULT_E2E_TARGET_IP");
Assumptions.assumeTrue(
targetIp != null && !targetIp.isBlank(),
"Set OPENSANDBOX_CREDENTIAL_VAULT_E2E_TARGET_IP to run Credential Vault E2E");
return targetIp;
}
private static String envOrDefault(String name, String defaultValue) {
String value = System.getenv(name);
return value == null || value.isBlank() ? defaultValue : value;
}
private static void killSandbox(Sandbox sandbox) {
try {
sandbox.kill();
} catch (Exception ignored) {
}
try {
sandbox.close();
} catch (Exception ignored) {
}
}
}
@@ -0,0 +1,839 @@
/*
* Copyright 2026 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.opensandbox.e2e;
import static org.junit.jupiter.api.Assertions.*;
import com.alibaba.opensandbox.sandbox.Sandbox;
import com.alibaba.opensandbox.sandbox.domain.exceptions.SandboxException;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.Execution;
import com.alibaba.opensandbox.sandbox.domain.models.execd.filesystem.ContentReplaceEntry;
import com.alibaba.opensandbox.sandbox.domain.models.execd.filesystem.EntryInfo;
import com.alibaba.opensandbox.sandbox.domain.models.execd.filesystem.MoveEntry;
import com.alibaba.opensandbox.sandbox.domain.models.execd.filesystem.SearchEntry;
import com.alibaba.opensandbox.sandbox.domain.models.execd.filesystem.SetPermissionEntry;
import com.alibaba.opensandbox.sandbox.domain.models.execd.filesystem.WriteEntry;
import com.alibaba.opensandbox.sandbox.domain.models.execd.isolated.BindMount;
import com.alibaba.opensandbox.sandbox.domain.models.execd.isolated.CreateIsolatedSessionRequest;
import com.alibaba.opensandbox.sandbox.domain.models.execd.isolated.IsolatedCapabilities;
import com.alibaba.opensandbox.sandbox.domain.models.execd.isolated.IsolatedRunRequest;
import com.alibaba.opensandbox.sandbox.domain.models.execd.isolated.IsolatedWorkspaceSpec;
import com.alibaba.opensandbox.sandbox.domain.services.IsolationSession;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.jupiter.api.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class IsolatedSessionE2ETest extends BaseE2ETest {
private static final Logger log = LoggerFactory.getLogger(IsolatedSessionE2ETest.class);
private Sandbox sandbox;
private static String stdoutText(Execution exec) {
return exec.getLogs().getStdout().stream()
.map(m -> m.getText())
.collect(Collectors.joining());
}
@BeforeAll
void setup() {
sandbox =
Sandbox.builder()
.connectionConfig(sharedConnectionConfig)
.image(getSandboxImage())
.readyTimeout(Duration.ofMinutes(2))
.extensions(Map.of("bootstrap.execd.isolation", "enable"))
.build();
IsolatedCapabilities caps = sandbox.isolation().capabilities();
log.info(
"Isolation capabilities: available={} isolator={} version={} message={}",
caps.getAvailable(),
caps.getIsolator(),
caps.getVersion(),
caps.getMessage());
if (!caps.getAvailable()) {
fail("Isolation NOT available: " + (caps.getMessage() != null ? caps.getMessage() : "unknown reason"));
}
}
@AfterAll
void tearDown() {
if (sandbox != null) {
sandbox.kill();
sandbox.close();
}
}
@Test
@Order(1)
void testCapabilities() {
IsolatedCapabilities caps = sandbox.isolation().capabilities();
assertTrue(caps.getAvailable());
}
@Test
@Order(2)
void testSessionLifecycle() {
IsolationSession session =
sandbox.isolation()
.create(new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/tmp", "rw"),
"balanced", null, null, null, null, null, null, null, null));
assertNotNull(session.getSessionId());
var state = session.get();
assertEquals("active", state.getStatus());
session.delete();
}
@Test
@Order(3)
void testRunEcho() {
IsolationSession session =
sandbox.isolation()
.create(new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/tmp", "rw"),
"balanced", null, null, null, null, null, null, null, null));
try {
Execution exec = session.run(new IsolatedRunRequest("echo hello-isolation", null, null));
assertTrue(stdoutText(exec).contains("hello-isolation"));
} finally {
session.delete();
}
}
@Test
@Order(4)
void testPidIsolation() {
IsolationSession session =
sandbox.isolation()
.create(new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/tmp", "rw"),
"balanced", null, null, null, null, null, null, null, null));
try {
Execution exec = session.run(new IsolatedRunRequest("echo $$", null, null));
int pid = Integer.parseInt(stdoutText(exec).trim());
assertTrue(pid <= 2, "expected PID 1 or 2, got " + pid);
} finally {
session.delete();
}
}
@Test
@Order(5)
void testRunWithEnvs() {
IsolationSession session =
sandbox.isolation()
.create(new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/tmp", "rw"),
"balanced", null, null, null, null, null, null, null, null));
try {
Execution exec =
session.run(new IsolatedRunRequest(
"echo $MY_VAR",
Map.of("MY_VAR", "test-value-42"),
null));
assertTrue(stdoutText(exec).contains("test-value-42"));
} finally {
session.delete();
}
}
@Test
@Order(6)
void testSessionStatePersists() {
IsolationSession session =
sandbox.isolation()
.create(new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/tmp", "rw"),
"balanced", null, null, null, null, null, null, null, null));
try {
session.run(new IsolatedRunRequest("export PERSIST_VAR=abc123", null, null));
Execution exec = session.run(new IsolatedRunRequest("echo $PERSIST_VAR", null, null));
assertTrue(stdoutText(exec).contains("abc123"));
} finally {
session.delete();
}
}
@Test
@Order(7)
void testTmpIsolation() {
sandbox.commands().run("mkdir -p /workspace");
IsolationSession sessionA =
sandbox.isolation()
.create(new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/workspace", "rw"),
"strict", null, null, null, null, null, null, null, null));
IsolationSession sessionB =
sandbox.isolation()
.create(new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/workspace", "rw"),
"strict", null, null, null, null, null, null, null, null));
try {
sessionA.run(new IsolatedRunRequest(
"echo secret > /tmp/isolated_test_file.txt", null, null));
Execution exec = sessionB.run(new IsolatedRunRequest(
"cat /tmp/isolated_test_file.txt 2>&1 || echo NOT_FOUND", null, null));
String text = stdoutText(exec);
assertTrue(
text.contains("NOT_FOUND") || text.contains("No such file"),
"expected /tmp isolation, got: " + text);
} finally {
sessionA.delete();
sessionB.delete();
}
}
// ── RW mode: filesystem API tests ───────────────────────────────
private IsolationSession createSession(String mode, String path) {
return sandbox.isolation()
.create(new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec(path, mode),
"balanced", null, null, null, null, null, null, null, null));
}
private IsolationSession createSession(String mode) {
return createSession(mode, "/tmp");
}
@Test
@Order(8)
void testRwFilesUploadDownload() {
IsolationSession session = createSession("rw");
try {
String path = "/tmp/upload_rw_" + System.currentTimeMillis() + ".txt";
session.getFiles().write(List.of(
WriteEntry.builder().path(path).data("rw upload").mode(644).build()));
String content = session.getFiles().readFile(path);
assertEquals("rw upload", content);
} finally {
session.delete();
}
}
@Test
@Order(9)
void testRwFilesReadBytes() {
IsolationSession session = createSession("rw");
try {
String path = "/tmp/bytes_rw_" + System.currentTimeMillis() + ".bin";
byte[] data = new byte[]{0x00, 0x01, 0x02, (byte) 0xff};
session.getFiles().write(List.of(
WriteEntry.builder().path(path).data(data).mode(644).build()));
byte[] read = session.getFiles().readByteArray(path);
assertArrayEquals(data, read);
} finally {
session.delete();
}
}
@Test
@Order(10)
void testRwFilesInfo() {
IsolationSession session = createSession("rw");
try {
String path = "/tmp/info_rw_" + System.currentTimeMillis() + ".txt";
session.getFiles().write(List.of(
WriteEntry.builder().path(path).data("info").mode(644).build()));
Map<String, EntryInfo> infoMap = session.getFiles().readFileInfo(List.of(path));
assertTrue(infoMap.containsKey(path));
assertEquals(4, infoMap.get(path).getSize());
assertEquals(644, infoMap.get(path).getMode());
} finally {
session.delete();
}
}
@Test
@Order(11)
void testRwFilesSearch() {
IsolationSession session = createSession("rw");
try {
String prefix = "/tmp/search_rw_" + System.currentTimeMillis();
session.run(new IsolatedRunRequest("mkdir -p " + prefix, null, null));
session.getFiles().write(List.of(
WriteEntry.builder().path(prefix + "/a.txt").data("a").mode(644).build(),
WriteEntry.builder().path(prefix + "/b.txt").data("b").mode(644).build(),
WriteEntry.builder().path(prefix + "/c.log").data("c").mode(644).build()));
List<EntryInfo> results = session.getFiles().search(
SearchEntry.builder().path(prefix).pattern("*.txt").build());
assertEquals(2, results.size());
List<String> paths = results.stream().map(EntryInfo::getPath).collect(Collectors.toList());
assertTrue(paths.stream().anyMatch(p -> p.contains("a.txt")));
assertTrue(paths.stream().anyMatch(p -> p.contains("b.txt")));
} finally {
session.delete();
}
}
@Test
@Order(12)
void testRwFilesMkdir() {
IsolationSession session = createSession("rw");
try {
String dir = "/tmp/mkdir_rw_" + System.currentTimeMillis();
session.getFiles().createDirectories(List.of(
WriteEntry.builder().path(dir).mode(755).build()));
Map<String, EntryInfo> infoMap = session.getFiles().readFileInfo(List.of(dir));
assertTrue(infoMap.containsKey(dir));
} finally {
session.delete();
}
}
@Test
@Order(13)
void testRwFilesDelete() {
IsolationSession session = createSession("rw");
try {
String path = "/tmp/delete_rw_" + System.currentTimeMillis() + ".txt";
session.getFiles().write(List.of(
WriteEntry.builder().path(path).data("del").mode(644).build()));
session.getFiles().deleteFiles(List.of(path));
assertThrows(SandboxException.class,
() -> session.getFiles().readFileInfo(List.of(path)));
} finally {
session.delete();
}
}
@Test
@Order(14)
void testRwFilesMove() {
IsolationSession session = createSession("rw");
try {
String src = "/tmp/mv_rw_src_" + System.currentTimeMillis() + ".txt";
String dst = "/tmp/mv_rw_dst_" + System.currentTimeMillis() + ".txt";
session.getFiles().write(List.of(
WriteEntry.builder().path(src).data("move").mode(644).build()));
session.getFiles().moveFiles(List.of(
MoveEntry.builder().src(src).dest(dst).build()));
assertEquals("move", session.getFiles().readFile(dst));
} finally {
session.delete();
}
}
@Test
@Order(15)
void testRwFilesChmod() {
IsolationSession session = createSession("rw");
try {
String path = "/tmp/chmod_rw_" + System.currentTimeMillis() + ".txt";
session.getFiles().write(List.of(
WriteEntry.builder().path(path).data("ch").mode(644).build()));
session.getFiles().setPermissions(List.of(
SetPermissionEntry.builder().path(path).mode(755).build()));
Map<String, EntryInfo> infoMap = session.getFiles().readFileInfo(List.of(path));
assertEquals(755, infoMap.get(path).getMode());
} finally {
session.delete();
}
}
@Test
@Order(16)
void testRwFilesReplace() {
IsolationSession session = createSession("rw");
try {
String path = "/tmp/replace_rw_" + System.currentTimeMillis() + ".txt";
session.getFiles().write(List.of(
WriteEntry.builder().path(path).data("hello old world").mode(644).build()));
session.getFiles().replaceContents(List.of(
ContentReplaceEntry.builder()
.path(path).oldContent("old").newContent("new").build()));
String content = session.getFiles().readFile(path);
assertTrue(content.contains("new"));
assertFalse(content.contains("old"));
} finally {
session.delete();
}
}
@Test
@Order(17)
void testRwFilesListDirectory() {
IsolationSession session = createSession("rw");
try {
String prefix = "/tmp/listdir_rw_" + System.currentTimeMillis();
session.run(new IsolatedRunRequest("mkdir -p " + prefix + "/sub", null, null));
session.getFiles().write(List.of(
WriteEntry.builder().path(prefix + "/f1.txt").data("f1").mode(644).build(),
WriteEntry.builder().path(prefix + "/sub/f2.txt").data("f2").mode(644).build()));
List<EntryInfo> entries = session.getFiles().listDirectory(prefix);
List<String> names = entries.stream().map(EntryInfo::getPath).collect(Collectors.toList());
assertTrue(names.stream().anyMatch(n -> n.contains("f1.txt")));
assertTrue(names.stream().anyMatch(n -> n.contains("sub")));
} finally {
session.delete();
}
}
// ── RO mode tests ───────────────────────────────────────────────
@Test
@Order(18)
void testRoCanReadExistingFiles() {
String marker = "ro_read_" + System.currentTimeMillis() + ".txt";
sandbox.commands().run("echo ro-data > /tmp/" + marker);
IsolationSession session = createSession("ro");
try {
Execution exec = session.run(
new IsolatedRunRequest("cat /tmp/" + marker, null, null));
assertTrue(stdoutText(exec).contains("ro-data"));
} finally {
session.delete();
sandbox.commands().run("rm -f /tmp/" + marker);
}
}
@Test
@Order(19)
void testRoCannotWrite() {
IsolationSession session = createSession("ro");
try {
Execution exec = session.run(new IsolatedRunRequest(
"echo fail > /tmp/ro_write_test.txt 2>&1; echo EXIT=$?", null, null));
String text = stdoutText(exec);
assertTrue(
text.contains("EXIT=1") || text.contains("Read-only") || text.contains("Permission denied"),
"expected write failure in RO mode, got: " + text);
} finally {
session.delete();
}
}
@Test
@Order(20)
void testRoFilesApiRead() {
String marker = "ro_api_" + System.currentTimeMillis() + ".txt";
sandbox.commands().run("echo ro-api-data > /tmp/" + marker);
IsolationSession session = createSession("ro");
try {
String content = session.getFiles().readFile("/tmp/" + marker);
assertTrue(content.contains("ro-api-data"));
} finally {
session.delete();
sandbox.commands().run("rm -f /tmp/" + marker);
}
}
@Test
@Order(21)
void testRoFilesApiSearch() {
String prefix = "/tmp/ro_search_" + System.currentTimeMillis();
sandbox.commands().run("mkdir -p " + prefix + " && echo x > " + prefix + "/file.txt");
IsolationSession session = createSession("ro");
try {
List<EntryInfo> results = session.getFiles().search(
SearchEntry.builder().path(prefix).pattern("*.txt").build());
assertTrue(results.size() >= 1);
} finally {
session.delete();
sandbox.commands().run("rm -rf " + prefix);
}
}
@Test
@Order(22)
void testRoFilesApiListDirectory() {
String prefix = "/tmp/ro_listdir_" + System.currentTimeMillis();
sandbox.commands().run("mkdir -p " + prefix + " && echo x > " + prefix + "/f.txt");
IsolationSession session = createSession("ro");
try {
List<EntryInfo> entries = session.getFiles().listDirectory(prefix);
assertTrue(entries.size() >= 1);
} finally {
session.delete();
sandbox.commands().run("rm -rf " + prefix);
}
}
// ── Overlay mode tests ──────────────────────────────────────────
private void requireOverlay() {
IsolatedCapabilities caps = sandbox.isolation().capabilities();
Assumptions.assumeTrue(
caps.getCommitSupported() || caps.getDiffSupported(),
"overlay mode not available in this environment");
}
@Test
@Order(23)
void testOverlayWritesNotVisibleOnHost() {
requireOverlay();
String marker = "overlay_invis_" + System.currentTimeMillis() + ".txt";
IsolationSession session = createSession("overlay");
try {
session.run(new IsolatedRunRequest(
"echo overlay-data > /tmp/" + marker, null, null));
Execution hostCheck = sandbox.commands().run(
"cat /tmp/" + marker + " 2>&1 || echo NOT_FOUND");
String text = stdoutText(hostCheck);
assertTrue(
text.contains("NOT_FOUND") || text.contains("No such file"),
"overlay write should not be visible on host, got: " + text);
} finally {
session.delete();
}
}
@Test
@Order(24)
void testOverlayCanReadHostFiles() {
requireOverlay();
String marker = "overlay_lower_" + System.currentTimeMillis() + ".txt";
sandbox.commands().run("echo lower-data > /tmp/" + marker);
IsolationSession session = createSession("overlay");
try {
Execution exec = session.run(new IsolatedRunRequest(
"cat /tmp/" + marker, null, null));
assertTrue(stdoutText(exec).contains("lower-data"));
} finally {
session.delete();
sandbox.commands().run("rm -f /tmp/" + marker);
}
}
@Test
@Order(25)
void testOverlayCowDoesNotMutateHost() {
requireOverlay();
String marker = "overlay_cow_" + System.currentTimeMillis() + ".txt";
sandbox.commands().run("echo original > /tmp/" + marker);
IsolationSession session = createSession("overlay");
try {
session.run(new IsolatedRunRequest(
"echo modified > /tmp/" + marker, null, null));
Execution inSession = session.run(new IsolatedRunRequest(
"cat /tmp/" + marker, null, null));
assertTrue(stdoutText(inSession).contains("modified"));
Execution hostCheck = sandbox.commands().run("cat /tmp/" + marker);
assertTrue(stdoutText(hostCheck).contains("original"));
} finally {
session.delete();
sandbox.commands().run("rm -f /tmp/" + marker);
}
}
@Test
@Order(26)
void testOverlayFilesApiUploadDownload() {
requireOverlay();
IsolationSession session = createSession("overlay");
try {
String path = "/tmp/ov_upload_" + System.currentTimeMillis() + ".txt";
session.getFiles().write(List.of(
WriteEntry.builder().path(path).data("overlay file").mode(644).build()));
String content = session.getFiles().readFile(path);
assertEquals("overlay file", content);
// Host should NOT see it
Execution hostCheck = sandbox.commands().run(
"cat " + path + " 2>&1 || echo NOT_FOUND");
String hostText = stdoutText(hostCheck);
assertTrue(
hostText.contains("NOT_FOUND") || hostText.contains("No such file"),
"overlay file should not be visible on host, got: " + hostText);
} finally {
session.delete();
}
}
@Test
@Order(27)
void testOverlayFilesApiSearch() {
requireOverlay();
String prefix = "/tmp/ov_search_" + System.currentTimeMillis();
sandbox.commands().run("mkdir -p " + prefix + " && echo lower > " + prefix + "/lower.txt");
IsolationSession session = createSession("overlay");
try {
session.getFiles().write(List.of(
WriteEntry.builder().path(prefix + "/upper.txt").data("upper").mode(644).build()));
List<EntryInfo> results = session.getFiles().search(
SearchEntry.builder().path(prefix).pattern("*.txt").build());
List<String> paths = results.stream().map(EntryInfo::getPath).collect(Collectors.toList());
assertTrue(paths.stream().anyMatch(p -> p.contains("lower.txt")));
assertTrue(paths.stream().anyMatch(p -> p.contains("upper.txt")));
} finally {
session.delete();
sandbox.commands().run("rm -rf " + prefix);
}
}
@Test
@Order(28)
void testOverlayFilesApiDelete() {
requireOverlay();
String prefix = "/tmp/ov_del_" + System.currentTimeMillis();
sandbox.commands().run("mkdir -p " + prefix + " && echo x > " + prefix + "/target.txt");
IsolationSession session = createSession("overlay");
try {
session.getFiles().deleteFiles(List.of(prefix + "/target.txt"));
assertThrows(SandboxException.class,
() -> session.getFiles().readFileInfo(List.of(prefix + "/target.txt")));
// Host file should be untouched
Execution hostCheck = sandbox.commands().run("cat " + prefix + "/target.txt");
assertTrue(stdoutText(hostCheck).contains("x"));
} finally {
session.delete();
sandbox.commands().run("rm -rf " + prefix);
}
}
@Test
@Order(29)
void testOverlayFilesApiMove() {
requireOverlay();
IsolationSession session = createSession("overlay");
try {
String src = "/tmp/ov_mv_src_" + System.currentTimeMillis() + ".txt";
String dst = "/tmp/ov_mv_dst_" + System.currentTimeMillis() + ".txt";
session.getFiles().write(List.of(
WriteEntry.builder().path(src).data("moveme").mode(644).build()));
session.getFiles().moveFiles(List.of(
MoveEntry.builder().src(src).dest(dst).build()));
assertEquals("moveme", session.getFiles().readFile(dst));
} finally {
session.delete();
}
}
@Test
@Order(30)
void testOverlayFilesApiChmod() {
requireOverlay();
String marker = "ov_chmod_" + System.currentTimeMillis() + ".txt";
sandbox.commands().run("echo ch > /tmp/" + marker + " && chmod 644 /tmp/" + marker);
IsolationSession session = createSession("overlay");
try {
session.getFiles().setPermissions(List.of(
SetPermissionEntry.builder().path("/tmp/" + marker).mode(755).build()));
Map<String, EntryInfo> infoMap =
session.getFiles().readFileInfo(List.of("/tmp/" + marker));
assertEquals(755, infoMap.get("/tmp/" + marker).getMode());
// Host should still be 644
Execution hostCheck = sandbox.commands().run("stat -c %a /tmp/" + marker);
assertTrue(stdoutText(hostCheck).contains("644"));
} finally {
session.delete();
sandbox.commands().run("rm -f /tmp/" + marker);
}
}
@Test
@Order(31)
void testOverlayFilesApiReplace() {
requireOverlay();
String marker = "ov_repl_" + System.currentTimeMillis() + ".txt";
sandbox.commands().run("printf 'hello old world' > /tmp/" + marker);
IsolationSession session = createSession("overlay");
try {
session.getFiles().replaceContents(List.of(
ContentReplaceEntry.builder()
.path("/tmp/" + marker)
.oldContent("old")
.newContent("new")
.build()));
String content = session.getFiles().readFile("/tmp/" + marker);
assertTrue(content.contains("new"));
assertFalse(content.contains("old"));
// Host unchanged
Execution hostCheck = sandbox.commands().run("cat /tmp/" + marker);
assertTrue(stdoutText(hostCheck).contains("old"));
} finally {
session.delete();
sandbox.commands().run("rm -f /tmp/" + marker);
}
}
@Test
@Order(32)
void testOverlayFilesApiListDirectory() {
requireOverlay();
String prefix = "/tmp/ov_ls_" + System.currentTimeMillis();
sandbox.commands().run("mkdir -p " + prefix + " && echo l > " + prefix + "/lower.txt");
IsolationSession session = createSession("overlay");
try {
session.getFiles().write(List.of(
WriteEntry.builder().path(prefix + "/upper.txt").data("u").mode(644).build()));
List<EntryInfo> entries = session.getFiles().listDirectory(prefix);
List<String> names = entries.stream().map(EntryInfo::getPath).collect(Collectors.toList());
assertTrue(names.stream().anyMatch(n -> n.contains("lower.txt")));
assertTrue(names.stream().anyMatch(n -> n.contains("upper.txt")));
} finally {
session.delete();
sandbox.commands().run("rm -rf " + prefix);
}
}
// ── run_once / withSession convenience API tests ─────────────────
@Test
@Order(33)
void testRunOnce() {
Execution exec = sandbox.isolation()
.runOnce("echo runonce-e2e", "/tmp", "rw", null, null, null, null, null);
assertTrue(stdoutText(exec).contains("runonce-e2e"));
}
@Test
@Order(34)
void testRunOnceWithEnvs() {
Execution exec = sandbox.isolation()
.runOnce(
"echo $E2E_RUN_ONCE",
"/tmp",
"rw",
Map.of("E2E_RUN_ONCE", "kt-value"),
null,
null,
null,
null);
assertTrue(stdoutText(exec).contains("kt-value"));
}
@Test
@Order(35)
void testWithSession() {
String output = sandbox.isolation().withSession(
new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/tmp", "rw"),
"balanced", null, null, null, null, null, null, null, null),
session -> {
session.run(new IsolatedRunRequest("export WS_VAR=with-session-kt", null, null));
Execution exec = session.run(new IsolatedRunRequest("echo $WS_VAR", null, null));
return stdoutText(exec);
});
assertTrue(output.contains("with-session-kt"));
}
@Test
@Order(36)
void testWithSessionMultiRun() {
String output = sandbox.isolation().withSession(
new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/tmp", "rw"),
"balanced", null, null, null, null, null, null, null, null),
session -> {
session.run(new IsolatedRunRequest("echo step1 > /tmp/ws_test_kt.txt", null, null));
Execution exec = session.run(new IsolatedRunRequest("cat /tmp/ws_test_kt.txt", null, null));
return stdoutText(exec);
});
assertTrue(output.contains("step1"));
}
// ── Bind mount tests (explicit source->dest binds) ─────────────────
@Test
@Order(37)
void testBindReadWriteHostVisible() {
long ts = System.currentTimeMillis();
// Source must be within the execd writable allowlist (e.g. /data).
String srcDir = "/data/bind_rw_" + ts;
String dest = "/mnt/bind_rw";
String fileName = "from_sandbox.txt";
String content = "bind-rw-visible-on-host";
// Create the source dir and the destination mount point (bwrap binds
// onto an existing dir; it cannot create one under the read-only root).
sandbox.commands().run("mkdir -p " + srcDir + " " + dest);
IsolationSession session =
sandbox.isolation()
.create(new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/tmp", "rw"),
"balanced",
null,
List.of(new BindMount(srcDir, dest, null)),
null, null, null, null, null, null));
try {
Execution exec = session.run(new IsolatedRunRequest(
"echo -n " + content + " > " + dest + "/" + fileName
+ " && cat " + dest + "/" + fileName,
null, null));
assertTrue(stdoutText(exec).contains(content));
Execution hostCheck = sandbox.commands().run("cat " + srcDir + "/" + fileName);
assertTrue(stdoutText(hostCheck).contains(content));
} finally {
session.delete();
sandbox.commands().run("rm -rf " + srcDir);
}
}
@Test
@Order(38)
void testBindIllegalRejected() {
assertThrows(
SandboxException.class,
() -> sandbox.isolation()
.create(new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/tmp", "rw"),
"balanced",
null,
// /etc is not in the writable allowlist.
List.of(new BindMount("/etc", "/mnt/etc", null)),
null, null, null, null, null, null)));
}
@Test
@Order(39)
void testBindReadOnlyReadable() {
long ts = System.currentTimeMillis();
String srcDir = "/data/bind_ro_" + ts;
String dest = "/mnt/bind_ro";
String fileName = "host_created.txt";
String content = "bind-ro-host-content";
sandbox.commands().run(
"mkdir -p " + srcDir + " " + dest + " && echo -n " + content + " > " + srcDir + "/" + fileName);
IsolationSession session =
sandbox.isolation()
.create(new CreateIsolatedSessionRequest(
new IsolatedWorkspaceSpec("/tmp", "rw"),
"balanced",
null,
List.of(new BindMount(srcDir, dest, true)),
null, null, null, null, null, null));
try {
Execution exec = session.run(new IsolatedRunRequest("cat " + dest + "/" + fileName, null, null));
assertTrue(stdoutText(exec).contains(content));
Execution write = session.run(new IsolatedRunRequest(
"echo x > " + dest + "/newfile.txt 2>&1; echo EXIT=$?", null, null));
String text = stdoutText(write);
assertTrue(
text.contains("EXIT=1") || text.contains("Read-only")
|| text.contains("read-only") || text.contains("Permission denied"),
"expected write to fail through read-only bind, got: " + text);
} finally {
session.delete();
sandbox.commands().run("rm -rf " + srcDir);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,272 @@
/*
* Copyright 2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.opensandbox.e2e;
import static org.junit.jupiter.api.Assertions.*;
import com.alibaba.opensandbox.sandbox.Sandbox;
import com.alibaba.opensandbox.sandbox.SandboxManager;
import com.alibaba.opensandbox.sandbox.domain.exceptions.SandboxException;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.*;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.*;
/**
* E2E tests for SandboxManager list/filter semantics.
*
* <p>Focus:
*
* <ul>
* <li>states filter uses OR logic
* <li>metadata filter uses AND logic
* </ul>
*
* <p>We create 3 dedicated sandboxes per run to keep assertions deterministic and avoid impacting
* the shared sandbox used by other tests.
*/
@Tag("e2e")
@DisplayName("SandboxManager E2E Tests (Java SDK) - List/Filter Semantics")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class SandboxManagerE2ETest extends BaseE2ETest {
private SandboxManager sandboxManager;
private Sandbox s1;
private Sandbox s2;
private Sandbox s3;
private String tag;
@BeforeAll
void setup() throws InterruptedException {
sandboxManager = SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
tag = "e2e-sandbox-manager-" + UUID.randomUUID().toString().substring(0, 8);
Map<String, String> resourceMap = new HashMap<>();
resourceMap.put("cpu", "1");
resourceMap.put("memory", "2Gi");
s1 =
Sandbox.builder()
.connectionConfig(sharedConnectionConfig)
.image(getSandboxImage())
.resource(resourceMap)
.timeout(Duration.ofMinutes(5))
.readyTimeout(Duration.ofSeconds(60))
.metadata(Map.of("tag", tag, "team", "t1", "env", "prod"))
.env("E2E_TEST", "true")
.env("EXECD_API_GRACE_SHUTDOWN", "3s")
.env("EXECD_JUPYTER_IDLE_POLL_INTERVAL", "200ms")
.healthCheckPollingInterval(Duration.ofMillis(500))
.build();
s2 =
Sandbox.builder()
.connectionConfig(sharedConnectionConfig)
.image(getSandboxImage())
.resource(resourceMap)
.timeout(Duration.ofMinutes(5))
.readyTimeout(Duration.ofSeconds(60))
.metadata(Map.of("tag", tag, "team", "t1", "env", "dev"))
.env("E2E_TEST", "true")
.env("EXECD_API_GRACE_SHUTDOWN", "3s")
.env("EXECD_JUPYTER_IDLE_POLL_INTERVAL", "200ms")
.healthCheckPollingInterval(Duration.ofMillis(500))
.build();
s3 =
Sandbox.builder()
.connectionConfig(sharedConnectionConfig)
.image(getSandboxImage())
.resource(resourceMap)
.timeout(Duration.ofMinutes(5))
.readyTimeout(Duration.ofSeconds(60))
.metadata(Map.of("tag", tag, "env", "prod"))
.env("E2E_TEST", "true")
.env("EXECD_API_GRACE_SHUTDOWN", "3s")
.env("EXECD_JUPYTER_IDLE_POLL_INTERVAL", "200ms")
.healthCheckPollingInterval(Duration.ofMillis(500))
.build();
assertTrue(s1.isHealthy());
assertTrue(s2.isHealthy());
assertTrue(s3.isHealthy());
// Pause s3 to create a deterministic non-Running state.
sandboxManager.pauseSandbox(s3.getId());
long deadline = System.currentTimeMillis() + 180_000;
while (System.currentTimeMillis() < deadline) {
SandboxInfo info = sandboxManager.getSandboxInfo(s3.getId());
if ("Paused".equals(info.getStatus().getState())) {
break;
}
Thread.sleep(1000);
}
assertEquals("Paused", sandboxManager.getSandboxInfo(s3.getId()).getStatus().getState());
}
@AfterAll
void teardown() {
for (Sandbox s : List.of(s1, s2, s3)) {
if (s == null) continue;
try {
s.kill();
} catch (Exception ignored) {
}
try {
s.close();
} catch (Exception ignored) {
}
}
if (sandboxManager != null) {
try {
sandboxManager.close();
} catch (Exception ignored) {
}
}
}
@Test
@Order(1)
@DisplayName("states filter uses OR semantics")
@Timeout(value = 2, unit = TimeUnit.MINUTES)
void testStatesFilterOrLogic() {
SandboxFilter filter =
SandboxFilter.builder()
.states("Running", "Paused")
.metadata(Map.of("tag", tag))
.pageSize(50)
.build();
PagedSandboxInfos infos = sandboxManager.listSandboxInfos(filter);
Set<String> ids = new HashSet<>();
for (SandboxInfo info : infos.getSandboxInfos()) {
ids.add(info.getId());
}
assertTrue(ids.containsAll(Set.of(s1.getId(), s2.getId(), s3.getId())));
PagedSandboxInfos pausedOnly =
sandboxManager.listSandboxInfos(
SandboxFilter.builder()
.states("Paused")
.metadata(Map.of("tag", tag))
.pageSize(50)
.build());
Set<String> pausedIds = new HashSet<>();
for (SandboxInfo info : pausedOnly.getSandboxInfos()) {
pausedIds.add(info.getId());
}
assertTrue(pausedIds.contains(s3.getId()));
assertFalse(pausedIds.contains(s1.getId()));
assertFalse(pausedIds.contains(s2.getId()));
PagedSandboxInfos runningOnly =
sandboxManager.listSandboxInfos(
SandboxFilter.builder()
.states("Running")
.metadata(Map.of("tag", tag))
.pageSize(50)
.build());
Set<String> runningIds = new HashSet<>();
for (SandboxInfo info : runningOnly.getSandboxInfos()) {
runningIds.add(info.getId());
}
assertTrue(runningIds.contains(s1.getId()));
assertTrue(runningIds.contains(s2.getId()));
assertFalse(runningIds.contains(s3.getId()));
}
@Test
@Order(2)
@DisplayName("metadata filter uses AND semantics")
@Timeout(value = 2, unit = TimeUnit.MINUTES)
void testMetadataFilterAndLogic() {
PagedSandboxInfos tagAndTeam =
sandboxManager.listSandboxInfos(
SandboxFilter.builder()
.metadata(Map.of("tag", tag, "team", "t1"))
.pageSize(50)
.build());
Set<String> ids = new HashSet<>();
for (SandboxInfo info : tagAndTeam.getSandboxInfos()) {
ids.add(info.getId());
}
assertTrue(ids.contains(s1.getId()));
assertTrue(ids.contains(s2.getId()));
assertFalse(ids.contains(s3.getId()));
PagedSandboxInfos tagTeamEnv =
sandboxManager.listSandboxInfos(
SandboxFilter.builder()
.metadata(Map.of("tag", tag, "team", "t1", "env", "prod"))
.pageSize(50)
.build());
Set<String> ids2 = new HashSet<>();
for (SandboxInfo info : tagTeamEnv.getSandboxInfos()) {
ids2.add(info.getId());
}
assertTrue(ids2.contains(s1.getId()));
assertFalse(ids2.contains(s2.getId()));
assertFalse(ids2.contains(s3.getId()));
PagedSandboxInfos tagEnv =
sandboxManager.listSandboxInfos(
SandboxFilter.builder()
.metadata(Map.of("tag", tag, "env", "prod"))
.pageSize(50)
.build());
Set<String> ids3 = new HashSet<>();
for (SandboxInfo info : tagEnv.getSandboxInfos()) {
ids3.add(info.getId());
}
assertTrue(ids3.contains(s1.getId()));
assertTrue(ids3.contains(s3.getId()));
assertFalse(ids3.contains(s2.getId()));
PagedSandboxInfos noneMatch =
sandboxManager.listSandboxInfos(
SandboxFilter.builder()
.metadata(Map.of("tag", tag, "team", "t2"))
.pageSize(50)
.build());
for (SandboxInfo info : noneMatch.getSandboxInfos()) {
assertFalse(Set.of(s1.getId(), s2.getId(), s3.getId()).contains(info.getId()));
}
Map<String, String> patch = new HashMap<>();
patch.put("env", "stage");
patch.put("team", null);
SandboxInfo patched = sandboxManager.patchSandboxMetadata(s2.getId(), patch);
assertEquals("stage", patched.getMetadata().get("env"));
assertFalse(patched.getMetadata().containsKey("team"));
SandboxInfo refreshed = sandboxManager.getSandboxInfo(s2.getId());
assertEquals("stage", refreshed.getMetadata().get("env"));
assertFalse(refreshed.getMetadata().containsKey("team"));
}
@Test
@Order(3)
@DisplayName("invalid operations raise SandboxException")
@Timeout(value = 1, unit = TimeUnit.MINUTES)
void testInvalidOperations() {
String nonExistentId = "non-existent-" + System.nanoTime();
assertThrows(SandboxException.class, () -> sandboxManager.getSandboxInfo(nonExistentId));
assertThrows(SandboxException.class, () -> sandboxManager.pauseSandbox(nonExistentId));
assertThrows(SandboxException.class, () -> sandboxManager.resumeSandbox(nonExistentId));
assertThrows(SandboxException.class, () -> sandboxManager.killSandbox(nonExistentId));
assertThrows(
SandboxException.class,
() -> sandboxManager.renewSandbox(nonExistentId, Duration.ofMinutes(5)));
}
}
@@ -0,0 +1,818 @@
/*
* Copyright 2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.opensandbox.e2e;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import com.alibaba.opensandbox.sandbox.Sandbox;
import com.alibaba.opensandbox.sandbox.SandboxManager;
import com.alibaba.opensandbox.sandbox.domain.exceptions.PoolAcquireFailedException;
import com.alibaba.opensandbox.sandbox.domain.exceptions.PoolDestroyedException;
import com.alibaba.opensandbox.sandbox.domain.exceptions.PoolEmptyException;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.Execution;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.RunCommandRequest;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.PagedSandboxInfos;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.SandboxFilter;
import com.alibaba.opensandbox.sandbox.domain.pool.AcquirePolicy;
import com.alibaba.opensandbox.sandbox.domain.pool.PoolCreationSpec;
import com.alibaba.opensandbox.sandbox.domain.pool.PoolDestroyOptions;
import com.alibaba.opensandbox.sandbox.domain.pool.PoolDestroyResult;
import com.alibaba.opensandbox.sandbox.domain.pool.PoolDestroyState;
import com.alibaba.opensandbox.sandbox.infrastructure.pool.RedisPoolStateStore;
import com.alibaba.opensandbox.sandbox.pool.SandboxPool;
import com.alibaba.opensandbox.sandbox.pool.SandboxPoolManager;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import redis.clients.jedis.JedisPooled;
@Tag("e2e")
@DisplayName("SandboxPool E2E Tests (Redis Distributed)")
public class SandboxPoolRedisDistributedE2ETest extends BaseE2ETest {
private static final Duration RECONCILE_INTERVAL = Duration.ofSeconds(1);
private static final Duration PRIMARY_LOCK_TTL = Duration.ofSeconds(4);
private static final Duration DRAIN_TIMEOUT = Duration.ofMillis(200);
private static final Duration AWAIT_TIMEOUT = Duration.ofMinutes(2);
private final List<SandboxPool> pools = new ArrayList<>();
private final List<Sandbox> borrowed = new CopyOnWriteArrayList<>();
private JedisPooled redis;
private SandboxManager sandboxManager;
private String keyPrefix;
private String tag;
@BeforeEach
void setupRedis() {
String redisUrl = System.getenv("OPENSANDBOX_TEST_REDIS_URL");
assumeTrue(
redisUrl != null && !redisUrl.isBlank(),
"Set OPENSANDBOX_TEST_REDIS_URL to run Redis-backed pool E2E tests");
redis = new JedisPooled(redisUrl);
keyPrefix = "opensandbox:e2e:" + UUID.randomUUID();
}
@AfterEach
void teardown() {
for (Sandbox sandbox : borrowed) {
killAndCloseQuietly(sandbox);
}
borrowed.clear();
for (SandboxPool pool : pools) {
try {
pool.resize(0);
} catch (Exception ignored) {
}
try {
pool.releaseAllIdle();
} catch (Exception ignored) {
}
try {
pool.shutdown(false);
} catch (Exception ignored) {
}
}
pools.clear();
if (sandboxManager != null && tag != null) {
cleanupTaggedSandboxes(tag);
}
if (sandboxManager != null) {
try {
sandboxManager.close();
} catch (Exception ignored) {
}
}
if (redis != null) {
cleanupRedisKeys();
try {
redis.close();
} catch (Exception ignored) {
}
}
}
@Test
@DisplayName("Redis store supports cross-node acquire, shared resize, and idle drain")
@Timeout(value = 6, unit = TimeUnit.MINUTES)
void testCrossNodeAcquireResizeAndDrain() throws Exception {
tag = "e2e-redis-pool-" + UUID.randomUUID().toString().substring(0, 8);
String poolName = "redis-pool-" + tag;
sandboxManager = SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
RedisPoolStateStore storeA = new RedisPoolStateStore(redis, keyPrefix);
RedisPoolStateStore storeB = new RedisPoolStateStore(redis, keyPrefix);
SandboxPool poolA = createPool(poolName, "owner-a-" + tag, storeA, 2);
SandboxPool poolB = createPool(poolName, "owner-b-" + tag, storeB, 2);
pools.add(poolA);
pools.add(poolB);
poolA.start();
poolB.start();
eventually(
"Redis-backed distributed pool warmed idle",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> poolA.snapshot().getIdleCount() >= 1);
Sandbox sandbox = poolB.acquire(Duration.ofMinutes(5), AcquirePolicy.FAIL_FAST);
borrowed.add(sandbox);
assertTrue(sandbox.isHealthy(), "cross-node acquire should return a healthy sandbox");
Execution execution =
sandbox.commands()
.run(RunCommandRequest.builder().command("echo redis-dist-ok").build());
assertNotNull(execution);
assertNull(execution.getError());
poolB.resize(0);
eventually(
"Redis-backed idle drains after resize(0)",
Duration.ofSeconds(45),
Duration.ofSeconds(1),
() -> poolA.snapshot().getIdleCount() == 0);
Thread.sleep(RECONCILE_INTERVAL.multipliedBy(3).toMillis());
assertEquals(
0, poolA.snapshot().getIdleCount(), "idle should stay at zero after resize(0)");
assertThrows(
PoolEmptyException.class,
() -> poolA.acquire(Duration.ofMinutes(2), AcquirePolicy.FAIL_FAST));
Sandbox direct = poolA.acquire(Duration.ofMinutes(5), AcquirePolicy.DIRECT_CREATE);
borrowed.add(direct);
assertTrue(
direct.isHealthy(), "direct create should still work when shared maxIdle is zero");
Execution directExecution =
direct.commands()
.run(RunCommandRequest.builder().command("echo redis-direct-ok").build());
assertNotNull(directExecution);
assertNull(directExecution.getError());
assertEquals(
0,
poolA.snapshot().getIdleCount(),
"DIRECT_CREATE must not repopulate the shared idle store");
}
@Test
@DisplayName("Redis primary lock fails over after leader shutdown")
@Timeout(value = 6, unit = TimeUnit.MINUTES)
void testPrimaryFailoverAfterLeaderShutdown() throws Exception {
tag = "e2e-redis-failover-" + UUID.randomUUID().toString().substring(0, 8);
String poolName = "redis-failover-" + tag;
sandboxManager = SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
RedisPoolStateStore storeA = new RedisPoolStateStore(redis, keyPrefix);
RedisPoolStateStore storeB = new RedisPoolStateStore(redis, keyPrefix);
SandboxPool poolA = createPool(poolName, "owner-a-" + tag, storeA, 1);
SandboxPool poolB = createPool(poolName, "owner-b-" + tag, storeB, 1);
pools.add(poolA);
pools.add(poolB);
poolA.start();
eventually(
"first Redis-backed node warms idle",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> poolA.snapshot().getIdleCount() >= 1);
int beforeShutdown = poolA.snapshot().getIdleCount();
poolB.start();
poolA.shutdown(false);
poolB.resize(0);
eventually(
"remaining node applies shared resize after peer shutdown",
Duration.ofSeconds(45),
Duration.ofSeconds(1),
() -> poolB.snapshot().getIdleCount() == 0);
poolB.resize(1);
eventually(
"remaining node replenishes after failover",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> poolB.snapshot().getIdleCount() >= 1);
assertTrue(beforeShutdown >= 1, "poolA should have warmed idle before shutdown");
}
@Test
@DisplayName("Redis start overwrites stale shared maxIdle after restart")
@Timeout(value = 7, unit = TimeUnit.MINUTES)
void testStartOverwritesStaleSharedMaxIdleAfterRestart() throws Exception {
tag = "e2e-redis-restart-config-" + UUID.randomUUID().toString().substring(0, 8);
String poolName = "redis-restart-config-" + tag;
sandboxManager = SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
RedisPoolStateStore storeA = new RedisPoolStateStore(redis, keyPrefix);
SandboxPool poolA = createPool(poolName, "owner-a-" + tag, storeA, 1);
pools.add(poolA);
poolA.start();
eventually(
"initial Redis-backed pool warms",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> poolA.snapshot().getIdleCount() >= 1);
poolA.resize(0);
eventually(
"initial Redis-backed pool drains to zero",
Duration.ofSeconds(45),
Duration.ofSeconds(1),
() -> poolA.snapshot().getIdleCount() == 0);
poolA.shutdown(false);
RedisPoolStateStore storeB = new RedisPoolStateStore(redis, keyPrefix);
SandboxPool poolB = createPool(poolName, "owner-b-" + tag, storeB, 2);
pools.add(poolB);
poolB.start();
eventually(
"restart with same Redis namespace uses new configured maxIdle",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> poolB.snapshot().getMaxIdle() == 2 && poolB.snapshot().getIdleCount() >= 2);
}
@Test
@DisplayName("Redis secondary resize is applied by primary periodic reconcile")
@Timeout(value = 7, unit = TimeUnit.MINUTES)
void testSecondaryResizeAppliedByPrimaryPeriodicReconcile() throws Exception {
tag = "e2e-redis-secondary-resize-" + UUID.randomUUID().toString().substring(0, 8);
String poolName = "redis-secondary-resize-" + tag;
String ownerA = "owner-a-" + tag;
String ownerB = "owner-b-" + tag;
sandboxManager = SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
RedisPoolStateStore storeA = new RedisPoolStateStore(redis, keyPrefix);
RedisPoolStateStore storeB = new RedisPoolStateStore(redis, keyPrefix);
SandboxPool poolA = createPool(poolName, ownerA, storeA, 2);
SandboxPool poolB = createPool(poolName, ownerB, storeB, 2);
pools.add(poolA);
pools.add(poolB);
String lockKey = poolKey(poolName, "lock");
poolA.start();
eventually(
"primary Redis-backed node owns lock and warms",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> ownerA.equals(redis.get(lockKey)) && poolA.snapshot().getIdleCount() >= 2);
poolB.start();
poolB.resize(0);
eventually(
"secondary resize to zero is applied by primary",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> ownerA.equals(redis.get(lockKey)) && poolA.snapshot().getIdleCount() == 0);
poolB.resize(2);
eventually(
"secondary resize up is applied by primary",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> ownerA.equals(redis.get(lockKey)) && poolA.snapshot().getIdleCount() >= 2);
}
@Test
@DisplayName("Redis idle take is unique under concurrent cross-node acquire")
@Timeout(value = 6, unit = TimeUnit.MINUTES)
void testConcurrentCrossNodeAcquireDoesNotDuplicateIdle() throws Exception {
tag = "e2e-redis-concurrent-" + UUID.randomUUID().toString().substring(0, 8);
String poolName = "redis-concurrent-" + tag;
sandboxManager = SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
RedisPoolStateStore storeA = new RedisPoolStateStore(redis, keyPrefix);
RedisPoolStateStore storeB = new RedisPoolStateStore(redis, keyPrefix);
SandboxPool poolA = createPool(poolName, "owner-a-" + tag, storeA, 2);
SandboxPool poolB = createPool(poolName, "owner-b-" + tag, storeB, 2);
pools.add(poolA);
pools.add(poolB);
poolA.start();
poolB.start();
eventually(
"Redis-backed pool warms two idle sandboxes",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> poolA.snapshot().getIdleCount() >= 2);
ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch start = new CountDownLatch(1);
Set<String> acquiredIds = ConcurrentHashMap.newKeySet();
List<Future<Sandbox>> futures = new ArrayList<>();
futures.add(
executor.submit(
() -> {
start.await();
return poolA.acquire(Duration.ofMinutes(5), AcquirePolicy.FAIL_FAST);
}));
futures.add(
executor.submit(
() -> {
start.await();
return poolB.acquire(Duration.ofMinutes(5), AcquirePolicy.FAIL_FAST);
}));
start.countDown();
try {
for (Future<Sandbox> future : futures) {
Sandbox sandbox = future.get(90, TimeUnit.SECONDS);
borrowed.add(sandbox);
assertTrue(sandbox.isHealthy(), "concurrent acquire should return healthy sandbox");
assertTrue(
acquiredIds.add(sandbox.getId()), "sandbox ID must not be acquired twice");
}
} finally {
executor.shutdownNow();
}
assertEquals(
2, acquiredIds.size(), "two concurrent acquires should get two distinct sandboxes");
}
@Test
@DisplayName("Redis DESTROYING fence blocks existing and replacement pool nodes")
@Timeout(value = 6, unit = TimeUnit.MINUTES)
void testDestroyingFenceBlocksRedisBackedPoolNodes() throws Exception {
tag = "e2e-redis-destroying-" + UUID.randomUUID().toString().substring(0, 8);
String poolName = "redis-destroying-" + tag;
String ownerA = "owner-a-" + tag;
sandboxManager = SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
RedisPoolStateStore storeA = new RedisPoolStateStore(redis, keyPrefix);
RedisPoolStateStore storeB = new RedisPoolStateStore(redis, keyPrefix);
SandboxPool poolA =
createPoolBuilder(poolName, ownerA, storeA, 1)
.reconcileInterval(Duration.ofMinutes(5))
.build();
SandboxPool poolB =
createPoolBuilder(poolName, "owner-b-" + tag, storeB, 1)
.reconcileInterval(Duration.ofMinutes(5))
.build();
pools.add(poolA);
pools.add(poolB);
poolA.start();
poolB.start();
eventually(
"Redis-backed DESTROYING fence target warms one shared idle sandbox",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> storeA.snapshotCounters(poolName).getIdleCount() >= 1);
storeA.beginDestroy(poolName, "destroyer-" + tag);
assertEquals(PoolDestroyState.DESTROYING, storeA.getDestroyState(poolName));
assertFalse(
storeA.tryAcquirePrimaryLock(poolName, "owner-c-" + tag, Duration.ofSeconds(30)));
assertFalse(storeA.renewPrimaryLock(poolName, ownerA, Duration.ofSeconds(30)));
assertThrows(PoolDestroyedException.class, () -> storeA.putIdle(poolName, "blocked-id"));
assertThrows(PoolDestroyedException.class, () -> storeB.setMaxIdle(poolName, 2));
assertThrows(
PoolDestroyedException.class,
() -> poolA.acquire(Duration.ofMinutes(5), AcquirePolicy.DIRECT_CREATE));
assertThrows(
PoolDestroyedException.class,
() -> poolB.acquire(Duration.ofMinutes(5), AcquirePolicy.DIRECT_CREATE));
assertThrows(PoolDestroyedException.class, () -> poolA.resize(1));
SandboxPool replacement =
createPoolBuilder(poolName, "owner-replacement-" + tag, storeB, 1)
.reconcileInterval(Duration.ofMinutes(5))
.build();
assertThrows(PoolDestroyedException.class, replacement::start);
}
@Test
@DisplayName("Redis destroy on one node fences all pool nodes and preserves tombstone")
@Timeout(value = 6, unit = TimeUnit.MINUTES)
void testDestroyFencesAllRedisBackedPoolNodes() throws Exception {
tag = "e2e-redis-destroy-" + UUID.randomUUID().toString().substring(0, 8);
String poolName = "redis-destroy-" + tag;
sandboxManager = SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
RedisPoolStateStore storeA = new RedisPoolStateStore(redis, keyPrefix);
RedisPoolStateStore storeB = new RedisPoolStateStore(redis, keyPrefix);
SandboxPool poolA =
createPoolBuilder(poolName, "owner-a-" + tag, storeA, 1)
.reconcileInterval(Duration.ofMinutes(5))
.build();
SandboxPool poolB =
createPoolBuilder(poolName, "owner-b-" + tag, storeB, 1)
.reconcileInterval(Duration.ofMinutes(5))
.build();
pools.add(poolA);
pools.add(poolB);
poolA.start();
poolB.start();
eventually(
"Redis-backed destroy target warms one shared idle sandbox",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> storeA.snapshotCounters(poolName).getIdleCount() >= 1);
SandboxPoolManager poolManager =
SandboxPoolManager.builder()
.stateStore(storeA)
.connectionConfig(sharedConnectionConfig)
.ownerId("manager-" + tag)
.build();
PoolDestroyResult result = poolManager.destroy(poolName, new PoolDestroyOptions());
assertEquals(PoolDestroyState.DESTROYED, result.getState());
assertTrue(result.getPersistentStateCleared(), "destroy should clear Redis pool state");
assertTrue(result.getDrainedIdleCount() >= 1, "destroy should drain shared idle ids");
assertEquals(result.getDrainedIdleCount(), result.getKilledIdleCount());
assertEquals(PoolDestroyState.DESTROYED, storeA.getDestroyState(poolName));
assertEquals(0, storeA.snapshotCounters(poolName).getIdleCount());
assertNull(storeA.getMaxIdle(poolName));
assertFalse(
storeA.tryAcquirePrimaryLock(poolName, "owner-c-" + tag, Duration.ofSeconds(30)));
assertFalse(storeA.renewPrimaryLock(poolName, "owner-a-" + tag, Duration.ofSeconds(30)));
assertThrows(
PoolDestroyedException.class,
() -> poolA.acquire(Duration.ofMinutes(5), AcquirePolicy.DIRECT_CREATE));
assertThrows(
PoolDestroyedException.class,
() -> poolB.acquire(Duration.ofMinutes(5), AcquirePolicy.DIRECT_CREATE));
assertThrows(PoolDestroyedException.class, () -> poolA.resize(1));
SandboxPool replacement =
createPoolBuilder(poolName, "owner-replacement-" + tag, storeB, 1)
.reconcileInterval(Duration.ofMinutes(5))
.build();
assertThrows(PoolDestroyedException.class, replacement::start);
}
@Test
@DisplayName("Redis store atomic take remains unique under local contention")
@Timeout(value = 1, unit = TimeUnit.MINUTES)
void testRedisStoreAtomicTakeUnderContention() throws Exception {
String poolName = "redis-store-contention-" + UUID.randomUUID();
RedisPoolStateStore store = new RedisPoolStateStore(redis, keyPrefix);
int idleCount = 50;
int workerCount = 16;
for (int i = 0; i < idleCount; i++) {
store.putIdle(poolName, "id-" + i);
}
ExecutorService executor = Executors.newFixedThreadPool(workerCount);
CountDownLatch start = new CountDownLatch(1);
Set<String> taken = ConcurrentHashMap.newKeySet();
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < workerCount; i++) {
futures.add(
executor.submit(
() -> {
start.await();
while (true) {
String id = store.tryTakeIdle(poolName);
if (id == null) {
return null;
}
assertTrue(taken.add(id), "duplicate idle ID taken: " + id);
}
}));
}
start.countDown();
try {
for (Future<?> future : futures) {
future.get(30, TimeUnit.SECONDS);
}
} finally {
executor.shutdownNow();
}
assertEquals(idleCount, taken.size());
assertEquals(0, store.snapshotCounters(poolName).getIdleCount());
}
@Test
@DisplayName("Redis expired idle is not removed by snapshot but take reaps it")
@Timeout(value = 1, unit = TimeUnit.MINUTES)
void testExpiredIdleIsNotRemovedBySnapshotButTakeReapsIt() throws Exception {
String poolName = "redis-expired-idle-" + UUID.randomUUID();
RedisPoolStateStore store = new RedisPoolStateStore(redis, keyPrefix);
store.setIdleEntryTtl(poolName, Duration.ofMillis(50));
store.putIdle(poolName, "expired-" + UUID.randomUUID());
Thread.sleep(100);
assertEquals(1, store.snapshotCounters(poolName).getIdleCount());
assertNull(store.tryTakeIdle(poolName));
assertEquals(0, store.snapshotCounters(poolName).getIdleCount());
}
@Test
@DisplayName("Redis concurrent acquire and resize jitter stay bounded")
@Timeout(value = 7, unit = TimeUnit.MINUTES)
void testConcurrentAcquireAndResizeJitterStayBounded() throws Exception {
tag = "e2e-redis-acquire-resize-jitter-" + UUID.randomUUID().toString().substring(0, 8);
String poolName = "redis-acquire-resize-jitter-" + tag;
sandboxManager = SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
RedisPoolStateStore storeA = new RedisPoolStateStore(redis, keyPrefix);
RedisPoolStateStore storeB = new RedisPoolStateStore(redis, keyPrefix);
SandboxPool poolA = createPool(poolName, "owner-a-" + tag, storeA, 2);
SandboxPool poolB = createPool(poolName, "owner-b-" + tag, storeB, 2);
pools.add(poolA);
pools.add(poolB);
poolA.start();
poolB.start();
eventually(
"Redis-backed jitter pool warms two idle sandboxes",
AWAIT_TIMEOUT,
Duration.ofSeconds(1),
() -> poolA.snapshot().getIdleCount() >= 2);
ExecutorService executor = Executors.newFixedThreadPool(5);
Set<String> acquiredIds = ConcurrentHashMap.newKeySet();
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < 4; i++) {
final int index = i;
futures.add(
executor.submit(
() -> {
SandboxPool pool = index % 2 == 0 ? poolA : poolB;
Sandbox sandbox =
pool.acquire(
Duration.ofMinutes(5), AcquirePolicy.DIRECT_CREATE);
borrowed.add(sandbox);
assertTrue(
acquiredIds.add(sandbox.getId()),
"sandbox ID must not be acquired twice");
Execution execution =
sandbox.commands()
.run(
RunCommandRequest.builder()
.command(
"echo redis-jitter-"
+ index)
.build());
assertNotNull(execution);
assertNull(execution.getError());
return null;
}));
}
futures.add(
executor.submit(
() -> {
for (int i = 0; i < 8; i++) {
(i % 2 == 0 ? poolA : poolB).resize(i % 3);
Thread.sleep(200);
}
poolB.resize(2);
return null;
}));
try {
for (Future<?> future : futures) {
future.get(180, TimeUnit.SECONDS);
}
} finally {
executor.shutdownNow();
}
assertEquals(4, acquiredIds.size());
eventually(
"Redis-backed acquire plus resize jitter converges and stays bounded",
Duration.ofSeconds(90),
Duration.ofSeconds(1),
() -> poolA.snapshot().getIdleCount() <= 2 && countTaggedSandboxes(tag) <= 8);
}
@Test
@DisplayName("Redis stale idle is removed and direct-create fallback works")
@Timeout(value = 6, unit = TimeUnit.MINUTES)
void testStaleIdleIsRemovedAndDirectCreateFallbackWorks() throws Exception {
tag = "e2e-redis-stale-" + UUID.randomUUID().toString().substring(0, 8);
String poolName = "redis-stale-" + tag;
sandboxManager = SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
RedisPoolStateStore storeA = new RedisPoolStateStore(redis, keyPrefix);
RedisPoolStateStore storeB = new RedisPoolStateStore(redis, keyPrefix);
SandboxPool poolA = createPool(poolName, "owner-a-" + tag, storeA, 0);
SandboxPool poolB = createPool(poolName, "owner-b-" + tag, storeB, 0);
pools.add(poolA);
pools.add(poolB);
poolA.start();
poolB.start();
storeA.putIdle(poolName, "missing-" + UUID.randomUUID());
assertThrows(
PoolAcquireFailedException.class,
() -> poolB.acquire(Duration.ofSeconds(2), AcquirePolicy.FAIL_FAST));
assertEquals(0, storeA.snapshotCounters(poolName).getIdleCount());
Sandbox sandbox = poolB.acquire(Duration.ofMinutes(5), AcquirePolicy.DIRECT_CREATE);
borrowed.add(sandbox);
assertTrue(sandbox.isHealthy(), "direct create should work after stale idle removal");
}
@Test
@DisplayName("Redis store drops lost-lock warmup orphan and recovers")
@Timeout(value = 7, unit = TimeUnit.MINUTES)
void testLostLockWindowDropsWarmupOrphanAndRecovers() throws Exception {
tag = "e2e-redis-renew-window-" + UUID.randomUUID().toString().substring(0, 8);
String poolName = "redis-renew-window-" + tag;
sandboxManager = SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
AtomicBoolean droppedOnce = new AtomicBoolean(false);
RedisPoolStateStore store = new RedisPoolStateStore(redis, keyPrefix);
String lockKey = poolKey(poolName, "lock");
SandboxPool pool =
createPoolBuilder(poolName, "owner-a-" + tag, store, 1)
.warmupSandboxPreparer(
sandbox -> {
if (droppedOnce.compareAndSet(false, true)) {
redis.del(lockKey);
}
})
.build();
pools.add(pool);
pool.start();
eventually(
"Redis-backed pool recovers after losing primary lock during warmup",
Duration.ofSeconds(90),
Duration.ofMillis(500),
() -> pool.snapshot().getIdleCount() == 1);
eventually(
"lost-lock orphan cleanup keeps remote tagged count bounded",
Duration.ofSeconds(60),
Duration.ofSeconds(1),
() -> countTaggedSandboxes(tag) == 1);
}
private SandboxPool createPool(
String poolName, String ownerId, RedisPoolStateStore store, int maxIdle) {
return createPoolBuilder(poolName, ownerId, store, maxIdle).build();
}
private SandboxPool.Builder createPoolBuilder(
String poolName, String ownerId, RedisPoolStateStore store, int maxIdle) {
PoolCreationSpec creationSpec =
PoolCreationSpec.builder()
.image(getSandboxImage())
.entrypoint(List.of("tail -f /dev/null"))
.metadata(Map.of("tag", tag, "suite", "sandbox-pool-redis-e2e"))
.env(
Map.of(
"E2E_TEST",
"true",
"EXECD_API_GRACE_SHUTDOWN",
"3s",
"EXECD_JUPYTER_IDLE_POLL_INTERVAL",
"1s"))
.build();
return SandboxPool.builder()
.poolName(poolName)
.ownerId(ownerId)
.maxIdle(maxIdle)
.warmupConcurrency(1)
.stateStore(store)
.connectionConfig(sharedConnectionConfig)
.creationSpec(creationSpec)
.reconcileInterval(RECONCILE_INTERVAL)
.primaryLockTtl(PRIMARY_LOCK_TTL)
.drainTimeout(DRAIN_TIMEOUT);
}
private void cleanupTaggedSandboxes(String cleanupTag) {
for (int i = 0; i < 5; i++) {
try {
PagedSandboxInfos infos =
sandboxManager.listSandboxInfos(
SandboxFilter.builder()
.metadata(Map.of("tag", cleanupTag))
.pageSize(50)
.build());
if (infos.getSandboxInfos().isEmpty()) {
return;
}
infos.getSandboxInfos()
.forEach(
info -> {
try {
sandboxManager.killSandbox(info.getId());
} catch (Exception ignored) {
}
});
} catch (Exception ignored) {
return;
}
}
}
private int countTaggedSandboxes(String queryTag) {
if (sandboxManager == null || queryTag == null || queryTag.isBlank()) {
return 0;
}
PagedSandboxInfos infos =
sandboxManager.listSandboxInfos(
SandboxFilter.builder()
.metadata(Map.of("tag", queryTag))
.pageSize(50)
.build());
return infos.getSandboxInfos().size();
}
private String poolKey(String poolName, String suffix) {
String tag =
java.util.Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(poolName.getBytes(java.nio.charset.StandardCharsets.UTF_8));
return keyPrefix + ":{" + tag + "}:" + suffix;
}
private void cleanupRedisKeys() {
if (keyPrefix == null) {
return;
}
try {
Set<String> keys = redis.keys(keyPrefix + "*");
if (!keys.isEmpty()) {
redis.del(keys.toArray(String[]::new));
}
} catch (Exception ignored) {
}
}
private void eventually(
String description, Duration timeout, Duration interval, BooleanSupplier condition)
throws InterruptedException {
long deadline = System.currentTimeMillis() + timeout.toMillis();
Throwable lastError = null;
while (System.currentTimeMillis() < deadline) {
try {
if (condition.getAsBoolean()) {
return;
}
} catch (Throwable t) {
lastError = t;
}
Thread.sleep(interval.toMillis());
}
if (lastError != null) {
fail(
"Timed out waiting for "
+ description
+ ", last error: "
+ lastError.getMessage());
} else {
fail("Timed out waiting for " + description);
}
}
private static void killAndCloseQuietly(Sandbox sandbox) {
if (sandbox == null) {
return;
}
try {
sandbox.kill();
} catch (Exception ignored) {
}
try {
sandbox.close();
} catch (Exception ignored) {
}
}
}
@@ -0,0 +1,209 @@
/*
* Copyright 2026 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.opensandbox.e2e;
import static org.junit.jupiter.api.Assertions.*;
import com.alibaba.opensandbox.sandbox.Sandbox;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.Execution;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.RunCommandRequest;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.SandboxEndpoint;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
@Tag("e2e")
@DisplayName("Sandbox secured access E2E Tests (Java SDK)")
public class SandboxSecureAccessE2ETest extends BaseE2ETest {
private static final String SECURE_ACCESS_HEADER = "OpenSandbox-Secure-Access";
private static final String SECURE_ACCESS_VERIFIABLE_ENV =
"OPENSANDBOX_TEST_SECURE_ACCESS_VERIFIABLE";
@Test
@DisplayName("secureAccess protects secured endpoints in K8s gateway mode")
@Timeout(value = 3, unit = TimeUnit.MINUTES)
void testSecureAccessTokenEndToEnd() throws Exception {
Assumptions.assumeTrue(
isSecureAccessVerifiable(),
SECURE_ACCESS_VERIFIABLE_ENV
+ "=true is required to verify secureAccess in a Kubernetes gateway environment");
Sandbox sandbox =
Sandbox.builder()
.connectionConfig(sharedConnectionConfig)
.image(getSandboxImage())
.timeout(Duration.ofMinutes(2))
.readyTimeout(Duration.ofSeconds(60))
.secureAccess()
.env("EXECD_API_GRACE_SHUTDOWN", "3s")
.env("EXECD_JUPYTER_IDLE_POLL_INTERVAL", "200ms")
.metadata(Map.of("tag", "secure-access-java-e2e-test"))
.build();
try {
SandboxEndpoint execdEndpoint = sandbox.getEndpoint(44772);
assertEndpointHasPort(execdEndpoint.getEndpoint(), 44772);
Map<String, String> execdHeaders = execdEndpoint.getHeaders();
assertNotNull(execdHeaders);
assertTrue(
execdHeaders.containsKey(SECURE_ACCESS_HEADER),
"secureAccess endpoint must include the secure access header");
String token = execdHeaders.get(SECURE_ACCESS_HEADER);
assertNotNull(token);
assertFalse(token.isBlank());
SandboxEndpoint userEndpoint = sandbox.getEndpoint(8080);
assertEquals(
token,
userEndpoint.getHeaders().get(SECURE_ACCESS_HEADER),
"all endpoints for a secured sandbox should return the same secure access token");
Execution sdkRun =
sandbox.commands()
.run(
RunCommandRequest.builder()
.command("echo secure-access-sdk-ok")
.build());
assertNotNull(sdkRun);
assertNull(sdkRun.getError(), "SDK command should include endpoint headers");
assertEquals(1, sdkRun.getLogs().getStdout().size());
assertEquals("secure-access-sdk-ok", sdkRun.getLogs().getStdout().get(0).getText());
HttpClient client =
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
URI pingUri = endpointUri(execdEndpoint, "/ping");
HttpResponse<String> missingToken = sendPing(client, pingUri, execdHeaders, null);
assertEquals(
401,
missingToken.statusCode(),
"secured endpoint must reject missing access token");
HttpResponse<String> wrongToken =
sendPing(client, pingUri, execdHeaders, "definitely-wrong-token");
assertEquals(
401,
wrongToken.statusCode(),
"secured endpoint must reject wrong access token");
HttpResponse<String> correctToken = sendPing(client, pingUri, execdHeaders, token);
assertEquals(
200,
correctToken.statusCode(),
"secured endpoint must accept the endpoint token");
} finally {
killAndClose(sandbox);
}
}
@Test
@DisplayName("default sandbox does not require secure access token")
@Timeout(value = 3, unit = TimeUnit.MINUTES)
void testDefaultSandboxDoesNotReturnAccessToken() throws Exception {
Assumptions.assumeTrue(
isSecureAccessVerifiable(),
SECURE_ACCESS_VERIFIABLE_ENV
+ "=true is required to verify secureAccess in a Kubernetes gateway environment");
Sandbox sandbox =
Sandbox.builder()
.connectionConfig(sharedConnectionConfig)
.image(getSandboxImage())
.timeout(Duration.ofMinutes(2))
.readyTimeout(Duration.ofSeconds(60))
.env("EXECD_API_GRACE_SHUTDOWN", "3s")
.env("EXECD_JUPYTER_IDLE_POLL_INTERVAL", "200ms")
.metadata(Map.of("tag", "non-secure-access-java-e2e-test"))
.build();
try {
SandboxEndpoint execdEndpoint = sandbox.getEndpoint(44772);
assertEndpointHasPort(execdEndpoint.getEndpoint(), 44772);
assertFalse(
execdEndpoint.getHeaders().containsKey(SECURE_ACCESS_HEADER),
"default sandbox endpoint should not include secure access token");
HttpClient client =
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
HttpResponse<String> ping =
sendPing(
client,
endpointUri(execdEndpoint, "/ping"),
execdEndpoint.getHeaders(),
null);
assertEquals(
200, ping.statusCode(), "default endpoint should allow requests without token");
} finally {
killAndClose(sandbox);
}
}
private static URI endpointUri(SandboxEndpoint endpoint, String path) {
String protocol = testProperties.getProperty("opensandbox.test.protocol", "https");
String base = endpoint.getEndpoint();
while (base.endsWith("/")) {
base = base.substring(0, base.length() - 1);
}
String normalizedPath = path.startsWith("/") ? path : "/" + path;
return URI.create(protocol + "://" + base + normalizedPath);
}
private static HttpResponse<String> sendPing(
HttpClient client, URI uri, Map<String, String> endpointHeaders, String token)
throws IOException, InterruptedException {
HttpRequest.Builder builder =
HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(20)).GET();
for (Map.Entry<String, String> header : endpointHeaders.entrySet()) {
if (!SECURE_ACCESS_HEADER.equalsIgnoreCase(header.getKey())) {
builder.header(header.getKey(), header.getValue());
}
}
if (token != null) {
builder.header(SECURE_ACCESS_HEADER, token);
}
return client.send(builder.build(), HttpResponse.BodyHandlers.ofString());
}
private static boolean isSecureAccessVerifiable() {
return Boolean.parseBoolean(System.getenv(SECURE_ACCESS_VERIFIABLE_ENV));
}
private static void killAndClose(Sandbox sandbox) {
if (sandbox == null) {
return;
}
try {
sandbox.kill();
} catch (Exception ignored) {
}
try {
sandbox.close();
} catch (Exception ignored) {
}
}
}
@@ -0,0 +1,348 @@
/*
* Copyright 2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.opensandbox.e2e;
import static org.junit.jupiter.api.Assertions.*;
import com.alibaba.opensandbox.sandbox.Sandbox;
import com.alibaba.opensandbox.sandbox.SandboxManager;
import com.alibaba.opensandbox.sandbox.domain.exceptions.SandboxApiException;
import com.alibaba.opensandbox.sandbox.domain.exceptions.SandboxException;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.Execution;
import com.alibaba.opensandbox.sandbox.domain.models.execd.executions.RunCommandRequest;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.PagedSnapshotInfos;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.SnapshotFilter;
import com.alibaba.opensandbox.sandbox.domain.models.sandboxes.SnapshotInfo;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
@Tag("e2e")
@DisplayName("Snapshot E2E Tests (Java SDK)")
public class SnapshotE2ETest extends BaseE2ETest {
private static final Duration SNAPSHOT_TIMEOUT = Duration.ofMinutes(5);
private static final Duration DELETE_TIMEOUT = Duration.ofMinutes(3);
private static final String SNAPSHOT_DIR = "/tmp/opensandbox-snapshot-e2e";
private static final String SNAPSHOT_FILE = SNAPSHOT_DIR + "/marker.txt";
@Test
@DisplayName("snapshot create, poll, list, restore, and delete")
@Timeout(value = 10, unit = TimeUnit.MINUTES)
void testSnapshotLifecycleEndToEnd() throws InterruptedException {
SandboxManager manager =
SandboxManager.builder().connectionConfig(sharedConnectionConfig).build();
Sandbox source = null;
Sandbox restored = null;
String readySnapshotId = null;
String tag = "snapshot-e2e-" + UUID.randomUUID().toString().substring(0, 8);
String marker = "snapshot-marker-" + UUID.randomUUID();
try {
source =
Sandbox.builder()
.connectionConfig(sharedConnectionConfig)
.image(getSandboxImage())
.timeout(Duration.ofMinutes(5))
.readyTimeout(Duration.ofSeconds(60))
.metadata(Map.of("tag", tag, "role", "source"))
.env("E2E_TEST", "true")
.healthCheckPollingInterval(Duration.ofMillis(500))
.build();
assertTrue(source.isHealthy(), "source sandbox should be healthy");
runOk(
source,
"mkdir -p "
+ SNAPSHOT_DIR
+ " && printf '%s' '"
+ marker
+ "' > "
+ SNAPSHOT_FILE);
SnapshotInfo accepted = source.createSnapshot(tag + "-ready");
readySnapshotId = accepted.getId();
assertSnapshotFields(
accepted,
readySnapshotId,
source.getId(),
tag + "-ready",
"Creating",
"snapshot_accepted",
"Snapshot creation accepted.",
null);
SnapshotInfo ready =
waitForSnapshotState(manager, readySnapshotId, "Ready", SNAPSHOT_TIMEOUT);
assertSnapshotFields(
ready,
readySnapshotId,
source.getId(),
tag + "-ready",
"Ready",
null,
null,
accepted.getCreatedAt());
assertFalse(
ready.getStatus()
.getLastTransitionAt()
.isBefore(accepted.getStatus().getLastTransitionAt()),
"Ready transition timestamp should not be earlier than Creating");
PagedSnapshotInfos allSnapshots =
manager.listSnapshots(
SnapshotFilter.builder()
.sandboxId(source.getId())
.page(1)
.pageSize(50)
.build());
assertSnapshotList(allSnapshots, 1, 50, ready);
PagedSnapshotInfos readySnapshots =
manager.listSnapshots(
SnapshotFilter.builder()
.sandboxId(source.getId())
.page(1)
.states("Ready")
.pageSize(50)
.build());
assertSnapshotList(readySnapshots, 1, 50, ready);
restored =
Sandbox.builder()
.connectionConfig(sharedConnectionConfig)
.snapshotId(readySnapshotId)
.timeout(Duration.ofMinutes(5))
.readyTimeout(Duration.ofSeconds(60))
.metadata(Map.of("tag", tag, "role", "restored"))
.env("E2E_TEST", "true")
.healthCheckPollingInterval(Duration.ofMillis(500))
.build();
assertTrue(restored.isHealthy(), "restored sandbox should be healthy");
Execution restoredRead =
runOk(restored, "test -f " + SNAPSHOT_FILE + " && cat " + SNAPSHOT_FILE);
assertEquals(1, restoredRead.getLogs().getStdout().size());
assertEquals(marker, restoredRead.getLogs().getStdout().get(0).getText());
closeSandbox(restored);
restored = null;
manager.deleteSnapshot(readySnapshotId);
waitForSnapshotDeleted(manager, readySnapshotId, DELETE_TIMEOUT);
readySnapshotId = null;
PagedSnapshotInfos readySnapshotsAfterDelete =
manager.listSnapshots(
SnapshotFilter.builder()
.sandboxId(source.getId())
.page(1)
.states("Ready")
.pageSize(50)
.build());
assertSnapshotList(readySnapshotsAfterDelete, 1, 50);
} finally {
closeSandbox(restored);
closeSandbox(source);
deleteSnapshotIfExists(manager, readySnapshotId);
manager.close();
}
}
private static Execution runOk(Sandbox sandbox, String command) {
Execution execution =
sandbox.commands().run(RunCommandRequest.builder().command(command).build());
assertNotNull(execution);
assertNull(execution.getError(), "command failed: " + command);
return execution;
}
private static SnapshotInfo waitForSnapshotState(
SandboxManager manager, String snapshotId, String expectedState, Duration timeout)
throws InterruptedException {
long deadline = System.nanoTime() + timeout.toNanos();
SnapshotInfo last = null;
while (System.nanoTime() < deadline) {
last = manager.getSnapshot(snapshotId);
String state = last.getStatus().getState();
if (expectedState.equals(state)) {
return last;
}
if ("Failed".equals(state)) {
fail(
"snapshot failed: reason="
+ last.getStatus().getReason()
+ " message="
+ last.getStatus().getMessage());
}
Thread.sleep(1000);
}
fail(
"snapshot did not become "
+ expectedState
+ " within "
+ timeout
+ ", lastState="
+ (last == null ? "<none>" : last.getStatus().getState()));
throw new IllegalStateException("unreachable");
}
private static void waitForSnapshotDeleted(
SandboxManager manager, String snapshotId, Duration timeout)
throws InterruptedException {
long deadline = System.nanoTime() + timeout.toNanos();
while (System.nanoTime() < deadline) {
try {
manager.getSnapshot(snapshotId);
} catch (SandboxApiException exception) {
if (Integer.valueOf(404).equals(exception.getStatusCode())) {
return;
}
throw exception;
}
Thread.sleep(1000);
}
fail("snapshot was not deleted within " + timeout + ": " + snapshotId);
}
private static void deleteSnapshotIfExists(SandboxManager manager, String snapshotId) {
if (snapshotId == null) {
return;
}
try {
manager.deleteSnapshot(snapshotId);
} catch (SandboxException ignored) {
}
}
private static void assertSnapshotFields(
SnapshotInfo snapshot,
String expectedId,
String expectedSandboxId,
String expectedName,
String expectedState,
String expectedReason,
String expectedMessage,
OffsetDateTime expectedCreatedAt) {
assertNotNull(snapshot);
assertEquals(expectedId, snapshot.getId());
assertEquals(expectedSandboxId, snapshot.getSandboxId());
assertEquals(expectedName, snapshot.getName());
assertNotNull(snapshot.getStatus());
assertEquals(expectedState, snapshot.getStatus().getState());
assertNotNull(snapshot.getCreatedAt(), "snapshot.createdAt should be present");
assertNotNull(
snapshot.getStatus().getLastTransitionAt(),
"snapshot.status.lastTransitionAt should be present");
assertFalse(
snapshot.getStatus().getLastTransitionAt().isBefore(snapshot.getCreatedAt()),
"snapshot.status.lastTransitionAt should not be earlier than createdAt");
if (expectedCreatedAt != null) {
assertEquals(expectedCreatedAt, snapshot.getCreatedAt());
}
if (expectedReason != null) {
assertEquals(expectedReason, snapshot.getStatus().getReason());
} else {
assertNotNull(
snapshot.getStatus().getReason(), "snapshot.status.reason should be present");
assertFalse(
snapshot.getStatus().getReason().isBlank(),
"snapshot.status.reason should not be blank");
}
if (expectedMessage != null) {
assertEquals(expectedMessage, snapshot.getStatus().getMessage());
} else {
assertNotNull(
snapshot.getStatus().getMessage(), "snapshot.status.message should be present");
assertFalse(
snapshot.getStatus().getMessage().isBlank(),
"snapshot.status.message should not be blank");
}
}
private static void assertSnapshotList(
PagedSnapshotInfos snapshotPage,
int expectedPage,
int expectedPageSize,
SnapshotInfo... expected) {
assertNotNull(snapshotPage);
assertNotNull(snapshotPage.getSnapshotInfos());
assertNotNull(snapshotPage.getPagination());
assertEquals(expectedPage, snapshotPage.getPagination().getPage());
assertEquals(expectedPageSize, snapshotPage.getPagination().getPageSize());
assertEquals(expected.length, snapshotPage.getSnapshotInfos().size());
assertEquals(expected.length, snapshotPage.getPagination().getTotalItems());
assertEquals(expected.length == 0 ? 0 : 1, snapshotPage.getPagination().getTotalPages());
assertFalse(snapshotPage.getPagination().getHasNextPage());
List<SnapshotInfo> expectedSnapshots = Arrays.asList(expected);
for (SnapshotInfo expectedSnapshot : expectedSnapshots) {
SnapshotInfo listedSnapshot =
snapshotPage.getSnapshotInfos().stream()
.filter(snapshot -> expectedSnapshot.getId().equals(snapshot.getId()))
.findFirst()
.orElseThrow(
() ->
new AssertionError(
"snapshot "
+ expectedSnapshot.getId()
+ " missing from listSnapshots response"));
assertSnapshotEquals(expectedSnapshot, listedSnapshot);
}
}
private static void assertSnapshotEquals(SnapshotInfo expected, SnapshotInfo actual) {
assertNotNull(expected);
assertNotNull(actual);
assertEquals(expected.getId(), actual.getId());
assertEquals(expected.getSandboxId(), actual.getSandboxId());
assertEquals(expected.getName(), actual.getName());
assertEquals(expected.getCreatedAt(), actual.getCreatedAt());
assertNotNull(actual.getStatus());
assertEquals(expected.getStatus().getState(), actual.getStatus().getState());
assertEquals(expected.getStatus().getReason(), actual.getStatus().getReason());
assertEquals(expected.getStatus().getMessage(), actual.getStatus().getMessage());
assertEquals(
expected.getStatus().getLastTransitionAt(),
actual.getStatus().getLastTransitionAt());
}
private static void closeSandbox(Sandbox sandbox) {
if (sandbox == null) {
return;
}
try {
sandbox.kill();
} catch (Exception ignored) {
}
try {
sandbox.close();
} catch (Exception ignored) {
}
}
}
@@ -0,0 +1,6 @@
# OpenSandbox E2E Test Configuration
# Default values for local/CI runs. Override via editing this file or by providing your own build.
opensandbox.test.domain=localhost:8080
opensandbox.test.protocol=http
opensandbox.test.api.key=e2e-test
opensandbox.sandbox.default.image=opensandbox/code-interpreter:latest