返回 DeepSeek-Reasonix
blocked_tool_e2e_test.go
根目录 / internal / agent / blocked_tool_e2e_test.go
1 package agent
2
3 import (
4 "context"
5 "strings"
6 "testing"
7
8 "reasonix/internal/agent/testutil"
9 "reasonix/internal/event"
10 "reasonix/internal/provider"
11 "reasonix/internal/tool"
12 )
13
14 // A read-only subagent refusing its own bash call must reach the frontend as a
15 // blocked result, not as a completed one: the command never ran.
16 func TestReadOnlySubagentBashRefusalReachesSinkAsBlocked(t *testing.T) {
17 parent := tool.NewRegistry()
18 parent.Add(subagentRegistryTool{
19 name: "bash",
20 schema: `{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}`,
21 result: "ran",
22 })
23 sub := ReadOnlySubagentToolRegistry(parent, nil)
24
25 mp := testutil.NewMock("m",
26 testutil.Turn{ToolCalls: []provider.ToolCall{
27 {ID: "call-1", Name: "bash", Arguments: `{"command":"node --version 2>&1; bash --version"}`},
28 }},
29 testutil.Turn{Text: "done"},
30 )
31 sink := &recordSink{}
32 a := New(mp, sub, NewSession(""), Options{}, sink)
33 if err := a.Run(context.Background(), "probe the toolchain"); err != nil {
34 t.Fatalf("Run: %v", err)
35 }
36
37 results := sink.kinds(event.ToolResult)
38 if len(results) != 1 {
39 t.Fatalf("ToolResult events = %d, want 1", len(results))
40 }
41 got := results[0].Tool
42 if got.Err == "" {
43 t.Fatalf("refused call must carry an error so a frontend can show it as blocked: %+v", got)
44 }
45 if !strings.Contains(got.Output, "blocked:") {
46 t.Fatalf("model-facing output should state the refusal, got %q", got.Output)
47 }
48 if got.Output == "ran" {
49 t.Fatal("refused command must not execute")
50 }
51 if got.Execution == nil || got.Execution.State != tool.ShellStateNotRun {
52 t.Fatalf("refused bash card needs not-run shell metadata, got %+v", got.Execution)
53 }
54 }
55
55 lines GO