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
|
package llm
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// unsetYouSearchEnv ensures no real YOU_API_KEY / HEXAI_YOUSEARCH_API_KEY in the
// environment can influence factory tests that should fail when no key is passed.
// We restore the previous values when the test ends.
func unsetYouSearchEnv(t *testing.T) {
t.Helper()
for _, k := range []string{"YOU_API_KEY", "HEXAI_YOUSEARCH_API_KEY", "HEXAI_YOUSEARCH_RESEARCH_EFFORT"} {
t.Setenv(k, "")
}
}
func TestYouSearchFactory_MissingKey(t *testing.T) {
unsetYouSearchEnv(t)
if _, err := NewFromConfig(Config{Provider: "yousearch"}, "", "", "", "", ""); err == nil {
t.Fatalf("expected error when YouSearch API key is missing")
} else if !strings.Contains(err.Error(), "HEXAI_YOUSEARCH_API_KEY") || !strings.Contains(err.Error(), "YOU_API_KEY") {
t.Fatalf("expected actionable API key hint, got %q", err.Error())
}
}
func TestYouSearchFactory_Success(t *testing.T) {
unsetYouSearchEnv(t)
c, err := NewFromConfig(Config{Provider: "yousearch", YouSearchResearchEffort: "deep"}, "", "", "", "", "ys-key")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.Name() != "yousearch" {
t.Fatalf("expected name yousearch, got %s", c.Name())
}
if c.DefaultModel() != "deep" {
t.Fatalf("expected default model 'deep', got %s", c.DefaultModel())
}
}
func TestYouSearchClient_DefaultModel_Empty(t *testing.T) {
c := youSearchClient{}
if got := c.DefaultModel(); got != "standard" {
t.Fatalf("expected 'standard', got %s", got)
}
}
func TestYouSearchChat_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Fatalf("expected POST, got %s", r.Method)
}
if r.Header.Get("X-API-Key") != "ys-key" {
t.Fatalf("expected X-API-Key header to be ys-key, got %q", r.Header.Get("X-API-Key"))
}
if r.Header.Get("Content-Type") != "application/json" {
t.Fatalf("expected application/json content type")
}
body, _ := io.ReadAll(r.Body)
var req youSearchRequest
if err := json.Unmarshal(body, &req); err != nil {
t.Fatalf("decode request: %v", err)
}
if req.Input != "what is foo?" {
t.Fatalf("expected input 'what is foo?', got %q", req.Input)
}
if req.ResearchEffort != "lite" {
t.Fatalf("expected research_effort lite, got %q", req.ResearchEffort)
}
resp := map[string]any{
"output": map[string]any{
"content": "Foo is a placeholder.",
"content_type": "text/markdown",
"sources": []map[string]any{
{"url": "https://example.com/a", "title": "A"},
{"url": "https://example.com/b", "title": ""},
},
},
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer srv.Close()
c := youSearchClient{
httpClient: srv.Client(),
apiKey: "ys-key",
baseURL: srv.URL,
researchEffort: "lite",
}
out, err := c.Chat(context.Background(), []Message{
{Role: "system", Content: "ignored"},
{Role: "user", Content: "what is foo?"},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(out, "Foo is a placeholder.") {
t.Fatalf("expected content in output, got %q", out)
}
if !strings.Contains(out, "**Sources:**") {
t.Fatalf("expected sources section, got %q", out)
}
if !strings.Contains(out, "[A](https://example.com/a)") {
t.Fatalf("expected first source linked, got %q", out)
}
if !strings.Contains(out, "[https://example.com/b](https://example.com/b)") {
t.Fatalf("expected fallback title when title is empty, got %q", out)
}
}
func TestYouSearchChat_NoUserMessage(t *testing.T) {
c := youSearchClient{apiKey: "ys-key", baseURL: "http://unused.invalid"}
_, err := c.Chat(context.Background(), []Message{{Role: "system", Content: "hi"}})
if err == nil || !strings.Contains(err.Error(), "no user message") {
t.Fatalf("expected no-user-message error, got %v", err)
}
}
func TestYouSearchChat_Non2xx(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":"nope"}`))
}))
defer srv.Close()
c := youSearchClient{httpClient: srv.Client(), apiKey: "ys-key", baseURL: srv.URL}
_, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "q"}})
if err == nil || !strings.Contains(err.Error(), "status 401") {
t.Fatalf("expected status 401 error, got %v", err)
}
}
func TestYouSearchChat_EmptyResponse(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"output":{"content":"","sources":[]}}`))
}))
defer srv.Close()
c := youSearchClient{httpClient: srv.Client(), apiKey: "ys-key", baseURL: srv.URL}
_, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "q"}})
if err == nil || !strings.Contains(err.Error(), "empty response") {
t.Fatalf("expected empty-response error, got %v", err)
}
}
func TestLastUserMessage(t *testing.T) {
got := lastUserMessage([]Message{
{Role: "system", Content: "sys"},
{Role: "user", Content: " first "},
{Role: "assistant", Content: "a"},
{Role: "USER", Content: " second "},
})
if got != "second" {
t.Fatalf("expected 'second', got %q", got)
}
if got := lastUserMessage(nil); got != "" {
t.Fatalf("expected empty for nil messages, got %q", got)
}
if got := lastUserMessage([]Message{{Role: "assistant", Content: "a"}}); got != "" {
t.Fatalf("expected empty when no user messages, got %q", got)
}
}
func TestFormatYouSearchContent_NonStringContent(t *testing.T) {
var r youSearchResponse
r.Output.Content = map[string]any{"k": "v"}
got := formatYouSearchContent(r)
if !strings.Contains(got, `"k"`) || !strings.Contains(got, `"v"`) {
t.Fatalf("expected marshalled map in output, got %q", got)
}
}
|