1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "testing"
7
8 "charm.land/fantasy"
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/permission"
11 "github.com/charmbracelet/crush/internal/pubsub"
12 "github.com/charmbracelet/crush/internal/shell"
13 "github.com/stretchr/testify/require"
14)
15
16type mockBashPermissionService struct {
17 *pubsub.Broker[permission.PermissionRequest]
18}
19
20func (m *mockBashPermissionService) Request(ctx context.Context, req permission.CreatePermissionRequest) (bool, error) {
21 return true, nil
22}
23
24func (m *mockBashPermissionService) Grant(req permission.PermissionRequest) {}
25
26func (m *mockBashPermissionService) Deny(req permission.PermissionRequest) {}
27
28func (m *mockBashPermissionService) GrantPersistent(req permission.PermissionRequest) {}
29
30func (m *mockBashPermissionService) AutoApproveSession(sessionID string) {}
31
32func (m *mockBashPermissionService) SetSkipRequests(skip bool) {}
33
34func (m *mockBashPermissionService) SkipRequests() bool {
35 return false
36}
37
38func (m *mockBashPermissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[permission.PermissionNotification] {
39 return make(<-chan pubsub.Event[permission.PermissionNotification])
40}
41
42func TestBashTool_DefaultAutoBackgroundThreshold(t *testing.T) {
43 workingDir := t.TempDir()
44 tool := newBashToolForTest(workingDir)
45 ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
46
47 resp := runBashTool(t, tool, ctx, BashParams{
48 Description: "default threshold",
49 Command: "echo done",
50 })
51
52 require.False(t, resp.IsError)
53 var meta BashResponseMetadata
54 require.NoError(t, json.Unmarshal([]byte(resp.Metadata), &meta))
55 require.False(t, meta.Background)
56 require.Empty(t, meta.ShellID)
57 require.Contains(t, meta.Output, "done")
58}
59
60func TestBashTool_CustomAutoBackgroundThreshold(t *testing.T) {
61 workingDir := t.TempDir()
62 tool := newBashToolForTest(workingDir)
63 ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
64
65 resp := runBashTool(t, tool, ctx, BashParams{
66 Description: "custom threshold",
67 Command: "sleep 1.5 && echo done",
68 AutoBackgroundAfter: 1,
69 })
70
71 require.False(t, resp.IsError)
72 var meta BashResponseMetadata
73 require.NoError(t, json.Unmarshal([]byte(resp.Metadata), &meta))
74 require.True(t, meta.Background)
75 require.NotEmpty(t, meta.ShellID)
76 require.Contains(t, resp.Content, "moved to background")
77
78 bgManager := shell.GetBackgroundShellManager()
79 require.NoError(t, bgManager.Kill(meta.ShellID))
80}
81
82func newBashToolForTest(workingDir string) fantasy.AgentTool {
83 permissions := &mockBashPermissionService{Broker: pubsub.NewBroker[permission.PermissionRequest]()}
84 attribution := &config.Attribution{TrailerStyle: config.TrailerStyleNone}
85 return NewBashTool(permissions, workingDir, attribution, "test-model")
86}
87
88func runBashTool(t *testing.T, tool fantasy.AgentTool, ctx context.Context, params BashParams) fantasy.ToolResponse {
89 t.Helper()
90
91 input, err := json.Marshal(params)
92 require.NoError(t, err)
93
94 call := fantasy.ToolCall{
95 ID: "test-call",
96 Name: BashToolName,
97 Input: string(input),
98 }
99
100 resp, err := tool.Run(ctx, call)
101 require.NoError(t, err)
102 return resp
103}