Merge pull request '[agent/claude:opus] DX audit and fix. 1) Review CLAUDE.md — update any outdate...' (#2) from agent/dx-audit-and-fix--1--review-claude-md into main
Some checks failed
Test / test (push) Successful in 41s
Security Scan / security (push) Failing after 10m40s

This commit is contained in:
Virgil 2026-03-17 08:11:54 +00:00
commit bb506863d1
4 changed files with 185 additions and 1 deletions

View file

@ -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`)

View file

@ -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)
}
}

View file

@ -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")

View file

@ -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)
}
}