fix(sandbox): pin Docker sessions so skill artifacts get collected

ArtifactCollector only drains /workspace/output from the pinned sandbox
config. Docker is session-persistent like Cube/E2B, but execution skipped
the pin, so generated HTML showed as unavailable after the turn.
This commit is contained in:
wizardchen
2026-08-27 11:39:38 +08:00
committed by lyingbug
parent e7582cf538
commit 2a20733682
3 changed files with 44 additions and 1 deletions
@@ -96,3 +96,23 @@ func TestArtifactSessionSourceSkipsUnpinnedSession(t *testing.T) {
require.Nil(t, collector.sessionSource(context.Background(), "s-1"))
}
// Docker (and other named backends) pin the workspace config on first
// execution. Collection must follow that pin rather than treating the
// session as having no sandbox.
func TestArtifactSessionSourceResolvesNamedPin(t *testing.T) {
pinner := NewSessionSandboxPinner(newPinTestDB(t))
ctx := context.WithValue(context.Background(), types.TenantIDContextKey, uint64(7))
_, err := pinner.Pin(ctx, "s-1", "cfg-docker")
require.NoError(t, err)
named := &artifactFallbackManager{source: &fakeSandboxSource{}}
collector := &ArtifactCollector{
source: &fakeSandboxSource{},
resolver: stubSandboxResolver{mgr: named},
pinner: pinner,
}
got := collector.sessionSource(ctx, "s-1")
require.Equal(t, named, got)
}
@@ -173,7 +173,10 @@ func resolveSandboxForExecution(
if err != nil || mgr == nil {
return mgr, configID, err
}
if mgr.GetType() != sandbox.SandboxTypeCube && mgr.GetType() != sandbox.SandboxTypeE2B {
// Named backends (Cube, E2B, Docker) all keep a session-scoped sandbox.
// Artifact collection and teardown resolve that sandbox from this pin, so
// skipping Docker here leaves /workspace/output files uncollected.
if !sandbox.IsNamedSandboxBackendType(string(mgr.GetType())) {
return mgr, configID, nil
}
if pinner == nil || strings.TrimSpace(sessionID) == "" {
@@ -181,6 +181,26 @@ func TestResolveSandboxForExecutionPinsRemoteBackend(t *testing.T) {
require.Equal(t, "cfg-cube", pinned)
}
// Docker is a session-persistent remote backend, same as Cube/E2B. Skipping
// the pin used to make ArtifactCollector treat the turn as "no live sandbox"
// and leave generated HTML/files showing as unavailable in chat.
func TestResolveSandboxForExecutionPinsDockerBackend(t *testing.T) {
pinner := NewSessionSandboxPinner(newPinTestDB(t))
want := &pinTestManager{typ: sandbox.SandboxTypeDocker}
got, configID, err := resolveSandboxForExecution(
context.Background(), stubSandboxResolver{mgr: want}, nil, pinner,
7, "s-1", "cfg-docker", nil,
)
require.NoError(t, err)
require.Same(t, want, got)
require.Equal(t, "cfg-docker", configID)
pinned, err := pinner.Read(context.Background(), "s-1")
require.NoError(t, err)
require.Equal(t, "cfg-docker", pinned)
}
func TestResolveSandboxForExecutionKeepsExistingRemotePin(t *testing.T) {
pinner := NewSessionSandboxPinner(newPinTestDB(t))
_, err := pinner.Pin(context.Background(), "s-1", "cfg-existing")