返回 DeepSeek-Reasonix
hardening_test.go
根目录 / internal / remote / bootstrap / hardening_test.go
1 package bootstrap
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "sync/atomic"
9 "testing"
10 "time"
11
12 "reasonix/internal/remote"
13 )
14
15 func TestEnsureServeRejectsStalePortFile(t *testing.T) {
16 skipOnWindows(t)
17 root := t.TempDir()
18 paths := pathsFor(root, root)
19 if err := os.MkdirAll(paths.Dir, 0o700); err != nil {
20 t.Fatal(err)
21 }
22 if err := os.WriteFile(paths.PortFile, []byte("127.0.0.1:49999\n"), 0o600); err != nil {
23 t.Fatal(err)
24 }
25 conn := newFakeConn(t, root, func(cmd string) (remote.ExecResult, error) {
26 switch {
27 case strings.Contains(cmd, "uname"):
28 return ok("Linux x86_64\n")
29 case strings.Contains(cmd, "command -v reasonix"):
30 return ok("/usr/bin/reasonix\nreasonix v9.9.0\nportfile:yes\nsessionevents:yes\ndetachedheal:yes\ncaps:yes\n")
31 case strings.Contains(cmd, "nohup"):
32 if strings.Contains(cmd, "rm -f "+shellQuote(paths.PortFile)) {
33 _ = os.Remove(paths.PortFile) // model the generated launch command
34 }
35 return ok("12345\n") // the new serve never publishes a port
36 default:
37 return ok("")
38 }
39 })
40 ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
41 defer cancel()
42 if res, err := EnsureServe(ctx, conn, Options{Workspace: "~"}); err == nil {
43 t.Fatalf("accepted a stale port as a successful launch: %+v", res.State)
44 }
45 }
46
47 func TestEnsureServeSerializesConcurrentClients(t *testing.T) {
48 skipOnWindows(t)
49 root := t.TempDir()
50 paths := pathsFor(root, root)
51 var launches atomic.Int32
52 conn := newFakeConn(t, root, func(cmd string) (remote.ExecResult, error) {
53 switch {
54 case strings.Contains(cmd, "uname"):
55 return ok("Linux x86_64\n")
56 case strings.Contains(cmd, "command -v reasonix"):
57 return ok("/usr/bin/reasonix\nreasonix v9.9.0\nportfile:yes\nsessionevents:yes\ndetachedheal:yes\ncaps:yes\n")
58 case strings.Contains(cmd, "nohup"):
59 launches.Add(1)
60 _ = os.WriteFile(paths.PortFile, []byte("127.0.0.1:45123\n"), 0o600)
61 return ok("321\n")
62 case strings.Contains(cmd, "readlink /proc/321/exe"):
63 return ok("yes\n")
64 case strings.Contains(cmd, "ps -p 321"):
65 return ok("1\n")
66 default:
67 return ok("")
68 }
69 })
70 type outcome struct {
71 res Result
72 err error
73 }
74 start := make(chan struct{})
75 out := make(chan outcome, 2)
76 for range 2 {
77 go func() {
78 <-start
79 res, err := EnsureServe(context.Background(), conn, Options{Workspace: "~"})
80 out <- outcome{res: res, err: err}
81 }()
82 }
83 close(start)
84 var reused int
85 for range 2 {
86 got := <-out
87 if got.err != nil {
88 t.Fatal(got.err)
89 }
90 if got.res.Reused {
91 reused++
92 }
93 }
94 if got := launches.Load(); got != 1 || reused != 1 {
95 t.Fatalf("launches=%d reused=%d, want 1/1", got, reused)
96 }
97 }
98
99 func TestAutoInstallPreservesNPMFailureWhenNoUploadBinaryExists(t *testing.T) {
100 skipOnWindows(t)
101 root := t.TempDir()
102 conn := newFakeConn(t, root, func(cmd string) (remote.ExecResult, error) {
103 switch {
104 case strings.Contains(cmd, "command -v reasonix"):
105 return ok("\n")
106 case strings.Contains(cmd, "npm i -g reasonix"):
107 return remote.ExecResult{Stdout: []byte("permission denied"), ExitCode: 1}, nil
108 default:
109 return ok("")
110 }
111 })
112 _, _, err := ensureBinary(context.Background(), conn, conn.fs, Options{Install: InstallAuto}, root, "linux", "amd64", pathsFor(root, root))
113 if err == nil {
114 t.Fatal("auto install unexpectedly succeeded")
115 }
116 message := err.Error()
117 if !strings.Contains(message, "npm install failed: permission denied") || !strings.Contains(message, "no local Reasonix CLI") {
118 t.Fatalf("auto install hid the actionable failures: %v", err)
119 }
120 }
121
122 func TestAutoInstallDownloadsVerifiedCrossPlatformBinaryAfterNPMFailure(t *testing.T) {
123 skipOnWindows(t)
124 root := t.TempDir()
125 uploaded := uploadedBinPath(root)
126 conn := newFakeConn(t, root, func(cmd string) (remote.ExecResult, error) {
127 switch {
128 case strings.Contains(cmd, "npm i -g reasonix"):
129 return remote.ExecResult{Stdout: []byte("npm: command not found"), ExitCode: 127}, nil
130 case strings.Contains(cmd, "BIN=; if [ -x "+shellQuote(uploaded)):
131 return ok(uploaded + "\nreasonix v1.2.3\nportfile:yes\nsessionevents:yes\ndetachedheal:yes\ncaps:yes\n")
132 case strings.Contains(cmd, "command -v reasonix"):
133 return ok("\n")
134 default:
135 return ok("")
136 }
137 })
138 fetched := false
139 bin, _, err := ensureBinary(context.Background(), conn, conn.fs, Options{
140 Install: InstallAuto, LocalBinary: "/local/reasonix", LocalGOOS: "darwin", LocalGOARCH: "arm64",
141 ProductVersion: "v1.2.3",
142 FetchBinary: func(_ context.Context, version, goos, goarch string) ([]byte, error) {
143 fetched = true
144 if version != "v1.2.3" || goos != "linux" || goarch != "amd64" {
145 t.Fatalf("fetch target = %s %s/%s", version, goos, goarch)
146 }
147 return []byte("linux-amd64-cli"), nil
148 },
149 }, root, "linux", "amd64", pathsFor(root, root))
150 if err != nil {
151 t.Fatal(err)
152 }
153 if !fetched || bin != uploaded {
154 t.Fatalf("bin=%q fetched=%v", bin, fetched)
155 }
156 }
157
158 func TestUploadInstallProbesFreshBinaryBeforeStalePathCandidate(t *testing.T) {
159 skipOnWindows(t)
160 root := t.TempDir()
161 uploaded := uploadedBinPath(root)
162 local := filepath.Join(root, "local-reasonix")
163 if err := os.WriteFile(local, []byte("fresh-cli"), 0o755); err != nil {
164 t.Fatal(err)
165 }
166 probedUpload := false
167 conn := newFakeConn(t, root, func(cmd string) (remote.ExecResult, error) {
168 switch {
169 case strings.Contains(cmd, "command -v reasonix"):
170 return ok("/usr/bin/reasonix\nreasonix v1.0.0\nportfile:yes\nsessionevents:no\ndetachedheal:no\ncaps:no\n")
171 case strings.Contains(cmd, "BIN=; if [ -x "+shellQuote(uploaded)):
172 probedUpload = true
173 if _, err := os.Stat(uploaded); err != nil {
174 t.Fatalf("uploaded probe ran before binary write: %v", err)
175 }
176 return ok(uploaded + "\nreasonix v9.9.0\nportfile:yes\nsessionevents:yes\ndetachedheal:yes\ncaps:yes\n")
177 default:
178 return ok("")
179 }
180 })
181 bin, _, err := ensureBinary(context.Background(), conn, conn.fs, Options{
182 Install: InstallUpload, LocalBinary: local, LocalGOOS: "linux", LocalGOARCH: "amd64",
183 }, root, "linux", "amd64", pathsFor(root, root))
184 if err != nil {
185 t.Fatal(err)
186 }
187 if !probedUpload || bin != uploaded {
188 t.Fatalf("fresh upload result = bin:%q probed:%v, want %q/true", bin, probedUpload, uploaded)
189 }
190 }
191
192 func TestNPMInstallProbesFreshGlobalBinaryBeforeStalePathCandidate(t *testing.T) {
193 skipOnWindows(t)
194 root := t.TempDir()
195 const installed = "/opt/npm/bin/reasonix"
196 probedGlobal := false
197 conn := newFakeConn(t, root, func(cmd string) (remote.ExecResult, error) {
198 switch {
199 case strings.Contains(cmd, "command -v reasonix"):
200 return ok("/usr/bin/reasonix\nreasonix v1.0.0\nportfile:yes\nsessionevents:no\ndetachedheal:no\ncaps:no\n")
201 case strings.Contains(cmd, "npm i -g reasonix"):
202 return ok("installed\n")
203 case strings.Contains(cmd, `BIN=; P="$(npm prefix -g 2>/dev/null)"`):
204 probedGlobal = true
205 return ok(installed + "\nreasonix v9.9.0\nportfile:yes\nsessionevents:yes\ndetachedheal:yes\ncaps:yes\n")
206 default:
207 return ok("")
208 }
209 })
210 bin, _, err := ensureBinary(context.Background(), conn, conn.fs, Options{Install: InstallNPM}, root, "linux", "amd64", pathsFor(root, root))
211 if err != nil {
212 t.Fatal(err)
213 }
214 if !probedGlobal || bin != installed {
215 t.Fatalf("fresh npm result = bin:%q probed:%v, want %q/true", bin, probedGlobal, installed)
216 }
217 }
218
218 lines GO