Files
wehub-resource-sync e0e362d700
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
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

601 lines
18 KiB
Go

// 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.
//go:build !windows
// +build !windows
package runtime
import (
"context"
"fmt"
"os/exec"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/alibaba/opensandbox/execd/pkg/jupyter/execute"
"github.com/alibaba/opensandbox/internal/safego"
)
func TestBashSession_NonZeroExitEmitsError(t *testing.T) {
if _, err := exec.LookPath("bash"); err != nil {
t.Skip("bash not found in PATH")
}
c := NewController("", "")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var (
sessionID string
stdoutLine string
errCh = make(chan *execute.ErrorOutput, 1)
completeCh = make(chan struct{}, 1)
)
req := &ExecuteCodeRequest{
Language: Bash,
Code: `echo "before"; exit 7`,
Cwd: t.TempDir(),
Timeout: 5 * time.Second,
Hooks: ExecuteResultHook{
OnExecuteInit: func(s string) { sessionID = s },
OnExecuteStdout: func(s string) { stdoutLine = s },
OnExecuteError: func(err *execute.ErrorOutput) { errCh <- err },
OnExecuteComplete: func(_ time.Duration) {
completeCh <- struct{}{}
},
},
}
session, err := c.createBashSession(&CreateContextRequest{})
assert.NoError(t, err)
req.Context = session
require.NoError(t, c.runBashSession(ctx, req))
var gotErr *execute.ErrorOutput
select {
case gotErr = <-errCh:
case <-time.After(2 * time.Second):
require.Fail(t, "expected error hook to be called")
}
require.NotNil(t, gotErr, "expected non-nil error output")
require.Equal(t, "CommandExecError", gotErr.EName)
require.Equal(t, "7", gotErr.EValue)
require.NotEmpty(t, sessionID, "expected session id to be set")
require.Equal(t, "before", stdoutLine)
select {
case <-completeCh:
require.Fail(t, "did not expect completion hook on non-zero exit")
default:
}
}
func TestBashSession_envAndExitCode(t *testing.T) {
session := newBashSession("")
t.Cleanup(func() { _ = session.close() })
require.NoError(t, session.start())
var (
initCalls int
completeCalls int
stdoutLines []string
)
hooks := ExecuteResultHook{
OnExecuteInit: func(ctx string) {
require.Equal(t, session.config.Session, ctx, "unexpected session in OnExecuteInit")
initCalls++
},
OnExecuteStdout: func(text string) {
t.Log(text)
stdoutLines = append(stdoutLines, text)
},
OnExecuteComplete: func(_ time.Duration) {
completeCalls++
},
}
// 1) export an env var
request := &ExecuteCodeRequest{
Code: "export FOO=hello",
Hooks: hooks,
Timeout: 3 * time.Second,
}
require.NoError(t, session.run(context.Background(), request))
exportStdoutCount := len(stdoutLines)
// 2) verify env is persisted
request = &ExecuteCodeRequest{
Code: "echo $FOO",
Hooks: hooks,
Timeout: 3 * time.Second,
}
require.NoError(t, session.run(context.Background(), request))
echoLines := stdoutLines[exportStdoutCount:]
foundHello := false
for _, line := range echoLines {
if strings.TrimSpace(line) == "hello" {
foundHello = true
break
}
}
require.True(t, foundHello, "expected echo $FOO to output 'hello', got %v", echoLines)
// 3) ensure exit code of previous command is reflected in shell state
request = &ExecuteCodeRequest{
Code: "false; echo EXIT:$?",
Hooks: hooks,
Timeout: 3 * time.Second,
}
prevCount := len(stdoutLines)
require.NoError(t, session.run(context.Background(), request))
exitLines := stdoutLines[prevCount:]
foundExit := false
for _, line := range exitLines {
if strings.Contains(line, "EXIT:1") {
foundExit = true
break
}
}
require.True(t, foundExit, "expected exit code output 'EXIT:1', got %v", exitLines)
require.Equal(t, 3, initCalls, "OnExecuteInit expected 3 calls")
require.Equal(t, 3, completeCalls, "OnExecuteComplete expected 3 calls")
}
func TestBashSession_envLargeOutputChained(t *testing.T) {
session := newBashSession("")
t.Cleanup(func() { _ = session.close() })
require.NoError(t, session.start())
var (
initCalls int
completeCalls int
stdoutLines []string
)
hooks := ExecuteResultHook{
OnExecuteInit: func(ctx string) {
require.Equal(t, session.config.Session, ctx, "unexpected session in OnExecuteInit")
initCalls++
},
OnExecuteStdout: func(text string) {
t.Log(text)
stdoutLines = append(stdoutLines, text)
},
OnExecuteComplete: func(_ time.Duration) {
completeCalls++
},
}
runAndCollect := func(cmd string) []string {
start := len(stdoutLines)
request := &ExecuteCodeRequest{
Code: cmd,
Hooks: hooks,
Timeout: 10 * time.Second,
}
require.NoError(t, session.run(context.Background(), request))
return append([]string(nil), stdoutLines[start:]...)
}
lines1 := runAndCollect("export FOO=hello1; for i in $(seq 1 60); do echo A${i}:$FOO; done")
require.GreaterOrEqual(t, len(lines1), 60, "expected >=60 lines for cmd1")
require.True(t, containsLine(lines1, "A1:hello1") && containsLine(lines1, "A60:hello1"), "env not reflected in cmd1 output, got %v", lines1[:3])
lines2 := runAndCollect("export FOO=${FOO}_next; export BAR=bar1; for i in $(seq 1 60); do echo B${i}:$FOO:$BAR; done")
require.GreaterOrEqual(t, len(lines2), 60, "expected >=60 lines for cmd2")
require.True(t, containsLine(lines2, "B1:hello1_next:bar1") && containsLine(lines2, "B60:hello1_next:bar1"), "env not propagated to cmd2 output, sample %v", lines2[:3])
lines3 := runAndCollect("export BAR=${BAR}_last; for i in $(seq 1 60); do echo C${i}:$FOO:$BAR; done; echo FINAL_FOO=$FOO; echo FINAL_BAR=$BAR")
require.GreaterOrEqual(t, len(lines3), 62, "expected >=62 lines for cmd3") // 60 lines + 2 finals
require.True(t, containsLine(lines3, "C1:hello1_next:bar1_last") && containsLine(lines3, "C60:hello1_next:bar1_last"), "env not propagated to cmd3 output, sample %v", lines3[:3])
require.True(t, containsLine(lines3, "FINAL_FOO=hello1_next") && containsLine(lines3, "FINAL_BAR=bar1_last"), "final env lines missing, got %v", lines3[len(lines3)-5:])
require.Equal(t, 3, initCalls, "OnExecuteInit expected 3 calls")
require.Equal(t, 3, completeCalls, "OnExecuteComplete expected 3 calls")
}
func TestBashSession_cwdPersistsWithoutOverride(t *testing.T) {
session := newBashSession("")
t.Cleanup(func() { _ = session.close() })
require.NoError(t, session.start())
targetDir := t.TempDir()
var stdoutLines []string
hooks := ExecuteResultHook{
OnExecuteStdout: func(line string) {
stdoutLines = append(stdoutLines, line)
},
}
runAndCollect := func(req *ExecuteCodeRequest) []string {
start := len(stdoutLines)
require.NoError(t, session.run(context.Background(), req))
return append([]string(nil), stdoutLines[start:]...)
}
firstRunLines := runAndCollect(&ExecuteCodeRequest{
Code: fmt.Sprintf("cd %s\npwd", targetDir),
Hooks: hooks,
Timeout: 3 * time.Second,
})
require.True(t, containsLine(firstRunLines, targetDir), "expected cd to update cwd to %q, got %v", targetDir, firstRunLines)
secondRunLines := runAndCollect(&ExecuteCodeRequest{
Code: "pwd",
Hooks: hooks,
Timeout: 3 * time.Second,
})
require.True(t, containsLine(secondRunLines, targetDir), "expected subsequent run to inherit cwd %q, got %v", targetDir, secondRunLines)
session.mu.Lock()
finalCwd := session.cwd
session.mu.Unlock()
require.Equal(t, targetDir, finalCwd, "expected session cwd to stay at %q", targetDir)
}
func TestBashSession_requestCwdOverridesAfterCd(t *testing.T) {
session := newBashSession("")
t.Cleanup(func() { _ = session.close() })
require.NoError(t, session.start())
initialDir := t.TempDir()
overrideDir := t.TempDir()
var stdoutLines []string
hooks := ExecuteResultHook{
OnExecuteStdout: func(line string) {
stdoutLines = append(stdoutLines, line)
},
}
runAndCollect := func(req *ExecuteCodeRequest) []string {
start := len(stdoutLines)
require.NoError(t, session.run(context.Background(), req))
return append([]string(nil), stdoutLines[start:]...)
}
// First request: change session cwd via script.
firstRunLines := runAndCollect(&ExecuteCodeRequest{
Code: fmt.Sprintf("cd %s\npwd", initialDir),
Hooks: hooks,
Timeout: 3 * time.Second,
})
require.True(t, containsLine(firstRunLines, initialDir), "expected cd to update cwd to %q, got %v", initialDir, firstRunLines)
// Second request: explicit Cwd overrides session cwd.
secondRunLines := runAndCollect(&ExecuteCodeRequest{
Code: "pwd",
Cwd: overrideDir,
Hooks: hooks,
Timeout: 3 * time.Second,
})
require.True(t, containsLine(secondRunLines, overrideDir), "expected command to run in override cwd %q, got %v", overrideDir, secondRunLines)
session.mu.Lock()
finalCwd := session.cwd
session.mu.Unlock()
require.Equal(t, overrideDir, finalCwd, "expected session cwd updated to override dir %q", overrideDir)
}
func TestBashSession_envDumpNotLeakedWhenNoTrailingNewline(t *testing.T) {
session := newBashSession("")
t.Cleanup(func() { _ = session.close() })
require.NoError(t, session.start())
var stdoutLines []string
hooks := ExecuteResultHook{
OnExecuteStdout: func(line string) {
stdoutLines = append(stdoutLines, line)
},
}
request := &ExecuteCodeRequest{
Code: `set +x; printf '{"foo":1}'`,
Hooks: hooks,
Timeout: 3 * time.Second,
}
require.NoError(t, session.run(context.Background(), request))
require.Len(t, stdoutLines, 1, "expected exactly one stdout line")
require.Equal(t, `{"foo":1}`, strings.TrimSpace(stdoutLines[0]))
for _, line := range stdoutLines {
require.NotContains(t, line, envDumpStartMarker, "env dump leaked into stdout: %v", stdoutLines)
require.NotContains(t, line, "declare -x", "env dump leaked into stdout: %v", stdoutLines)
}
}
func TestBashSession_envDumpNotLeakedWhenNoOutput(t *testing.T) {
session := newBashSession("")
t.Cleanup(func() { _ = session.close() })
require.NoError(t, session.start())
var stdoutLines []string
hooks := ExecuteResultHook{
OnExecuteStdout: func(line string) {
stdoutLines = append(stdoutLines, line)
},
}
request := &ExecuteCodeRequest{
Code: `set +x; true`,
Hooks: hooks,
Timeout: 3 * time.Second,
}
require.NoError(t, session.run(context.Background(), request))
require.LessOrEqual(t, len(stdoutLines), 1, "expected at most one stdout line, got %v", stdoutLines)
if len(stdoutLines) == 1 {
require.Empty(t, strings.TrimSpace(stdoutLines[0]), "expected empty stdout")
}
for _, line := range stdoutLines {
require.NotContains(t, line, envDumpStartMarker, "env dump leaked into stdout: %v", stdoutLines)
require.NotContains(t, line, "declare -x", "env dump leaked into stdout: %v", stdoutLines)
}
}
func TestBashSession_heredoc(t *testing.T) {
rewardDir := t.TempDir()
controller := NewController("", "")
sessionID, err := controller.CreateBashSession(&CreateContextRequest{})
require.NoError(t, err)
t.Cleanup(func() { _ = controller.DeleteBashSession(sessionID) })
hooks := ExecuteResultHook{
OnExecuteStdout: func(line string) {
fmt.Printf("[stdout] %s\n", line)
},
OnExecuteComplete: func(d time.Duration) {
fmt.Printf("[complete] %s\n", d)
},
}
// First run: heredoc + reward file write.
script := fmt.Sprintf(`
set -x
reward_dir=%q
mkdir -p "$reward_dir"
cat > /tmp/repro_script.sh <<'SHEOF'
#!/usr/bin/env sh
echo "hello heredoc"
SHEOF
chmod +x /tmp/repro_script.sh
/tmp/repro_script.sh
echo "after heredoc"
echo 1 > "$reward_dir/reward.txt"
cat "$reward_dir/reward.txt"
`, rewardDir)
ctx := context.Background()
require.NoError(t, controller.RunInBashSession(ctx, &ExecuteCodeRequest{
Context: sessionID,
Language: Bash,
Timeout: 10 * time.Second,
Code: script,
Hooks: hooks,
}))
// Second run: ensure the session keeps working.
require.NoError(t, controller.RunInBashSession(ctx, &ExecuteCodeRequest{
Context: sessionID,
Language: Bash,
Timeout: 5 * time.Second,
Code: "echo 'second command works'",
Hooks: hooks,
}))
}
func TestBashSession_execReplacesShell(t *testing.T) {
session := newBashSession("")
t.Cleanup(func() { _ = session.close() })
require.NoError(t, session.start())
var stdoutLines []string
hooks := ExecuteResultHook{
OnExecuteStdout: func(line string) {
stdoutLines = append(stdoutLines, line)
},
}
script := `
cat > /tmp/exec_child.sh <<'EOF'
echo "child says hi"
EOF
chmod +x /tmp/exec_child.sh
exec /tmp/exec_child.sh
`
request := &ExecuteCodeRequest{
Code: script,
Hooks: hooks,
Timeout: 5 * time.Second,
}
require.NoError(t, session.run(context.Background(), request), "expected exec to complete without killing the session")
require.True(t, containsLine(stdoutLines, "child says hi"), "expected child output, got %v", stdoutLines)
// Subsequent run should still work because we restart bash per run.
request = &ExecuteCodeRequest{
Code: "echo still-alive",
Hooks: hooks,
Timeout: 2 * time.Second,
}
stdoutLines = nil
require.NoError(t, session.run(context.Background(), request), "expected run to succeed after exec replaced the shell")
require.True(t, containsLine(stdoutLines, "still-alive"), "expected follow-up output, got %v", stdoutLines)
}
func TestBashSession_complexExec(t *testing.T) {
session := newBashSession("")
t.Cleanup(func() { _ = session.close() })
require.NoError(t, session.start())
var stdoutLines []string
hooks := ExecuteResultHook{
OnExecuteStdout: func(line string) {
stdoutLines = append(stdoutLines, line)
},
}
script := `
LOG_FILE=$(mktemp)
export LOG_FILE
exec 3>&1 4>&2
exec > >(tee "$LOG_FILE") 2>&1
set -x
echo "from-complex-exec"
exec 1>&3 2>&4 # step record
echo "after-restore"
`
request := &ExecuteCodeRequest{
Code: script,
Hooks: hooks,
Timeout: 5 * time.Second,
}
require.NoError(t, session.run(context.Background(), request), "expected complex exec to finish")
require.True(t, containsLine(stdoutLines, "from-complex-exec") && containsLine(stdoutLines, "after-restore"), "expected exec outputs, got %v", stdoutLines)
// Session should still be usable.
request = &ExecuteCodeRequest{
Code: "echo still-alive",
Hooks: hooks,
Timeout: 2 * time.Second,
}
stdoutLines = nil
require.NoError(t, session.run(context.Background(), request), "expected run to succeed after complex exec")
require.True(t, containsLine(stdoutLines, "still-alive"), "expected follow-up output, got %v", stdoutLines)
}
func containsLine(lines []string, target string) bool {
for _, l := range lines {
if strings.TrimSpace(l) == target {
return true
}
}
return false
}
// TestBashSession_CloseKillsRunningProcess verifies that session.close() kills the active
// process group so that a long-running command (e.g. sleep) does not keep running after close.
func TestBashSession_CloseKillsRunningProcess(t *testing.T) {
if _, err := exec.LookPath("bash"); err != nil {
t.Skip("bash not found in PATH")
}
session := newBashSession("")
require.NoError(t, session.start())
runDone := make(chan error, 1)
req := &ExecuteCodeRequest{
Code: "sleep 30",
Timeout: 60 * time.Second,
Hooks: ExecuteResultHook{},
}
safego.Go(func() {
runDone <- session.run(context.Background(), req)
})
// Give the child process time to start.
time.Sleep(200 * time.Millisecond)
// Close should kill the process group; run() should return soon (it may return nil
// because the code path treats non-zero exit as success after calling OnExecuteError).
require.NoError(t, session.close())
select {
case <-runDone:
// run() returned; process was killed so we did not wait 30s
case <-time.After(3 * time.Second):
require.Fail(t, "run did not return within 3s after close (process was not killed)")
}
}
// TestBashSession_DeleteBashSessionKillsRunningProcess verifies that DeleteBashSession
// (close path) kills the active run and removes the session from the controller.
func TestBashSession_DeleteBashSessionKillsRunningProcess(t *testing.T) {
if _, err := exec.LookPath("bash"); err != nil {
t.Skip("bash not found in PATH")
}
c := NewController("", "")
sessionID, err := c.CreateBashSession(&CreateContextRequest{})
require.NoError(t, err)
runDone := make(chan error, 1)
req := &ExecuteCodeRequest{
Language: Bash,
Context: sessionID,
Code: "sleep 30",
Timeout: 60 * time.Second,
Hooks: ExecuteResultHook{},
}
safego.Go(func() {
runDone <- c.RunInBashSession(context.Background(), req)
})
time.Sleep(200 * time.Millisecond)
require.NoError(t, c.DeleteBashSession(sessionID))
select {
case <-runDone:
// RunInBashSession returned; process was killed
case <-time.After(3 * time.Second):
require.Fail(t, "RunInBashSession did not return within 3s after DeleteBashSession")
}
// Session should be gone; deleting again should return ErrContextNotFound.
err = c.DeleteBashSession(sessionID)
require.Error(t, err)
require.ErrorIs(t, err, ErrContextNotFound)
}
// TestBashSession_CloseWithNoActiveRun verifies that close() with no running command
// completes without error and does not hang.
func TestBashSession_CloseWithNoActiveRun(t *testing.T) {
session := newBashSession("")
require.NoError(t, session.start())
done := make(chan struct{}, 1)
safego.Go(func() {
_ = session.close()
done <- struct{}{}
})
select {
case <-done:
// close() returned
case <-time.After(2 * time.Second):
require.Fail(t, "close() did not return within 2s when no run was active")
}
}