返回 DeepSeek-Reasonix
multiworkspace_test.go
根目录 / internal / remote / bootstrap / multiworkspace_test.go
1 package bootstrap
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9 "time"
10
11 "reasonix/internal/remote"
12 )
13
14 func (f *fakeConn) execsContaining(sub string) []string {
15 f.mu.Lock()
16 defer f.mu.Unlock()
17 var out []string
18 for _, c := range f.execs {
19 if strings.Contains(c, sub) {
20 out = append(out, c)
21 }
22 }
23 return out
24 }
25
26 // multiWS is one fake remote host (one Conn, one home) with two workspaces,
27 // each backed by its own fake serve: the launch handler keys on the cd
28 // operand, writes that workspace's port file, and echoes a per-workspace pid.
29 type multiWS struct {
30 conn *fakeConn
31 root, wsA, wsB string
32 pathsA, pathsB StatePaths
33 }
34
35 func newMultiWS(t *testing.T) *multiWS {
36 t.Helper()
37 root := t.TempDir()
38 m := &multiWS{
39 root: root,
40 wsA: filepath.Join(root, "srv-a"),
41 wsB: filepath.Join(root, "srv-b"),
42 }
43 for _, ws := range []string{m.wsA, m.wsB} {
44 if err := os.MkdirAll(ws, 0o755); err != nil {
45 t.Fatal(err)
46 }
47 }
48 m.pathsA = pathsFor(root, m.wsA)
49 m.pathsB = pathsFor(root, m.wsB)
50 m.conn = newFakeConn(t, root, func(cmd string) (remote.ExecResult, error) {
51 switch {
52 case strings.Contains(cmd, "uname"):
53 return ok("Linux x86_64\n")
54 case strings.Contains(cmd, "command -v reasonix"):
55 return ok("/usr/bin/reasonix\nreasonix v9.9.0\nportfile:yes\nsessionevents:yes\ndetachedheal:yes\ncaps:yes\n")
56 case strings.Contains(cmd, "nohup"):
57 if strings.Contains(cmd, "cd '"+m.wsA+"'") {
58 _ = os.WriteFile(m.pathsA.PortFile, []byte("127.0.0.1:44321\n"), 0o600)
59 return ok("111\n")
60 }
61 _ = os.WriteFile(m.pathsB.PortFile, []byte("127.0.0.1:44322\n"), 0o600)
62 return ok("222\n")
63 case strings.Contains(cmd, "readlink /proc/111/exe"), strings.Contains(cmd, "readlink /proc/222/exe"):
64 return ok("yes\n")
65 case strings.Contains(cmd, "ps -p 111"), strings.Contains(cmd, "ps -p 222"):
66 return ok("1\n")
67 default:
68 return ok("")
69 }
70 })
71 return m
72 }
73
74 func (m *multiWS) ensure(t *testing.T, ws string) Result {
75 t.Helper()
76 res, err := EnsureServe(context.Background(), m.conn, Options{
77 Workspace: ws,
78 MinVersion: "1.0.0",
79 Clock: time.Now,
80 })
81 if err != nil {
82 t.Fatalf("EnsureServe(%s): %v", ws, err)
83 }
84 return res
85 }
86
87 // TestEnsureServeTwoWorkspacesCoexist: one host, two workspaces, two serves —
88 // state/token files stay side by side and neither ensure disturbs the other.
89 func TestEnsureServeTwoWorkspacesCoexist(t *testing.T) {
90 skipOnWindows(t)
91 m := newMultiWS(t)
92
93 resA := m.ensure(t, m.wsA)
94 resB := m.ensure(t, m.wsB)
95
96 if resA.Reused || resB.Reused {
97 t.Fatalf("cold starts should not reuse: %+v %+v", resA, resB)
98 }
99 if resA.State.PID != 111 || resB.State.PID != 222 {
100 t.Fatalf("pids wrong: A=%d B=%d", resA.State.PID, resB.State.PID)
101 }
102 if resA.State.Addr == resB.State.Addr {
103 t.Fatalf("both serves share addr %q", resA.State.Addr)
104 }
105 if m.pathsA.StateJSON == m.pathsB.StateJSON {
106 t.Fatal("workspaces share one state file")
107 }
108 for _, p := range []struct {
109 stateJSON, token string
110 pid int
111 }{
112 {m.pathsA.StateJSON, m.pathsA.TokenFile, 111},
113 {m.pathsB.StateJSON, m.pathsB.TokenFile, 222},
114 } {
115 data, err := os.ReadFile(p.stateJSON)
116 if err != nil {
117 t.Fatal(err)
118 }
119 st, err := UnmarshalState(data)
120 if err != nil || st.PID != p.pid {
121 t.Fatalf("persisted state wrong: %+v (%v)", st, err)
122 }
123 if _, err := os.Stat(p.token); err != nil {
124 t.Fatal(err)
125 }
126 }
127
128 // A third ensure of workspace A reuses A's live serve — B never leaks in.
129 resA2 := m.ensure(t, m.wsA)
130 if !resA2.Reused || resA2.State.PID != 111 {
131 t.Fatalf("re-ensure of A should reuse pid 111: %+v", resA2.State)
132 }
133
134 // Both remain independently alive.
135 for _, ws := range []struct {
136 path string
137 pid int
138 }{
139 {m.wsA, 111},
140 {m.wsB, 222},
141 } {
142 st, alive, err := Status(context.Background(), m.conn, ws.path)
143 if err != nil || !alive || st.PID != ws.pid {
144 t.Fatalf("Status(%s) = %+v alive=%v (%v)", ws.path, st, alive, err)
145 }
146 }
147 }
148
149 // TestStopOneWorkspaceLeavesOther: stopping A removes only A's state; B keeps
150 // serving.
151 func TestStopOneWorkspaceLeavesOther(t *testing.T) {
152 skipOnWindows(t)
153 m := newMultiWS(t)
154 m.ensure(t, m.wsA)
155 m.ensure(t, m.wsB)
156
157 if err := Stop(context.Background(), m.conn, m.wsA); err != nil {
158 t.Fatalf("Stop(A): %v", err)
159 }
160 if _, alive, _ := Status(context.Background(), m.conn, m.wsA); alive {
161 t.Fatal("workspace A still alive after Stop")
162 }
163 if _, err := os.Stat(m.pathsA.StateJSON); !os.IsNotExist(err) {
164 t.Error("A state file not removed")
165 }
166 if _, err := os.Stat(m.pathsA.TokenFile); !os.IsNotExist(err) {
167 t.Error("A token file not removed")
168 }
169
170 if _, err := os.Stat(m.pathsB.StateJSON); err != nil {
171 t.Fatalf("B state file disturbed: %v", err)
172 }
173 st, alive, err := Status(context.Background(), m.conn, m.wsB)
174 if err != nil || !alive || st.PID != 222 {
175 t.Fatalf("workspace B should survive: %+v alive=%v (%v)", st, alive, err)
176 }
177 }
178
179 // TestLaunchCommandsCdIntoEachWorkspace: each workspace's launch script gets
180 // only its own state paths as concrete operands and cds into its own tree.
181 func TestLaunchCommandsCdIntoEachWorkspace(t *testing.T) {
182 skipOnWindows(t)
183 m := newMultiWS(t)
184 m.ensure(t, m.wsA)
185 m.ensure(t, m.wsB)
186
187 for _, ws := range []struct {
188 cd string
189 own, other StatePaths
190 }{
191 {"cd '" + m.wsA + "'", m.pathsA, m.pathsB},
192 {"cd '" + m.wsB + "'", m.pathsB, m.pathsA},
193 } {
194 var launch string
195 for _, cmd := range m.conn.execsContaining("nohup") {
196 if strings.Contains(cmd, ws.cd) {
197 launch = cmd
198 break
199 }
200 }
201 if launch == "" {
202 t.Fatalf("no launch command for %s", ws.cd)
203 }
204 if !strings.Contains(launch, ws.own.TokenFile) || !strings.Contains(launch, ws.own.PortFile) {
205 t.Fatalf("launch for %s misses its own state paths: %s", ws.cd, launch)
206 }
207 if strings.Contains(launch, ws.other.TokenFile) || strings.Contains(launch, ws.other.PortFile) {
208 t.Fatalf("launch for %s references the other workspace's paths: %s", ws.cd, launch)
209 }
210 }
211 }
212
212 lines GO