refactor(supply-api): declarify runtime http adapter
This commit is contained in:
134
docs/plans/2026-04-16-supply-api-runtime-http-adapter-plan.md
Normal file
134
docs/plans/2026-04-16-supply-api-runtime-http-adapter-plan.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Supply API Runtime HTTP Adapter Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 把 `Runtime.BuildServer` 里剩余的 `BuildServerOptions` 构造收成更明确的 adapter,让 runtime 到 HTTP 启动的边界完全声明式。
|
||||
|
||||
**Architecture:** 保留 `Runtime.BuildServer` 和 `BuildServer` 的现有外部接口,新增内部 helper 负责从 runtime 提取健康检查和构造 `BuildServerOptions`。`Runtime.BuildServer` 最终只做 `adapt runtime -> BuildServer(opts)`,把边界映射与 HTTP 启动解耦。
|
||||
|
||||
**Tech Stack:** Go, Go test
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 提取 runtime health check adapter
|
||||
|
||||
**Files:**
|
||||
- Modify: `supply-api/internal/app/runtime.go`
|
||||
- Modify: `supply-api/internal/app/runtime_test.go`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```go
|
||||
func TestResolveRuntimeHealthChecks_OmitsUnavailableDependencies(t *testing.T) {
|
||||
checks := resolveRuntimeHealthChecks(&Runtime{})
|
||||
if checks.DBHealthCheck != nil || checks.RedisHealthCheck != nil {
|
||||
t.Fatal("expected nil health checks without dependencies")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveRuntimeHealthChecks_(OmitsUnavailableDependencies|ExposesAvailableDependencies)' -v`
|
||||
Expected: FAIL,因为 helper 尚不存在
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```go
|
||||
type runtimeHealthChecks struct { ... }
|
||||
func resolveRuntimeHealthChecks(runtime *Runtime) runtimeHealthChecks { ... }
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveRuntimeHealthChecks_(OmitsUnavailableDependencies|ExposesAvailableDependencies)' -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add supply-api/internal/app/runtime.go supply-api/internal/app/runtime_test.go
|
||||
git commit -m "refactor(supply-api): extract runtime health check adapter"
|
||||
```
|
||||
|
||||
### Task 2: 提取 BuildServerOptions adapter
|
||||
|
||||
**Files:**
|
||||
- Modify: `supply-api/internal/app/runtime.go`
|
||||
- Modify: `supply-api/internal/app/runtime_test.go`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```go
|
||||
func TestAdaptRuntimeToBuildServerOptions_MapsRuntimeFields(t *testing.T) {
|
||||
opts, err := adaptRuntimeToBuildServerOptions(runtime)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if opts.SupplyAPI != runtime.supplyAPI {
|
||||
t.Fatal("expected supply api to be preserved")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app -run 'TestAdaptRuntimeToBuildServerOptions_(RequiresRuntime|MapsRuntimeFields)' -v`
|
||||
Expected: FAIL,因为 helper 尚不存在
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```go
|
||||
func adaptRuntimeToBuildServerOptions(runtime *Runtime) (BuildServerOptions, error) { ... }
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app -run 'TestAdaptRuntimeToBuildServerOptions_(RequiresRuntime|MapsRuntimeFields)' -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add supply-api/internal/app/runtime.go supply-api/internal/app/runtime_test.go
|
||||
git commit -m "refactor(supply-api): extract runtime server options adapter"
|
||||
```
|
||||
|
||||
### Task 3: 回归验证与收尾
|
||||
|
||||
**Files:**
|
||||
- Modify: `supply-api/internal/app/runtime.go`
|
||||
- Verify: `supply-api/internal/app/runtime_test.go`
|
||||
- Verify: `supply-api/internal/app/bootstrap.go`
|
||||
|
||||
**Step 1: Run focused tests**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app -run 'Test(ResolveRuntimeHealthChecks_(OmitsUnavailableDependencies|ExposesAvailableDependencies)|AdaptRuntimeToBuildServerOptions_(RequiresRuntime|MapsRuntimeFields)|BuildServer_.*|BuildRuntime_.*)' -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 2: Run package regression**
|
||||
|
||||
Run: `cd "supply-api" && go test ./internal/app ./cmd/supply-api ./internal/httpapi`
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Run e2e tests**
|
||||
|
||||
Run: `cd "supply-api" && go test -tags=e2e ./e2e`
|
||||
Expected: PASS
|
||||
|
||||
**Step 4: Run repo exit verification**
|
||||
|
||||
Run: `bash "scripts/ci/repo_integrity_check.sh"`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Check formatting**
|
||||
|
||||
Run: `git diff --check`
|
||||
Expected: no output
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/plans/2026-04-16-supply-api-runtime-http-adapter-plan.md supply-api/internal/app/runtime.go supply-api/internal/app/runtime_test.go
|
||||
git commit -m "refactor(supply-api): declarify runtime http adapter"
|
||||
```
|
||||
@@ -87,6 +87,11 @@ type runtimeAPIBundle struct {
|
||||
rateLimitConfig *middleware.RateLimitConfig
|
||||
}
|
||||
|
||||
type runtimeHealthChecks struct {
|
||||
DBHealthCheck func(context.Context) error
|
||||
RedisHealthCheck func(context.Context) error
|
||||
}
|
||||
|
||||
// BuildRuntime 构建 supply-api 运行时依赖。
|
||||
func BuildRuntime(opts RuntimeOptions) (*Runtime, error) {
|
||||
return buildRuntimeWithFactory(opts, runtimeFactory{
|
||||
@@ -333,30 +338,44 @@ func defaultRuntimeTuning() runtimeTuning {
|
||||
|
||||
// BuildServer 使用运行时依赖构建 HTTP server。
|
||||
func (r *Runtime) BuildServer() (*http.Server, error) {
|
||||
if r == nil {
|
||||
return nil, errors.New("runtime is required")
|
||||
opts, err := adaptRuntimeToBuildServerOptions(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return BuildServer(opts)
|
||||
}
|
||||
|
||||
func resolveRuntimeHealthChecks(runtime *Runtime) runtimeHealthChecks {
|
||||
var checks runtimeHealthChecks
|
||||
if runtime == nil {
|
||||
return checks
|
||||
}
|
||||
if runtime.db != nil {
|
||||
checks.DBHealthCheck = runtime.db.HealthCheck
|
||||
}
|
||||
if runtime.redisCache != nil {
|
||||
checks.RedisHealthCheck = runtime.redisCache.HealthCheck
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
func adaptRuntimeToBuildServerOptions(runtime *Runtime) (BuildServerOptions, error) {
|
||||
if runtime == nil {
|
||||
return BuildServerOptions{}, errors.New("runtime is required")
|
||||
}
|
||||
|
||||
var dbHealthCheck func(context.Context) error
|
||||
var redisHealthCheck func(context.Context) error
|
||||
if r.db != nil {
|
||||
dbHealthCheck = r.db.HealthCheck
|
||||
}
|
||||
if r.redisCache != nil {
|
||||
redisHealthCheck = r.redisCache.HealthCheck
|
||||
}
|
||||
|
||||
return BuildServer(BuildServerOptions{
|
||||
Env: r.env,
|
||||
ServerConfig: r.serverConfig,
|
||||
Logger: r.logger,
|
||||
SupplyAPI: r.supplyAPI,
|
||||
AlertAPI: r.alertAPI,
|
||||
AuthMiddleware: r.authMiddleware,
|
||||
RateLimitConfig: r.rateLimitConfig,
|
||||
DBHealthCheck: dbHealthCheck,
|
||||
RedisHealthCheck: redisHealthCheck,
|
||||
})
|
||||
healthChecks := resolveRuntimeHealthChecks(runtime)
|
||||
return BuildServerOptions{
|
||||
Env: runtime.env,
|
||||
ServerConfig: runtime.serverConfig,
|
||||
Logger: runtime.logger,
|
||||
SupplyAPI: runtime.supplyAPI,
|
||||
AlertAPI: runtime.alertAPI,
|
||||
AuthMiddleware: runtime.authMiddleware,
|
||||
RateLimitConfig: runtime.rateLimitConfig,
|
||||
DBHealthCheck: healthChecks.DBHealthCheck,
|
||||
RedisHealthCheck: healthChecks.RedisHealthCheck,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close 关闭运行时持有的外部资源。
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"lijiaoqiao/supply-api/internal/config"
|
||||
"lijiaoqiao/supply-api/internal/domain"
|
||||
"lijiaoqiao/supply-api/internal/messaging"
|
||||
"lijiaoqiao/supply-api/internal/middleware"
|
||||
"lijiaoqiao/supply-api/internal/repository"
|
||||
)
|
||||
|
||||
@@ -354,6 +355,84 @@ func TestBuildRuntime_DevFallbackLogsWarnings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRuntimeHealthChecks_OmitsUnavailableDependencies(t *testing.T) {
|
||||
checks := resolveRuntimeHealthChecks(&Runtime{})
|
||||
if checks.DBHealthCheck != nil {
|
||||
t.Fatal("expected nil db health check without database")
|
||||
}
|
||||
if checks.RedisHealthCheck != nil {
|
||||
t.Fatal("expected nil redis health check without redis")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRuntimeHealthChecks_ExposesAvailableDependencies(t *testing.T) {
|
||||
checks := resolveRuntimeHealthChecks(&Runtime{
|
||||
db: &repository.DB{},
|
||||
redisCache: &cache.RedisCache{},
|
||||
})
|
||||
if checks.DBHealthCheck == nil {
|
||||
t.Fatal("expected db health check")
|
||||
}
|
||||
if checks.RedisHealthCheck == nil {
|
||||
t.Fatal("expected redis health check")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdaptRuntimeToBuildServerOptions_RequiresRuntime(t *testing.T) {
|
||||
_, err := adaptRuntimeToBuildServerOptions(nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected nil runtime to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "runtime is required") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdaptRuntimeToBuildServerOptions_MapsRuntimeFields(t *testing.T) {
|
||||
supplyAPI, alertAPI := mustBuildTestAPIs(t)
|
||||
authMiddleware := &middleware.AuthMiddleware{}
|
||||
rateLimitConfig := &middleware.RateLimitConfig{Enabled: true}
|
||||
|
||||
opts, err := adaptRuntimeToBuildServerOptions(&Runtime{
|
||||
env: "staging",
|
||||
logger: testLogger{},
|
||||
serverConfig: config.ServerConfig{Addr: ":19090"},
|
||||
supplyAPI: supplyAPI,
|
||||
alertAPI: alertAPI,
|
||||
authMiddleware: authMiddleware,
|
||||
rateLimitConfig: rateLimitConfig,
|
||||
db: &repository.DB{},
|
||||
redisCache: &cache.RedisCache{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected adapter to succeed, got %v", err)
|
||||
}
|
||||
if opts.Env != "staging" {
|
||||
t.Fatalf("unexpected env: %s", opts.Env)
|
||||
}
|
||||
if opts.ServerConfig.Addr != ":19090" {
|
||||
t.Fatalf("unexpected server addr: %s", opts.ServerConfig.Addr)
|
||||
}
|
||||
if opts.SupplyAPI != supplyAPI {
|
||||
t.Fatal("expected supply api to be preserved")
|
||||
}
|
||||
if opts.AlertAPI != alertAPI {
|
||||
t.Fatal("expected alert api to be preserved")
|
||||
}
|
||||
if opts.AuthMiddleware != authMiddleware {
|
||||
t.Fatal("expected auth middleware to be preserved")
|
||||
}
|
||||
if opts.RateLimitConfig != rateLimitConfig {
|
||||
t.Fatal("expected rate limit config to be preserved")
|
||||
}
|
||||
if opts.DBHealthCheck == nil {
|
||||
t.Fatal("expected db health check")
|
||||
}
|
||||
if opts.RedisHealthCheck == nil {
|
||||
t.Fatal("expected redis health check")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntime_StartBackgroundWorkers_WithoutDatabaseIsNoop(t *testing.T) {
|
||||
var outboxRepoCalled bool
|
||||
|
||||
|
||||
Reference in New Issue
Block a user