返回 DeepSeek-Reasonix
bootstrap_pure_test.go
根目录 / internal / remote / bootstrap / bootstrap_pure_test.go
1 package bootstrap
2
3 import (
4 "strings"
5 "testing"
6 )
7
8 func TestParseUname(t *testing.T) {
9 cases := []struct {
10 in string
11 goos, goarch string
12 wantErr bool
13 }{
14 {"Linux x86_64", "linux", "amd64", false},
15 {"Linux aarch64", "linux", "arm64", false},
16 {"Darwin arm64", "darwin", "arm64", false},
17 {"Darwin x86_64", "darwin", "amd64", false},
18 {"Linux armv7l", "linux", "arm", false},
19 {" Linux x86_64 \n", "linux", "amd64", false},
20 {"MINGW64_NT-10.0 x86_64", "", "", true}, // Windows shell
21 {"Linux mips", "", "", true},
22 {"garbage", "", "", true},
23 }
24 for _, c := range cases {
25 goos, goarch, err := ParseUname(c.in)
26 if c.wantErr {
27 if err == nil {
28 t.Errorf("ParseUname(%q): expected error", c.in)
29 }
30 continue
31 }
32 if err != nil || goos != c.goos || goarch != c.goarch {
33 t.Errorf("ParseUname(%q) = (%q,%q,%v), want (%q,%q)", c.in, goos, goarch, err, c.goos, c.goarch)
34 }
35 }
36 }
37
38 func TestParseVersion(t *testing.T) {
39 cases := map[string]string{
40 "reasonix v1.9.0": "1.9.0",
41 "1.9.0": "1.9.0",
42 "reasonix version 2.0.1": "2.0.1",
43 "v1.10.0-rc.1": "1.10.0-rc.1",
44 }
45 for in, want := range cases {
46 got, err := ParseVersion(in)
47 if err != nil || got != want {
48 t.Errorf("ParseVersion(%q) = (%q,%v), want %q", in, got, err, want)
49 }
50 }
51 if _, err := ParseVersion("no version here"); err == nil {
52 t.Error("expected error for versionless output")
53 }
54 }
55
56 func TestCompareVersions(t *testing.T) {
57 cases := []struct {
58 a, b string
59 want int
60 }{
61 {"1.9.0", "1.9.0", 0},
62 {"1.9.0", "1.10.0", -1},
63 {"1.10.0", "1.9.0", 1},
64 {"2.0.0", "1.99.99", 1},
65 {"1.9", "1.9.0", 0},
66 {"1.9.1-rc.1", "1.9.1", 0}, // pre-release ignored for ordering
67 }
68 for _, c := range cases {
69 if got := CompareVersions(c.a, c.b); got != c.want {
70 t.Errorf("CompareVersions(%q,%q) = %d, want %d", c.a, c.b, got, c.want)
71 }
72 }
73 }
74
75 // TestLaunchCommandQuotesHostilePaths is the security golden: a workspace or
76 // log path containing shell metacharacters must be fully single-quoted so it
77 // cannot break out of the launch command.
78 func TestLaunchCommandQuotesHostilePaths(t *testing.T) {
79 paths := StatePaths{
80 Dir: "/home/dev/.reasonix/remote",
81 TokenFile: "/home/dev/.reasonix/remote/serve-x.token",
82 PortFile: "/home/dev/.reasonix/remote/serve-x.port",
83 PidFile: "/home/dev/.reasonix/remote/serve-x.pid",
84 LogFile: "/home/dev/.reasonix/remote/serve-x.log",
85 }
86 hostile := "/tmp/'; rm -rf ~; echo '"
87 cmd := LaunchCommand("/usr/bin/reasonix", hostile, paths)
88
89 // The hostile workspace must appear only inside a quoted operand, escaped.
90 if strings.Contains(cmd, "; rm -rf ~; echo") && !strings.Contains(cmd, `'\''; rm -rf ~; echo '\''`) {
91 t.Fatalf("hostile workspace not properly escaped:\n%s", cmd)
92 }
93 // No unescaped `rm -rf` sequence that would execute.
94 if strings.Contains(cmd, "cd /tmp/'; rm -rf") {
95 t.Fatalf("workspace broke out of quoting:\n%s", cmd)
96 }
97 // Sanity: the essential flags are present.
98 for _, want := range []string{"--addr 127.0.0.1:0", "--auth token", "--token-file", "--port-file", "$SX nohup", "echo $!"} {
99 if !strings.Contains(cmd, want) {
100 t.Errorf("launch command missing %q:\n%s", want, cmd)
101 }
102 }
103 }
104
105 func TestShellQuote(t *testing.T) {
106 cases := map[string]string{
107 "simple": "'simple'",
108 "has space": "'has space'",
109 "a'b": `'a'\''b'`,
110 "'; rm -rf ~": `''\''; rm -rf ~'`,
111 }
112 for in, want := range cases {
113 if got := shellQuote(in); got != want {
114 t.Errorf("shellQuote(%q) = %q, want %q", in, got, want)
115 }
116 }
117 }
118
119 func TestStopAndServeAliveCommands(t *testing.T) {
120 paths := StatePaths{TokenFile: "/state/ws.token", PortFile: "/state/ws.port"}
121 stop := StopCommand(4321, paths)
122 for _, want := range []string{"kill -TERM 4321", "kill -0 4321", "kill -KILL 4321"} {
123 if !strings.Contains(stop, want) {
124 t.Errorf("StopCommand missing %q: %s", want, stop)
125 }
126 }
127 alive := ServeAliveCommand(99, paths)
128 // Must check liveness AND that the process is a reasonix serve (guards PID
129 // reuse), not just kill -0.
130 for _, want := range []string{"kill -0 99", "ps -p 99", "*reasonix*serve*", paths.TokenFile, paths.PortFile} {
131 if !strings.Contains(alive, want) {
132 t.Errorf("ServeAliveCommand missing %q: %s", want, alive)
133 }
134 }
135 if strings.Count(stop, "ours") < 3 {
136 t.Fatalf("StopCommand must revalidate ownership during TERM/KILL wait: %s", stop)
137 }
138 }
139
140 func TestLaunchCommandDetachAndLogHardening(t *testing.T) {
141 cmd := LaunchCommand("/usr/bin/reasonix", "/ws", StatePaths{
142 Dir: "/d", TokenFile: "/d/t", PortFile: "/d/p", PidFile: "/d/i", LogFile: "/d/l",
143 })
144 // setsid must be optional (macOS lacks it) and the log created 0600 so the
145 // serve token line (already suppressed under --port-file) can't leak.
146 for _, want := range []string{"command -v setsid", "$SX nohup", "chmod 600", "umask 077", "--port-file"} {
147 if !strings.Contains(cmd, want) {
148 t.Errorf("LaunchCommand missing %q:\n%s", want, cmd)
149 }
150 }
151 if !strings.Contains(cmd, "rm -f '/d/p' '/d/i'") {
152 t.Fatalf("LaunchCommand does not clear stale port/pid files before launch:\n%s", cmd)
153 }
154 if strings.Contains(cmd, "setsid nohup") {
155 t.Errorf("setsid must be conditional, not hard-wired:\n%s", cmd)
156 }
157 }
158
159 func TestLocateCommandProbesPortFileFlag(t *testing.T) {
160 cmd := LocateCommand("/home/x/.reasonix/remote/bin/reasonix")
161 for _, want := range []string{"serve --help", "port-file", "portfile:yes", "portfile:no"} {
162 if !strings.Contains(cmd, want) {
163 t.Errorf("LocateCommand missing %q:\n%s", want, cmd)
164 }
165 }
166 }
167
167 lines GO