Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
kardolus
GitHub Repository: kardolus/chatgpt-cli
Path: blob/main/cmd/chatgpt/utils/utils_test.go
3456 views
1
package utils_test
2
3
import (
4
"fmt"
5
"github.com/kardolus/chatgpt-cli/agent/core"
6
"github.com/kardolus/chatgpt-cli/cmd/chatgpt/utils"
7
"github.com/kardolus/chatgpt-cli/config"
8
"testing"
9
"time"
10
11
. "github.com/onsi/gomega"
12
"github.com/sclevine/spec"
13
"github.com/sclevine/spec/report"
14
)
15
16
func TestUnitUtils(t *testing.T) {
17
spec.Run(t, "Testing the Utils", testUtils, spec.Report(report.Terminal{}))
18
}
19
20
func testUtils(t *testing.T, when spec.G, it spec.S) {
21
it.Before(func() {
22
RegisterTestingT(t)
23
})
24
25
when("ColorToAnsi()", func() {
26
it("should return an empty color and reset if the input is an empty string", func() {
27
color, reset := utils.ColorToAnsi("")
28
Expect(color).To(Equal(""))
29
Expect(reset).To(Equal(""))
30
})
31
32
it("should return an empty color and reset if the input is an unsupported color", func() {
33
color, reset := utils.ColorToAnsi("unsupported")
34
Expect(color).To(Equal(""))
35
Expect(reset).To(Equal(""))
36
})
37
38
it("should return the correct ANSI code for red", func() {
39
color, reset := utils.ColorToAnsi("red")
40
Expect(color).To(Equal("\033[31m"))
41
Expect(reset).To(Equal("\033[0m"))
42
})
43
44
it("should return the correct ANSI code for green", func() {
45
color, reset := utils.ColorToAnsi("green")
46
Expect(color).To(Equal("\033[32m"))
47
Expect(reset).To(Equal("\033[0m"))
48
})
49
50
it("should return the correct ANSI code for yellow", func() {
51
color, reset := utils.ColorToAnsi("yellow")
52
Expect(color).To(Equal("\033[33m"))
53
Expect(reset).To(Equal("\033[0m"))
54
})
55
56
it("should return the correct ANSI code for blue", func() {
57
color, reset := utils.ColorToAnsi("blue")
58
Expect(color).To(Equal("\033[34m"))
59
Expect(reset).To(Equal("\033[0m"))
60
})
61
62
it("should return the correct ANSI code for magenta", func() {
63
color, reset := utils.ColorToAnsi("magenta")
64
Expect(color).To(Equal("\033[35m"))
65
Expect(reset).To(Equal("\033[0m"))
66
})
67
68
it("should handle case-insensitivity correctly", func() {
69
color, reset := utils.ColorToAnsi("ReD")
70
Expect(color).To(Equal("\033[31m"))
71
Expect(reset).To(Equal("\033[0m"))
72
})
73
74
it("should handle leading and trailing spaces", func() {
75
color, reset := utils.ColorToAnsi(" blue ")
76
Expect(color).To(Equal("\033[34m"))
77
Expect(reset).To(Equal("\033[0m"))
78
})
79
})
80
81
when("FormatPrompt()", func() {
82
const (
83
counter = 1
84
usage = 2
85
)
86
87
now := time.Now()
88
89
it("should add a trailing whitespace", func() {
90
input := "prompt"
91
expected := "prompt "
92
Expect(utils.FormatPrompt(input, counter, usage, now)).To(Equal(expected))
93
})
94
95
it("should handle empty input as expected", func() {
96
input := ""
97
expected := ""
98
Expect(utils.FormatPrompt(input, counter, usage, now)).To(Equal(expected))
99
})
100
101
it("should replace %date with the current date", func() {
102
currentDate := now.Format("2006-01-02")
103
input := "Today's date is %date"
104
expected := "Today's date is " + currentDate + " "
105
Expect(utils.FormatPrompt(input, counter, usage, now)).To(Equal(expected))
106
})
107
108
it("should replace %time with the current time", func() {
109
currentTime := now.Format("15:04:05")
110
input := "Current time is %time"
111
expected := "Current time is " + currentTime + " "
112
Expect(utils.FormatPrompt(input, counter, usage, now)).To(Equal(expected))
113
})
114
115
it("should replace %datetime with the current date and time", func() {
116
currentDatetime := now.Format("2006-01-02 15:04:05")
117
input := "Current date and time is %datetime"
118
expected := "Current date and time is " + currentDatetime + " "
119
Expect(utils.FormatPrompt(input, counter, usage, now)).To(Equal(expected))
120
})
121
122
it("should replace %counter with the current counter value", func() {
123
input := "The counter is %counter"
124
expected := "The counter is 1 "
125
Expect(utils.FormatPrompt(input, counter, usage, now)).To(Equal(expected))
126
})
127
128
it("should replace %usage with the current usage value", func() {
129
input := "The usage is %usage"
130
expected := "The usage is 2 "
131
Expect(utils.FormatPrompt(input, counter, usage, now)).To(Equal(expected))
132
})
133
134
it("should handle complex cases correctly", func() {
135
input := "command_prompt: [%time] [Q%counter]"
136
expected := fmt.Sprintf("command_prompt: [%s] [Q%d] ", now.Format("15:04:05"), counter)
137
Expect(utils.FormatPrompt(input, counter, usage, now)).To(Equal(expected))
138
})
139
140
it("should replace \\n with an actual newline", func() {
141
input := "Line 1\\nLine 2"
142
expected := "Line 1\nLine 2 "
143
Expect(utils.FormatPrompt(input, counter, usage, now)).To(Equal(expected))
144
})
145
})
146
147
when("IsBinary()", func() {
148
it("should return false for a regular string", func() {
149
Expect(utils.IsBinary([]byte("regular string"))).To(BeFalse())
150
})
151
it("should return false for a string containing emojis", func() {
152
Expect(utils.IsBinary([]byte("☮️✅❤️"))).To(BeFalse())
153
})
154
it("should return true for a binary string", func() {
155
Expect(utils.IsBinary([]byte{0xFF, 0xFE, 0xFD, 0xFC, 0xFB})).To(BeTrue())
156
})
157
it("should return false when the data is empty", func() {
158
Expect(utils.IsBinary([]byte{})).To(BeFalse())
159
})
160
it("should handle large text files correctly", func() {
161
// Create a large slice > 512KB with normal text
162
largeText := make([]byte, 1024*1024) // 1MB
163
for i := range largeText {
164
largeText[i] = 'a'
165
}
166
167
Expect(utils.IsBinary(largeText)).To(BeFalse())
168
})
169
it("should return true when data contains null bytes", func() {
170
Expect(utils.IsBinary([]byte{'h', 'e', 'l', 'l', 0x00, 'o'})).To(BeTrue())
171
})
172
173
it("should return true for invalid UTF-8 sequences", func() {
174
// Invalid UTF-8: 0xED 0xA0 0x80 is a surrogate pair which is invalid in UTF-8
175
Expect(utils.IsBinary([]byte{0xED, 0xA0, 0x80})).To(BeTrue())
176
})
177
178
it("should return false for valid UTF-8 special characters", func() {
179
// Testing with Chinese characters, Arabic, and other non-ASCII but valid UTF-8
180
Expect(utils.IsBinary([]byte("你好世界مرحبا"))).To(BeFalse())
181
})
182
183
it("should handle control characters correctly", func() {
184
// Test with allowed control characters (tab, newline, carriage return)
185
Expect(utils.IsBinary([]byte("Hello\tWorld\r\nTest"))).To(BeFalse())
186
187
// Test with other control characters that should trigger binary detection
188
data := []byte{0x01, 0x02, 0x03, 0x04}
189
Expect(utils.IsBinary(data)).To(BeTrue())
190
})
191
192
})
193
194
when("ValidateFlags()", func() {
195
const defaultModel = "mock-model"
196
197
var flags map[string]bool
198
199
it.Before(func() {
200
flags = make(map[string]bool)
201
})
202
203
it("doesn't throw an error when no flags are provided", func() {
204
Expect(utils.ValidateFlags(defaultModel, flags)).To(Succeed())
205
})
206
it("should return an error when --new-thread and --set-thread are both used", func() {
207
flags["new-thread"] = true
208
flags["set-thread"] = true
209
210
err := utils.ValidateFlags(defaultModel, flags)
211
Expect(err).To(HaveOccurred())
212
})
213
it("should return an error when --new-thread and --thread are both used", func() {
214
flags["new-thread"] = true
215
flags["thread"] = true
216
217
err := utils.ValidateFlags(defaultModel, flags)
218
Expect(err).To(HaveOccurred())
219
})
220
it("should return an error when --speak is used but --output is omitted", func() {
221
flags["speak"] = true
222
223
err := utils.ValidateFlags(defaultModel, flags)
224
Expect(err).To(HaveOccurred())
225
})
226
it("should return an error when --draw is used but --output is omitted", func() {
227
flags["draw"] = true
228
229
err := utils.ValidateFlags(defaultModel, flags)
230
Expect(err).To(HaveOccurred())
231
})
232
it("should return an error when --output is used but --speak or --draw are omitted", func() {
233
flags["output"] = true
234
235
err := utils.ValidateFlags(defaultModel, flags)
236
Expect(err).To(HaveOccurred())
237
})
238
it("should return an error when --agent-mode is used but --agent is omitted", func() {
239
flags["agent-mode"] = true
240
241
err := utils.ValidateFlags(defaultModel, flags)
242
Expect(err).To(HaveOccurred())
243
})
244
it("should NOT return an error when --agent-mode and --agent are both used", func() {
245
flags["agent-mode"] = true
246
flags["agent"] = true
247
248
err := utils.ValidateFlags(defaultModel, flags)
249
Expect(err).NotTo(HaveOccurred())
250
})
251
it("should return an error when --audio is used with an incompatible model", func() {
252
flags["audio"] = true
253
254
err := utils.ValidateFlags(defaultModel, flags)
255
Expect(err).To(HaveOccurred())
256
})
257
it("should NOT return an error when --audio is used with a compatible model", func() {
258
flags["audio"] = true
259
260
err := utils.ValidateFlags(defaultModel+utils.AudioPattern, flags)
261
Expect(err).NotTo(HaveOccurred())
262
})
263
it("should return an error when --transcribe is used with an incompatible model", func() {
264
flags["transcribe"] = true
265
266
err := utils.ValidateFlags(defaultModel, flags)
267
Expect(err).To(HaveOccurred())
268
})
269
it("should NOT return an error when --transcribe is used with a compatible model", func() {
270
flags["transcribe"] = true
271
272
err := utils.ValidateFlags(defaultModel+utils.TranscribePattern, flags)
273
Expect(err).NotTo(HaveOccurred())
274
})
275
it("should return an error when --speak and --output flags are used with an incompatible model", func() {
276
flags["speak"] = true
277
flags["output"] = true
278
279
err := utils.ValidateFlags(defaultModel, flags)
280
Expect(err).To(HaveOccurred())
281
})
282
it("should NOT return an error when --speak and --output flags are used with a compatible model", func() {
283
flags["speak"] = true
284
flags["output"] = true
285
286
err := utils.ValidateFlags(defaultModel+utils.TTSPattern, flags)
287
Expect(err).NotTo(HaveOccurred())
288
})
289
it("should return an error when --draw and --output flags are used with an incompatible model", func() {
290
flags["draw"] = true
291
flags["output"] = true
292
293
err := utils.ValidateFlags(defaultModel, flags)
294
Expect(err).To(HaveOccurred())
295
})
296
it("should NOT return an error when --draw and --output flags are used with a compatible model", func() {
297
flags["draw"] = true
298
flags["output"] = true
299
300
err := utils.ValidateFlags(defaultModel+utils.ImagePattern, flags)
301
Expect(err).NotTo(HaveOccurred())
302
})
303
it("should NOT return an error when --draw, --image and --output flags are used with a compatible model", func() {
304
flags["draw"] = true
305
flags["output"] = true
306
flags["image"] = true
307
308
err := utils.ValidateFlags(defaultModel+utils.ImagePattern, flags)
309
Expect(err).NotTo(HaveOccurred())
310
})
311
it("should return an error when --voice is used with an incompatible model", func() {
312
flags["voice"] = true
313
314
err := utils.ValidateFlags(defaultModel, flags)
315
Expect(err).To(HaveOccurred())
316
})
317
it("should NOT return an error when --voice is used with a compatible model", func() {
318
flags["voice"] = true
319
320
err := utils.ValidateFlags(defaultModel+utils.TTSPattern, flags)
321
Expect(err).NotTo(HaveOccurred())
322
})
323
it("should return an error when --effort is used with an incompatible model", func() {
324
flags["effort"] = true
325
326
err := utils.ValidateFlags(defaultModel, flags)
327
Expect(err).To(HaveOccurred())
328
})
329
it("should NOT return an error when --effort is used with a compatible model", func() {
330
flags["effort"] = true
331
332
Expect(utils.ValidateFlags(defaultModel+utils.O1ProPattern, flags)).To(Succeed())
333
Expect(utils.ValidateFlags(defaultModel+utils.GPT5Pattern, flags)).To(Succeed())
334
})
335
it("should return an error when the --mcp-param flag is used without --mcp", func() {
336
flags["mcp-param"] = true
337
err := utils.ValidateFlags(defaultModel, flags)
338
Expect(err).To(HaveOccurred())
339
})
340
it("should return an error when the --mcp-params flag is used without --mcp", func() {
341
flags["mcp-params"] = true
342
err := utils.ValidateFlags(defaultModel, flags)
343
Expect(err).To(HaveOccurred())
344
})
345
})
346
347
when("GenerateThreadName()", func() {
348
const (
349
doNotCreateNewThread = false
350
doNotUseInteractiveMode = false
351
useInteractiveMode = true
352
createNewThread = true
353
threadName = "threadName"
354
)
355
var cfg config.Config
356
357
it.Before(func() {
358
cfg = config.Config{Thread: threadName}
359
})
360
361
it("returns the configured thread name when new-thread and auto-new-thread are disabled (regardless of interactive mode)", func() {
362
result, updateConfig := utils.GenerateThreadName(cfg, doNotUseInteractiveMode, doNotCreateNewThread)
363
Expect(result).To(Equal(threadName))
364
Expect(updateConfig).To(BeFalse())
365
366
result, updateConfig = utils.GenerateThreadName(cfg, useInteractiveMode, doNotCreateNewThread)
367
Expect(result).To(Equal(threadName))
368
Expect(updateConfig).To(BeFalse())
369
})
370
371
it("prioritizes --new-thread over auto-create-new-thread when interactive mode is enabled", func() {
372
result, updateConfig := utils.GenerateThreadName(cfg, useInteractiveMode, createNewThread)
373
Expect(result).To(HavePrefix(utils.CommandPrefix))
374
Expect(updateConfig).To(BeTrue())
375
})
376
377
it("generates an interactive prefix slug when auto-create-new-thread is enabled", func() {
378
cfg.AutoCreateNewThread = createNewThread
379
result, updateConfig := utils.GenerateThreadName(cfg, useInteractiveMode, doNotCreateNewThread)
380
Expect(result).To(HavePrefix(utils.InteractivePrefix))
381
Expect(updateConfig).To(BeFalse())
382
})
383
384
it("does not generate a prefix in interactive mode when auto-create-new-thread is disabled", func() {
385
cfg.AutoCreateNewThread = doNotCreateNewThread
386
result, updateConfig := utils.GenerateThreadName(cfg, useInteractiveMode, doNotCreateNewThread)
387
Expect(result).To(Equal(threadName))
388
Expect(updateConfig).To(BeFalse())
389
})
390
391
it("should return the configured thread when in command mode when auto-create-new-thread is enabled", func() {
392
cfg.AutoCreateNewThread = createNewThread
393
result, updateConfig := utils.GenerateThreadName(cfg, doNotUseInteractiveMode, doNotCreateNewThread)
394
Expect(result).To(Equal(threadName))
395
Expect(updateConfig).To(BeFalse())
396
})
397
})
398
399
when("ParseMCPParams()", func() {
400
const (
401
key = "key"
402
value = "value"
403
pair = key + "=" + value
404
)
405
406
it("throws and error when the params are not valid JSON or a valid pair", func() {
407
_, err := utils.ParseMCPParams("invalid-params")
408
Expect(err).To(HaveOccurred())
409
Expect(err).To(MatchError(utils.InvalidParams))
410
})
411
it("parses the input as expected when a valid pair is provided", func() {
412
result, err := utils.ParseMCPParams(pair)
413
Expect(err).NotTo(HaveOccurred())
414
Expect(result).To(HaveLen(1))
415
Expect(result[key]).To(Equal(value))
416
})
417
it("parses the input as expected when a valid json is provided", func() {
418
jsonInput := `{"key": "value"}`
419
420
result, err := utils.ParseMCPParams(jsonInput)
421
Expect(err).NotTo(HaveOccurred())
422
Expect(result).To(HaveLen(1))
423
Expect(result["key"]).To(Equal("value"))
424
})
425
it("does not throw an error when no input is provided", func() {
426
result, err := utils.ParseMCPParams()
427
Expect(err).NotTo(HaveOccurred())
428
Expect(result).To(BeEmpty())
429
})
430
it("throws an error when the 2nd pair is malformed", func() {
431
_, err := utils.ParseMCPParams([]string{pair, "invalid-pair"}...)
432
Expect(err).To(HaveOccurred())
433
Expect(err).To(MatchError(utils.InvalidParams))
434
})
435
it("produces the expected output when multiple pairs are provided", func() {
436
result, err := utils.ParseMCPParams(pair, fmt.Sprintf("%s2=%s2", key, value))
437
Expect(err).NotTo(HaveOccurred())
438
Expect(result).To(HaveLen(2))
439
Expect(result[key]).To(Equal(value))
440
Expect(result[key+"2"]).To(Equal(value + "2"))
441
})
442
it("parses key=value pairs where the value is a JSON array or boolean", func() {
443
result, err := utils.ParseMCPParams(
444
`locations=["Brooklyn","Queens"]`,
445
`forecasts=true`,
446
`language="en"`,
447
)
448
Expect(err).NotTo(HaveOccurred())
449
450
Expect(result).To(HaveLen(3))
451
452
Expect(result["locations"]).To(Equal([]interface{}{"Brooklyn", "Queens"}))
453
Expect(result["forecasts"]).To(Equal(true))
454
Expect(result["language"]).To(Equal("en")) // NOTE: quoted value gets parsed as string
455
})
456
})
457
458
when("ParseMCPHeaders()", func() {
459
it("does not throw an error when no input is provided", func() {
460
result, err := utils.ParseMCPHeaders(nil)
461
Expect(err).NotTo(HaveOccurred())
462
Expect(result).To(BeEmpty())
463
})
464
465
it("parses a single header as expected", func() {
466
result, err := utils.ParseMCPHeaders([]string{"Authorization: Bearer token"})
467
Expect(err).NotTo(HaveOccurred())
468
Expect(result).To(HaveLen(1))
469
Expect(result["Authorization"]).To(Equal("Bearer token"))
470
})
471
472
it("trims whitespace around the key and value", func() {
473
result, err := utils.ParseMCPHeaders([]string{" Accept : application/json, text/event-stream "})
474
Expect(err).NotTo(HaveOccurred())
475
Expect(result).To(HaveLen(1))
476
Expect(result["Accept"]).To(Equal("application/json, text/event-stream"))
477
})
478
479
it("supports values that contain ':' by splitting only on the first ':'", func() {
480
result, err := utils.ParseMCPHeaders([]string{"X-Test: a:b:c"})
481
Expect(err).NotTo(HaveOccurred())
482
Expect(result).To(HaveLen(1))
483
Expect(result["X-Test"]).To(Equal("a:b:c"))
484
})
485
486
it("overwrites earlier values when the same header key is provided multiple times", func() {
487
result, err := utils.ParseMCPHeaders([]string{
488
"Accept: application/json",
489
"Accept: application/json, text/event-stream",
490
})
491
Expect(err).NotTo(HaveOccurred())
492
Expect(result).To(HaveLen(1))
493
Expect(result["Accept"]).To(Equal("application/json, text/event-stream"))
494
})
495
496
it("throws an error when the header is missing ':'", func() {
497
_, err := utils.ParseMCPHeaders([]string{"invalid-header"})
498
Expect(err).To(HaveOccurred())
499
Expect(err).To(MatchError(fmt.Sprintf("invalid --mcp-header %q (expected 'Key: Value')", "invalid-header")))
500
})
501
502
it("throws an error when the key is empty after trimming", func() {
503
_, err := utils.ParseMCPHeaders([]string{": value"})
504
Expect(err).To(HaveOccurred())
505
Expect(err).To(MatchError(fmt.Sprintf("invalid --mcp-header %q (empty key)", ": value")))
506
})
507
})
508
509
when("BudgetLimitsFromConfig()", func() {
510
it("maps agent config into budget limits", func() {
511
cfg := config.Config{
512
Agent: config.AgentConfig{
513
MaxIterations: 2,
514
MaxWallTime: 123,
515
MaxShellCalls: 7,
516
MaxLLMCalls: 9,
517
MaxFileOps: 11,
518
MaxLLMTokens: 13,
519
},
520
}
521
522
limits := utils.BudgetLimitsFromConfig(cfg)
523
524
Expect(limits).To(Equal(core.BudgetLimits{
525
MaxIterations: 2,
526
MaxWallTime: 123 * time.Second,
527
MaxShellCalls: 7,
528
MaxLLMCalls: 9,
529
MaxFileOps: 11,
530
MaxLLMTokens: 13,
531
}))
532
})
533
})
534
}
535
536