返回 DeepSeek-Reasonix
process_mode_test.go
根目录 / internal / plugin / process_mode_test.go
1 package plugin
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "reasonix/internal/sandbox"
11 )
12
13 func TestResolvedProcessModeDefaultsToHost(t *testing.T) {
14 if got := (Spec{}).ResolvedProcessMode(); got != MCPProcessHost {
15 t.Fatalf("empty ProcessMode = %q, want host", got)
16 }
17 if got := (Spec{ProcessMode: MCPProcessConfined}).ResolvedProcessMode(); got != MCPProcessConfined {
18 t.Fatalf("confined ProcessMode = %q", got)
19 }
20 }
21
22 func TestHostModeEnsureConnectedSkipsCommandSandboxAndSharesProcess(t *testing.T) {
23 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
24 defer cancel()
25
26 startCount := filepath.Join(t.TempDir(), "starts")
27 stateDir := t.TempDir()
28 spec := Spec{
29 Name: "hostmode",
30 Command: os.Args[0],
31 Args: []string{"-test.run=TestHelperProcess", "--"},
32 ProcessMode: MCPProcessHost,
33 StateDir: stateDir,
34 // Host mode must ignore an enforce sandbox; private StateDir still applies.
35 Sandbox: sandbox.Spec{
36 Mode: "enforce",
37 MinimalWrites: true,
38 WriteRoots: []string{stateDir},
39 },
40 Env: map[string]string{
41 "GO_WANT_HELPER_PROCESS": "1",
42 "GO_WANT_HELPER_START_COUNT": startCount,
43 "GO_WANT_HELPER_INIT_MS": "150",
44 },
45 }
46
47 host := NewHost()
48 defer host.Close()
49
50 type result struct {
51 n int
52 err error
53 }
54 ch := make(chan result, 3)
55 for i := 0; i < 3; i++ {
56 go func() {
57 tools, err := host.EnsureConnected(ctx, spec)
58 ch <- result{len(tools), err}
59 }()
60 }
61 for i := 0; i < 3; i++ {
62 r := <-ch
63 if r.err != nil {
64 t.Fatalf("EnsureConnected: %v", r.err)
65 }
66 if r.n != 2 {
67 t.Fatalf("tools = %d, want 2", r.n)
68 }
69 }
70 if got := readHelperCounter(t, startCount); got != 1 {
71 t.Fatalf("stdio process starts = %d, want 1", got)
72 }
73 // Private state directory is created even in host mode.
74 if _, err := os.Stat(filepath.Join(stateDir, "cache")); err != nil {
75 t.Fatalf("host mode should still create private cache dir: %v", err)
76 }
77 }
78
79 func TestEnsureConnectedCancelWaitDoesNotKillSharedProcess(t *testing.T) {
80 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
81 defer cancel()
82
83 startCount := filepath.Join(t.TempDir(), "starts")
84 spec := Spec{
85 Name: "shared-cancel",
86 Command: os.Args[0],
87 Args: []string{"-test.run=TestHelperProcess", "--"},
88 ProcessMode: MCPProcessHost,
89 StateDir: t.TempDir(),
90 Env: map[string]string{
91 "GO_WANT_HELPER_PROCESS": "1",
92 "GO_WANT_HELPER_START_COUNT": startCount,
93 "GO_WANT_HELPER_INIT_MS": "400",
94 },
95 }
96 host := NewHost()
97 defer host.Close()
98
99 // Start the long-lived owner first so it claims the single-flight spawn.
100 // A later short-timeout waiter must only cancel its wait, not the shared process.
101 ownerStarted := make(chan struct{})
102 ownerDone := make(chan error, 1)
103 go func() {
104 close(ownerStarted)
105 _, err := host.EnsureConnected(ctx, spec)
106 ownerDone <- err
107 }()
108 <-ownerStarted
109 // Give the owner time to claim beginSpawn before the waiter races it.
110 time.Sleep(30 * time.Millisecond)
111
112 waitCtx, waitCancel := context.WithTimeout(ctx, 40*time.Millisecond)
113 _, waitErr := host.EnsureConnected(waitCtx, spec)
114 waitCancel()
115 if waitErr == nil {
116 t.Fatal("cancelled waiter should return an error")
117 }
118
119 if err := <-ownerDone; err != nil {
120 t.Fatalf("owner EnsureConnected should still succeed after waiter cancel: %v", err)
121 }
122 if got := readHelperCounter(t, startCount); got != 1 {
123 t.Fatalf("shared process starts = %d, want 1", got)
124 }
125 if !host.HasClient("shared-cancel") {
126 t.Fatal("shared client should remain after waiter cancel")
127 }
128 }
129
129 lines GO