返回 DeepSeek-Reasonix
transport_stdio_dir_test.go
根目录 / internal / plugin / transport_stdio_dir_test.go
1 package plugin
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "runtime"
8 "testing"
9
10 "reasonix/internal/mcplaunch"
11 )
12
13 // TestNewStdioTransportDirExplicit verifies that explicit Spec.Dir takes
14 // precedence over WorkspaceRoot for cmd.Dir.
15 func TestNewStdioTransportDirExplicit(t *testing.T) {
16 exe, err := os.Executable()
17 if err != nil {
18 t.Fatal(err)
19 }
20 explicitDir := filepath.Join(t.TempDir(), "explicit")
21 if err := os.MkdirAll(explicitDir, 0o755); err != nil {
22 t.Fatal(err)
23 }
24 workspaceRoot := filepath.Join(t.TempDir(), "workspace")
25 if err := os.MkdirAll(workspaceRoot, 0o755); err != nil {
26 t.Fatal(err)
27 }
28 spec := Spec{
29 Name: "test-dir",
30 Command: exe,
31 Args: []string{"-test.run=TestHelperProcess", "--"},
32 Dir: explicitDir,
33 WorkspaceRoot: workspaceRoot,
34 Env: map[string]string{"GO_WANT_HELPER_PROCESS": "1"},
35 }
36 tr, err := newStdioTransport(context.Background(), spec)
37 if err != nil {
38 t.Fatalf("newStdioTransport: %v", err)
39 }
40 defer tr.close()
41 if tr.cmd.Dir != explicitDir {
42 t.Fatalf("cmd.Dir = %q, want %q (explicit Dir should take precedence)", tr.cmd.Dir, explicitDir)
43 }
44 }
45
46 // TestNewStdioTransportProjectDirFallbackWorkspaceRoot verifies that when
47 // Spec.Dir is empty, a project-provided subprocess falls back to WorkspaceRoot.
48 // This prevents relative config file paths (e.g. --config-file ssh-config.json
49 // in .mcp.json) from resolving against the desktop process CWD instead of the
50 // project root where the config file lives (#6778).
51 func TestNewStdioTransportProjectDirFallbackWorkspaceRoot(t *testing.T) {
52 exe, err := os.Executable()
53 if err != nil {
54 t.Fatal(err)
55 }
56 workspaceRoot := filepath.Join(t.TempDir(), "workspace")
57 if err := os.MkdirAll(workspaceRoot, 0o755); err != nil {
58 t.Fatal(err)
59 }
60 spec := Spec{
61 Name: "test-fallback",
62 Command: exe,
63 Args: []string{"-test.run=TestHelperProcess", "--"},
64 WorkspaceRoot: workspaceRoot,
65 RequireLaunchApproval: true,
66 Env: map[string]string{"GO_WANT_HELPER_PROCESS": "1"},
67 }
68 tr, err := newStdioTransport(context.Background(), spec)
69 if err != nil {
70 t.Fatalf("newStdioTransport: %v", err)
71 }
72 defer tr.close()
73 if tr.cmd.Dir != workspaceRoot {
74 t.Fatalf("cmd.Dir = %q, want %q (should fall back to WorkspaceRoot when Dir is empty)", tr.cmd.Dir, workspaceRoot)
75 }
76 }
77
78 // TestNewStdioTransportUserScopesKeepInheritedDir preserves WorkspaceRoot as
79 // roots/list metadata for installed servers without changing their process CWD.
80 func TestNewStdioTransportUserScopesKeepInheritedDir(t *testing.T) {
81 for _, source := range []string{"user_config", "legacy_user_config", "plugin_package"} {
82 t.Run(source, func(t *testing.T) {
83 exe, err := os.Executable()
84 if err != nil {
85 t.Fatal(err)
86 }
87 spec := Spec{
88 Name: "test-user-scope",
89 Command: exe,
90 Args: []string{"-test.run=TestHelperProcess", "--"},
91 WorkspaceRoot: t.TempDir(),
92 ConfigSource: source,
93 Env: map[string]string{"GO_WANT_HELPER_PROCESS": "1"},
94 }
95 tr, err := newStdioTransport(context.Background(), spec)
96 if err != nil {
97 t.Fatalf("newStdioTransport: %v", err)
98 }
99 defer tr.close()
100 if tr.cmd.Dir != "" {
101 t.Fatalf("cmd.Dir = %q, want inherited process CWD", tr.cmd.Dir)
102 }
103 })
104 }
105 }
106
107 func TestProjectRelativeExecutableResolutionMatchesLaunchIdentity(t *testing.T) {
108 processRoot := t.TempDir()
109 workspaceRoot := t.TempDir()
110 t.Chdir(processRoot)
111
112 name := "server"
113 if runtime.GOOS == "windows" {
114 name += ".exe"
115 }
116 relativeCommand := "." + string(os.PathSeparator) + name
117 processExecutable := filepath.Join(processRoot, name)
118 workspaceExecutable := filepath.Join(workspaceRoot, name)
119 if err := os.WriteFile(processExecutable, []byte("process cwd executable"), 0o755); err != nil {
120 t.Fatal(err)
121 }
122 if err := os.WriteFile(workspaceExecutable, []byte("workspace executable"), 0o755); err != nil {
123 t.Fatal(err)
124 }
125
126 spec := Spec{
127 Name: "project-relative-command",
128 Command: relativeCommand,
129 WorkspaceRoot: workspaceRoot,
130 RequireLaunchApproval: true,
131 }
132 exe, _, err := resolveStdioExecutable(context.Background(), spec, os.Environ())
133 if err != nil {
134 t.Fatalf("resolveStdioExecutable: %v", err)
135 }
136 identity, err := buildProjectLaunchIdentity(context.Background(), spec)
137 if err != nil {
138 t.Fatalf("buildProjectLaunchIdentity: %v", err)
139 }
140 if exe != workspaceExecutable || identity.CommandPath != workspaceExecutable {
141 t.Fatalf("resolved executable = %q, identity path = %q, want %q", exe, identity.CommandPath, workspaceExecutable)
142 }
143 if identity.Dir != workspaceRoot {
144 t.Fatalf("identity.Dir = %q, want %q", identity.Dir, workspaceRoot)
145 }
146 wantWorkspaceHash, err := mcplaunch.FileSHA256(workspaceExecutable)
147 if err != nil {
148 t.Fatal(err)
149 }
150 processHash, err := mcplaunch.FileSHA256(processExecutable)
151 if err != nil {
152 t.Fatal(err)
153 }
154 if identity.CommandSHA256 != wantWorkspaceHash || identity.CommandSHA256 == processHash {
155 t.Fatalf("identity executable hash = %q, want workspace hash %q and not process hash %q", identity.CommandSHA256, wantWorkspaceHash, processHash)
156 }
157
158 before, err := mcplaunch.ProjectLaunchIdentityDigest(identity)
159 if err != nil {
160 t.Fatal(err)
161 }
162 if err := os.WriteFile(workspaceExecutable, []byte("changed workspace executable"), 0o755); err != nil {
163 t.Fatal(err)
164 }
165 changedIdentity, err := buildProjectLaunchIdentity(context.Background(), spec)
166 if err != nil {
167 t.Fatal(err)
168 }
169 after, err := mcplaunch.ProjectLaunchIdentityDigest(changedIdentity)
170 if err != nil {
171 t.Fatal(err)
172 }
173 if before == after {
174 t.Fatal("workspace executable mutation did not invalidate the launch identity")
175 }
176 }
177
178 // TestNewStdioTransportDirEmptyWhenBothEmpty verifies that cmd.Dir remains
179 // empty (inherits parent CWD) when both Dir and WorkspaceRoot are empty.
180 func TestNewStdioTransportDirEmptyWhenBothEmpty(t *testing.T) {
181 exe, err := os.Executable()
182 if err != nil {
183 t.Fatal(err)
184 }
185 spec := Spec{
186 Name: "test-empty",
187 Command: exe,
188 Args: []string{"-test.run=TestHelperProcess", "--"},
189 Env: map[string]string{"GO_WANT_HELPER_PROCESS": "1"},
190 }
191 tr, err := newStdioTransport(context.Background(), spec)
192 if err != nil {
193 t.Fatalf("newStdioTransport: %v", err)
194 }
195 defer tr.close()
196 if tr.cmd.Dir != "" {
197 t.Fatalf("cmd.Dir = %q, want empty (should inherit parent CWD when both Dir and WorkspaceRoot are empty)", tr.cmd.Dir)
198 }
199 }
200
201 // TestNewStdioTransportDirDoesNotOverwriteForCodeGraph confirms the fix does
202 // not regress CodeGraph / codebase-memory-mcp which set Dir via
203 // ApplyKnownOverrides.
204 func TestNewStdioTransportDirDoesNotOverwriteForCodeGraph(t *testing.T) {
205 exe, err := os.Executable()
206 if err != nil {
207 t.Fatal(err)
208 }
209 projectRoot := filepath.Join(t.TempDir(), "project")
210 if err := os.MkdirAll(projectRoot, 0o755); err != nil {
211 t.Fatal(err)
212 }
213 // Simulate what ApplyKnownOverrides does: set Dir = workspaceRoot for CodeGraph.
214 spec := Spec{
215 Name: "codegraph",
216 Command: exe,
217 Args: []string{"-test.run=TestHelperProcess", "--"},
218 Dir: projectRoot, // set by ApplyKnownOverrides
219 WorkspaceRoot: projectRoot,
220 Env: map[string]string{"GO_WANT_HELPER_PROCESS": "1"},
221 LowPriority: true,
222 }
223 tr, err := newStdioTransport(context.Background(), spec)
224 if err != nil {
225 t.Fatalf("newStdioTransport: %v", err)
226 }
227 defer tr.close()
228 if tr.cmd.Dir != projectRoot {
229 t.Fatalf("cmd.Dir = %q, want %q (CodeGraph Dir should be preserved)", tr.cmd.Dir, projectRoot)
230 }
231 }
232
232 lines GO