ga-plugins-cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config-patcher.d.ts +20 -50
- package/dist/config-patcher.d.ts.map +1 -1
- package/dist/config-patcher.js +138 -102
- package/dist/config-patcher.js.map +1 -1
- package/dist/index.js +41 -5
- package/dist/index.js.map +1 -1
- package/dist/installer.d.ts +0 -18
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +19 -39
- package/dist/installer.js.map +1 -1
- package/dist/types.d.ts +10 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/uninstaller.d.ts +0 -23
- package/dist/uninstaller.d.ts.map +1 -1
- package/dist/uninstaller.js +22 -68
- package/dist/uninstaller.js.map +1 -1
- package/package.json +3 -2
- package/plugins/go-reviewer/.claude-plugin/plugin.json +12 -0
- package/plugins/go-reviewer/commands/go-review.md +424 -0
- package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/README.md +236 -0
- package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/main.go +678 -0
- package/plugins/go-scaffolder/.claude-plugin/plugin.json +12 -0
- package/plugins/go-scaffolder/commands/scaffold-service.md +802 -0
- package/plugins/go-scaffolder/reference-service/.env.example +27 -0
- package/plugins/go-scaffolder/reference-service/Dockerfile +55 -0
- package/plugins/go-scaffolder/reference-service/REFERENCE-SERVICE-NOTICE.md +104 -0
- package/plugins/go-scaffolder/reference-service/cmd/server/main.go +266 -0
- package/plugins/go-scaffolder/reference-service/config/config.go +67 -0
- package/plugins/go-scaffolder/reference-service/go.mod +17 -0
- package/plugins/go-scaffolder/reference-service/internal/domain/booking.go +118 -0
- package/plugins/go-scaffolder/reference-service/internal/handler/booking.go +242 -0
- package/plugins/go-scaffolder/reference-service/internal/handler/booking_test.go +451 -0
- package/plugins/go-scaffolder/reference-service/internal/repository/booking_postgres.go +124 -0
- package/plugins/go-scaffolder/reference-service/internal/usecase/booking.go +181 -0
- package/plugins/go-standards/.claude-plugin/plugin.json +22 -0
- package/plugins/go-standards/commands/go-standards-check.md +232 -0
- package/plugins/go-standards/skills/concurrency.md +336 -0
- package/plugins/go-standards/skills/config.md +267 -0
- package/plugins/go-standards/skills/error-handling.md +286 -0
- package/plugins/go-standards/skills/http-chi.md +390 -0
- package/plugins/go-standards/skills/logging-observability.md +340 -0
- package/plugins/go-standards/skills/naming-and-style.md +315 -0
- package/plugins/go-standards/skills/project-layout.md +313 -0
- package/plugins/go-standards/skills/testing.md +366 -0
- package/plugins/java2go-porter/.claude-plugin/plugin.json +21 -0
- package/plugins/java2go-porter/agents/analyzer.md +232 -0
- package/plugins/java2go-porter/agents/reviewer.md +241 -0
- package/plugins/java2go-porter/agents/test-pairer.md +365 -0
- package/plugins/java2go-porter/agents/translator.md +419 -0
- package/plugins/java2go-porter/commands/port-java-service.md +149 -0
- package/plugins/java2go-porter/skills/idiom-mapping.md +75 -0
- package/plugins/migration-safety/.claude-plugin/plugin.json +20 -0
- package/plugins/migration-safety/commands/gen-characterization-test.md +452 -0
- package/plugins/migration-safety/commands/strangler-plan.md +356 -0
- package/plugins/migration-safety/skills/strangler-fig.md +382 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Project Layout Standard
|
|
2
|
+
|
|
3
|
+
Canonical folder structure for every chi-based service in this migration.
|
|
4
|
+
All 30 services MUST follow this layout without exception — consistency is
|
|
5
|
+
what makes the consolidated codebase navigable.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Canonical Tree
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
my-service/
|
|
13
|
+
cmd/
|
|
14
|
+
server/
|
|
15
|
+
main.go # entrypoint only — wires deps, calls ListenAndServe
|
|
16
|
+
internal/
|
|
17
|
+
handler/ # HTTP handlers — thin, delegate immediately to usecase
|
|
18
|
+
user_handler.go
|
|
19
|
+
user_handler_test.go
|
|
20
|
+
usecase/ # business logic — the core of the service
|
|
21
|
+
user_usecase.go
|
|
22
|
+
user_usecase_test.go
|
|
23
|
+
repository/ # data-access implementations (DB, cache, external APIs)
|
|
24
|
+
user_repository.go
|
|
25
|
+
user_repository_test.go
|
|
26
|
+
domain/ # structs, interfaces, sentinel errors — zero external imports
|
|
27
|
+
user.go
|
|
28
|
+
errors.go
|
|
29
|
+
pkg/ # importable by external packages (shared clients, helpers)
|
|
30
|
+
config/ # env loading, Config struct
|
|
31
|
+
config.go
|
|
32
|
+
go.mod
|
|
33
|
+
go.sum
|
|
34
|
+
Dockerfile
|
|
35
|
+
.env.example
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Rule 1 — `cmd/server/main.go` wires dependencies only
|
|
41
|
+
|
|
42
|
+
`main.go` is a composition root. It reads config, constructs every concrete
|
|
43
|
+
type, wires them together, and starts the HTTP server. No business logic lives
|
|
44
|
+
here.
|
|
45
|
+
|
|
46
|
+
### DO
|
|
47
|
+
|
|
48
|
+
```go
|
|
49
|
+
// cmd/server/main.go
|
|
50
|
+
package main
|
|
51
|
+
|
|
52
|
+
import (
|
|
53
|
+
"context"
|
|
54
|
+
"log"
|
|
55
|
+
"net/http"
|
|
56
|
+
"os/signal"
|
|
57
|
+
"syscall"
|
|
58
|
+
"time"
|
|
59
|
+
|
|
60
|
+
"github.com/go-chi/chi/v5"
|
|
61
|
+
"github.com/go-chi/chi/v5/middleware"
|
|
62
|
+
"go.uber.org/zap"
|
|
63
|
+
|
|
64
|
+
"github.com/zokypesch/go-ga-lib/config"
|
|
65
|
+
"github.com/zokypesch/go-ga-lib/internal/handler"
|
|
66
|
+
"github.com/zokypesch/go-ga-lib/internal/repository"
|
|
67
|
+
"github.com/zokypesch/go-ga-lib/internal/usecase"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
func main() {
|
|
71
|
+
cfg, err := config.Load()
|
|
72
|
+
if err != nil {
|
|
73
|
+
log.Fatalf("loading config: %v", err)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logger, err := zap.NewProduction()
|
|
77
|
+
if err != nil {
|
|
78
|
+
log.Fatalf("building logger: %v", err)
|
|
79
|
+
}
|
|
80
|
+
defer logger.Sync()
|
|
81
|
+
|
|
82
|
+
db, err := repository.OpenDB(cfg.DSN)
|
|
83
|
+
if err != nil {
|
|
84
|
+
log.Fatalf("opening db: %v", err)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
userRepo := repository.NewUserRepository(db)
|
|
88
|
+
userUC := usecase.NewUserUsecase(userRepo, logger)
|
|
89
|
+
userH := handler.NewUserHandler(userUC, logger)
|
|
90
|
+
|
|
91
|
+
r := chi.NewRouter()
|
|
92
|
+
r.Use(middleware.RequestID)
|
|
93
|
+
r.Use(middleware.Logger)
|
|
94
|
+
r.Use(middleware.Recoverer)
|
|
95
|
+
|
|
96
|
+
r.Route("/api/v1", func(r chi.Router) {
|
|
97
|
+
r.Mount("/users", userH.Routes())
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
srv := &http.Server{Addr: ":" + cfg.Port, Handler: r}
|
|
101
|
+
|
|
102
|
+
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
103
|
+
defer stop()
|
|
104
|
+
|
|
105
|
+
go func() {
|
|
106
|
+
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
107
|
+
logger.Fatal("server error", zap.Error(err))
|
|
108
|
+
}
|
|
109
|
+
}()
|
|
110
|
+
|
|
111
|
+
<-ctx.Done()
|
|
112
|
+
shutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
113
|
+
defer cancel()
|
|
114
|
+
_ = srv.Shutdown(shutCtx)
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### DO NOT
|
|
119
|
+
|
|
120
|
+
```go
|
|
121
|
+
// cmd/server/main.go — WRONG: business logic leaking into main
|
|
122
|
+
func main() {
|
|
123
|
+
// WRONG: querying the DB directly in main
|
|
124
|
+
rows, _ := db.Query("SELECT * FROM users WHERE active = true")
|
|
125
|
+
// WRONG: inline HTTP handler in main
|
|
126
|
+
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
|
|
127
|
+
// ... business logic here ...
|
|
128
|
+
})
|
|
129
|
+
http.ListenAndServe(":8080", nil)
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Rule 2 — `internal/` is private to this module
|
|
136
|
+
|
|
137
|
+
Go enforces this at the compiler level: packages under `internal/` cannot be
|
|
138
|
+
imported by any module outside the module root. Rely on this — never fight it.
|
|
139
|
+
|
|
140
|
+
### DO
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
// Correct: a sibling service imports only pkg/, never internal/
|
|
144
|
+
import "github.com/zokypesch/go-ga-lib/pkg/apierror" // ok — pkg is public
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### DO NOT
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
// WRONG: trying to import internal/ from another module
|
|
151
|
+
import "github.com/zokypesch/go-ga-lib/internal/domain" // compile error
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Rule 3 — `domain/` has zero external imports
|
|
157
|
+
|
|
158
|
+
`domain/` defines the vocabulary of the service: structs, interfaces, and
|
|
159
|
+
sentinel errors. It MUST import nothing outside the Go standard library. This
|
|
160
|
+
makes the domain portable and prevents import cycles.
|
|
161
|
+
|
|
162
|
+
### DO
|
|
163
|
+
|
|
164
|
+
```go
|
|
165
|
+
// internal/domain/user.go
|
|
166
|
+
package domain
|
|
167
|
+
|
|
168
|
+
import "time" // stdlib only
|
|
169
|
+
|
|
170
|
+
type User struct {
|
|
171
|
+
ID string
|
|
172
|
+
Email string
|
|
173
|
+
Name string
|
|
174
|
+
CreatedAt time.Time
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// UserRepository is the storage contract — implemented in repository/.
|
|
178
|
+
type UserRepository interface {
|
|
179
|
+
FindByID(ctx context.Context, id string) (*User, error)
|
|
180
|
+
Create(ctx context.Context, u *User) error
|
|
181
|
+
Update(ctx context.Context, u *User) error
|
|
182
|
+
Delete(ctx context.Context, id string) error
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// UserUsecase is the business contract — implemented in usecase/.
|
|
186
|
+
type UserUsecase interface {
|
|
187
|
+
Get(ctx context.Context, id string) (*User, error)
|
|
188
|
+
Create(ctx context.Context, input CreateUserInput) (*User, error)
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```go
|
|
193
|
+
// internal/domain/errors.go
|
|
194
|
+
package domain
|
|
195
|
+
|
|
196
|
+
import "errors"
|
|
197
|
+
|
|
198
|
+
var (
|
|
199
|
+
ErrNotFound = errors.New("not found")
|
|
200
|
+
ErrAlreadyExists = errors.New("already exists")
|
|
201
|
+
ErrInvalidInput = errors.New("invalid input")
|
|
202
|
+
ErrUnauthorized = errors.New("unauthorized")
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### DO NOT
|
|
207
|
+
|
|
208
|
+
```go
|
|
209
|
+
// internal/domain/user.go — WRONG: external import in domain
|
|
210
|
+
package domain
|
|
211
|
+
|
|
212
|
+
import (
|
|
213
|
+
"gorm.io/gorm" // WRONG: ORM in domain
|
|
214
|
+
"github.com/google/uuid" // WRONG: external lib in domain
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
type User struct {
|
|
218
|
+
gorm.Model // WRONG: ORM base struct in domain
|
|
219
|
+
ID uuid.UUID
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Rule 4 — Handlers NEVER call repositories directly
|
|
226
|
+
|
|
227
|
+
The dependency flow is strict and unidirectional:
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
handler → usecase → repository
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Handlers know about usecases. Usecases know about repositories. Nothing flows
|
|
234
|
+
backwards. Handlers NEVER import `repository/`.
|
|
235
|
+
|
|
236
|
+
### DO
|
|
237
|
+
|
|
238
|
+
```go
|
|
239
|
+
// internal/handler/user_handler.go
|
|
240
|
+
package handler
|
|
241
|
+
|
|
242
|
+
import "github.com/zokypesch/go-ga-lib/internal/domain"
|
|
243
|
+
|
|
244
|
+
type UserHandler struct {
|
|
245
|
+
uc domain.UserUsecase // depends on the interface, not the concrete type
|
|
246
|
+
logger *zap.Logger
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
func NewUserHandler(uc domain.UserUsecase, logger *zap.Logger) *UserHandler {
|
|
250
|
+
return &UserHandler{uc: uc, logger: logger}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|
254
|
+
id := chi.URLParam(r, "id")
|
|
255
|
+
user, err := h.uc.Get(r.Context(), id) // calls usecase, not repository
|
|
256
|
+
if err != nil {
|
|
257
|
+
respondError(w, err)
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
respondJSON(w, http.StatusOK, user)
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### DO NOT
|
|
265
|
+
|
|
266
|
+
```go
|
|
267
|
+
// internal/handler/user_handler.go — WRONG: handler calls repository
|
|
268
|
+
package handler
|
|
269
|
+
|
|
270
|
+
import "github.com/zokypesch/go-ga-lib/internal/repository" // WRONG
|
|
271
|
+
|
|
272
|
+
type UserHandler struct {
|
|
273
|
+
repo *repository.UserRepository // WRONG: skip the usecase layer
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|
277
|
+
id := chi.URLParam(r, "id")
|
|
278
|
+
user, _ := h.repo.FindByID(r.Context(), id) // WRONG: bypasses business logic
|
|
279
|
+
json.NewEncoder(w).Encode(user)
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Dependency Direction Summary
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
cmd/server/main.go
|
|
289
|
+
└── internal/handler (imports domain interfaces)
|
|
290
|
+
└── internal/usecase (imports domain interfaces)
|
|
291
|
+
└── internal/repository (imports domain structs)
|
|
292
|
+
└── internal/domain (imports stdlib only)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Breaking this chain in any direction is a FAIL in code review.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## File Naming Conventions
|
|
300
|
+
|
|
301
|
+
| Concern | File name |
|
|
302
|
+
|---------|-----------|
|
|
303
|
+
| HTTP handlers for User | `internal/handler/user_handler.go` |
|
|
304
|
+
| Usecase for User | `internal/usecase/user_usecase.go` |
|
|
305
|
+
| Repository for User | `internal/repository/user_repository.go` |
|
|
306
|
+
| Domain types for User | `internal/domain/user.go` |
|
|
307
|
+
| Sentinel errors | `internal/domain/errors.go` |
|
|
308
|
+
| Config struct | `config/config.go` |
|
|
309
|
+
| Test for usecase | `internal/usecase/user_usecase_test.go` |
|
|
310
|
+
|
|
311
|
+
One file per major type. Do not create `utils.go`, `helpers.go`, or
|
|
312
|
+
`common.go` files — if code is shared, put it in a purpose-named file or
|
|
313
|
+
in `pkg/`.
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# Testing Standard
|
|
2
|
+
|
|
3
|
+
Tests are first-class citizens. Every usecase and repository must have unit
|
|
4
|
+
tests. Handlers get integration tests. The default pattern is table-driven.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Rule 1 — Table-Driven Tests as Default
|
|
9
|
+
|
|
10
|
+
Table-driven tests make it easy to add cases without duplicating test
|
|
11
|
+
scaffolding and produce clear failure messages via `t.Run`.
|
|
12
|
+
|
|
13
|
+
```go
|
|
14
|
+
// internal/usecase/user_usecase_test.go
|
|
15
|
+
package usecase_test
|
|
16
|
+
|
|
17
|
+
import (
|
|
18
|
+
"context"
|
|
19
|
+
"errors"
|
|
20
|
+
"testing"
|
|
21
|
+
|
|
22
|
+
"github.com/stretchr/testify/assert"
|
|
23
|
+
"github.com/stretchr/testify/require"
|
|
24
|
+
"go.uber.org/zap/zaptest"
|
|
25
|
+
|
|
26
|
+
"github.com/zokypesch/go-ga-lib/internal/domain"
|
|
27
|
+
"github.com/zokypesch/go-ga-lib/internal/usecase"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
func TestUserUsecase_Create(t *testing.T) {
|
|
31
|
+
cases := []struct {
|
|
32
|
+
name string
|
|
33
|
+
input domain.CreateUserInput
|
|
34
|
+
repoErr error // error the mock repo returns
|
|
35
|
+
want *domain.User
|
|
36
|
+
wantErr error // sentinel error the usecase should wrap
|
|
37
|
+
}{
|
|
38
|
+
{
|
|
39
|
+
name: "valid input creates user",
|
|
40
|
+
input: domain.CreateUserInput{Email: "alice@example.com", Name: "Alice"},
|
|
41
|
+
want: &domain.User{ID: "u1", Email: "alice@example.com", Name: "Alice"},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "missing email returns invalid input",
|
|
45
|
+
input: domain.CreateUserInput{Name: "Alice"},
|
|
46
|
+
wantErr: domain.ErrInvalidInput,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "duplicate email returns already exists",
|
|
50
|
+
input: domain.CreateUserInput{Email: "alice@example.com", Name: "Alice"},
|
|
51
|
+
repoErr: domain.ErrAlreadyExists,
|
|
52
|
+
wantErr: domain.ErrAlreadyExists,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "repo error propagates",
|
|
56
|
+
input: domain.CreateUserInput{Email: "bob@example.com", Name: "Bob"},
|
|
57
|
+
repoErr: errors.New("connection refused"),
|
|
58
|
+
wantErr: errors.New("connection refused"), // non-sentinel: check message
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for _, tc := range cases {
|
|
63
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
64
|
+
t.Parallel() // safe — each test case uses its own mock
|
|
65
|
+
|
|
66
|
+
repo := &mockUserRepository{createErr: tc.repoErr, createUser: tc.want}
|
|
67
|
+
logger := zaptest.NewLogger(t)
|
|
68
|
+
uc := usecase.NewUserUsecase(repo, logger)
|
|
69
|
+
|
|
70
|
+
got, err := uc.Create(context.Background(), tc.input)
|
|
71
|
+
|
|
72
|
+
if tc.wantErr != nil {
|
|
73
|
+
require.Error(t, err)
|
|
74
|
+
if errors.Is(tc.wantErr, domain.ErrInvalidInput) ||
|
|
75
|
+
errors.Is(tc.wantErr, domain.ErrAlreadyExists) {
|
|
76
|
+
assert.ErrorIs(t, err, tc.wantErr) // sentinel: use errors.Is
|
|
77
|
+
} else {
|
|
78
|
+
assert.ErrorContains(t, err, tc.wantErr.Error()) // infra: check message
|
|
79
|
+
}
|
|
80
|
+
assert.Nil(t, got)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
require.NoError(t, err)
|
|
85
|
+
assert.Equal(t, tc.want.Email, got.Email)
|
|
86
|
+
assert.Equal(t, tc.want.Name, got.Name)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Rule 2 — File Naming
|
|
95
|
+
|
|
96
|
+
| Scenario | Package declaration | File name |
|
|
97
|
+
|----------|---------------------|-----------|
|
|
98
|
+
| White-box (access unexported symbols) | `package usecase` | `user_usecase_test.go` |
|
|
99
|
+
| Black-box (test exported API only) | `package usecase_test` | `user_usecase_test.go` |
|
|
100
|
+
| Integration test | `package usecase_test` | `user_usecase_integration_test.go` |
|
|
101
|
+
|
|
102
|
+
Prefer black-box tests (`_test` package suffix) — they test the public
|
|
103
|
+
contract and catch accidental leaks of internal details.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Rule 3 — Use testify/assert and testify/require
|
|
108
|
+
|
|
109
|
+
- `require.*` — stops the test immediately on failure (use for preconditions)
|
|
110
|
+
- `assert.*` — records failure but continues (use for post-conditions)
|
|
111
|
+
|
|
112
|
+
```go
|
|
113
|
+
import (
|
|
114
|
+
"github.com/stretchr/testify/assert"
|
|
115
|
+
"github.com/stretchr/testify/require"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// require for setup / preconditions
|
|
119
|
+
user, err := uc.Create(ctx, input)
|
|
120
|
+
require.NoError(t, err) // stop if creation failed — rest of test is meaningless
|
|
121
|
+
|
|
122
|
+
// assert for post-conditions — collect all failures
|
|
123
|
+
assert.Equal(t, "alice@example.com", user.Email)
|
|
124
|
+
assert.NotEmpty(t, user.ID)
|
|
125
|
+
assert.False(t, user.CreatedAt.IsZero())
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Rule 4 — Interfaces Enable Mocking
|
|
131
|
+
|
|
132
|
+
All dependencies are injected as interfaces. Mocks are hand-written structs
|
|
133
|
+
(or generated with `mockery`) that implement the interface.
|
|
134
|
+
|
|
135
|
+
```go
|
|
136
|
+
// internal/usecase/user_usecase_test.go — hand-written mock
|
|
137
|
+
type mockUserRepository struct {
|
|
138
|
+
createUser *domain.User
|
|
139
|
+
createErr error
|
|
140
|
+
findUser *domain.User
|
|
141
|
+
findErr error
|
|
142
|
+
calls []string // optional: record calls for assertion
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func (m *mockUserRepository) Create(_ context.Context, u *domain.User) (*domain.User, error) {
|
|
146
|
+
m.calls = append(m.calls, "Create")
|
|
147
|
+
if m.createErr != nil {
|
|
148
|
+
return nil, m.createErr
|
|
149
|
+
}
|
|
150
|
+
if m.createUser != nil {
|
|
151
|
+
return m.createUser, nil
|
|
152
|
+
}
|
|
153
|
+
return u, nil
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func (m *mockUserRepository) FindByID(_ context.Context, id string) (*domain.User, error) {
|
|
157
|
+
m.calls = append(m.calls, "FindByID")
|
|
158
|
+
return m.findUser, m.findErr
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
For complex mocks, use `github.com/vektra/mockery` to generate:
|
|
163
|
+
```bash
|
|
164
|
+
go generate ./...
|
|
165
|
+
// File: internal/domain/user.go
|
|
166
|
+
//go:generate mockery --name=UserRepository --output=../mocks --outpkg=mocks
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Rule 5 — Integration Tests with Build Tags
|
|
172
|
+
|
|
173
|
+
Integration tests require real infrastructure (DB, cache, external APIs).
|
|
174
|
+
Gate them behind a build tag so `go test ./...` only runs unit tests in CI
|
|
175
|
+
by default.
|
|
176
|
+
|
|
177
|
+
```go
|
|
178
|
+
// internal/repository/user_repository_integration_test.go
|
|
179
|
+
//go:build integration
|
|
180
|
+
|
|
181
|
+
package repository_test
|
|
182
|
+
|
|
183
|
+
import (
|
|
184
|
+
"context"
|
|
185
|
+
"database/sql"
|
|
186
|
+
"testing"
|
|
187
|
+
|
|
188
|
+
_ "github.com/lib/pq"
|
|
189
|
+
"github.com/stretchr/testify/assert"
|
|
190
|
+
"github.com/stretchr/testify/require"
|
|
191
|
+
|
|
192
|
+
"github.com/zokypesch/go-ga-lib/internal/domain"
|
|
193
|
+
"github.com/zokypesch/go-ga-lib/internal/repository"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
func TestUserRepository_Create_Integration(t *testing.T) {
|
|
197
|
+
db, err := sql.Open("postgres", testDSN(t))
|
|
198
|
+
require.NoError(t, err)
|
|
199
|
+
t.Cleanup(func() { db.Close() })
|
|
200
|
+
|
|
201
|
+
repo := repository.NewUserRepository(db)
|
|
202
|
+
|
|
203
|
+
user, err := repo.Create(context.Background(), &domain.User{
|
|
204
|
+
Email: "integration@example.com",
|
|
205
|
+
Name: "Integration User",
|
|
206
|
+
})
|
|
207
|
+
require.NoError(t, err)
|
|
208
|
+
assert.NotEmpty(t, user.ID)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// testDSN reads the test DB connection string from env or t.Skip if absent
|
|
212
|
+
func testDSN(t *testing.T) string {
|
|
213
|
+
t.Helper()
|
|
214
|
+
dsn := os.Getenv("TEST_DB_DSN")
|
|
215
|
+
if dsn == "" {
|
|
216
|
+
t.Skip("TEST_DB_DSN not set — skipping integration test")
|
|
217
|
+
}
|
|
218
|
+
return dsn
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Run integration tests explicitly:
|
|
223
|
+
```bash
|
|
224
|
+
go test -tags=integration ./internal/repository/...
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Rule 6 — No `time.Sleep` in Tests
|
|
230
|
+
|
|
231
|
+
Sleep makes tests slow and flaky. Use channels, context timeouts, or
|
|
232
|
+
testify's `Eventually` helper to wait for asynchronous conditions.
|
|
233
|
+
|
|
234
|
+
### DO
|
|
235
|
+
|
|
236
|
+
```go
|
|
237
|
+
// Waiting for an async result with a channel
|
|
238
|
+
func TestWorker_ProcessesMessage(t *testing.T) {
|
|
239
|
+
processed := make(chan string, 1)
|
|
240
|
+
worker := NewWorker(func(msg string) {
|
|
241
|
+
processed <- msg
|
|
242
|
+
})
|
|
243
|
+
worker.Start(context.Background())
|
|
244
|
+
|
|
245
|
+
worker.Send("hello")
|
|
246
|
+
|
|
247
|
+
select {
|
|
248
|
+
case msg := <-processed:
|
|
249
|
+
assert.Equal(t, "hello", msg)
|
|
250
|
+
case <-time.After(2 * time.Second):
|
|
251
|
+
t.Fatal("timed out waiting for message to be processed")
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Polling with testify Eventually for eventually-consistent state
|
|
256
|
+
assert.Eventually(t,
|
|
257
|
+
func() bool { return cache.Has("key") },
|
|
258
|
+
2*time.Second, // max wait
|
|
259
|
+
10*time.Millisecond, // poll interval
|
|
260
|
+
"cache never populated",
|
|
261
|
+
)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### DO NOT
|
|
265
|
+
|
|
266
|
+
```go
|
|
267
|
+
worker.Start(context.Background())
|
|
268
|
+
worker.Send("hello")
|
|
269
|
+
time.Sleep(500 * time.Millisecond) // WRONG: flaky, slow, arbitrary
|
|
270
|
+
assert.True(t, worker.Processed("hello"))
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Rule 7 — Handler Tests use `httptest`
|
|
276
|
+
|
|
277
|
+
Test handlers with the standard `net/http/httptest` package. No need to
|
|
278
|
+
start a real server.
|
|
279
|
+
|
|
280
|
+
```go
|
|
281
|
+
// internal/handler/user_handler_test.go
|
|
282
|
+
package handler_test
|
|
283
|
+
|
|
284
|
+
import (
|
|
285
|
+
"bytes"
|
|
286
|
+
"encoding/json"
|
|
287
|
+
"net/http"
|
|
288
|
+
"net/http/httptest"
|
|
289
|
+
"testing"
|
|
290
|
+
|
|
291
|
+
"github.com/stretchr/testify/assert"
|
|
292
|
+
"github.com/stretchr/testify/require"
|
|
293
|
+
"go.uber.org/zap/zaptest"
|
|
294
|
+
|
|
295
|
+
"github.com/zokypesch/go-ga-lib/internal/domain"
|
|
296
|
+
"github.com/zokypesch/go-ga-lib/internal/handler"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
func TestUserHandler_Create(t *testing.T) {
|
|
300
|
+
cases := []struct {
|
|
301
|
+
name string
|
|
302
|
+
body any
|
|
303
|
+
ucErr error
|
|
304
|
+
ucUser *domain.User
|
|
305
|
+
wantStatus int
|
|
306
|
+
wantCode string // error code in JSON response
|
|
307
|
+
}{
|
|
308
|
+
{
|
|
309
|
+
name: "creates user — 201",
|
|
310
|
+
body: map[string]string{"email": "a@b.com", "name": "A"},
|
|
311
|
+
ucUser: &domain.User{ID: "u1", Email: "a@b.com"},
|
|
312
|
+
wantStatus: http.StatusCreated,
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: "invalid input — 400",
|
|
316
|
+
body: map[string]string{"name": "A"}, // missing email
|
|
317
|
+
ucErr: domain.ErrInvalidInput,
|
|
318
|
+
wantStatus: http.StatusBadRequest,
|
|
319
|
+
wantCode: "BAD_REQUEST",
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "duplicate — 409",
|
|
323
|
+
body: map[string]string{"email": "a@b.com", "name": "A"},
|
|
324
|
+
ucErr: domain.ErrAlreadyExists,
|
|
325
|
+
wantStatus: http.StatusConflict,
|
|
326
|
+
wantCode: "CONFLICT",
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for _, tc := range cases {
|
|
331
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
332
|
+
bodyBytes, err := json.Marshal(tc.body)
|
|
333
|
+
require.NoError(t, err)
|
|
334
|
+
|
|
335
|
+
uc := &mockUserUsecase{createUser: tc.ucUser, createErr: tc.ucErr}
|
|
336
|
+
h := handler.NewUserHandler(uc, zaptest.NewLogger(t))
|
|
337
|
+
|
|
338
|
+
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewReader(bodyBytes))
|
|
339
|
+
req.Header.Set("Content-Type", "application/json")
|
|
340
|
+
rr := httptest.NewRecorder()
|
|
341
|
+
|
|
342
|
+
h.Create(rr, req)
|
|
343
|
+
|
|
344
|
+
assert.Equal(t, tc.wantStatus, rr.Code)
|
|
345
|
+
if tc.wantCode != "" {
|
|
346
|
+
var resp map[string]string
|
|
347
|
+
require.NoError(t, json.NewDecoder(rr.Body).Decode(&resp))
|
|
348
|
+
assert.Equal(t, tc.wantCode, resp["code"])
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Test Coverage Targets
|
|
358
|
+
|
|
359
|
+
| Layer | Minimum coverage |
|
|
360
|
+
|-------|-----------------|
|
|
361
|
+
| `domain/` | 100% (pure logic, no I/O) |
|
|
362
|
+
| `usecase/` | 90% |
|
|
363
|
+
| `handler/` | 80% |
|
|
364
|
+
| `repository/` | covered by integration tests |
|
|
365
|
+
|
|
366
|
+
Run with: `go test -cover ./...`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "java2go-porter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Java→Go translation accelerator (DRAFT only — requires human review + green tests). Runs analyzer→translator→test-pairer→reviewer pipeline.",
|
|
5
|
+
"commands": [
|
|
6
|
+
{
|
|
7
|
+
"name": "port-java-service",
|
|
8
|
+
"description": "Translate a Java/Spring Boot service to a Go draft using chi + go-ga-lib",
|
|
9
|
+
"path": "commands/port-java-service.md"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"agents": [
|
|
13
|
+
{ "name": "analyzer", "path": "agents/analyzer.md" },
|
|
14
|
+
{ "name": "translator", "path": "agents/translator.md" },
|
|
15
|
+
{ "name": "test-pairer", "path": "agents/test-pairer.md" },
|
|
16
|
+
{ "name": "reviewer", "path": "agents/reviewer.md" }
|
|
17
|
+
],
|
|
18
|
+
"skills": [
|
|
19
|
+
{ "name": "idiom-mapping", "path": "skills/idiom-mapping.md" }
|
|
20
|
+
]
|
|
21
|
+
}
|