|
7 | 7 | "fmt" |
8 | 8 | "net/http" |
9 | 9 | "net/http/httptest" |
| 10 | + "os" |
| 11 | + "path/filepath" |
10 | 12 | "runtime" |
11 | 13 | "strings" |
12 | 14 | "testing" |
@@ -97,18 +99,25 @@ func postSignal(t *testing.T, handler http.Handler, id string, req workspacesdk. |
97 | 99 | // execer, returning the handler and API. |
98 | 100 | func newTestAPI(t *testing.T) http.Handler { |
99 | 101 | t.Helper() |
100 | | - return newTestAPIWithUpdateEnv(t, nil) |
| 102 | + return newTestAPIWithOptions(t, nil, nil) |
101 | 103 | } |
102 | 104 |
|
103 | 105 | // newTestAPIWithUpdateEnv creates a new API with an optional |
104 | 106 | // updateEnv hook for testing environment injection. |
105 | 107 | func newTestAPIWithUpdateEnv(t *testing.T, updateEnv func([]string) ([]string, error)) http.Handler { |
106 | 108 | t.Helper() |
| 109 | + return newTestAPIWithOptions(t, updateEnv, nil) |
| 110 | +} |
| 111 | + |
| 112 | +// newTestAPIWithOptions creates a new API with optional |
| 113 | +// updateEnv and workingDir hooks. |
| 114 | +func newTestAPIWithOptions(t *testing.T, updateEnv func([]string) ([]string, error), workingDir func() string) http.Handler { |
| 115 | + t.Helper() |
107 | 116 |
|
108 | 117 | logger := slogtest.Make(t, &slogtest.Options{ |
109 | 118 | IgnoreErrors: true, |
110 | 119 | }).Leveled(slog.LevelDebug) |
111 | | - api := agentproc.NewAPI(logger, agentexec.DefaultExecer, updateEnv, nil) |
| 120 | + api := agentproc.NewAPI(logger, agentexec.DefaultExecer, updateEnv, nil, workingDir) |
112 | 121 | t.Cleanup(func() { |
113 | 122 | _ = api.Close() |
114 | 123 | }) |
@@ -253,6 +262,89 @@ func TestStartProcess(t *testing.T) { |
253 | 262 | require.Contains(t, resp.Output, "marker.txt") |
254 | 263 | }) |
255 | 264 |
|
| 265 | + t.Run("DefaultWorkDirIsHome", func(t *testing.T) { |
| 266 | + t.Parallel() |
| 267 | + |
| 268 | + // No working directory closure, so the process |
| 269 | + // should fall back to $HOME. We verify via a |
| 270 | + // marker file instead of comparing pwd output, |
| 271 | + // which resolves symlinks and breaks on macOS |
| 272 | + // where /tmp -> /private/tmp. |
| 273 | + handler := newTestAPI(t) |
| 274 | + |
| 275 | + homeDir, err := os.UserHomeDir() |
| 276 | + require.NoError(t, err) |
| 277 | + |
| 278 | + markerName := fmt.Sprintf("marker-%d.txt", time.Now().UnixNano()) |
| 279 | + markerPath := filepath.Join(homeDir, markerName) |
| 280 | + require.NoError(t, os.WriteFile(markerPath, nil, 0o600)) |
| 281 | + t.Cleanup(func() { os.Remove(markerPath) }) |
| 282 | + |
| 283 | + id := startAndGetID(t, handler, workspacesdk.StartProcessRequest{ |
| 284 | + Command: fmt.Sprintf("ls %s", markerName), |
| 285 | + }) |
| 286 | + |
| 287 | + resp := waitForExit(t, handler, id) |
| 288 | + require.NotNil(t, resp.ExitCode) |
| 289 | + require.Equal(t, 0, *resp.ExitCode) |
| 290 | + require.Contains(t, resp.Output, markerName) |
| 291 | + }) |
| 292 | + |
| 293 | + t.Run("DefaultWorkDirFromClosure", func(t *testing.T) { |
| 294 | + t.Parallel() |
| 295 | + |
| 296 | + // The closure provides a valid directory, so the |
| 297 | + // process should start there. We verify via a |
| 298 | + // marker file instead of comparing pwd output, |
| 299 | + // which resolves symlinks and breaks on macOS |
| 300 | + // where /tmp -> /private/tmp. |
| 301 | + tmpDir := t.TempDir() |
| 302 | + handler := newTestAPIWithOptions(t, nil, func() string { |
| 303 | + return tmpDir |
| 304 | + }) |
| 305 | + |
| 306 | + markerName := fmt.Sprintf("marker-%d.txt", time.Now().UnixNano()) |
| 307 | + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, markerName), nil, 0o600)) |
| 308 | + |
| 309 | + id := startAndGetID(t, handler, workspacesdk.StartProcessRequest{ |
| 310 | + Command: fmt.Sprintf("ls %s", markerName), |
| 311 | + }) |
| 312 | + |
| 313 | + resp := waitForExit(t, handler, id) |
| 314 | + require.NotNil(t, resp.ExitCode) |
| 315 | + require.Equal(t, 0, *resp.ExitCode) |
| 316 | + require.Contains(t, resp.Output, markerName) |
| 317 | + }) |
| 318 | + |
| 319 | + t.Run("DefaultWorkDirClosureNonExistentFallsBackToHome", func(t *testing.T) { |
| 320 | + t.Parallel() |
| 321 | + |
| 322 | + // The closure returns a path that doesn't exist, |
| 323 | + // so the process should fall back to $HOME. We |
| 324 | + // verify via a marker file instead of comparing |
| 325 | + // pwd output, which resolves symlinks. |
| 326 | + handler := newTestAPIWithOptions(t, nil, func() string { |
| 327 | + return "/tmp/nonexistent-dir-" + fmt.Sprintf("%d", time.Now().UnixNano()) |
| 328 | + }) |
| 329 | + |
| 330 | + homeDir, err := os.UserHomeDir() |
| 331 | + require.NoError(t, err) |
| 332 | + |
| 333 | + markerName := fmt.Sprintf("marker-%d.txt", time.Now().UnixNano()) |
| 334 | + markerPath := filepath.Join(homeDir, markerName) |
| 335 | + require.NoError(t, os.WriteFile(markerPath, nil, 0o600)) |
| 336 | + t.Cleanup(func() { os.Remove(markerPath) }) |
| 337 | + |
| 338 | + id := startAndGetID(t, handler, workspacesdk.StartProcessRequest{ |
| 339 | + Command: fmt.Sprintf("ls %s", markerName), |
| 340 | + }) |
| 341 | + |
| 342 | + resp := waitForExit(t, handler, id) |
| 343 | + require.NotNil(t, resp.ExitCode) |
| 344 | + require.Equal(t, 0, *resp.ExitCode) |
| 345 | + require.Contains(t, resp.Output, markerName) |
| 346 | + }) |
| 347 | + |
256 | 348 | t.Run("CustomEnv", func(t *testing.T) { |
257 | 349 | t.Parallel() |
258 | 350 |
|
@@ -781,7 +873,7 @@ func TestHandleStartProcess_ChatHeaders_EmptyWorkDir_StillNotifies(t *testing.T) |
781 | 873 | logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) |
782 | 874 | api := agentproc.NewAPI(logger, agentexec.DefaultExecer, func(current []string) ([]string, error) { |
783 | 875 | return current, nil |
784 | | - }, pathStore) |
| 876 | + }, pathStore, nil) |
785 | 877 | defer api.Close() |
786 | 878 |
|
787 | 879 | routes := api.Routes() |
|
0 commit comments