events_test.go

 1package server
 2
 3import (
 4	"encoding/json"
 5	"errors"
 6	"testing"
 7
 8	"github.com/charmbracelet/crush/internal/message"
 9	"github.com/charmbracelet/crush/internal/proto"
10	"github.com/charmbracelet/crush/internal/pubsub"
11	"github.com/charmbracelet/crush/internal/skills"
12	"github.com/stretchr/testify/require"
13)
14
15// TestMessageToProtoToolResult ensures that ToolResult metadata,
16// data, and MIME type survive the conversion to proto. Without these
17// fields the TUI cannot render rich tool output (e.g. syntax-
18// highlighted code from view, diffs from edit, images, etc.) and
19// falls back to the raw LLM-facing string.
20func TestMessageToProtoToolResult(t *testing.T) {
21	t.Parallel()
22
23	src := message.Message{
24		ID:   "m1",
25		Role: message.Tool,
26		Parts: []message.ContentPart{
27			message.ToolResult{
28				ToolCallID: "call-1",
29				Name:       "view",
30				Content:    "<file>\n  1| hi\n</file>",
31				Data:       "base64data",
32				MIMEType:   "image/png",
33				Metadata:   `{"file_path":"/tmp/x","content":"hi"}`,
34				IsError:    false,
35			},
36		},
37	}
38
39	got := messageToProto(src)
40	require.Len(t, got.Parts, 1)
41	tr, ok := got.Parts[0].(proto.ToolResult)
42	require.True(t, ok, "expected proto.ToolResult, got %T", got.Parts[0])
43	require.Equal(t, "call-1", tr.ToolCallID)
44	require.Equal(t, "view", tr.Name)
45	require.Equal(t, "<file>\n  1| hi\n</file>", tr.Content)
46	require.Equal(t, "base64data", tr.Data)
47	require.Equal(t, "image/png", tr.MIMEType)
48	require.Equal(t, `{"file_path":"/tmp/x","content":"hi"}`, tr.Metadata)
49	require.False(t, tr.IsError)
50}
51
52// TestSkillsEventToProto_RoundTrip verifies that a pubsub.Event[skills.Event]
53// can be wrapped, marshaled, and unmarshaled back through the SSE
54// envelope without losing state values or error messages.
55func TestSkillsEventToProto_RoundTrip(t *testing.T) {
56	t.Parallel()
57
58	src := pubsub.Event[skills.Event]{
59		Type: pubsub.UpdatedEvent,
60		Payload: skills.Event{
61			States: []*skills.SkillState{
62				{Name: "ok", Path: "/p/ok", State: skills.StateNormal},
63				{Name: "broken", Path: "/p/broken", State: skills.StateError, Err: errors.New("bad frontmatter")},
64			},
65		},
66	}
67
68	env := wrapEvent(src)
69	require.NotNil(t, env)
70	require.Equal(t, pubsub.PayloadTypeSkillsEvent, env.Type)
71
72	var decoded pubsub.Event[proto.SkillsEvent]
73	require.NoError(t, json.Unmarshal(env.Payload, &decoded))
74	require.Equal(t, pubsub.UpdatedEvent, decoded.Type)
75	require.Len(t, decoded.Payload.States, 2)
76
77	require.Equal(t, "ok", decoded.Payload.States[0].Name)
78	require.Equal(t, "/p/ok", decoded.Payload.States[0].Path)
79	require.Equal(t, proto.SkillStateNormal, decoded.Payload.States[0].State)
80	require.Empty(t, decoded.Payload.States[0].Error)
81
82	require.Equal(t, "broken", decoded.Payload.States[1].Name)
83	require.Equal(t, proto.SkillStateError, decoded.Payload.States[1].State)
84	require.Equal(t, "bad frontmatter", decoded.Payload.States[1].Error)
85}