1package workspace
2
3import (
4 "encoding/json"
5 "io"
6 "net/http"
7 "net/http/httptest"
8 "net/url"
9 "testing"
10
11 "github.com/charmbracelet/crush/internal/client"
12 "github.com/charmbracelet/crush/internal/message"
13 "github.com/charmbracelet/crush/internal/permission"
14 "github.com/charmbracelet/crush/internal/proto"
15 "github.com/stretchr/testify/require"
16)
17
18// TestProtoToMessageToolResult ensures that ToolResult metadata,
19// data, and MIME type survive the conversion from proto on the
20// client. Without these fields the TUI cannot render rich tool
21// output (e.g. syntax-highlighted code from view, diffs from edit,
22// images, etc.) and falls back to the raw LLM-facing string.
23func TestProtoToMessageToolResult(t *testing.T) {
24 t.Parallel()
25
26 src := proto.Message{
27 ID: "m1",
28 Role: proto.Tool,
29 Parts: []proto.ContentPart{
30 proto.ToolResult{
31 ToolCallID: "call-1",
32 Name: "view",
33 Content: "<file>\n 1| hi\n</file>",
34 Data: "base64data",
35 MIMEType: "image/png",
36 Metadata: `{"file_path":"/tmp/x","content":"hi"}`,
37 IsError: false,
38 },
39 },
40 }
41
42 got := protoToMessage(src)
43 require.Len(t, got.Parts, 1)
44 tr, ok := got.Parts[0].(message.ToolResult)
45 require.True(t, ok, "expected message.ToolResult, got %T", got.Parts[0])
46 require.Equal(t, "call-1", tr.ToolCallID)
47 require.Equal(t, "view", tr.Name)
48 require.Equal(t, "<file>\n 1| hi\n</file>", tr.Content)
49 require.Equal(t, "base64data", tr.Data)
50 require.Equal(t, "image/png", tr.MIMEType)
51 require.Equal(t, `{"file_path":"/tmp/x","content":"hi"}`, tr.Metadata)
52 require.False(t, tr.IsError)
53}
54
55// TestClientWorkspace_PermissionGrantMapping verifies that
56// PermissionGrant on the ClientWorkspace serializes a one-time grant
57// (proto.PermissionAllow) and PermissionGrantPersistent serializes a
58// persistent grant (proto.PermissionAllowForSession). A swap between
59// these two would silently flip "allow once" into "remember for the
60// session", and vice versa, so we pin the wire mapping here.
61func TestClientWorkspace_PermissionGrantMapping(t *testing.T) {
62 t.Parallel()
63
64 cases := []struct {
65 name string
66 call func(*ClientWorkspace, permission.PermissionRequest)
67 want proto.PermissionAction
68 }{
69 {
70 name: "Grant -> PermissionAllow",
71 call: func(w *ClientWorkspace, p permission.PermissionRequest) {
72 w.PermissionGrant(p)
73 },
74 want: proto.PermissionAllow,
75 },
76 {
77 name: "GrantPersistent -> PermissionAllowForSession",
78 call: func(w *ClientWorkspace, p permission.PermissionRequest) {
79 w.PermissionGrantPersistent(p)
80 },
81 want: proto.PermissionAllowForSession,
82 },
83 {
84 name: "Deny -> PermissionDeny",
85 call: func(w *ClientWorkspace, p permission.PermissionRequest) {
86 w.PermissionDeny(p)
87 },
88 want: proto.PermissionDeny,
89 },
90 }
91
92 for _, tc := range cases {
93 t.Run(tc.name, func(t *testing.T) {
94 t.Parallel()
95
96 var got proto.PermissionGrant
97 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
98 require.Equal(t, http.MethodPost, r.Method)
99 require.Equal(t, "/v1/workspaces/ws-1/permissions/grant", r.URL.Path)
100 body, err := io.ReadAll(r.Body)
101 require.NoError(t, err)
102 require.NoError(t, json.Unmarshal(body, &got))
103 require.NoError(t, json.NewEncoder(w).Encode(proto.PermissionGrantResponse{Resolved: true}))
104 }))
105 defer srv.Close()
106
107 u, err := url.Parse(srv.URL)
108 require.NoError(t, err)
109 c, err := client.NewClient(t.TempDir(), "tcp", u.Host)
110 require.NoError(t, err)
111
112 ws := NewClientWorkspace(c, proto.Workspace{ID: "ws-1"})
113
114 perm := permission.PermissionRequest{
115 ID: "req-1",
116 SessionID: "sess-1",
117 ToolCallID: "tc-1",
118 ToolName: "tool",
119 Description: "do thing",
120 Action: "act",
121 Path: "/tmp/p",
122 }
123 tc.call(ws, perm)
124
125 require.Equal(t, tc.want, got.Action)
126 require.Equal(t, "req-1", got.Permission.ID)
127 require.Equal(t, "sess-1", got.Permission.SessionID)
128 require.Equal(t, "tc-1", got.Permission.ToolCallID)
129 require.Equal(t, "tool", got.Permission.ToolName)
130 require.Equal(t, "act", got.Permission.Action)
131 require.Equal(t, "/tmp/p", got.Permission.Path)
132 })
133 }
134}