bash_test.go

  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}