1package tools
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "github.com/stretchr/testify/require"
12)
13
14func TestReadTextFileBoundaryCases(t *testing.T) {
15 t.Parallel()
16
17 tmpDir := t.TempDir()
18 filePath := filepath.Join(tmpDir, "sample.txt")
19
20 var allLines []string
21 for i := range 5 {
22 allLines = append(allLines, fmt.Sprintf("line %d", i+1))
23 }
24 require.NoError(t, os.WriteFile(filePath, []byte(strings.Join(allLines, "\n")), 0o644))
25
26 tests := []struct {
27 name string
28 offset int
29 limit int
30 wantContent string
31 wantHasMore bool
32 }{
33 {
34 name: "exactly limit lines remaining",
35 offset: 0,
36 limit: 5,
37 wantContent: "line 1\nline 2\nline 3\nline 4\nline 5",
38 wantHasMore: false,
39 },
40 {
41 name: "limit plus one line remaining",
42 offset: 0,
43 limit: 4,
44 wantContent: "line 1\nline 2\nline 3\nline 4",
45 wantHasMore: true,
46 },
47 {
48 name: "offset at last line",
49 offset: 4,
50 limit: 3,
51 wantContent: "line 5",
52 wantHasMore: false,
53 },
54 {
55 name: "offset beyond eof",
56 offset: 10,
57 limit: 3,
58 wantContent: "",
59 wantHasMore: false,
60 },
61 }
62
63 for _, tt := range tests {
64 t.Run(tt.name, func(t *testing.T) {
65 t.Parallel()
66
67 gotContent, gotHasMore, err := readTextFile(filePath, tt.offset, tt.limit)
68 require.NoError(t, err)
69 require.Equal(t, tt.wantContent, gotContent)
70 require.Equal(t, tt.wantHasMore, gotHasMore)
71 })
72 }
73}
74
75func TestReadTextFileTruncatesLongLines(t *testing.T) {
76 t.Parallel()
77
78 tmpDir := t.TempDir()
79 filePath := filepath.Join(tmpDir, "longline.txt")
80
81 longLine := strings.Repeat("a", MaxLineLength+10)
82 require.NoError(t, os.WriteFile(filePath, []byte(longLine), 0o644))
83
84 content, hasMore, err := readTextFile(filePath, 0, 1)
85 require.NoError(t, err)
86 require.False(t, hasMore)
87 require.Equal(t, strings.Repeat("a", MaxLineLength)+"...", content)
88}
89
90func TestReadBuiltinFile(t *testing.T) {
91 t.Parallel()
92
93 t.Run("reads crush-config skill", func(t *testing.T) {
94 t.Parallel()
95
96 resp, err := readBuiltinFile(ViewParams{
97 FilePath: "crush://skills/crush-config/SKILL.md",
98 }, nil)
99 require.NoError(t, err)
100 require.NotEmpty(t, resp.Content)
101 require.Contains(t, resp.Content, "Crush Configuration")
102 })
103
104 t.Run("not found", func(t *testing.T) {
105 t.Parallel()
106
107 resp, err := readBuiltinFile(ViewParams{
108 FilePath: "crush://skills/nonexistent/SKILL.md",
109 }, nil)
110 require.NoError(t, err)
111 require.True(t, resp.IsError)
112 })
113
114 t.Run("metadata has skill info", func(t *testing.T) {
115 t.Parallel()
116
117 resp, err := readBuiltinFile(ViewParams{
118 FilePath: "crush://skills/crush-config/SKILL.md",
119 }, nil)
120 require.NoError(t, err)
121
122 var meta ViewResponseMetadata
123 require.NoError(t, json.Unmarshal([]byte(resp.Metadata), &meta))
124 require.Equal(t, ViewResourceSkill, meta.ResourceType)
125 require.Equal(t, "crush-config", meta.ResourceName)
126 require.NotEmpty(t, meta.ResourceDescription)
127 })
128
129 t.Run("respects offset", func(t *testing.T) {
130 t.Parallel()
131
132 resp, err := readBuiltinFile(ViewParams{
133 FilePath: "crush://skills/crush-config/SKILL.md",
134 Offset: 5,
135 }, nil)
136 require.NoError(t, err)
137 require.NotContains(t, resp.Content, " 1|")
138 })
139}
140
141func TestSniffImageMimeType(t *testing.T) {
142 t.Parallel()
143
144 jpegMagic := []byte{0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 'J', 'F', 'I', 'F'}
145 pngMagic := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}
146 gifMagic := []byte("GIF89a")
147 // Minimal RIFF/WEBP header.
148 webpMagic := append([]byte("RIFF\x00\x00\x00\x00WEBPVP8 "), make([]byte, 16)...)
149 random := []byte("not an image at all, just text")
150
151 cases := []struct {
152 name string
153 data []byte
154 fallback string
155 want string
156 }{
157 {"jpeg bytes in .png file uses sniffed", jpegMagic, "image/png", "image/jpeg"},
158 {"png bytes in .jpg file uses sniffed", pngMagic, "image/jpeg", "image/png"},
159 {"gif bytes uses sniffed", gifMagic, "image/png", "image/gif"},
160 {"webp bytes uses sniffed", webpMagic, "image/png", "image/webp"},
161 {"matching extension and content keeps sniffed", pngMagic, "image/png", "image/png"},
162 {"unsniffable content falls back", random, "image/png", "image/png"},
163 {"empty content falls back", nil, "image/jpeg", "image/jpeg"},
164 }
165 for _, tc := range cases {
166 t.Run(tc.name, func(t *testing.T) {
167 t.Parallel()
168 require.Equal(t, tc.want, sniffImageMimeType(tc.data, tc.fallback))
169 })
170 }
171}