@@ -1,174 +0,0 @@
1−package tools
2−
3−import (
4− "context"
5− "encoding/json"
6− "os"
7− "path/filepath"
8− "strings"
9− "testing"
10− "time"
11−
12− "charm.land/fantasy"
13− "github.com/charmbracelet/crush/internal/permission"
14− "github.com/charmbracelet/crush/internal/pubsub"
15− "github.com/stretchr/testify/require"
16−)
17−
18−// recordingPermissionService captures permission requests and answers them
19−// according to a configurable response.
20−type recordingPermissionService struct {
21− *pubsub.Broker[permission.PermissionRequest]
22− requests []permission.CreatePermissionRequest
23− grant bool
24−}
25−
26−func (m *recordingPermissionService) Request(ctx context.Context, req permission.CreatePermissionRequest) (bool, error) {
27− m.requests = append(m.requests, req)
28− return m.grant, nil
29−}
30−
31−func (m *recordingPermissionService) Grant(req permission.PermissionRequest) {}
32−func (m *recordingPermissionService) Deny(req permission.PermissionRequest) {}
33−func (m *recordingPermissionService) GrantPersistent(req permission.PermissionRequest) {}
34−func (m *recordingPermissionService) AutoApproveSession(sessionID string) {}
35−func (m *recordingPermissionService) SetSkipRequests(skip bool) {}
36−func (m *recordingPermissionService) SkipRequests() bool { return false }
37−func (m *recordingPermissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[permission.PermissionNotification] {
38− return make(<-chan pubsub.Event[permission.PermissionNotification])
39−}
40−
41−type mockFileTrackerService struct{}
42−
43−func (m mockFileTrackerService) RecordRead(ctx context.Context, sessionID, path string) {}
44−
45−func (m mockFileTrackerService) LastReadTime(ctx context.Context, sessionID, path string) time.Time {
46− return time.Now()
47−}
48−
49−func (m mockFileTrackerService) ListReadFiles(ctx context.Context, sessionID string) ([]string, error) {
50− return nil, nil
51−}
52−
53−func TestTouchToolCreatesEmptyFile(t *testing.T) {
54− t.Parallel()
55−
56− workingDir := t.TempDir()
57− tool := NewTouchTool(nil, &mockPermissionService{}, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
58− ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
59−
60− resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "nested/empty.txt"})
61− require.False(t, resp.IsError)
62−
63− filePath := filepath.Join(workingDir, "nested", "empty.txt")
64− info, err := os.Stat(filePath)
65− require.NoError(t, err)
66− require.False(t, info.IsDir())
67− require.Zero(t, info.Size())
68−}
69−
70−func TestTouchToolRefusesExistingFile(t *testing.T) {
71− t.Parallel()
72−
73− workingDir := t.TempDir()
74− filePath := filepath.Join(workingDir, "existing.txt")
75− require.NoError(t, os.WriteFile(filePath, []byte("content"), 0o644))
76−
77− tool := NewTouchTool(nil, &mockPermissionService{}, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
78− ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
79−
80− resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "existing.txt"})
81− require.True(t, resp.IsError)
82− require.Contains(t, resp.Content, "File already exists")
83−
84− content, err := os.ReadFile(filePath)
85− require.NoError(t, err)
86− require.Equal(t, "content", string(content))
87−}
88−
89−func TestTouchToolStaysInsideWorkingDir(t *testing.T) {
90− t.Parallel()
91−
92− workingDir := t.TempDir()
93− perms := &recordingPermissionService{grant: true}
94− tool := NewTouchTool(nil, perms, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
95− ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
96−
97− resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "inside.txt"})
98− require.False(t, resp.IsError)
99−
100− for _, req := range perms.requests {
101− require.NotContains(t, req.Description, "outside working directory",
102− "inside-workingDir touch should not trigger an outside-workingDir permission prompt")
103− }
104−
105− _, err := os.Stat(filepath.Join(workingDir, "inside.txt"))
106− require.NoError(t, err)
107−}
108−
109−func TestTouchToolOutsideWorkingDirRequiresPermission(t *testing.T) {
110− t.Parallel()
111−
112− parent := t.TempDir()
113− workingDir := filepath.Join(parent, "wd")
114− require.NoError(t, os.MkdirAll(workingDir, 0o755))
115−
116− // Denied: file outside workingDir must not be created.
117− deny := &recordingPermissionService{grant: false}
118− tool := NewTouchTool(nil, deny, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
119− ctx := context.WithValue(context.Background(), SessionIDContextKey, "test-session")
120−
121− resp := runTouchTool(t, tool, ctx, TouchParams{FilePath: "../escape.txt"})
122− require.True(t, resp.IsError)
123−
124− require.Len(t, deny.requests, 1)
125− require.True(t, strings.Contains(deny.requests[0].Description, "outside working directory"),
126− "expected outside-working-directory permission prompt, got %q", deny.requests[0].Description)
127−
128− _, err := os.Stat(filepath.Join(parent, "escape.txt"))
129− require.True(t, os.IsNotExist(err), "denied permission should not create the file")
130−
131− // Granted: same path now succeeds.
132− grant := &recordingPermissionService{grant: true}
133− tool = NewTouchTool(nil, grant, &mockHistoryService{}, mockFileTrackerService{}, workingDir)
134− resp = runTouchTool(t, tool, ctx, TouchParams{FilePath: "../escape.txt"})
135− require.False(t, resp.IsError)
136− require.GreaterOrEqual(t, len(grant.requests), 1)
137− require.Contains(t, grant.requests[0].Description, "outside working directory")
138−
139− _, err = os.Stat(filepath.Join(parent, "escape.txt"))
140− require.NoError(t, err)
141−}
142−
143−func TestWriteToolEmptyContentPointsToTouch(t *testing.T) {
144− t.Parallel()
145−
146− tool := NewWriteTool(nil, nil, nil, nil, t.TempDir())
147−
148− input, err := json.Marshal(WriteParams{FilePath: "empty.txt"})
149− require.NoError(t, err)
150−
151− resp, err := tool.Run(context.Background(), fantasy.ToolCall{
152− ID: "test-call",
153− Name: WriteToolName,
154− Input: string(input),
155− })
156− require.NoError(t, err)
157− require.True(t, resp.IsError)
158− require.Equal(t, `content is required. use the "touch" tool to create an empty file`, resp.Content)
159−}
160−
161−func runTouchTool(t *testing.T, tool fantasy.AgentTool, ctx context.Context, params TouchParams) fantasy.ToolResponse {
162− t.Helper()
163−
164− input, err := json.Marshal(params)
165− require.NoError(t, err)
166−
167− resp, err := tool.Run(ctx, fantasy.ToolCall{
168− ID: "test-call",
169− Name: TouchToolName,
170− Input: string(input),
171− })
172− require.NoError(t, err)
173− return resp
174−}