code.oscarkilo.com/okg

Hash:
e5db207499a73b4d671489b690e367a6214deadb
Author:
Igor Naverniouk <[email protected]>
Date:
Thu Mar 12 12:43:35 2026 -0400
Message:
Add klee/ library package for Klee API client Extracts the Klee HTTP client into a reusable Go package (oscarkilo.com/okg/klee) that can be imported by other projects. Includes types (PR, Comment, RepoInfo), CRUD operations (GetPR, GetComments, MergePR, AddComment, CreatePR, ListPRs, SetPRState, CreateRepo, ListRepos), and AgentName helper for extracting agent names from sub-user usernames. The okg CLI will be refactored to use this package next. For now the two coexist — the CLI still has its own types and HTTP methods in package main.
diff --git a/klee/klee.go b/klee/klee.go
new file mode 100644
index 0000000..3ac5577
--- /dev/null
+++ b/klee/klee.go
@@ -0,0 +1,318 @@
+// Package klee provides a Go client for the Klee
+// git server API (code.oscarkilo.com).
+package klee
+
+import "encoding/json"
+import "fmt"
+import "io"
+import "net/http"
+import "strings"
+import "time"
+
+// Client talks to a Klee git server.
+type Client struct {
+ Host string // e.g. "https://code.oscarkilo.com"
+ APIKey string // Bearer token
+ HTTP *http.Client
+}
+
+// NewClient creates a Klee client.
+func NewClient(host, api_key string) *Client {
+ return &Client{
+ Host: host,
+ APIKey: api_key,
+ HTTP: &http.Client{},
+ }
+}
+
+// PR is a pull request on Klee.
+type PR struct {
+ Number int `json:"number"`
+ Title string `json:"title"`
+ Body string `json:"body"`
+ State string `json:"state"`
+ Merged bool `json:"merged"`
+ Head string `json:"head"`
+ Base string `json:"base"`
+ Author string `json:"author"`
+ Files []string `json:"files"`
+ Created time.Time `json:"created"`
+ Updated time.Time `json:"updated"`
+ MergedBy string `json:"merged_by,omitempty"`
+ MergedAt time.Time `json:"merged_at,omitempty"`
+}
+
+// Comment is a PR comment on Klee.
+type Comment struct {
+ ID int `json:"id"`
+ Author string `json:"author"`
+ Body string `json:"body"`
+ Verdict string `json:"verdict,omitempty"`
+ File string `json:"file,omitempty"`
+ Line int `json:"line,omitempty"`
+ Created time.Time `json:"created"`
+}
+
+// CreatePRRequest is the body for creating a PR.
+type CreatePRRequest struct {
+ Head string `json:"head"`
+ Base string `json:"base"`
+ Title string `json:"title"`
+ Body string `json:"body"`
+}
+
+// AddCommentRequest is the body for adding a
+// comment to a PR.
+type AddCommentRequest struct {
+ Body string `json:"body"`
+ Verdict string `json:"verdict,omitempty"`
+}
+
+// CreateRepoRequest is the body for creating a
+// repo on Klee.
+type CreateRepoRequest struct {
+ RepoName string `json:"repo_name"`
+ ReaderUsername string `json:"reader_username"`
+}
+
+// RepoInfo describes a repository.
+type RepoInfo struct {
+ Name string `json:"name"`
+ IsPublic bool `json:"is_public"`
+ Authz *RepoInfoAuthz `json:"authz"`
+}
+
+// RepoInfoAuthz describes repo access.
+type RepoInfoAuthz struct {
+ IsOwner bool `json:"is_owner"`
+ IsReader bool `json:"is_reader"`
+ OwnerUsername string `json:"owner_username"`
+ ReaderUsername string `json:"reader_username"`
+}
+
+// LsResponse is the response from /.ls.
+type LsResponse struct {
+ Repos []RepoInfo `json:"repos"`
+ CanCreateRepos bool `json:"can_create_repos"`
+}
+
+// --- PR operations ---
+
+// ListPRs lists PRs for a repo by state.
+func (c *Client) ListPRs(
+ repo, state string,
+) ([]PR, error) {
+ path := fmt.Sprintf(
+ "/%s/prs?state=%s", repo, state)
+ var prs []PR
+ if err := c.getJSON(path, &prs); err != nil {
+ return nil, err
+ }
+ return prs, nil
+}
+
+// GetPR fetches a single PR.
+func (c *Client) GetPR(
+ repo string, number int,
+) (*PR, error) {
+ path := fmt.Sprintf(
+ "/%s/pr/%d", repo, number)
+ var pr PR
+ if err := c.getJSON(path, &pr); err != nil {
+ return nil, err
+ }
+ return &pr, nil
+}
+
+// CreatePR creates a new PR.
+func (c *Client) CreatePR(
+ repo string, req CreatePRRequest,
+) (*PR, error) {
+ path := fmt.Sprintf("/%s/prs", repo)
+ var pr PR
+ if err := c.postJSON(path, req, &pr); err != nil {
+ return nil, err
+ }
+ return &pr, nil
+}
+
+// GetComments fetches comments on a PR.
+func (c *Client) GetComments(
+ repo string, number int,
+) ([]Comment, error) {
+ path := fmt.Sprintf(
+ "/%s/pr/%d/comments", repo, number)
+ var comments []Comment
+ if err := c.getJSON(path, &comments); err != nil {
+ return nil, err
+ }
+ return comments, nil
+}
+
+// AddComment adds a comment to a PR.
+func (c *Client) AddComment(
+ repo string, number int, req AddCommentRequest,
+) (*Comment, error) {
+ path := fmt.Sprintf(
+ "/%s/pr/%d/comments", repo, number)
+ var comment Comment
+ if err := c.postJSON(
+ path, req, &comment); err != nil {
+ return nil, err
+ }
+ return &comment, nil
+}
+
+// MergePR merges a PR.
+func (c *Client) MergePR(
+ repo string, number int,
+) (*PR, error) {
+ path := fmt.Sprintf(
+ "/%s/pr/%d/merge", repo, number)
+ var pr PR
+ if err := c.postJSON(path, nil, &pr); err != nil {
+ return nil, err
+ }
+ return &pr, nil
+}
+
+// SetPRState changes a PR's state (open/closed).
+func (c *Client) SetPRState(
+ repo string, number int, state string,
+) (*PR, error) {
+ path := fmt.Sprintf(
+ "/%s/pr/%d", repo, number)
+ payload := map[string]string{"state": state}
+ var pr PR
+ if err := c.patchJSON(
+ path, payload, &pr); err != nil {
+ return nil, err
+ }
+ return &pr, nil
+}
+
+// --- Repo operations ---
+
+// ListRepos lists repos accessible to the caller.
+func (c *Client) ListRepos() (
+ *LsResponse, error,
+) {
+ var res LsResponse
+ if err := c.getJSON("/.ls", &res); err != nil {
+ return nil, err
+ }
+ return &res, nil
+}
+
+// CreateRepo creates a new repo on Klee.
+func (c *Client) CreateRepo(
+ req CreateRepoRequest,
+) error {
+ return c.postJSON("/.add-repo", req, nil)
+}
+
+// --- HTTP helpers ---
+
+func (c *Client) do(
+ method, path string, body io.Reader,
+) (*http.Response, error) {
+ url := c.Host + path
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Accept", "application/json")
+ if c.APIKey != "" {
+ req.Header.Set("Authorization",
+ "Bearer "+c.APIKey)
+ }
+ if body != nil {
+ req.Header.Set(
+ "Content-Type", "application/json")
+ }
+ return c.HTTP.Do(req)
+}
+
+func (c *Client) getJSON(
+ path string, dst interface{},
+) error {
+ resp, err := c.do("GET", path, nil)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode >= 400 {
+ b, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf(
+ "HTTP %d: %s", resp.StatusCode, b)
+ }
+ return json.NewDecoder(resp.Body).Decode(dst)
+}
+
+func (c *Client) postJSON(
+ path string,
+ payload interface{},
+ dst interface{},
+) error {
+ var body io.Reader
+ if payload != nil {
+ data, err := json.Marshal(payload)
+ if err != nil {
+ return err
+ }
+ body = strings.NewReader(string(data))
+ }
+ resp, err := c.do("POST", path, body)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode >= 400 {
+ b, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf(
+ "HTTP %d: %s", resp.StatusCode, b)
+ }
+ if dst != nil {
+ return json.NewDecoder(
+ resp.Body).Decode(dst)
+ }
+ return nil
+}
+
+func (c *Client) patchJSON(
+ path string,
+ payload interface{},
+ dst interface{},
+) error {
+ data, err := json.Marshal(payload)
+ if err != nil {
+ return err
+ }
+ body := strings.NewReader(string(data))
+ resp, err := c.do("PATCH", path, body)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode >= 400 {
+ b, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf(
+ "HTTP %d: %s", resp.StatusCode, b)
+ }
+ if dst != nil {
+ return json.NewDecoder(
+ resp.Body).Decode(dst)
+ }
+ return nil
+}
+
+// AgentName extracts the agent name from a Klee
+// sub-user username. Given "operator.claude",
+// returns "claude". If no dot, returns the input.
+func AgentName(klee_user string) string {
+ i := strings.LastIndex(klee_user, ".")
+ if i < 0 {
+ return klee_user
+ }
+ return klee_user[i+1:]
+}
diff --git a/klee/klee_test.go b/klee/klee_test.go
new file mode 100644
index 0000000..9f2fa28
--- /dev/null
+++ b/klee/klee_test.go
@@ -0,0 +1,176 @@
+package klee
+
+import "encoding/json"
+import "net/http"
+import "net/http/httptest"
+import "testing"
+
+func TestGetPR(t *testing.T) {
+ srv := mockPR(t, &PR{
+ Number: 1,
+ Author: "op.claude",
+ State: "open",
+ Files: []string{"root/reports/claude/x.md"},
+ })
+ defer srv.Close()
+
+ c := NewClient(srv.URL, "key")
+ pr, err := c.GetPR("brain", 1)
+ check(t, err == nil, "err: %v", err)
+ check(t, pr.Number == 1, "number: %d", pr.Number)
+ check(t, pr.Author == "op.claude",
+ "author: %s", pr.Author)
+ check(t, len(pr.Files) == 1,
+ "files: %d", len(pr.Files))
+}
+
+func TestGetComments(t *testing.T) {
+ srv := mockComments(t, []Comment{
+ {Author: "op.helper", Verdict: "approve"},
+ {Author: "op.claude", Verdict: ""},
+ })
+ defer srv.Close()
+
+ c := NewClient(srv.URL, "key")
+ cs, err := c.GetComments("brain", 1)
+ check(t, err == nil, "err: %v", err)
+ check(t, len(cs) == 2, "len: %d", len(cs))
+ check(t, cs[0].Verdict == "approve",
+ "verdict: %s", cs[0].Verdict)
+}
+
+func TestMergePR(t *testing.T) {
+ merged := false
+ srv := httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ check(t, r.Method == "POST",
+ "method: %s", r.Method)
+ check(t, r.URL.Path == "/brain/pr/1/merge",
+ "path: %s", r.URL.Path)
+ merged = true
+ w.Header().Set(
+ "Content-Type", "application/json")
+ json.NewEncoder(w).Encode(
+ &PR{Number: 1, Merged: true})
+ }))
+ defer srv.Close()
+
+ c := NewClient(srv.URL, "key")
+ pr, err := c.MergePR("brain", 1)
+ check(t, err == nil, "err: %v", err)
+ check(t, merged, "merge not called")
+ check(t, pr.Merged, "not merged")
+}
+
+func TestGetPR_NotFound(t *testing.T) {
+ srv := httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(404)
+ }))
+ defer srv.Close()
+
+ c := NewClient(srv.URL, "key")
+ _, err := c.GetPR("brain", 99)
+ check(t, err != nil, "want error")
+}
+
+func TestCreateRepo(t *testing.T) {
+ var got CreateRepoRequest
+ srv := httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ json.NewDecoder(r.Body).Decode(&got)
+ w.WriteHeader(204)
+ }))
+ defer srv.Close()
+
+ c := NewClient(srv.URL, "key")
+ err := c.CreateRepo(CreateRepoRequest{
+ RepoName: "test-repo",
+ ReaderUsername: "igor.agents",
+ })
+ check(t, err == nil, "err: %v", err)
+ check(t, got.RepoName == "test-repo",
+ "name: %s", got.RepoName)
+ check(t, got.ReaderUsername == "igor.agents",
+ "reader: %s", got.ReaderUsername)
+}
+
+func TestAgentName(t *testing.T) {
+ check(t, AgentName("op.claude") == "claude",
+ "got %s", AgentName("op.claude"))
+ check(t, AgentName("op.sub.helper") == "helper",
+ "got %s", AgentName("op.sub.helper"))
+ check(t, AgentName("root") == "root",
+ "got %s", AgentName("root"))
+}
+
+func TestAddComment(t *testing.T) {
+ srv := httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ var req AddCommentRequest
+ json.NewDecoder(r.Body).Decode(&req)
+ w.Header().Set(
+ "Content-Type", "application/json")
+ json.NewEncoder(w).Encode(&Comment{
+ ID: 1,
+ Author: "op.claude",
+ Body: req.Body,
+ Verdict: req.Verdict,
+ })
+ }))
+ defer srv.Close()
+
+ c := NewClient(srv.URL, "key")
+ comment, err := c.AddComment("brain", 1,
+ AddCommentRequest{
+ Body: "LGTM",
+ Verdict: "approve",
+ })
+ check(t, err == nil, "err: %v", err)
+ check(t, comment.Body == "LGTM",
+ "body: %s", comment.Body)
+ check(t, comment.Verdict == "approve",
+ "verdict: %s", comment.Verdict)
+}
+
+// --- helpers ---
+
+func check(
+ t *testing.T, ok bool, format string,
+ args ...interface{},
+) {
+ t.Helper()
+ if !ok {
+ t.Errorf(format, args...)
+ }
+}
+
+func mockPR(
+ t *testing.T, pr *PR,
+) *httptest.Server {
+ t.Helper()
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set(
+ "Content-Type", "application/json")
+ json.NewEncoder(w).Encode(pr)
+ }))
+}
+
+func mockComments(
+ t *testing.T, comments []Comment,
+) *httptest.Server {
+ t.Helper()
+ return httptest.NewServer(
+ http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set(
+ "Content-Type", "application/json")
+ json.NewEncoder(w).Encode(comments)
+ }))
+}
/dev/null
b/klee/klee.go
1
// Package klee provides a Go client for the Klee
2
// git server API (code.oscarkilo.com).
3
package klee
4
5
import "encoding/json"
6
import "fmt"
7
import "io"
8
import "net/http"
9
import "strings"
10
import "time"
11
12
// Client talks to a Klee git server.
13
type Client struct {
14
Host string // e.g. "https://code.oscarkilo.com"
15
APIKey string // Bearer token
16
HTTP *http.Client
17
}
18
19
// NewClient creates a Klee client.
20
func NewClient(host, api_key string) *Client {
21
return &Client{
22
Host: host,
23
APIKey: api_key,
24
HTTP: &http.Client{},
25
}
26
}
27
28
// PR is a pull request on Klee.
29
type PR struct {
30
Number int `json:"number"`
31
Title string `json:"title"`
32
Body string `json:"body"`
33
State string `json:"state"`
34
Merged bool `json:"merged"`
35
Head string `json:"head"`
36
Base string `json:"base"`
37
Author string `json:"author"`
38
Files []string `json:"files"`
39
Created time.Time `json:"created"`
40
Updated time.Time `json:"updated"`
41
MergedBy string `json:"merged_by,omitempty"`
42
MergedAt time.Time `json:"merged_at,omitempty"`
43
}
44
45
// Comment is a PR comment on Klee.
46
type Comment struct {
47
ID int `json:"id"`
48
Author string `json:"author"`
49
Body string `json:"body"`
50
Verdict string `json:"verdict,omitempty"`
51
File string `json:"file,omitempty"`
52
Line int `json:"line,omitempty"`
53
Created time.Time `json:"created"`
54
}
55
56
// CreatePRRequest is the body for creating a PR.
57
type CreatePRRequest struct {
58
Head string `json:"head"`
59
Base string `json:"base"`
60
Title string `json:"title"`
61
Body string `json:"body"`
62
}
63
64
// AddCommentRequest is the body for adding a
65
// comment to a PR.
66
type AddCommentRequest struct {
67
Body string `json:"body"`
68
Verdict string `json:"verdict,omitempty"`
69
}
70
71
// CreateRepoRequest is the body for creating a
72
// repo on Klee.
73
type CreateRepoRequest struct {
74
RepoName string `json:"repo_name"`
75
ReaderUsername string `json:"reader_username"`
76
}
77
78
// RepoInfo describes a repository.
79
type RepoInfo struct {
80
Name string `json:"name"`
81
IsPublic bool `json:"is_public"`
82
Authz *RepoInfoAuthz `json:"authz"`
83
}
84
85
// RepoInfoAuthz describes repo access.
86
type RepoInfoAuthz struct {
87
IsOwner bool `json:"is_owner"`
88
IsReader bool `json:"is_reader"`
89
OwnerUsername string `json:"owner_username"`
90
ReaderUsername string `json:"reader_username"`
91
}
92
93
// LsResponse is the response from /.ls.
94
type LsResponse struct {
95
Repos []RepoInfo `json:"repos"`
96
CanCreateRepos bool `json:"can_create_repos"`
97
}
98
99
// --- PR operations ---
100
101
// ListPRs lists PRs for a repo by state.
102
func (c *Client) ListPRs(
103
repo, state string,
104
) ([]PR, error) {
105
path := fmt.Sprintf(
106
"/%s/prs?state=%s", repo, state)
107
var prs []PR
108
if err := c.getJSON(path, &prs); err != nil {
109
return nil, err
110
}
111
return prs, nil
112
}
113
114
// GetPR fetches a single PR.
115
func (c *Client) GetPR(
116
repo string, number int,
117
) (*PR, error) {
118
path := fmt.Sprintf(
119
"/%s/pr/%d", repo, number)
120
var pr PR
121
if err := c.getJSON(path, &pr); err != nil {
122
return nil, err
123
}
124
return &pr, nil
125
}
126
127
// CreatePR creates a new PR.
128
func (c *Client) CreatePR(
129
repo string, req CreatePRRequest,
130
) (*PR, error) {
131
path := fmt.Sprintf("/%s/prs", repo)
132
var pr PR
133
if err := c.postJSON(path, req, &pr); err != nil {
134
return nil, err
135
}
136
return &pr, nil
137
}
138
139
// GetComments fetches comments on a PR.
140
func (c *Client) GetComments(
141
repo string, number int,
142
) ([]Comment, error) {
143
path := fmt.Sprintf(
144
"/%s/pr/%d/comments", repo, number)
145
var comments []Comment
146
if err := c.getJSON(path, &comments); err != nil {
147
return nil, err
148
}
149
return comments, nil
150
}
151
152
// AddComment adds a comment to a PR.
153
func (c *Client) AddComment(
154
repo string, number int, req AddCommentRequest,
155
) (*Comment, error) {
156
path := fmt.Sprintf(
157
"/%s/pr/%d/comments", repo, number)
158
var comment Comment
159
if err := c.postJSON(
160
path, req, &comment); err != nil {
161
return nil, err
162
}
163
return &comment, nil
164
}
165
166
// MergePR merges a PR.
167
func (c *Client) MergePR(
168
repo string, number int,
169
) (*PR, error) {
170
path := fmt.Sprintf(
171
"/%s/pr/%d/merge", repo, number)
172
var pr PR
173
if err := c.postJSON(path, nil, &pr); err != nil {
174
return nil, err
175
}
176
return &pr, nil
177
}
178
179
// SetPRState changes a PR's state (open/closed).
180
func (c *Client) SetPRState(
181
repo string, number int, state string,
182
) (*PR, error) {
183
path := fmt.Sprintf(
184
"/%s/pr/%d", repo, number)
185
payload := map[string]string{"state": state}
186
var pr PR
187
if err := c.patchJSON(
188
path, payload, &pr); err != nil {
189
return nil, err
190
}
191
return &pr, nil
192
}
193
194
// --- Repo operations ---
195
196
// ListRepos lists repos accessible to the caller.
197
func (c *Client) ListRepos() (
198
*LsResponse, error,
199
) {
200
var res LsResponse
201
if err := c.getJSON("/.ls", &res); err != nil {
202
return nil, err
203
}
204
return &res, nil
205
}
206
207
// CreateRepo creates a new repo on Klee.
208
func (c *Client) CreateRepo(
209
req CreateRepoRequest,
210
) error {
211
return c.postJSON("/.add-repo", req, nil)
212
}
213
214
// --- HTTP helpers ---
215
216
func (c *Client) do(
217
method, path string, body io.Reader,
218
) (*http.Response, error) {
219
url := c.Host + path
220
req, err := http.NewRequest(method, url, body)
221
if err != nil {
222
return nil, err
223
}
224
req.Header.Set("Accept", "application/json")
225
if c.APIKey != "" {
226
req.Header.Set("Authorization",
227
"Bearer "+c.APIKey)
228
}
229
if body != nil {
230
req.Header.Set(
231
"Content-Type", "application/json")
232
}
233
return c.HTTP.Do(req)
234
}
235
236
func (c *Client) getJSON(
237
path string, dst interface{},
238
) error {
239
resp, err := c.do("GET", path, nil)
240
if err != nil {
241
return err
242
}
243
defer resp.Body.Close()
244
if resp.StatusCode >= 400 {
245
b, _ := io.ReadAll(resp.Body)
246
return fmt.Errorf(
247
"HTTP %d: %s", resp.StatusCode, b)
248
}
249
return json.NewDecoder(resp.Body).Decode(dst)
250
}
251
252
func (c *Client) postJSON(
253
path string,
254
payload interface{},
255
dst interface{},
256
) error {
257
var body io.Reader
258
if payload != nil {
259
data, err := json.Marshal(payload)
260
if err != nil {
261
return err
262
}
263
body = strings.NewReader(string(data))
264
}
265
resp, err := c.do("POST", path, body)
266
if err != nil {
267
return err
268
}
269
defer resp.Body.Close()
270
if resp.StatusCode >= 400 {
271
b, _ := io.ReadAll(resp.Body)
272
return fmt.Errorf(
273
"HTTP %d: %s", resp.StatusCode, b)
274
}
275
if dst != nil {
276
return json.NewDecoder(
277
resp.Body).Decode(dst)
278
}
279
return nil
280
}
281
282
func (c *Client) patchJSON(
283
path string,
284
payload interface{},
285
dst interface{},
286
) error {
287
data, err := json.Marshal(payload)
288
if err != nil {
289
return err
290
}
291
body := strings.NewReader(string(data))
292
resp, err := c.do("PATCH", path, body)
293
if err != nil {
294
return err
295
}
296
defer resp.Body.Close()
297
if resp.StatusCode >= 400 {
298
b, _ := io.ReadAll(resp.Body)
299
return fmt.Errorf(
300
"HTTP %d: %s", resp.StatusCode, b)
301
}
302
if dst != nil {
303
return json.NewDecoder(
304
resp.Body).Decode(dst)
305
}
306
return nil
307
}
308
309
// AgentName extracts the agent name from a Klee
310
// sub-user username. Given "operator.claude",
311
// returns "claude". If no dot, returns the input.
312
func AgentName(klee_user string) string {
313
i := strings.LastIndex(klee_user, ".")
314
if i < 0 {
315
return klee_user
316
}
317
return klee_user[i+1:]
318
}
/dev/null
b/klee/klee_test.go
1
package klee
2
3
import "encoding/json"
4
import "net/http"
5
import "net/http/httptest"
6
import "testing"
7
8
func TestGetPR(t *testing.T) {
9
srv := mockPR(t, &PR{
10
Number: 1,
11
Author: "op.claude",
12
State: "open",
13
Files: []string{"root/reports/claude/x.md"},
14
})
15
defer srv.Close()
16
17
c := NewClient(srv.URL, "key")
18
pr, err := c.GetPR("brain", 1)
19
check(t, err == nil, "err: %v", err)
20
check(t, pr.Number == 1, "number: %d", pr.Number)
21
check(t, pr.Author == "op.claude",
22
"author: %s", pr.Author)
23
check(t, len(pr.Files) == 1,
24
"files: %d", len(pr.Files))
25
}
26
27
func TestGetComments(t *testing.T) {
28
srv := mockComments(t, []Comment{
29
{Author: "op.helper", Verdict: "approve"},
30
{Author: "op.claude", Verdict: ""},
31
})
32
defer srv.Close()
33
34
c := NewClient(srv.URL, "key")
35
cs, err := c.GetComments("brain", 1)
36
check(t, err == nil, "err: %v", err)
37
check(t, len(cs) == 2, "len: %d", len(cs))
38
check(t, cs[0].Verdict == "approve",
39
"verdict: %s", cs[0].Verdict)
40
}
41
42
func TestMergePR(t *testing.T) {
43
merged := false
44
srv := httptest.NewServer(
45
http.HandlerFunc(
46
func(w http.ResponseWriter, r *http.Request) {
47
check(t, r.Method == "POST",
48
"method: %s", r.Method)
49
check(t, r.URL.Path == "/brain/pr/1/merge",
50
"path: %s", r.URL.Path)
51
merged = true
52
w.Header().Set(
53
"Content-Type", "application/json")
54
json.NewEncoder(w).Encode(
55
&PR{Number: 1, Merged: true})
56
}))
57
defer srv.Close()
58
59
c := NewClient(srv.URL, "key")
60
pr, err := c.MergePR("brain", 1)
61
check(t, err == nil, "err: %v", err)
62
check(t, merged, "merge not called")
63
check(t, pr.Merged, "not merged")
64
}
65
66
func TestGetPR_NotFound(t *testing.T) {
67
srv := httptest.NewServer(
68
http.HandlerFunc(
69
func(w http.ResponseWriter, r *http.Request) {
70
w.WriteHeader(404)
71
}))
72
defer srv.Close()
73
74
c := NewClient(srv.URL, "key")
75
_, err := c.GetPR("brain", 99)
76
check(t, err != nil, "want error")
77
}
78
79
func TestCreateRepo(t *testing.T) {
80
var got CreateRepoRequest
81
srv := httptest.NewServer(
82
http.HandlerFunc(
83
func(w http.ResponseWriter, r *http.Request) {
84
json.NewDecoder(r.Body).Decode(&got)
85
w.WriteHeader(204)
86
}))
87
defer srv.Close()
88
89
c := NewClient(srv.URL, "key")
90
err := c.CreateRepo(CreateRepoRequest{
91
RepoName: "test-repo",
92
ReaderUsername: "igor.agents",
93
})
94
check(t, err == nil, "err: %v", err)
95
check(t, got.RepoName == "test-repo",
96
"name: %s", got.RepoName)
97
check(t, got.ReaderUsername == "igor.agents",
98
"reader: %s", got.ReaderUsername)
99
}
100
101
func TestAgentName(t *testing.T) {
102
check(t, AgentName("op.claude") == "claude",
103
"got %s", AgentName("op.claude"))
104
check(t, AgentName("op.sub.helper") == "helper",
105
"got %s", AgentName("op.sub.helper"))
106
check(t, AgentName("root") == "root",
107
"got %s", AgentName("root"))
108
}
109
110
func TestAddComment(t *testing.T) {
111
srv := httptest.NewServer(
112
http.HandlerFunc(
113
func(w http.ResponseWriter, r *http.Request) {
114
var req AddCommentRequest
115
json.NewDecoder(r.Body).Decode(&req)
116
w.Header().Set(
117
"Content-Type", "application/json")
118
json.NewEncoder(w).Encode(&Comment{
119
ID: 1,
120
Author: "op.claude",
121
Body: req.Body,
122
Verdict: req.Verdict,
123
})
124
}))
125
defer srv.Close()
126
127
c := NewClient(srv.URL, "key")
128
comment, err := c.AddComment("brain", 1,
129
AddCommentRequest{
130
Body: "LGTM",
131
Verdict: "approve",
132
})
133
check(t, err == nil, "err: %v", err)
134
check(t, comment.Body == "LGTM",
135
"body: %s", comment.Body)
136
check(t, comment.Verdict == "approve",
137
"verdict: %s", comment.Verdict)
138
}
139
140
// --- helpers ---
141
142
func check(
143
t *testing.T, ok bool, format string,
144
args ...interface{},
145
) {
146
t.Helper()
147
if !ok {
148
t.Errorf(format, args...)
149
}
150
}
151
152
func mockPR(
153
t *testing.T, pr *PR,
154
) *httptest.Server {
155
t.Helper()
156
return httptest.NewServer(
157
http.HandlerFunc(
158
func(w http.ResponseWriter, r *http.Request) {
159
w.Header().Set(
160
"Content-Type", "application/json")
161
json.NewEncoder(w).Encode(pr)
162
}))
163
}
164
165
func mockComments(
166
t *testing.T, comments []Comment,
167
) *httptest.Server {
168
t.Helper()
169
return httptest.NewServer(
170
http.HandlerFunc(
171
func(w http.ResponseWriter, r *http.Request) {
172
w.Header().Set(
173
"Content-Type", "application/json")
174
json.NewEncoder(w).Encode(comments)
175
}))
176
}