Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
kardolus
GitHub Repository: kardolus/chatgpt-cli
Path: blob/main/test/contract/contract_test.go
2649 views
1
package contract_test
2
3
import (
4
"bytes"
5
"encoding/json"
6
"github.com/kardolus/chatgpt-cli/api"
7
"github.com/kardolus/chatgpt-cli/api/client"
8
"github.com/kardolus/chatgpt-cli/api/http"
9
"github.com/kardolus/chatgpt-cli/config"
10
. "github.com/onsi/gomega"
11
"github.com/sclevine/spec"
12
"github.com/sclevine/spec/report"
13
"io"
14
"mime/multipart"
15
"os"
16
"path/filepath"
17
"strings"
18
"testing"
19
)
20
21
func TestContract(t *testing.T) {
22
spec.Run(t, "Contract Tests", testContract, spec.Report(report.Terminal{}), spec.Parallel())
23
}
24
25
func testContract(t *testing.T, when spec.G, it spec.S) {
26
var (
27
restCaller *http.RestCaller
28
cfg config.Config
29
)
30
31
it.Before(func() {
32
RegisterTestingT(t)
33
34
apiKey := os.Getenv(config.NewManager(config.NewStore()).WithEnvironment().APIKeyEnvVarName())
35
Expect(apiKey).NotTo(BeEmpty())
36
37
cfg = config.NewStore().ReadDefaults()
38
cfg.APIKey = apiKey
39
40
restCaller = http.New(cfg)
41
})
42
43
when("accessing the completion endpoint", func() {
44
it("should return a successful response with expected keys", func() {
45
body := api.CompletionsRequest{
46
Messages: []api.Message{{
47
Role: client.SystemRole,
48
Content: cfg.Role,
49
}},
50
MaxTokens: 1234,
51
Model: cfg.Model,
52
Stream: false,
53
}
54
55
bytes, err := json.Marshal(body)
56
Expect(err).NotTo(HaveOccurred())
57
58
resp, err := restCaller.Post(cfg.URL+cfg.CompletionsPath, bytes, false)
59
Expect(err).NotTo(HaveOccurred())
60
61
var data api.CompletionsResponse
62
err = json.Unmarshal(resp, &data)
63
Expect(err).NotTo(HaveOccurred())
64
65
Expect(data.ID).ShouldNot(BeEmpty(), "Expected ID to be present in the response")
66
Expect(data.Object).ShouldNot(BeEmpty(), "Expected Object to be present in the response")
67
Expect(data.Created).ShouldNot(BeZero(), "Expected Created to be present in the response")
68
Expect(data.Model).ShouldNot(BeEmpty(), "Expected Model to be present in the response")
69
Expect(data.Usage).ShouldNot(BeNil(), "Expected Usage to be present in the response")
70
Expect(data.Choices).ShouldNot(BeNil(), "Expected Choices to be present in the response")
71
})
72
73
it("should return an error response with appropriate error details", func() {
74
body := api.CompletionsRequest{
75
Messages: []api.Message{{
76
Role: client.SystemRole,
77
Content: cfg.Role,
78
}},
79
Model: "no-such-model",
80
Stream: false,
81
}
82
83
bytes, err := json.Marshal(body)
84
Expect(err).NotTo(HaveOccurred())
85
86
resp, err := restCaller.Post(cfg.URL+cfg.CompletionsPath, bytes, false)
87
Expect(err).To(HaveOccurred())
88
89
var errorData api.ErrorResponse
90
err = json.Unmarshal(resp, &errorData)
91
Expect(err).NotTo(HaveOccurred())
92
93
Expect(errorData.Error.Message).ShouldNot(BeEmpty(), "Expected error message to be present in the response")
94
Expect(errorData.Error.Type).ShouldNot(BeEmpty(), "Expected error type to be present in the response")
95
Expect(errorData.Error.Code).ShouldNot(BeEmpty(), "Expected error code to be present in the response")
96
})
97
})
98
99
when("accessing the models endpoint", func() {
100
it("should have the expected keys in the response", func() {
101
resp, err := restCaller.Get(cfg.URL + cfg.ModelsPath)
102
Expect(err).NotTo(HaveOccurred())
103
104
var data api.ListModelsResponse
105
err = json.Unmarshal(resp, &data)
106
Expect(err).NotTo(HaveOccurred())
107
108
Expect(data.Object).ShouldNot(BeEmpty(), "Expected Object to be present in the response")
109
Expect(data.Data).ShouldNot(BeNil(), "Expected Data to be present in the response")
110
Expect(data.Data).NotTo(BeEmpty())
111
112
for _, model := range data.Data {
113
Expect(model.Id).ShouldNot(BeEmpty(), "Expected Model Id to be present in the response")
114
Expect(model.Object).ShouldNot(BeEmpty(), "Expected Model Object to be present in the response")
115
Expect(model.Created).ShouldNot(BeZero(), "Expected Model Created to be present in the response")
116
Expect(model.OwnedBy).ShouldNot(BeEmpty(), "Expected Model OwnedBy to be present in the response")
117
}
118
})
119
})
120
121
when("accessing the responses endpoint", func() {
122
it("should return a successful response with expected keys and content", func() {
123
body := api.ResponsesRequest{
124
Model: "o1-pro",
125
Input: []api.Message{{
126
Role: client.UserRole,
127
Content: "what is the capital of sweden",
128
}},
129
MaxOutputTokens: 64,
130
Reasoning: api.Reasoning{
131
Effort: "low",
132
},
133
}
134
135
bytes, err := json.Marshal(body)
136
Expect(err).NotTo(HaveOccurred())
137
138
resp, err := restCaller.Post(cfg.URL+cfg.ResponsesPath, bytes, false)
139
Expect(err).NotTo(HaveOccurred())
140
141
var data api.ResponsesResponse
142
err = json.Unmarshal(resp, &data)
143
Expect(err).NotTo(HaveOccurred())
144
145
// High-level structure
146
Expect(data.ID).To(HavePrefix("resp_"))
147
Expect(data.Object).To(Equal("response"))
148
Expect(data.CreatedAt).To(BeNumerically(">", 0))
149
Expect(data.Model).To(ContainSubstring("o1-pro"))
150
Expect(data.Status).To(SatisfyAny(Equal("completed"), Equal("incomplete")))
151
Expect(data.Output).NotTo(BeEmpty())
152
153
// Check for a message block with output_text
154
var textFound bool
155
for _, block := range data.Output {
156
if block.Type == "message" {
157
for _, content := range block.Content {
158
if content.Type == "output_text" && strings.Contains(strings.ToLower(content.Text), "stockholm") {
159
textFound = true
160
}
161
}
162
}
163
}
164
Expect(textFound).To(BeTrue(), "expected to find 'Stockholm' in an output_text block")
165
166
// Usage stats present
167
Expect(data.Usage.TotalTokens).To(BeNumerically(">", 0))
168
Expect(data.Usage.InputTokens).To(BeNumerically(">", 0))
169
Expect(data.Usage.OutputTokens).To(BeNumerically(">", 0))
170
})
171
})
172
173
when("accessing the speech endpoint", func() {
174
it("should return audio data for a valid request", func() {
175
body := api.Speech{
176
Model: "gpt-4o-mini-tts",
177
Input: "Hello world",
178
Voice: "nova",
179
ResponseFormat: "mp3",
180
}
181
182
bytes, err := json.Marshal(body)
183
Expect(err).NotTo(HaveOccurred())
184
185
resp, err := restCaller.Post(cfg.URL+cfg.SpeechPath, bytes, false)
186
Expect(err).NotTo(HaveOccurred())
187
Expect(resp).NotTo(BeEmpty())
188
189
tmpFile, err := os.CreateTemp("", "speech-*.mp3")
190
Expect(err).NotTo(HaveOccurred())
191
defer os.Remove(tmpFile.Name())
192
193
_, err = tmpFile.Write(resp)
194
Expect(err).NotTo(HaveOccurred())
195
196
err = tmpFile.Close()
197
Expect(err).NotTo(HaveOccurred())
198
199
isMP3 := strings.HasPrefix(string(resp), "ID3") || (len(resp) > 2 && resp[0] == 0xFF && resp[1]&0xE0 == 0xE0)
200
Expect(isMP3).To(BeTrue(), "response does not appear to be valid MP3 audio")
201
})
202
})
203
204
when("accessing the transcriptions endpoint", func() {
205
it("should return transcribed text for a valid audio file", func() {
206
audioPath := "../data/hello.wav"
207
file, err := os.Open(audioPath)
208
Expect(err).NotTo(HaveOccurred())
209
defer file.Close()
210
211
var buf bytes.Buffer
212
writer := multipart.NewWriter(&buf)
213
214
err = writer.WriteField("model", "gpt-4o-transcribe")
215
Expect(err).NotTo(HaveOccurred())
216
217
part, err := writer.CreateFormFile("file", filepath.Base(audioPath))
218
Expect(err).NotTo(HaveOccurred())
219
220
_, err = io.Copy(part, file)
221
Expect(err).NotTo(HaveOccurred())
222
223
err = writer.Close()
224
Expect(err).NotTo(HaveOccurred())
225
226
resp, err := restCaller.PostWithHeaders(cfg.URL+cfg.TranscriptionsPath, buf.Bytes(), map[string]string{
227
"Content-Type": writer.FormDataContentType(),
228
"Authorization": "Bearer " + cfg.APIKey,
229
})
230
Expect(err).NotTo(HaveOccurred())
231
232
var res struct {
233
Text string `json:"text"`
234
}
235
err = json.Unmarshal(resp, &res)
236
Expect(err).NotTo(HaveOccurred())
237
238
Expect(res.Text).NotTo(BeEmpty(), "Expected transcribed text to be returned")
239
Expect(strings.ToLower(res.Text)).To(ContainSubstring("hello"))
240
})
241
})
242
}
243
244