返回 DeepSeek-Reasonix
outcome_test.go
根目录 / internal / agent / outcome_test.go
1 package agent
2
3 import (
4 "context"
5 "strings"
6 "testing"
7
8 "reasonix/internal/event"
9 "reasonix/internal/provider"
10 "reasonix/internal/tool"
11 )
12
13 type mcpAliasTool struct {
14 fakeTool
15 server string
16 raw string
17 visible string
18 pkg string
19 }
20
21 func (t mcpAliasTool) MCPServerName() string { return t.server }
22 func (t mcpAliasTool) MCPRawToolName() string { return t.raw }
23 func (t mcpAliasTool) MCPVisibleToolName() string { return t.visible }
24 func (t mcpAliasTool) MCPPackageName() string { return t.pkg }
25 func (t mcpAliasTool) MCPServerAuthorized() bool { return true }
26
27 // TestFailedCallsSurfaceError guards the bug where a failed tool call (an unknown
28 // tool, e.g. a hallucinated "find", or a permission-denied writer) was reported
29 // with an empty Err and so rendered with a success check. A failed call must set
30 // errMsg; a successful one must not.
31 func TestFailedCallsSurfaceError(t *testing.T) {
32 reg := tool.NewRegistry()
33 reg.Add(fakeTool{name: "ok_tool", readOnly: true})
34 reg.Add(fakeTool{name: "writer", readOnly: false})
35 gate := &recordingPermissionGate{allow: true}
36 a := New(nil, reg, NewSession(""), Options{Gate: gate}, event.Discard)
37
38 if o := a.executeOne(context.Background(), provider.ToolCall{Name: "ok_tool"}); o.errMsg != "" {
39 t.Errorf("successful call should have empty errMsg, got %q", o.errMsg)
40 }
41 if o := a.executeOne(context.Background(), provider.ToolCall{Name: "find"}); o.errMsg == "" {
42 t.Errorf("unknown tool should surface an errMsg (renders as failed), got %+v", o)
43 }
44
45 a.SetPlanMode(true)
46 gate.allow = false
47 gate.reason = "denied by permission policy"
48 if o := a.executeOne(context.Background(), provider.ToolCall{Name: "writer"}); o.errMsg == "" {
49 t.Errorf("permission-denied writer should surface an errMsg, got %+v", o)
50 }
51 }
52
53 func TestCompletedMCPConnectIsNotReportedAsUnknown(t *testing.T) {
54 reg := tool.NewRegistry()
55 reg.Add(mcpAliasTool{
56 fakeTool: fakeTool{name: "mcp__mock__echo", readOnly: true},
57 server: "mock",
58 raw: "echo",
59 visible: "echo",
60 })
61 a := New(nil, reg, NewSession(""), Options{}, event.Discard)
62
63 out := a.executeOne(context.Background(), provider.ToolCall{Name: "mcp__mock__connect", Arguments: `{}`})
64 if out.errMsg != "" || !strings.Contains(out.output, `MCP server "mock" is connected`) {
65 t.Fatalf("completed connect was not recovered: %+v", out)
66 }
67 for _, name := range []string{"mcp__missing__connect", "mcp__mock__not_connect"} {
68 if out := a.executeOne(context.Background(), provider.ToolCall{Name: name, Arguments: `{}`}); out.errMsg == "" {
69 t.Fatalf("unadvertised call %q should remain unknown: %+v", name, out)
70 }
71 }
72 }
73
74 func TestPortableMCPCallUsesCanonicalSecurityIdentity(t *testing.T) {
75 reg := tool.NewRegistry()
76 reg.Add(mcpAliasTool{
77 fakeTool: fakeTool{name: "mcp__figma__get_design_context", readOnly: true},
78 server: "figma",
79 raw: "figma_get_design_context",
80 visible: "get_design_context",
81 pkg: "figma",
82 })
83 gate := &recordingPermissionGate{allow: true}
84 a := New(nil, reg, NewSession(""), Options{Gate: gate}, event.Discard)
85
86 out := a.executeOne(context.Background(), provider.ToolCall{Name: "get_design_context", Arguments: `{}`})
87 if out.errMsg != "" {
88 t.Fatalf("portable MCP call failed: %+v", out)
89 }
90 if len(gate.denyCalls) != 1 || gate.denyCalls[0] != "mcp__figma__get_design_context" {
91 t.Fatalf("permission deny checks = %v, want canonical MCP name", gate.denyCalls)
92 }
93 }
94
95 func TestAmbiguousPortableMCPCallIsRejected(t *testing.T) {
96 reg := tool.NewRegistry()
97 reg.Add(mcpAliasTool{fakeTool: fakeTool{name: "mcp__one__search", readOnly: true}, server: "one", raw: "search", visible: "search"})
98 reg.Add(mcpAliasTool{fakeTool: fakeTool{name: "mcp__two__search", readOnly: true}, server: "two", raw: "search", visible: "search"})
99 a := New(nil, reg, NewSession(""), Options{}, event.Discard)
100
101 out := a.executeOne(context.Background(), provider.ToolCall{Name: "search", Arguments: `{}`})
102 if out.errMsg == "" || !strings.Contains(out.errMsg, "ambiguous MCP tool reference") {
103 t.Fatalf("ambiguous MCP alias was not rejected: %+v", out)
104 }
105 }
106
106 lines GO