@@ -1,8 +1,11 @@
1package notification_test
1package notification_test
8+ tea "charm.land/bubbletea/v2"
6 "github.com/charmbracelet/crush/internal/ui/notification"
9 "github.com/charmbracelet/crush/internal/ui/notification"
7 "github.com/stretchr/testify/require"
10 "github.com/stretchr/testify/require"
@@ -11,11 +14,11 @@ func TestNoopBackend_Send(t *testing.T) {
11 t.Parallel()
14 t.Parallel()
13 backend := notification.NoopBackend{}
16 backend := notification.NoopBackend{}
14− err := backend.Send(notification.Notification{
17+ cmd := backend.Send(notification.Notification{
15 Title: "Test Title",
18 Title: "Test Title",
16 Message: "Test Message",
19 Message: "Test Message",
18− require.NoError(t, err)
21+ require.Nil(t, cmd)
21func TestNativeBackend_Send(t *testing.T) {
24func TestNativeBackend_Send(t *testing.T) {
@@ -32,12 +35,164 @@ func TestNativeBackend_Send(t *testing.T) {
32 return nil
35 return nil
35− err := backend.Send(notification.Notification{
38+ cmd := backend.Send(notification.Notification{
36 Title: "Hello",
39 Title: "Hello",
37 Message: "World",
40 Message: "World",
39− require.NoError(t, err)
42+ require.NotNil(t, cmd)
40 require.Equal(t, "Hello", capturedTitle)
45 require.Equal(t, "Hello", capturedTitle)
41 require.Equal(t, "World", capturedMessage)
46 require.Equal(t, "World", capturedMessage)
42 require.Nil(t, capturedIcon)
47 require.Nil(t, capturedIcon)
50+func extractRawString(t *testing.T, cmd tea.Cmd) string {
52+ require.NotNil(t, cmd)
55+ raw, ok := msg.(tea.RawMsg)
58+ s, ok := raw.Msg.(string)
63+func TestOSC99Backend_Send(t *testing.T) {
66+ backend := notification.NewOSC99Backend(nil)
67+ s := extractRawString(t, backend.Send(notification.Notification{
68+ Title: "Crush is waiting...",
69+ Message: "Agent's turn completed",
72+ require.Contains(t, s, "p=title")
73+ require.Contains(t, s, "p=body")
74+ require.Contains(t, s, "Crush is waiting...")
75+ require.Contains(t, s, "Agent's turn completed")
76+ require.NotContains(t, s, "p=icon")
77+ require.NotContains(t, s, "\x1b]777;")
78+ require.NotContains(t, s, "\x1b]9;")
81+func TestOSC99Backend_Send_TitleOnly(t *testing.T) {
84+ backend := notification.NewOSC99Backend(nil)
85+ s := extractRawString(t, backend.Send(notification.Notification{
86+ Title: "Crush is waiting...",
89+ require.Contains(t, s, "p=title")
90+ require.NotContains(t, s, "p=body")
91+ require.NotContains(t, s, "\x1b]777;")
92+ require.NotContains(t, s, "\x1b]9;")
95+func TestOSC99Backend_Send_WithIcon(t *testing.T) {
98+ iconData := []byte("fake-png-data")
99+ backend := notification.NewOSC99Backend(iconData)
100+ s := extractRawString(t, backend.Send(notification.Notification{
102+ Message: "With icon",
105+ require.Contains(t, s, "p=icon")
106+ require.Contains(t, s, "e=1")
108+ encoded := base64.StdEncoding.EncodeToString(iconData)
109+ require.Contains(t, s, fmt.Sprintf(";%s\x07", encoded))
110+ require.NotContains(t, s, "\x1b]777;")
111+ require.NotContains(t, s, "\x1b]9;")
114+func TestOSC777Backend_Send(t *testing.T) {
117+ backend := notification.NewOSC777Backend()
118+ s := extractRawString(t, backend.Send(notification.Notification{
120+ Message: "With body",
123+ require.Equal(t, "\x1b]777;notify;Test;With body\x07", s)
124+ require.NotContains(t, s, "\x1b]99;")
125+ require.NotContains(t, s, "\x1b]9;")
128+func TestDetectOSC99Support_ValidResponse(t *testing.T) {
131+ // Simulate a valid OSC 99 response with title support.
132+ seq := "\x1b]99;i=crush-osc99-query:p=?;p=title\x07"
133+ require.True(t, notification.DetectOSC99Support(seq))
136+func TestDetectOSC99Support_MultipleCapabilities(t *testing.T) {
139+ // Response indicating support for title, body, and icon.
140+ seq := "\x1b]99;i=crush-osc99-query:p=?;p=title,body,icon\x07"
141+ require.True(t, notification.DetectOSC99Support(seq))
144+func TestDetectOSC99Support_InvalidCommand(t *testing.T) {
147+ // OSC 98 instead of 99.
148+ seq := "\x1b]98;i=crush-osc99-query:p=?;p=title\x07"
149+ require.False(t, notification.DetectOSC99Support(seq))
152+func TestDetectOSC99Support_WrongQueryID(t *testing.T) {
155+ // Correct OSC 99 but wrong query ID.
156+ seq := "\x1b]99;i=some-other-id:p=?;p=title\x07"
157+ require.False(t, notification.DetectOSC99Support(seq))
160+func TestDetectOSC99Support_NoQueryFlag(t *testing.T) {
163+ // Missing p=? query flag.
164+ seq := "\x1b]99;i=crush-osc99-query;p=title\x07"
165+ require.False(t, notification.DetectOSC99Support(seq))
168+func TestDetectOSC99Support_NoTitleCapability(t *testing.T) {
171+ // Response without title capability (only body).
172+ seq := "\x1b]99;i=crush-osc99-query:p=?;p=body\x07"
173+ require.False(t, notification.DetectOSC99Support(seq))
176+func TestDetectOSC99Support_EmptySequence(t *testing.T) {
179+ require.False(t, notification.DetectOSC99Support(""))
182+func TestDetectOSC99Support_MalformedSequence(t *testing.T) {
185+ // Missing semicolon separator.
186+ seq := "\x1b]99;i=crush-osc99-query:p=?p=title\x07"
187+ require.False(t, notification.DetectOSC99Support(seq))
190+func TestOSC99QuerySequence(t *testing.T) {
193+ seq := notification.OSC99QuerySequence()
194+ require.Contains(t, seq, "\x1b]99;")
195+ require.Contains(t, seq, "i=crush-osc99-query")
196+ require.Contains(t, seq, "p=?")
197+ require.Contains(t, seq, "\x07")