返回 DeepSeek-Reasonix
manager_test.go
根目录 / internal / persistentshell / manager_test.go
1 package persistentshell
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "runtime"
8 "strings"
9 "testing"
10 "time"
11
12 "reasonix/internal/sandbox"
13 )
14
15 func testManager(t *testing.T) *Manager {
16 t.Helper()
17 m := New()
18 m.Retain()
19 t.Cleanup(m.Release)
20 return m
21 }
22
23 func posixShell(t *testing.T) sandbox.Shell {
24 t.Helper()
25 sh := sandbox.ResolveShell("bash", "", nil)
26 if !sh.Kind.IsPOSIX() {
27 t.Skip("POSIX shell required")
28 }
29 return sh
30 }
31
32 func runPersistent(t *testing.T, m *Manager, sh sandbox.Shell, dir, command string, timeout time.Duration) Result {
33 t.Helper()
34 ctx := context.Background()
35 path := os.Getenv("PATH")
36 if path == "" {
37 path = "/usr/bin:/bin:/usr/sbin:/sbin"
38 }
39 return m.Run(ctx, Request{
40 Argv: InteractiveArgv(sh),
41 Dir: dir,
42 Env: []string{
43 "PATH=" + path,
44 "HOME=" + dir,
45 "TERM=dumb",
46 "NO_COLOR=1",
47 "PAGER=cat",
48 "GIT_PAGER=cat",
49 "BASH_SILENCE_DEPRECATION_WARNING=1",
50 },
51 Command: command,
52 Timeout: timeout,
53 Shell: sh,
54 })
55 }
56
57 func TestPersistentShellKeepsWorkingDirectory(t *testing.T) {
58 if runtime.GOOS == "windows" {
59 t.Skip("covered by the PowerShell persistence test on Windows")
60 }
61 sh := posixShell(t)
62 dir := t.TempDir()
63 sub := filepath.Join(dir, "sub")
64 if err := os.Mkdir(sub, 0o755); err != nil {
65 t.Fatal(err)
66 }
67 m := testManager(t)
68 if res := runPersistent(t, m, sh, dir, "cd sub", 5*time.Second); res.Err != nil {
69 t.Fatalf("cd: %v (%q)", res.Err, res.Output)
70 }
71 res := runPersistent(t, m, sh, dir, "pwd", 5*time.Second)
72 if res.Err != nil {
73 t.Fatalf("pwd: %v (%q)", res.Err, res.Output)
74 }
75 want, err := filepath.EvalSymlinks(sub)
76 if err != nil {
77 want = sub
78 }
79 got, err := filepath.EvalSymlinks(strings.TrimSpace(res.Output))
80 if err != nil {
81 got = strings.TrimSpace(res.Output)
82 }
83 if got != want && !strings.Contains(res.Output, filepath.Base(sub)) {
84 t.Fatalf("pwd output %q, want %q", res.Output, want)
85 }
86 }
87
88 func TestPersistentShellKeepsExportedEnv(t *testing.T) {
89 if runtime.GOOS == "windows" {
90 t.Skip("covered by the PowerShell persistence test on Windows")
91 }
92 sh := posixShell(t)
93 dir := t.TempDir()
94 m := testManager(t)
95 if res := runPersistent(t, m, sh, dir, "export REASONIX_PERSIST=ok", 5*time.Second); res.Err != nil {
96 t.Fatalf("export: %v (%q)", res.Err, res.Output)
97 }
98 res := runPersistent(t, m, sh, dir, "printf '%s\\n' \"$REASONIX_PERSIST\"", 5*time.Second)
99 if res.Err != nil {
100 t.Fatalf("printf: %v (%q)", res.Err, res.Output)
101 }
102 if !strings.Contains(res.Output, "ok") {
103 t.Fatalf("env output %q", res.Output)
104 }
105 }
106
107 func TestPersistentShellTimeoutResetsState(t *testing.T) {
108 if runtime.GOOS == "windows" {
109 t.Skip("POSIX timeout reset")
110 }
111 sh := posixShell(t)
112 dir := t.TempDir()
113 m := testManager(t)
114 if res := runPersistent(t, m, sh, dir, "cd /", 5*time.Second); res.Err != nil {
115 t.Fatalf("cd: %v", res.Err)
116 }
117 res := runPersistent(t, m, sh, dir, "sleep 30", 200*time.Millisecond)
118 if !res.TimedOut {
119 t.Fatalf("want timeout, got %+v", res)
120 }
121 pwd := runPersistent(t, m, sh, dir, "pwd", 5*time.Second)
122 if pwd.Err != nil {
123 t.Fatalf("pwd after timeout: %v (%q)", pwd.Err, pwd.Output)
124 }
125 if strings.TrimSpace(pwd.Output) == "/" {
126 t.Fatalf("timeout must reset cwd, still at /: %q", pwd.Output)
127 }
128 if !strings.Contains(pwd.Output, dir) {
129 t.Fatalf("pwd after timeout = %q, want workspace %q", pwd.Output, dir)
130 }
131 }
132
133 func TestPersistentShellMissingPowerShellDoesNotStartCommand(t *testing.T) {
134 sh := sandbox.Shell{Kind: sandbox.ShellPowerShell, Path: filepath.Join(t.TempDir(), "missing-powershell")}
135 if !Supports(sh) {
136 t.Fatal("PowerShell must support the framed session protocol")
137 }
138 m := testManager(t)
139 res := m.Run(context.Background(), Request{
140 Argv: InteractiveArgv(sh),
141 Command: "Get-Location",
142 Shell: sh,
143 })
144 if res.Started {
145 t.Fatalf("a PowerShell request must not start a shell: %+v", res)
146 }
147 if res.Err == nil || res.ExitCodeKnown {
148 t.Fatalf("missing executable must have an unknown exit status: %+v", res)
149 }
150 }
151
152 func TestPersistentShellNonzeroExit(t *testing.T) {
153 if runtime.GOOS == "windows" {
154 t.Skip("POSIX nonzero exit")
155 }
156 sh := posixShell(t)
157 m := testManager(t)
158 res := runPersistent(t, m, sh, t.TempDir(), "(exit 7)", 5*time.Second)
159 if res.ExitCode != 7 {
160 t.Fatalf("exit code=%d err=%v out=%q", res.ExitCode, res.Err, res.Output)
161 }
162 if res.Err == nil || !strings.Contains(res.Err.Error(), "exit status 7") {
163 t.Fatalf("err=%v", res.Err)
164 }
165 }
166
166 lines GO