|
24a217e…
|
lmata
|
1 |
package sessionrelay |
|
24a217e…
|
lmata
|
2 |
|
|
24a217e…
|
lmata
|
3 |
import ( |
|
24a217e…
|
lmata
|
4 |
"context" |
|
24a217e…
|
lmata
|
5 |
"encoding/json" |
|
24a217e…
|
lmata
|
6 |
"net/http" |
|
24a217e…
|
lmata
|
7 |
"net/http/httptest" |
|
1d3caa2…
|
lmata
|
8 |
"os" |
|
1d3caa2…
|
lmata
|
9 |
"slices" |
|
24a217e…
|
lmata
|
10 |
"testing" |
|
24a217e…
|
lmata
|
11 |
"time" |
|
24a217e…
|
lmata
|
12 |
) |
|
24a217e…
|
lmata
|
13 |
|
|
24a217e…
|
lmata
|
14 |
func TestHTTPConnectorPostMessagesAndTouch(t *testing.T) { |
|
24a217e…
|
lmata
|
15 |
t.Helper() |
|
24a217e…
|
lmata
|
16 |
|
|
24a217e…
|
lmata
|
17 |
base := time.Date(2026, 3, 31, 22, 0, 0, 0, time.UTC) |
|
1d3caa2…
|
lmata
|
18 |
var gotAuth []string |
|
1d3caa2…
|
lmata
|
19 |
var posted []string |
|
1d3caa2…
|
lmata
|
20 |
var touched []string |
|
24a217e…
|
lmata
|
21 |
|
|
24a217e…
|
lmata
|
22 |
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
1d3caa2…
|
lmata
|
23 |
gotAuth = append(gotAuth, r.Header.Get("Authorization")) |
|
24a217e…
|
lmata
|
24 |
switch { |
|
24a217e…
|
lmata
|
25 |
case r.Method == http.MethodPost && r.URL.Path == "/v1/channels/general/messages": |
|
1d3caa2…
|
lmata
|
26 |
var body map[string]string |
|
1d3caa2…
|
lmata
|
27 |
if err := json.NewDecoder(r.Body).Decode(&body); err != nil { |
|
1d3caa2…
|
lmata
|
28 |
t.Fatalf("decode general post body: %v", err) |
|
1d3caa2…
|
lmata
|
29 |
} |
|
1d3caa2…
|
lmata
|
30 |
posted = append(posted, "general:"+body["nick"]+":"+body["text"]) |
|
1d3caa2…
|
lmata
|
31 |
w.WriteHeader(http.StatusNoContent) |
|
1d3caa2…
|
lmata
|
32 |
case r.Method == http.MethodPost && r.URL.Path == "/v1/channels/release/messages": |
|
1d3caa2…
|
lmata
|
33 |
var body map[string]string |
|
1d3caa2…
|
lmata
|
34 |
if err := json.NewDecoder(r.Body).Decode(&body); err != nil { |
|
1d3caa2…
|
lmata
|
35 |
t.Fatalf("decode release post body: %v", err) |
|
24a217e…
|
lmata
|
36 |
} |
|
1d3caa2…
|
lmata
|
37 |
posted = append(posted, "release:"+body["nick"]+":"+body["text"]) |
|
24a217e…
|
lmata
|
38 |
w.WriteHeader(http.StatusNoContent) |
|
24a217e…
|
lmata
|
39 |
case r.Method == http.MethodPost && r.URL.Path == "/v1/channels/general/presence": |
|
1d3caa2…
|
lmata
|
40 |
var body map[string]string |
|
1d3caa2…
|
lmata
|
41 |
if err := json.NewDecoder(r.Body).Decode(&body); err != nil { |
|
1d3caa2…
|
lmata
|
42 |
t.Fatalf("decode general touch body: %v", err) |
|
1d3caa2…
|
lmata
|
43 |
} |
|
1d3caa2…
|
lmata
|
44 |
touched = append(touched, "general:"+body["nick"]) |
|
1d3caa2…
|
lmata
|
45 |
w.WriteHeader(http.StatusNoContent) |
|
1d3caa2…
|
lmata
|
46 |
case r.Method == http.MethodPost && r.URL.Path == "/v1/channels/release/presence": |
|
1d3caa2…
|
lmata
|
47 |
var body map[string]string |
|
1d3caa2…
|
lmata
|
48 |
if err := json.NewDecoder(r.Body).Decode(&body); err != nil { |
|
1d3caa2…
|
lmata
|
49 |
t.Fatalf("decode release touch body: %v", err) |
|
24a217e…
|
lmata
|
50 |
} |
|
1d3caa2…
|
lmata
|
51 |
touched = append(touched, "release:"+body["nick"]) |
|
24a217e…
|
lmata
|
52 |
w.WriteHeader(http.StatusNoContent) |
|
24a217e…
|
lmata
|
53 |
case r.Method == http.MethodGet && r.URL.Path == "/v1/channels/general/messages": |
|
24a217e…
|
lmata
|
54 |
_ = json.NewEncoder(w).Encode(map[string]any{"messages": []map[string]string{ |
|
24a217e…
|
lmata
|
55 |
{"at": base.Add(-time.Second).Format(time.RFC3339Nano), "nick": "old", "text": "ignore"}, |
|
24a217e…
|
lmata
|
56 |
{"at": base.Add(time.Second).Format(time.RFC3339Nano), "nick": "glengoolie", "text": "codex-test: check README"}, |
|
24a217e…
|
lmata
|
57 |
}}) |
|
1d3caa2…
|
lmata
|
58 |
case r.Method == http.MethodGet && r.URL.Path == "/v1/channels/release/messages": |
|
1d3caa2…
|
lmata
|
59 |
_ = json.NewEncoder(w).Encode(map[string]any{"messages": []map[string]string{ |
|
1d3caa2…
|
lmata
|
60 |
{"at": base.Add(2 * time.Second).Format(time.RFC3339Nano), "nick": "glengoolie", "text": "codex-test: /join #task-42"}, |
|
1d3caa2…
|
lmata
|
61 |
}}) |
|
763c873…
|
lmata
|
62 |
case r.Method == http.MethodPost && r.URL.Path == "/v1/agents/register": |
|
763c873…
|
lmata
|
63 |
w.WriteHeader(http.StatusCreated) |
|
24a217e…
|
lmata
|
64 |
default: |
|
24a217e…
|
lmata
|
65 |
http.NotFound(w, r) |
|
24a217e…
|
lmata
|
66 |
} |
|
24a217e…
|
lmata
|
67 |
})) |
|
24a217e…
|
lmata
|
68 |
defer srv.Close() |
|
24a217e…
|
lmata
|
69 |
|
|
24a217e…
|
lmata
|
70 |
conn, err := New(Config{ |
|
24a217e…
|
lmata
|
71 |
Transport: TransportHTTP, |
|
24a217e…
|
lmata
|
72 |
URL: srv.URL, |
|
24a217e…
|
lmata
|
73 |
Token: "test-token", |
|
24a217e…
|
lmata
|
74 |
Channel: "general", |
|
1d3caa2…
|
lmata
|
75 |
Channels: []string{"general", "release"}, |
|
24a217e…
|
lmata
|
76 |
Nick: "codex-test", |
|
24a217e…
|
lmata
|
77 |
HTTPClient: srv.Client(), |
|
24a217e…
|
lmata
|
78 |
}) |
|
24a217e…
|
lmata
|
79 |
if err != nil { |
|
24a217e…
|
lmata
|
80 |
t.Fatal(err) |
|
24a217e…
|
lmata
|
81 |
} |
|
24a217e…
|
lmata
|
82 |
if err := conn.Connect(context.Background()); err != nil { |
|
24a217e…
|
lmata
|
83 |
t.Fatal(err) |
|
24a217e…
|
lmata
|
84 |
} |
|
24a217e…
|
lmata
|
85 |
if err := conn.Post(context.Background(), "online"); err != nil { |
|
24a217e…
|
lmata
|
86 |
t.Fatal(err) |
|
24a217e…
|
lmata
|
87 |
} |
|
1d3caa2…
|
lmata
|
88 |
if want := []string{"general:codex-test:online", "release:codex-test:online"}; !slices.Equal(posted, want) { |
|
1d3caa2…
|
lmata
|
89 |
t.Fatalf("posted = %#v, want %#v", posted, want) |
|
24a217e…
|
lmata
|
90 |
} |
|
1d3caa2…
|
lmata
|
91 |
for _, auth := range gotAuth { |
|
1d3caa2…
|
lmata
|
92 |
if auth != "Bearer test-token" { |
|
1d3caa2…
|
lmata
|
93 |
t.Fatalf("authorization = %q", auth) |
|
1d3caa2…
|
lmata
|
94 |
} |
|
24a217e…
|
lmata
|
95 |
} |
|
24a217e…
|
lmata
|
96 |
|
|
24a217e…
|
lmata
|
97 |
msgs, err := conn.MessagesSince(context.Background(), base) |
|
24a217e…
|
lmata
|
98 |
if err != nil { |
|
24a217e…
|
lmata
|
99 |
t.Fatal(err) |
|
24a217e…
|
lmata
|
100 |
} |
|
1d3caa2…
|
lmata
|
101 |
if len(msgs) != 2 { |
|
1d3caa2…
|
lmata
|
102 |
t.Fatalf("MessagesSince len = %d, want 2", len(msgs)) |
|
1d3caa2…
|
lmata
|
103 |
} |
|
1d3caa2…
|
lmata
|
104 |
if msgs[0].Channel != "#general" || msgs[1].Channel != "#release" { |
|
1d3caa2…
|
lmata
|
105 |
t.Fatalf("MessagesSince channels = %#v", msgs) |
|
1d3caa2…
|
lmata
|
106 |
} |
|
1d3caa2…
|
lmata
|
107 |
|
|
1d3caa2…
|
lmata
|
108 |
if err := conn.Touch(context.Background()); err != nil { |
|
1d3caa2…
|
lmata
|
109 |
t.Fatal(err) |
|
1d3caa2…
|
lmata
|
110 |
} |
|
1d3caa2…
|
lmata
|
111 |
if want := []string{"general:codex-test", "release:codex-test"}; !slices.Equal(touched, want) { |
|
1d3caa2…
|
lmata
|
112 |
t.Fatalf("touches = %#v, want %#v", touched, want) |
|
1d3caa2…
|
lmata
|
113 |
} |
|
1d3caa2…
|
lmata
|
114 |
} |
|
1d3caa2…
|
lmata
|
115 |
|
|
1d3caa2…
|
lmata
|
116 |
func TestHTTPConnectorJoinPartAndControlChannel(t *testing.T) { |
|
1d3caa2…
|
lmata
|
117 |
t.Helper() |
|
1d3caa2…
|
lmata
|
118 |
|
|
1d3caa2…
|
lmata
|
119 |
conn, err := New(Config{ |
|
1d3caa2…
|
lmata
|
120 |
Transport: TransportHTTP, |
|
1d3caa2…
|
lmata
|
121 |
URL: "http://example.com", |
|
1d3caa2…
|
lmata
|
122 |
Token: "test-token", |
|
1d3caa2…
|
lmata
|
123 |
Channel: "general", |
|
1d3caa2…
|
lmata
|
124 |
Channels: []string{"general", "release"}, |
|
1d3caa2…
|
lmata
|
125 |
Nick: "codex-test", |
|
1d3caa2…
|
lmata
|
126 |
}) |
|
1d3caa2…
|
lmata
|
127 |
if err != nil { |
|
1d3caa2…
|
lmata
|
128 |
t.Fatal(err) |
|
1d3caa2…
|
lmata
|
129 |
} |
|
1d3caa2…
|
lmata
|
130 |
|
|
1d3caa2…
|
lmata
|
131 |
if got := conn.ControlChannel(); got != "#general" { |
|
1d3caa2…
|
lmata
|
132 |
t.Fatalf("ControlChannel = %q, want #general", got) |
|
1d3caa2…
|
lmata
|
133 |
} |
|
1d3caa2…
|
lmata
|
134 |
if err := conn.JoinChannel(context.Background(), "#task-42"); err != nil { |
|
1d3caa2…
|
lmata
|
135 |
t.Fatal(err) |
|
1d3caa2…
|
lmata
|
136 |
} |
|
1d3caa2…
|
lmata
|
137 |
if want := []string{"#general", "#release", "#task-42"}; !slices.Equal(conn.Channels(), want) { |
|
1d3caa2…
|
lmata
|
138 |
t.Fatalf("Channels after join = %#v, want %#v", conn.Channels(), want) |
|
1d3caa2…
|
lmata
|
139 |
} |
|
1d3caa2…
|
lmata
|
140 |
if err := conn.PartChannel(context.Background(), "#general"); err == nil { |
|
1d3caa2…
|
lmata
|
141 |
t.Fatal("PartChannel(control) = nil, want error") |
|
1d3caa2…
|
lmata
|
142 |
} |
|
1d3caa2…
|
lmata
|
143 |
if err := conn.PartChannel(context.Background(), "#release"); err != nil { |
|
1d3caa2…
|
lmata
|
144 |
t.Fatal(err) |
|
1d3caa2…
|
lmata
|
145 |
} |
|
1d3caa2…
|
lmata
|
146 |
if want := []string{"#general", "#task-42"}; !slices.Equal(conn.Channels(), want) { |
|
1d3caa2…
|
lmata
|
147 |
t.Fatalf("Channels after part = %#v, want %#v", conn.Channels(), want) |
|
24a217e…
|
lmata
|
148 |
} |
|
24a217e…
|
lmata
|
149 |
} |
|
24a217e…
|
lmata
|
150 |
|
|
24a217e…
|
lmata
|
151 |
func TestIRCRegisterOrRotateCreatesAndDeletes(t *testing.T) { |
|
24a217e…
|
lmata
|
152 |
t.Helper() |
|
24a217e…
|
lmata
|
153 |
|
|
24a217e…
|
lmata
|
154 |
var deletedPath string |
|
1d3caa2…
|
lmata
|
155 |
var registerChannels []string |
|
24a217e…
|
lmata
|
156 |
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
24a217e…
|
lmata
|
157 |
switch { |
|
24a217e…
|
lmata
|
158 |
case r.Method == http.MethodPost && r.URL.Path == "/v1/agents/register": |
|
1d3caa2…
|
lmata
|
159 |
var body struct { |
|
1d3caa2…
|
lmata
|
160 |
Channels []string `json:"channels"` |
|
1d3caa2…
|
lmata
|
161 |
} |
|
1d3caa2…
|
lmata
|
162 |
if err := json.NewDecoder(r.Body).Decode(&body); err != nil { |
|
1d3caa2…
|
lmata
|
163 |
t.Fatalf("decode register body: %v", err) |
|
1d3caa2…
|
lmata
|
164 |
} |
|
1d3caa2…
|
lmata
|
165 |
registerChannels = body.Channels |
|
24a217e…
|
lmata
|
166 |
w.WriteHeader(http.StatusCreated) |
|
24a217e…
|
lmata
|
167 |
_ = json.NewEncoder(w).Encode(map[string]any{ |
|
24a217e…
|
lmata
|
168 |
"credentials": map[string]string{"passphrase": "created-pass"}, |
|
24a217e…
|
lmata
|
169 |
}) |
|
24a217e…
|
lmata
|
170 |
case r.Method == http.MethodDelete && r.URL.Path == "/v1/agents/codex-1234": |
|
24a217e…
|
lmata
|
171 |
deletedPath = r.URL.Path |
|
24a217e…
|
lmata
|
172 |
w.WriteHeader(http.StatusNoContent) |
|
24a217e…
|
lmata
|
173 |
default: |
|
24a217e…
|
lmata
|
174 |
http.NotFound(w, r) |
|
24a217e…
|
lmata
|
175 |
} |
|
24a217e…
|
lmata
|
176 |
})) |
|
24a217e…
|
lmata
|
177 |
defer srv.Close() |
|
24a217e…
|
lmata
|
178 |
|
|
24a217e…
|
lmata
|
179 |
conn := &ircConnector{ |
|
24a217e…
|
lmata
|
180 |
http: srv.Client(), |
|
24a217e…
|
lmata
|
181 |
apiURL: srv.URL, |
|
24a217e…
|
lmata
|
182 |
token: "test-token", |
|
24a217e…
|
lmata
|
183 |
nick: "codex-1234", |
|
1d3caa2…
|
lmata
|
184 |
primary: "#general", |
|
1d3caa2…
|
lmata
|
185 |
channels: []string{"#general", "#release"}, |
|
24a217e…
|
lmata
|
186 |
agentType: "worker", |
|
24a217e…
|
lmata
|
187 |
deleteOnClose: true, |
|
24a217e…
|
lmata
|
188 |
} |
|
24a217e…
|
lmata
|
189 |
|
|
24a217e…
|
lmata
|
190 |
created, pass, err := conn.registerOrRotate(context.Background()) |
|
24a217e…
|
lmata
|
191 |
if err != nil { |
|
24a217e…
|
lmata
|
192 |
t.Fatal(err) |
|
24a217e…
|
lmata
|
193 |
} |
|
24a217e…
|
lmata
|
194 |
if !created || pass != "created-pass" { |
|
24a217e…
|
lmata
|
195 |
t.Fatalf("registerOrRotate = (%v, %q), want (true, created-pass)", created, pass) |
|
1d3caa2…
|
lmata
|
196 |
} |
|
1d3caa2…
|
lmata
|
197 |
if want := []string{"#general", "#release"}; !slices.Equal(registerChannels, want) { |
|
1d3caa2…
|
lmata
|
198 |
t.Fatalf("register channels = %#v, want %#v", registerChannels, want) |
|
24a217e…
|
lmata
|
199 |
} |
|
24a217e…
|
lmata
|
200 |
conn.registeredByRelay = created |
|
24a217e…
|
lmata
|
201 |
if err := conn.cleanupRegistration(context.Background()); err != nil { |
|
24a217e…
|
lmata
|
202 |
t.Fatal(err) |
|
24a217e…
|
lmata
|
203 |
} |
|
24a217e…
|
lmata
|
204 |
if deletedPath != "/v1/agents/codex-1234" { |
|
24a217e…
|
lmata
|
205 |
t.Fatalf("delete path = %q", deletedPath) |
|
24a217e…
|
lmata
|
206 |
} |
|
24a217e…
|
lmata
|
207 |
} |
|
24a217e…
|
lmata
|
208 |
|
|
24a217e…
|
lmata
|
209 |
func TestIRCRegisterOrRotateFallsBackToRotate(t *testing.T) { |
|
24a217e…
|
lmata
|
210 |
t.Helper() |
|
24a217e…
|
lmata
|
211 |
|
|
24a217e…
|
lmata
|
212 |
var rotateCalled bool |
|
24a217e…
|
lmata
|
213 |
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
24a217e…
|
lmata
|
214 |
switch { |
|
24a217e…
|
lmata
|
215 |
case r.Method == http.MethodPost && r.URL.Path == "/v1/agents/register": |
|
24a217e…
|
lmata
|
216 |
w.WriteHeader(http.StatusConflict) |
|
24a217e…
|
lmata
|
217 |
case r.Method == http.MethodPost && r.URL.Path == "/v1/agents/codex-1234/rotate": |
|
24a217e…
|
lmata
|
218 |
rotateCalled = true |
|
24a217e…
|
lmata
|
219 |
_ = json.NewEncoder(w).Encode(map[string]string{"passphrase": "rotated-pass"}) |
|
24a217e…
|
lmata
|
220 |
default: |
|
24a217e…
|
lmata
|
221 |
http.NotFound(w, r) |
|
24a217e…
|
lmata
|
222 |
} |
|
24a217e…
|
lmata
|
223 |
})) |
|
24a217e…
|
lmata
|
224 |
defer srv.Close() |
|
24a217e…
|
lmata
|
225 |
|
|
24a217e…
|
lmata
|
226 |
conn := &ircConnector{ |
|
24a217e…
|
lmata
|
227 |
http: srv.Client(), |
|
24a217e…
|
lmata
|
228 |
apiURL: srv.URL, |
|
24a217e…
|
lmata
|
229 |
token: "test-token", |
|
24a217e…
|
lmata
|
230 |
nick: "codex-1234", |
|
1d3caa2…
|
lmata
|
231 |
primary: "#general", |
|
1d3caa2…
|
lmata
|
232 |
channels: []string{"#general"}, |
|
24a217e…
|
lmata
|
233 |
agentType: "worker", |
|
24a217e…
|
lmata
|
234 |
} |
|
24a217e…
|
lmata
|
235 |
|
|
24a217e…
|
lmata
|
236 |
created, pass, err := conn.registerOrRotate(context.Background()) |
|
24a217e…
|
lmata
|
237 |
if err != nil { |
|
24a217e…
|
lmata
|
238 |
t.Fatal(err) |
|
24a217e…
|
lmata
|
239 |
} |
|
24a217e…
|
lmata
|
240 |
if created { |
|
24a217e…
|
lmata
|
241 |
t.Fatal("created = true, want false when register conflicts") |
|
24a217e…
|
lmata
|
242 |
} |
|
24a217e…
|
lmata
|
243 |
if !rotateCalled || pass != "rotated-pass" { |
|
24a217e…
|
lmata
|
244 |
t.Fatalf("rotate fallback = (called=%v, pass=%q)", rotateCalled, pass) |
|
1d3caa2…
|
lmata
|
245 |
} |
|
1d3caa2…
|
lmata
|
246 |
} |
|
1d3caa2…
|
lmata
|
247 |
|
|
1d3caa2…
|
lmata
|
248 |
func TestWriteChannelStateFile(t *testing.T) { |
|
1d3caa2…
|
lmata
|
249 |
t.Helper() |
|
1d3caa2…
|
lmata
|
250 |
|
|
1d3caa2…
|
lmata
|
251 |
dir := t.TempDir() |
|
1d3caa2…
|
lmata
|
252 |
path := dir + "/channels.env" |
|
1d3caa2…
|
lmata
|
253 |
if err := WriteChannelStateFile(path, "general", []string{"#general", "#release"}); err != nil { |
|
1d3caa2…
|
lmata
|
254 |
t.Fatal(err) |
|
1d3caa2…
|
lmata
|
255 |
} |
|
1d3caa2…
|
lmata
|
256 |
data, err := os.ReadFile(path) |
|
1d3caa2…
|
lmata
|
257 |
if err != nil { |
|
1d3caa2…
|
lmata
|
258 |
t.Fatal(err) |
|
1d3caa2…
|
lmata
|
259 |
} |
|
1d3caa2…
|
lmata
|
260 |
want := "SCUTTLEBOT_CHANNEL=general\nSCUTTLEBOT_CHANNELS=general,release\n" |
|
1d3caa2…
|
lmata
|
261 |
if string(data) != want { |
|
1d3caa2…
|
lmata
|
262 |
t.Fatalf("state file = %q, want %q", string(data), want) |
|
1d3caa2…
|
lmata
|
263 |
} |
|
1d3caa2…
|
lmata
|
264 |
} |
|
1d3caa2…
|
lmata
|
265 |
|
|
1d3caa2…
|
lmata
|
266 |
func TestParseBrokerCommand(t *testing.T) { |
|
1d3caa2…
|
lmata
|
267 |
t.Helper() |
|
1d3caa2…
|
lmata
|
268 |
|
|
1d3caa2…
|
lmata
|
269 |
tests := []struct { |
|
1d3caa2…
|
lmata
|
270 |
input string |
|
1d3caa2…
|
lmata
|
271 |
want BrokerCommand |
|
1d3caa2…
|
lmata
|
272 |
ok bool |
|
1d3caa2…
|
lmata
|
273 |
}{ |
|
1d3caa2…
|
lmata
|
274 |
{input: "/channels", want: BrokerCommand{Name: "channels"}, ok: true}, |
|
1d3caa2…
|
lmata
|
275 |
{input: "/join task-42", want: BrokerCommand{Name: "join", Channel: "#task-42"}, ok: true}, |
|
1d3caa2…
|
lmata
|
276 |
{input: "/part #release", want: BrokerCommand{Name: "part", Channel: "#release"}, ok: true}, |
|
1d3caa2…
|
lmata
|
277 |
{input: "please read README", ok: false}, |
|
1d3caa2…
|
lmata
|
278 |
} |
|
1d3caa2…
|
lmata
|
279 |
|
|
1d3caa2…
|
lmata
|
280 |
for _, tt := range tests { |
|
1d3caa2…
|
lmata
|
281 |
got, ok := ParseBrokerCommand(tt.input) |
|
1d3caa2…
|
lmata
|
282 |
if ok != tt.ok || got != tt.want { |
|
1d3caa2…
|
lmata
|
283 |
t.Fatalf("ParseBrokerCommand(%q) = (%#v, %v), want (%#v, %v)", tt.input, got, ok, tt.want, tt.ok) |
|
1d3caa2…
|
lmata
|
284 |
} |
|
24a217e…
|
lmata
|
285 |
} |
|
24a217e…
|
lmata
|
286 |
} |