返回 DeepSeek-Reasonix
token_profile_lock_test.go
根目录 / internal / boot / token_profile_lock_test.go
1 package boot
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8 "testing"
9 "time"
10
11 "reasonix/internal/agent"
12 "reasonix/internal/event"
13 )
14
15 func TestConnectToolSourcePlanModeLoadsOptionalSources(t *testing.T) {
16 tsc := &toolSourceConnector{
17 docs: func(context.Context) (string, error) { return "enabled docs.", nil },
18 sessions: func(context.Context) (string, error) { return "enabled sessions.", nil },
19 commands: func(context.Context) (string, error) { return "enabled commands.", nil },
20 search: func(context.Context) (string, error) { return "enabled search.", nil },
21 workflow: func(context.Context) (string, error) { return "enabled todo_write.", nil },
22 memory: func(context.Context) (string, error) { return "enabled memory.", nil },
23 }
24 ctx := agent.WithToolCallContext(context.Background(), "call", event.Discard, nil, true)
25 for _, source := range []string{"docs", "sessions", "commands", "search", "workflow", "memory"} {
26 out, err := tsc.Execute(ctx, json.RawMessage(fmt.Sprintf(`{"source":%q}`, source)))
27 if err != nil {
28 t.Fatalf("source %s: %v", source, err)
29 }
30 if strings.Contains(out, "blocked:") {
31 t.Fatalf("source %s should load in Plan before permissioned tool use: %s", source, out)
32 }
33 }
34 }
35
36 // A slow MCP connect (spawning the server subprocess) must run outside
37 // t.mu: the callback probes the lock and fails if Execute still holds it.
38 func TestConnectToolSourceMCPConnectRunsWithoutLock(t *testing.T) {
39 tsc := &toolSourceConnector{}
40 tsc.mcp = func(context.Context, string) (string, error) {
41 free := make(chan struct{})
42 go func() {
43 tsc.mu.Lock()
44 tsc.mu.Unlock() //nolint:staticcheck // probe: lock must be immediately acquirable
45 close(free)
46 }()
47 select {
48 case <-free:
49 case <-time.After(500 * time.Millisecond):
50 t.Error("t.mu still held while the MCP connect callback was running")
51 }
52 return `enabled MCP server "srv" tools: mcp__srv__x.`, nil
53 }
54
55 out, err := tsc.Execute(context.Background(), json.RawMessage(`{"source":"mcp","name":"srv"}`))
56 if err != nil {
57 t.Fatalf("Execute error: %v", err)
58 }
59 if want := `enabled MCP server "srv" tools: mcp__srv__x.`; out != want {
60 t.Fatalf("Execute output = %q, want %q", out, want)
61 }
62 }
63
64 // A connect_tool_source call for a fast source (web_fetch) must not queue
65 // behind a concurrent MCP connect that is stuck spawning its server.
66 func TestConnectToolSourceSlowMCPDoesNotBlockFastSource(t *testing.T) {
67 started := make(chan struct{})
68 release := make(chan struct{})
69 tsc := &toolSourceConnector{
70 webFetch: func(context.Context) (string, error) { return "enabled web_fetch.", nil },
71 mcp: func(context.Context, string) (string, error) {
72 close(started)
73 <-release
74 return `enabled MCP server "slow" tools: mcp__slow__x.`, nil
75 },
76 mcpNames: []string{"slow"},
77 }
78
79 slowDone := make(chan error, 1)
80 go func() {
81 _, err := tsc.Execute(context.Background(), json.RawMessage(`{"source":"mcp","name":"slow"}`))
82 slowDone <- err
83 }()
84
85 select {
86 case <-started:
87 case <-time.After(time.Second):
88 t.Fatal("slow MCP connect callback never started")
89 }
90
91 fastDone := make(chan struct{})
92 go func() {
93 defer close(fastDone)
94 out, err := tsc.Execute(context.Background(), json.RawMessage(`{"source":"web_fetch"}`))
95 if err != nil {
96 t.Errorf("web_fetch Execute error: %v", err)
97 return
98 }
99 if out != "enabled web_fetch." {
100 t.Errorf("web_fetch Execute output = %q, want %q", out, "enabled web_fetch.")
101 }
102 }()
103
104 select {
105 case <-fastDone:
106 case <-time.After(time.Second):
107 t.Fatal("web_fetch connect blocked behind an in-flight MCP connect")
108 }
109
110 close(release)
111 if err := <-slowDone; err != nil {
112 t.Fatalf("slow MCP Execute error: %v", err)
113 }
114 }
115
116 // The fast MCP paths (listing servers, missing callback) keep their existing
117 // behavior and still run under the lock.
118 func TestConnectToolSourceMCPFastPathsUnchanged(t *testing.T) {
119 tsc := &toolSourceConnector{mcpNames: []string{"b", "a"}}
120 out, err := tsc.Execute(context.Background(), json.RawMessage(`{"source":"mcp"}`))
121 if err != nil {
122 t.Fatalf("list Execute error: %v", err)
123 }
124 want := `Configured MCP servers: a, b. Call connect_tool_source again with source="mcp" and name set to connect one server.`
125 if out != want {
126 t.Fatalf("list output = %q, want %q", out, want)
127 }
128
129 if _, err := tsc.Execute(context.Background(), json.RawMessage(`{"source":"mcp","name":"a"}`)); err == nil {
130 t.Fatal("expected error when MCP callback is unavailable")
131 } else if err.Error() != "MCP source is unavailable in this session" {
132 t.Fatalf("unavailable error = %q", err.Error())
133 }
134 }
135
135 lines GO