[agent/claude:opus] DX audit and fix. 1) Review CLAUDE.md — update any outdate... #2
4 changed files with 185 additions and 1 deletions
|
|
@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
Full-coverage Go client for the Forgejo API (~450 endpoints). Uses a generic `Resource[T,C,U]` pattern for CRUD operations and types generated from `swagger.v1.json`.
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-forge` (Go 1.26, zero dependencies)
|
||||
**Module:** `forge.lthn.ai/core/go-forge` (Go 1.26, depends on `go-io` and `go-log`)
|
||||
|
||||
## Build & Test
|
||||
|
||||
|
|
@ -51,6 +51,8 @@ The library is a flat package (`package forge`) with a layered design:
|
|||
|
||||
- All methods accept `context.Context` as first parameter
|
||||
- Errors wrapped as `*APIError` with StatusCode, Message, URL; use `IsNotFound()`, `IsForbidden()`, `IsConflict()` helpers
|
||||
- Internal errors use `coreerr.E()` from `go-log` (imported as `coreerr "forge.lthn.ai/core/go-log"`), never `fmt.Errorf` or `errors.New`
|
||||
- File I/O uses `go-io` (imported as `coreio "forge.lthn.ai/core/go-io"`), never `os.ReadFile`/`os.WriteFile`/`os.MkdirAll`
|
||||
- UK English in comments (organisation, colour, etc.)
|
||||
- `Co-Authored-By: Virgil <virgil@lethean.io>` in commits
|
||||
- `Client` uses functional options pattern (`WithHTTPClient`, `WithUserAgent`)
|
||||
|
|
|
|||
|
|
@ -136,3 +136,96 @@ func TestClient_Good_Options(t *testing.T) {
|
|||
t.Errorf("got user agent=%q", c.userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Good_WithHTTPClient(t *testing.T) {
|
||||
custom := &http.Client{}
|
||||
c := NewClient("https://forge.lthn.ai", "tok", WithHTTPClient(custom))
|
||||
if c.httpClient != custom {
|
||||
t.Error("expected custom HTTP client to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIError_Good_Error(t *testing.T) {
|
||||
e := &APIError{StatusCode: 404, Message: "not found", URL: "/api/v1/repos/x/y"}
|
||||
got := e.Error()
|
||||
want := "forge: /api/v1/repos/x/y 404: not found"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsConflict_Good(t *testing.T) {
|
||||
err := &APIError{StatusCode: http.StatusConflict, Message: "conflict", URL: "/test"}
|
||||
if !IsConflict(err) {
|
||||
t.Error("expected IsConflict to return true for 409")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsConflict_Bad_NotConflict(t *testing.T) {
|
||||
err := &APIError{StatusCode: http.StatusNotFound, Message: "not found", URL: "/test"}
|
||||
if IsConflict(err) {
|
||||
t.Error("expected IsConflict to return false for 404")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsForbidden_Bad_NotForbidden(t *testing.T) {
|
||||
err := &APIError{StatusCode: http.StatusNotFound, Message: "not found", URL: "/test"}
|
||||
if IsForbidden(err) {
|
||||
t.Error("expected IsForbidden to return false for 404")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Good_RateLimit(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-RateLimit-Limit", "100")
|
||||
w.Header().Set("X-RateLimit-Remaining", "99")
|
||||
w.Header().Set("X-RateLimit-Reset", "1700000000")
|
||||
json.NewEncoder(w).Encode(map[string]string{"login": "test"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "tok")
|
||||
var out map[string]string
|
||||
if err := c.Get(context.Background(), "/api/v1/user", &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rl := c.RateLimit()
|
||||
if rl.Limit != 100 {
|
||||
t.Errorf("got limit=%d, want 100", rl.Limit)
|
||||
}
|
||||
if rl.Remaining != 99 {
|
||||
t.Errorf("got remaining=%d, want 99", rl.Remaining)
|
||||
}
|
||||
if rl.Reset != 1700000000 {
|
||||
t.Errorf("got reset=%d, want 1700000000", rl.Reset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Bad_Forbidden(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "forbidden"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "tok")
|
||||
err := c.Get(context.Background(), "/api/v1/admin", nil)
|
||||
if !IsForbidden(err) {
|
||||
t.Fatalf("expected forbidden, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Bad_Conflict(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "already exists"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "tok")
|
||||
err := c.Post(context.Background(), "/api/v1/repos", map[string]string{"name": "dup"}, nil)
|
||||
if !IsConflict(err) {
|
||||
t.Fatalf("expected conflict, got %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,17 @@ func TestForge_Good_NewForge(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestForge_Good_Client(t *testing.T) {
|
||||
f := NewForge("https://forge.lthn.ai", "tok")
|
||||
c := f.Client()
|
||||
if c == nil {
|
||||
t.Fatal("Client() returned nil")
|
||||
}
|
||||
if c.baseURL != "https://forge.lthn.ai" {
|
||||
t.Errorf("got baseURL=%q", c.baseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoService_Good_List(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Total-Count", "1")
|
||||
|
|
|
|||
|
|
@ -153,3 +153,81 @@ func TestResource_Good_ListAll(t *testing.T) {
|
|||
t.Errorf("got %d items, want 3", len(items))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResource_Good_Iter(t *testing.T) {
|
||||
page := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
page++
|
||||
w.Header().Set("X-Total-Count", "3")
|
||||
if page == 1 {
|
||||
json.NewEncoder(w).Encode([]testItem{{1, "a"}, {2, "b"}})
|
||||
} else {
|
||||
json.NewEncoder(w).Encode([]testItem{{3, "c"}})
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "tok")
|
||||
res := NewResource[testItem, testCreate, testUpdate](c, "/api/v1/repos")
|
||||
|
||||
var collected []testItem
|
||||
for item, err := range res.Iter(context.Background(), nil) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
collected = append(collected, item)
|
||||
}
|
||||
if len(collected) != 3 {
|
||||
t.Errorf("got %d items, want 3", len(collected))
|
||||
}
|
||||
if collected[2].Name != "c" {
|
||||
t.Errorf("got last item name=%q, want \"c\"", collected[2].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResource_Bad_IterError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "server error"})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "tok")
|
||||
res := NewResource[testItem, testCreate, testUpdate](c, "/api/v1/repos")
|
||||
|
||||
var gotErr error
|
||||
for _, err := range res.Iter(context.Background(), nil) {
|
||||
if err != nil {
|
||||
gotErr = err
|
||||
break
|
||||
}
|
||||
}
|
||||
if gotErr == nil {
|
||||
t.Fatal("expected error from Iter on server error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResource_Good_IterBreakEarly(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Total-Count", "100")
|
||||
json.NewEncoder(w).Encode([]testItem{{1, "a"}, {2, "b"}, {3, "c"}})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := NewClient(srv.URL, "tok")
|
||||
res := NewResource[testItem, testCreate, testUpdate](c, "/api/v1/repos")
|
||||
|
||||
count := 0
|
||||
for _, err := range res.Iter(context.Background(), nil) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count++
|
||||
if count == 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
t.Errorf("expected to break after 1 item, got %d", count)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue