-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils_test.go
More file actions
398 lines (358 loc) · 11 KB
/
utils_test.go
File metadata and controls
398 lines (358 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
package main
import (
"strings"
"testing"
"github.com/FreePeak/commitgen/pkg/commitrules"
)
const (
providerClaude = "claude"
providerGemini = "gemini"
providerCopilot = "copilot"
providerOpenCode = "opencode"
)
func TestCleanCommitMessage(t *testing.T) {
tests := getCleanCommitMessageTestCases()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := commitrules.CleanCommitMessage(test.input)
if result != test.expected {
t.Errorf("commitrules.CleanCommitMessage(%q) = %q, want %q", test.input, result, test.expected)
}
})
}
}
func getCleanCommitMessageTestCases() []struct {
name string
input string
expected string
} {
return append(getBasicTestCases(), getComplexTestCases()...)
}
func getBasicTestCases() []struct {
name string
input string
expected string
} {
return []struct {
name string
input string
expected string
}{
{
name: "simple message",
input: "feat: add new feature",
expected: "feat: add new feature",
},
{
name: "message with quotes",
input: `"feat: add new feature"`,
expected: "feat: add new feature",
},
{
name: "message with extra whitespace",
input: " feat: add new feature ",
expected: "feat: add new feature",
},
{
name: "message with single quotes",
input: `'feat: add new feature'`,
expected: "feat: add new feature",
},
{
name: "multiline message",
input: "feat: add new feature\n\nThis is the description",
expected: "feat: add new feature",
},
}
}
func getComplexTestCases() []struct {
name string
input string
expected string
} {
return []struct {
name string
input string
expected string
}{
{
name: "empty message",
input: "",
expected: "",
},
{
name: "message with only whitespace",
input: " ",
expected: "",
},
{
name: "message with explanatory text",
input: "feat: add new feature\nThis commit adds a new feature to the application",
expected: "feat: add new feature",
},
{
name: "message with trailing spaces and newlines",
input: "fix: resolve bug \n\n ",
expected: "fix: resolve bug",
},
{
name: "descriptive response with commit message",
input: "Based on the changes, the commit message should be: \"feat: add new feature\"",
expected: "feat: add new feature",
},
{
name: "long descriptive response",
input: "Looking at the git diff, I can see this is a major refactoring. The commit message is: refactor(core): extract validation logic",
expected: "refactor(core): extract validation logic",
},
}
}
// resolveProviderCommand returns the command for a given provider and whether it's a Claude provider.
func resolveProviderCommand(provider string) (string, bool) {
switch {
case strings.HasPrefix(provider, providerClaude):
return providerClaude, true
case provider == providerGemini:
return providerGemini, false
case provider == providerCopilot:
return providerCopilot, false
case provider == providerOpenCode:
return providerOpenCode, false
default:
return "", false
}
}
// validateProviderMapping validates that a provider maps to the expected command.
func validateProviderMapping(t *testing.T, provider, expectedCmd, actualCmd string, isClaude bool) {
switch {
case strings.HasPrefix(provider, providerClaude):
if !isClaude {
t.Errorf("Provider %s should be recognized as claude provider", provider)
}
if actualCmd != providerClaude {
t.Errorf("Provider %s should map to 'claude' command, got %s", provider, actualCmd)
}
case provider == providerGemini || provider == providerCopilot || provider == providerOpenCode:
if provider != expectedCmd {
t.Errorf("Provider %s should map to '%s' command, got %s", provider, expectedCmd, actualCmd)
}
default:
// Unsupported providers
if isClaude || actualCmd != "" {
t.Errorf("Provider %s should be unsupported", provider)
}
}
}
func TestProviderValidation(t *testing.T) {
tests := []struct {
provider string
isSupported bool
expectedCmd string
}{
// Claude variants
{providerClaude, true, providerClaude},
{"claude-external", true, providerClaude},
{"claude-custom", true, providerClaude},
{"claude-2", true, providerClaude},
// Other providers
{providerGemini, true, providerGemini},
{providerCopilot, true, providerCopilot},
{providerOpenCode, true, providerOpenCode},
// Unsupported providers
{"openai", false, ""},
{"chatgpt", false, ""},
{"claud", false, ""}, // Too short
{"cclaude", false, ""}, // Doesn't start with claude
{"", false, ""},
}
for _, test := range tests {
t.Run(test.provider, func(t *testing.T) {
command, isClaude := resolveProviderCommand(test.provider)
validateProviderMapping(t, test.provider, test.expectedCmd, command, isClaude)
// Validate expected behavior
if test.isSupported && command == "" && !strings.HasPrefix(test.provider, "claude") {
t.Errorf("Provider %s should be supported but got empty command", test.provider)
}
})
}
}
func TestGetProviderCmd(t *testing.T) {
tests := []struct {
provider string
expectedCmd string
expectedArgs []string
}{
{providerClaude, providerClaude, []string{}},
{providerGemini, providerGemini, []string{}},
{providerCopilot, providerCopilot, []string{}},
{providerOpenCode, providerOpenCode, []string{"run"}},
}
for _, test := range tests {
t.Run(test.provider, func(t *testing.T) {
cmd, args := getProviderCmd(test.provider)
if cmd != test.expectedCmd {
t.Errorf("Provider %s should have command %s, got %s", test.provider, test.expectedCmd, cmd)
}
if len(args) != len(test.expectedArgs) {
t.Errorf("Provider %s should have %d args, got %d", test.provider, len(test.expectedArgs), len(args))
}
for i, arg := range args {
if arg != test.expectedArgs[i] {
t.Errorf("Provider %s arg %d should be %s, got %s", test.provider, i, test.expectedArgs[i], arg)
}
}
})
}
}
func TestPromptStructure(t *testing.T) {
analysisInput := "feat(service): add new endpoint"
prompt := commitrules.GetPrompt(analysisInput)
// Verify prompt contains all required elements
requiredElements := []string{
"type(scope): description",
"Maximum 50 characters total",
"conventional commit message",
"CRITICAL: Respond with ONLY the commit message",
"Git diff to analyze:",
analysisInput,
}
for _, element := range requiredElements {
if !strings.Contains(prompt, element) {
t.Errorf("Prompt missing required element: %s", element)
}
}
// Verify prompt doesn't contain unwanted elements
unwantedElements := []string{
"TODO",
"FIXME",
"<placeholder>",
}
for _, element := range unwantedElements {
if strings.Contains(prompt, element) {
t.Errorf("Prompt contains unwanted element: %s", element)
}
}
}
func TestCommitMessageFormatValidation(t *testing.T) {
validFormats := []string{
"feat: add new feature",
"fix(auth): resolve login issue",
"docs(readme): update installation guide",
"style: format code",
"refactor(api): simplify endpoint logic",
"test: add unit tests for auth",
"chore: update dependencies",
"feat(service:auth): add JWT validation",
"fix(ui): resolve button rendering issue",
}
invalidFormats := []string{
"add new feature", // Missing type and scope
"feat", // Missing description
"feat:", // Empty description
": add new feature", // Missing type
"Add new feature", // Capital first letter (should be lowercase)
"feat Add new feature", // Missing colon
"feat: Add new feature", // Capital description (should be lowercase)
"feat: add new feature.", // Ends with period
}
// Test that valid formats pass our basic validation
for _, msg := range validFormats {
t.Run("valid/"+msg, func(t *testing.T) {
cleaned := commitrules.CleanCommitMessage(msg)
if cleaned == "" {
t.Errorf("Valid commit message %s was cleaned to empty string", msg)
}
// Basic format check: type: description
parts := strings.SplitN(cleaned, ":", 2)
if len(parts) != 2 {
t.Errorf("Valid commit message %s doesn't contain colon separator", msg)
return
}
typePart := strings.TrimSpace(parts[0])
descPart := strings.TrimSpace(parts[1])
if typePart == "" {
t.Errorf("Valid commit message %s has empty type part", msg)
}
if descPart == "" {
t.Errorf("Valid commit message %s has empty description part", msg)
}
})
}
// Test that invalid formats are still processed (cleanCommitMessage doesn't validate format)
for _, msg := range invalidFormats {
t.Run("invalid/"+msg, func(t *testing.T) {
cleaned := commitrules.CleanCommitMessage(msg)
// commitrules.CleanCommitMessage should still return the message even if format is invalid
// The format validation would happen elsewhere
if cleaned != msg && msg != `"add new feature"` && msg != `'add new feature'` {
t.Logf("Message %s was cleaned to %s", msg, cleaned)
}
})
}
}
func TestGitRepositoryDetection(t *testing.T) {
// Test that the function exists and returns a boolean
result := isGitRepo()
// We don't assert a specific value since it depends on the test environment
// but we can verify it returns a boolean
if result != true && result != false {
t.Errorf("isGitRepo() should return true or false, got %v", result)
}
// Type check
_ = isGitRepo()
}
// Edge case tests.
func TestEdgeCases(t *testing.T) {
t.Run("empty analysis input", func(t *testing.T) {
// Test that empty analysis input is handled
prompt := commitrules.GetPrompt("")
if len(prompt) == 0 {
t.Error("Prompt should not be empty even with empty analysis input")
}
})
t.Run("very long commit message", func(t *testing.T) {
longMsg := strings.Repeat("a", 1000)
cleaned := commitrules.CleanCommitMessage(longMsg)
if cleaned != longMsg {
t.Errorf("Long message should not be truncated by commitrules.CleanCommitMessage")
}
})
t.Run("special characters in commit message", func(t *testing.T) {
specialMsg := "feat: add support for émojis 🎉 and ñoños"
cleaned := commitrules.CleanCommitMessage(specialMsg)
if cleaned != specialMsg {
t.Errorf("Special characters should be preserved")
}
})
}
// Performance tests.
func BenchmarkCleanCommitMessageSimple(b *testing.B) {
message := "feat: add new feature"
for i := 0; i < b.N; i++ {
commitrules.CleanCommitMessage(message)
}
}
func BenchmarkCleanCommitMessageComplex(b *testing.B) {
message := `" feat: add new feature with quotes and whitespace
This is a multiline message with extra content.
"`
for i := 0; i < b.N; i++ {
commitrules.CleanCommitMessage(message)
}
}
func BenchmarkProviderValidation(b *testing.B) {
providers := []string{providerClaude, "claude-external", providerGemini, providerCopilot, "unknown"}
for i := 0; i < b.N; i++ {
provider := providers[i%len(providers)]
switch {
case strings.HasPrefix(provider, providerClaude):
_ = providerClaude
case provider == providerGemini:
_ = providerGemini
case provider == providerCopilot:
_ = providerCopilot
default:
_ = ""
}
}
}