Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ public void start() throws Exception {
log.debug("[sandbox] Branch B: workspace lost, restoring from snapshot");
Optional<InputStream> archive = openSnapshotArchive(snapshot);
if (archive.isPresent()) {
doSetupWorkspace();
try (InputStream stream = archive.get()) {
doSetupWorkspace();
doHydrateWorkspace(stream);
}
workspaceSpecApplier.applyWorkspaceSpec(spec, true);
Expand All @@ -104,8 +104,8 @@ public void start() throws Exception {
if (archive.isPresent()) {
// Branch C: restore from snapshot + all spec entries
log.debug("[sandbox] Branch C: restoring from snapshot");
doSetupWorkspace();
try (InputStream stream = archive.get()) {
doSetupWorkspace();
doHydrateWorkspace(stream);
}
workspaceSpecApplier.applyWorkspaceSpec(spec, false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* 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 io.agentscope.harness.agent.sandbox;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;

import io.agentscope.harness.agent.sandbox.snapshot.SandboxSnapshot;
import java.io.IOException;
import java.io.InputStream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InOrder;

class AbstractBaseSandboxSnapshotCleanupTest {

@ParameterizedTest
@ValueSource(booleans = {false, true})
void closesSnapshotAfterSuccessfulHydration(boolean previouslyReady) throws Exception {
InputStream archive = mock(InputStream.class);
AbstractBaseSandbox sandbox = sandboxForRestore(previouslyReady, archive);

sandbox.start();

InOrder order = inOrder(sandbox, archive);
order.verify(sandbox).doSetupWorkspace();
order.verify(sandbox).doHydrateWorkspace(archive);
order.verify(archive).close();
assertTrue(sandbox.isRunning());
assertTrue(sandbox.getState().isWorkspaceRootReady());
}

@ParameterizedTest
@ValueSource(booleans = {false, true})
void closesOpenedSnapshotWhenWorkspaceSetupFails(boolean previouslyReady) throws Exception {
InputStream archive = mock(InputStream.class);
AbstractBaseSandbox sandbox = sandboxForRestore(previouslyReady, archive);
IOException setupFailure = new IOException("workspace setup failed");
doThrow(setupFailure).when(sandbox).doSetupWorkspace();

SandboxException.WorkspaceStartException failure =
assertThrows(SandboxException.WorkspaceStartException.class, sandbox::start);

assertSame(setupFailure, failure.getCause());
assertFalse(sandbox.isRunning());
assertFalse(sandbox.getState().isWorkspaceRootReady());
verify(sandbox, never()).doHydrateWorkspace(any());
verify(archive).close();
}

@ParameterizedTest
@ValueSource(booleans = {false, true})
void preservesSetupFailureWhenClosingSnapshotAlsoFails(boolean previouslyReady)
throws Exception {
InputStream archive = mock(InputStream.class);
IOException closeFailure = new IOException("archive close failed");
doThrow(closeFailure).when(archive).close();
AbstractBaseSandbox sandbox = sandboxForRestore(previouslyReady, archive);
IOException setupFailure = new IOException("workspace setup failed");
doThrow(setupFailure).when(sandbox).doSetupWorkspace();

SandboxException.WorkspaceStartException failure =
assertThrows(SandboxException.WorkspaceStartException.class, sandbox::start);

assertSame(setupFailure, failure.getCause());
verify(archive).close();
assertArrayEquals(new Throwable[] {closeFailure}, setupFailure.getSuppressed());
}

private AbstractBaseSandbox sandboxForRestore(boolean previouslyReady, InputStream archive)
throws Exception {
SandboxSnapshot snapshot = mock(SandboxSnapshot.class);
when(snapshot.isRestorable()).thenReturn(true);
when(snapshot.restore()).thenReturn(archive);
SandboxState state = new SandboxState() {};
state.setWorkspaceSpec(new WorkspaceSpec());
state.setWorkspaceRootReady(previouslyReady);
state.setSnapshot(snapshot);
AbstractBaseSandbox sandbox =
mock(
AbstractBaseSandbox.class,
withSettings().useConstructor(state).defaultAnswer(CALLS_REAL_METHODS));
doReturn(false).when(sandbox).probeWorkspaceRootForPreservedResume();
return sandbox;
}
}
2 changes: 2 additions & 0 deletions docs/v2/en/docs/harness/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ The sandbox snapshots its workspace at each `call()` end and restores at the nex
- Container gone → reboot from snapshot, restore workspace
- No snapshot → full init from `WorkspaceSpec` (cold start)

An opened snapshot archive is closed even if workspace setup fails during restoration.

Where snapshots land is decided by `snapshotSpec`:

| Option | When |
Expand Down
2 changes: 2 additions & 0 deletions docs/v2/zh/docs/harness/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ agent.call(msg, RuntimeContext.builder()
- 容器没了 → 拿快照重新起一个,恢复工作区
- 没快照 → 按 `WorkspaceSpec` 全量初始化(冷启动)

恢复时已经打开的快照归档流,即使工作区初始化失败也会关闭。

快照存到哪里取决于你配的 `snapshotSpec`:

| 选项 | 适合 |
Expand Down
Loading