Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit 5f72781

Browse files
committed
fix: tests
1 parent 01c3c68 commit 5f72781

File tree

3 files changed

+170
-64
lines changed

3 files changed

+170
-64
lines changed

commands/run.go

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,8 @@ func newRunCmd() *cobra.Command {
167167
case strings.HasPrefix(question, "/"):
168168
printHelp(helpUnknownCommand)
169169

170-
case strings.HasPrefix(question, `"""`):
171-
initialText := strings.TrimPrefix(question, `"""`)
172-
restOfText, err := readMultilineString(cmd.Context(), initialText)
170+
case strings.HasPrefix(question, `"""`) || strings.HasPrefix(question, `'''`):
171+
restOfText, err := readMultilineString(cmd.Context(), os.Stdin, question)
173172
if err != nil {
174173
return err
175174
}
@@ -323,54 +322,72 @@ func copyToClipboard(command string) error {
323322
return nil
324323
}
325324

326-
// readMultilineString reads a multiline string from stdin, starting with the initial text if provided.
327-
// The initialText parameter is optional and can be used to provide a starting point for the input.
328-
// It reads from stdin until it encounters a closing triple quote (""").
329-
func readMultilineString(ctx context.Context, initialText string) (string, error) {
325+
// readMultilineString reads a multiline string from r, starting with the initial text if provided.
326+
// The text must start either with triple quotes (""") or single quotes (”'). readMultilineString
327+
// will scan the input until a matching closing quote is found, or return an error if the input
328+
// is not properly closed. It returns the string content without the surrounding quotes but
329+
// preserving the original newlines and indentation.
330+
func readMultilineString(ctx context.Context, r io.Reader, initialText string) (string, error) {
330331
var question string
331332

332333
// Start with the initial text if provided
333334
if initialText != "" {
334-
question = initialText + "\n"
335+
tr := strings.NewReader(initialText)
336+
mr := io.MultiReader(tr, r)
337+
r = mr
338+
}
339+
340+
br := bufio.NewReader(r)
341+
342+
// Read the first 3 bytes
343+
pd := make([]byte, 3)
344+
if _, err := io.ReadFull(br, pd); err != nil {
345+
return "", err
346+
}
347+
prefix := string(pd)
348+
if prefix != `"""` && prefix != `'''` {
349+
return "", fmt.Errorf("invalid multiline string prefix: %q", prefix)
335350
}
336351

337352
for {
338353
fmt.Print("... ")
339354

340-
line, err := readLine(ctx)
355+
line, err := readLine(ctx, br)
341356
if err != nil {
357+
if errors.Is(err, io.EOF) {
358+
return "", fmt.Errorf("unclosed multiline input: %w", err)
359+
}
342360
return "", err
343361
}
344362

345-
question += line + "\n"
346-
if strings.TrimSpace(line) == `"""` || strings.HasSuffix(strings.TrimSpace(line), `"""`) {
363+
question += line
364+
if strings.TrimSpace(line) == prefix || strings.HasSuffix(strings.TrimSpace(line), prefix) {
347365
break
348366
}
349367
}
350368

351-
// Find and remove the closing triple quotes
369+
// Find and remove the closing triple terminator
352370
content := question
353-
if idx := strings.LastIndex(content, `"""`); idx >= 0 {
371+
if idx := strings.LastIndex(content, prefix); idx >= 0 {
354372
before := content[:idx]
355-
after := content[idx+3:]
373+
after := content[idx+len(prefix):]
356374
content = before + after
357375
}
358376

359377
return strings.TrimRight(content, "\n"), nil
360378
}
361379

362-
// readLine reads a single line from stdin.
363-
func readLine(ctx context.Context) (string, error) {
380+
// readLine reads a single line from r.
381+
func readLine(ctx context.Context, r *bufio.Reader) (string, error) {
364382
lines := make(chan string)
365383
errs := make(chan error)
366384

367385
go func() {
368386
defer close(lines)
369387
defer close(errs)
370388

371-
reader := bufio.NewReader(os.Stdin)
372-
line, err := reader.ReadString('\n')
373-
if err != nil {
389+
line, err := r.ReadString('\n')
390+
if err != nil && line == "" {
374391
errs <- err
375392
} else {
376393
lines <- line

commands/run_test.go

Lines changed: 132 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package commands
22

33
import (
44
"bufio"
5+
"errors"
6+
"io"
57
"strings"
68
"testing"
79

8-
"github.com/spf13/cobra"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
912
)
1013

1114
func TestReadMultilineInput(t *testing.T) {
@@ -15,40 +18,34 @@ func TestReadMultilineInput(t *testing.T) {
1518
expected string
1619
wantErr bool
1720
}{
18-
{
19-
name: "single line input",
20-
input: "hello world",
21-
expected: "hello world",
22-
wantErr: false,
23-
},
2421
{
2522
name: "single line with triple quotes",
2623
input: `"""hello world"""`,
27-
expected: `"""hello world"""`,
24+
expected: `hello world`,
2825
wantErr: false,
2926
},
3027
{
3128
name: "multiline input with double quotes",
3229
input: `"""tell
33-
me
34-
a
35-
joke"""`,
36-
expected: `"""tell
37-
me
38-
a
39-
joke"""`,
30+
me
31+
a
32+
joke"""`,
33+
expected: `tell
34+
me
35+
a
36+
joke`,
4037
wantErr: false,
4138
},
4239
{
4340
name: "multiline input with single quotes",
4441
input: `'''tell
45-
me
46-
a
47-
joke'''`,
48-
expected: `'''tell
49-
me
50-
a
51-
joke'''`,
42+
me
43+
a
44+
joke'''`,
45+
expected: `tell
46+
me
47+
a
48+
joke`,
5249
wantErr: false,
5350
},
5451
{
@@ -61,27 +58,124 @@ joke'''`,
6158
name: "multiline with empty lines",
6259
input: `"""first line
6360
64-
third line"""`,
65-
expected: `"""first line
61+
third line"""`,
62+
expected: `first line
6663
67-
third line"""`,
64+
third line`,
65+
wantErr: false,
66+
},
67+
{
68+
name: "multiline with spaces and closing quotes on new line",
69+
input: `"""first line
70+
second line
71+
third line
72+
"""`,
73+
expected: `first line
74+
second line
75+
third line`, // this will intentionally trim the last newline
76+
wantErr: false,
77+
},
78+
{
79+
name: "multiline with closing quotes and trailing spaces",
80+
input: `"""first line
81+
second line
82+
third line """`,
83+
expected: `first line
84+
second line
85+
third line `,
86+
wantErr: false,
87+
},
88+
{
89+
name: "single quotes with spaces",
90+
input: `'''foo bar'''`,
91+
expected: `foo bar`,
92+
wantErr: false,
93+
},
94+
{
95+
name: "triple quotes only",
96+
input: `""""""`,
97+
expected: "",
98+
wantErr: false,
99+
},
100+
{
101+
name: "single quotes only",
102+
input: `''''''`,
103+
expected: "",
104+
wantErr: false,
105+
},
106+
{
107+
name: "closing quotes in middle of line",
108+
input: `"""foo"""bar"""`,
109+
expected: `foo"""bar`,
110+
wantErr: false,
111+
},
112+
{
113+
name: "no closing quotes",
114+
input: `"""foo
115+
bar
116+
baz`,
117+
expected: "",
118+
wantErr: true,
119+
},
120+
{
121+
name: "invalid prefix",
122+
input: `"foo bar"`,
123+
expected: "",
124+
wantErr: true,
125+
},
126+
{
127+
name: "prefix but no content",
128+
input: `"""`,
129+
expected: "",
130+
wantErr: true,
131+
},
132+
{
133+
name: "prefix and newline only",
134+
input: `"""
135+
"""`,
136+
expected: "",
137+
wantErr: false,
138+
},
139+
{
140+
name: "multiline with only whitespace",
141+
input: `"""
142+
143+
"""`,
144+
expected: `
145+
146+
`,
68147
wantErr: false,
69148
},
70149
}
71150

151+
// Strings can be read either as the first line followed by the rest of the text,
152+
// or as a single block of text. Make sure we test both scenarios.
153+
72154
for _, tt := range tests {
73155
t.Run(tt.name, func(t *testing.T) {
74-
// Create a mock command for testing
75-
cmd := &cobra.Command{}
156+
r := bufio.NewReader(strings.NewReader(tt.input))
157+
result, err := readMultilineString(t.Context(), r, "")
76158

77-
// Create a scanner from the test input
78-
scanner := bufio.NewScanner(strings.NewReader(tt.input))
159+
if (err != nil) != tt.wantErr {
160+
t.Errorf("readMultilineInput() error = %v, wantErr %v", err, tt.wantErr)
161+
return
162+
}
79163

80-
// Capture output to avoid printing during tests
81-
var output strings.Builder
82-
cmd.SetOut(&output)
164+
if result != tt.expected {
165+
t.Errorf("readMultilineInput() = %q, want %q", result, tt.expected)
166+
}
167+
})
83168

84-
result, err := readMultilineInput(cmd, scanner)
169+
t.Run(tt.name+"_chunked", func(t *testing.T) {
170+
r := bufio.NewReader(strings.NewReader(tt.input))
171+
firstLine, err := r.ReadString('\n') // Simulate reading the first line
172+
if errors.Is(err, io.EOF) {
173+
// Some test cases are single line, EOF is ok here
174+
firstLine = tt.input
175+
} else {
176+
require.NoError(t, err)
177+
}
178+
result, err := readMultilineString(t.Context(), r, firstLine)
85179

86180
if (err != nil) != tt.wantErr {
87181
t.Errorf("readMultilineInput() error = %v, wantErr %v", err, tt.wantErr)
@@ -98,18 +192,12 @@ third line"""`,
98192
func TestReadMultilineInputUnclosed(t *testing.T) {
99193
// Test unclosed multiline input (should return error)
100194
input := `"""unclosed multiline`
101-
cmd := &cobra.Command{}
102-
var output strings.Builder
103-
cmd.SetOut(&output)
104-
105-
scanner := bufio.NewScanner(strings.NewReader(input))
106-
107-
_, err := readMultilineInput(cmd, scanner)
195+
_, err := readMultilineString(t.Context(), strings.NewReader(input), "")
108196
if err == nil {
109-
t.Error("readMultilineInput() should return error for unclosed multiline input")
197+
t.Fatal("readMultilineInput() should return an error for unclosed multiline input")
110198
}
111199

112-
if !strings.Contains(err.Error(), "unclosed multiline input") {
113-
t.Errorf("readMultilineInput() error should mention unclosed multiline input, got: %v", err)
114-
}
200+
assert.Contains(t, err.Error(), "unclosed multiline input", "error should mention unclosed multiline input")
201+
// Error should also be io.EOF
202+
assert.True(t, errors.Is(err, io.EOF), "error should be io.EOF")
115203
}

desktop/desktop_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ func TestChatHuggingFaceModel(t *testing.T) {
6363
Body: io.NopCloser(bytes.NewBufferString("data: {\"choices\":[{\"delta\":{\"content\":\"Hello there!\"}}]}\n")),
6464
}, nil)
6565

66-
err := client.Chat("", modelName, prompt, "")
66+
resp, err := client.Chat("", modelName, prompt, "")
6767
assert.NoError(t, err)
68+
assert.Equal(t, []string{"Hello there!"}, resp)
6869
}
6970

7071
func TestInspectHuggingFaceModel(t *testing.T) {

0 commit comments

Comments
 (0)