diff --git a/docs/plans/2026-04-15-supply-api-main-env-precheck-plan.md b/docs/plans/2026-04-15-supply-api-main-env-precheck-plan.md new file mode 100644 index 00000000..1061b992 --- /dev/null +++ b/docs/plans/2026-04-15-supply-api-main-env-precheck-plan.md @@ -0,0 +1,129 @@ +# Supply API Main Env Precheck Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 让 `supply-api/cmd/supply-api/main.go` 在加载配置之前就拦截非法环境名,统一复用 app 层环境解析,避免输出低价值的配置文件错误。 + +**Architecture:** 提升 app 层环境解析 helper 为可复用入口,`main` 在推导默认配置路径与调用 `config.LoadFromPath` 前先做 env 校验。空值仍回退 `dev`,非法值直接失败。 + +**Tech Stack:** Go, Go test + +--- + +### Task 1: 为 main 增加非法 env 预校验 + +**Files:** +- Modify: `supply-api/cmd/supply-api/main.go` +- Modify: `supply-api/cmd/supply-api/main_test.go` + +**Step 1: Write the failing test** + +```go +func TestMain_RejectsUnsupportedEnvBeforeLoadingConfig(t *testing.T) { + ... + if !strings.Contains(string(output), "unsupported env") { + t.Fatalf("expected unsupported env error, got: %s", string(output)) + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cd "supply-api" && go test ./cmd/supply-api -run 'TestMain_RejectsUnsupportedEnvBeforeLoadingConfig' -v` +Expected: FAIL,因为当前会先报配置文件读取失败 + +**Step 3: Write minimal implementation** + +```go + envName, err := app.ResolveEnv(*env) + if err != nil { + jsonLogger.Fatalf("%v", err) + } +``` + +**Step 4: Run test to verify it passes** + +Run: `cd "supply-api" && go test ./cmd/supply-api -run 'TestMain_RejectsUnsupportedEnvBeforeLoadingConfig' -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add supply-api/cmd/supply-api/main.go supply-api/cmd/supply-api/main_test.go +git commit -m "refactor(supply-api): precheck main env values" +``` + +### Task 2: 让 app 层公开统一环境解析 helper + +**Files:** +- Modify: `supply-api/internal/app/runtime.go` +- Modify: `supply-api/internal/app/runtime_test.go` +- Modify: `supply-api/internal/app/bootstrap.go` + +**Step 1: Write the failing test** + +```go +func TestResolveEnv_RejectsUnsupportedValue(t *testing.T) { + _, err := ResolveEnv("qa") + if err == nil { + t.Fatal("expected unsupported env to fail") + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveEnv_RejectsUnsupportedValue' -v` +Expected: FAIL,因为 helper 尚未导出 + +**Step 3: Write minimal implementation** + +```go +func ResolveEnv(env string) (string, error) { ... } +``` + +**Step 4: Run test to verify it passes** + +Run: `cd "supply-api" && go test ./internal/app -run 'TestResolveEnv_RejectsUnsupportedValue' -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add supply-api/internal/app/runtime.go supply-api/internal/app/runtime_test.go supply-api/internal/app/bootstrap.go +git commit -m "refactor(supply-api): reuse env resolver across app layer" +``` + +### Task 3: 验证与收尾 + +**Files:** +- Verify: `supply-api/cmd/supply-api/main.go` +- Verify: `supply-api/internal/app/runtime.go` +- Verify: `supply-api/internal/app/bootstrap.go` + +**Step 1: Run focused tests** + +Run: `cd "supply-api" && go test ./internal/app ./cmd/supply-api ./internal/httpapi` +Expected: PASS + +**Step 2: Run e2e build-tag tests** + +Run: `cd "supply-api" && go test -tags=e2e ./e2e` +Expected: PASS + +**Step 3: Run repo exit verification** + +Run: `bash "scripts/ci/repo_integrity_check.sh"` +Expected: PASS + +**Step 4: Check formatting** + +Run: `git diff --check` +Expected: no output + +**Step 5: Commit** + +```bash +git add docs/plans/2026-04-15-supply-api-main-env-precheck-plan.md supply-api/cmd/supply-api/main.go supply-api/cmd/supply-api/main_test.go supply-api/internal/app/runtime.go supply-api/internal/app/runtime_test.go supply-api/internal/app/bootstrap.go +git commit -m "refactor(supply-api): precheck main env before config load" +``` diff --git a/supply-api/cmd/supply-api/main.go b/supply-api/cmd/supply-api/main.go index 39db9db3..0d2f7aac 100644 --- a/supply-api/cmd/supply-api/main.go +++ b/supply-api/cmd/supply-api/main.go @@ -20,6 +20,12 @@ func main() { configPath := flag.String("config", "", "config file path") flag.Parse() + envName, err := app.ResolveEnv(*env) + if err != nil { + logging.NewLogger("supply-api", logging.LogLevelInfo).Fatalf("%v", err) + } + *env = envName + // 确定配置文件路径 if *configPath == "" { *configPath = "./config/config." + *env + ".yaml" diff --git a/supply-api/cmd/supply-api/main_test.go b/supply-api/cmd/supply-api/main_test.go index 2922f358..60a9a220 100644 --- a/supply-api/cmd/supply-api/main_test.go +++ b/supply-api/cmd/supply-api/main_test.go @@ -53,6 +53,25 @@ token: } } +func TestMain_RejectsUnsupportedEnvBeforeLoadingConfig(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=TestMainHelperProcess", "--", "-env", "qa") + cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1") + output, err := cmd.CombinedOutput() + + if ctx.Err() == context.DeadlineExceeded { + t.Fatalf("expected unsupported env to fail fast, but process timed out. output=%s", string(output)) + } + if err == nil { + t.Fatalf("expected unsupported env to fail, but process exited successfully. output=%s", string(output)) + } + if !strings.Contains(string(output), "unsupported env") { + t.Fatalf("expected unsupported env error, got: %s", string(output)) + } +} + func TestMainHelperProcess(t *testing.T) { if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { return diff --git a/supply-api/internal/app/bootstrap.go b/supply-api/internal/app/bootstrap.go index 0a4f26ca..25481147 100644 --- a/supply-api/internal/app/bootstrap.go +++ b/supply-api/internal/app/bootstrap.go @@ -37,7 +37,7 @@ func BuildServer(opts BuildServerOptions) (*http.Server, error) { return nil, errors.New("logger is required") } - env, err := resolveEnv(opts.Env) + env, err := ResolveEnv(opts.Env) if err != nil { return nil, err } diff --git a/supply-api/internal/app/runtime.go b/supply-api/internal/app/runtime.go index 88e5fa4e..dec84c04 100644 --- a/supply-api/internal/app/runtime.go +++ b/supply-api/internal/app/runtime.go @@ -87,7 +87,7 @@ func buildRuntimeWithFactory(opts RuntimeOptions, factory runtimeFactory) (*Runt factory.newRedisCache = cache.NewRedisCache } - env, err := resolveEnv(opts.Env) + env, err := ResolveEnv(opts.Env) if err != nil { return nil, err } @@ -331,7 +331,7 @@ func (r *Runtime) ShutdownTimeout() time.Duration { return r.serverConfig.ShutdownTimeout } -func resolveEnv(env string) (string, error) { +func ResolveEnv(env string) (string, error) { normalized := strings.ToLower(strings.TrimSpace(env)) if normalized == "" { return "dev", nil diff --git a/supply-api/internal/app/runtime_test.go b/supply-api/internal/app/runtime_test.go index 34d2487d..513a317d 100644 --- a/supply-api/internal/app/runtime_test.go +++ b/supply-api/internal/app/runtime_test.go @@ -64,6 +64,16 @@ func TestBuildRuntime_ProdRequiresDatabase(t *testing.T) { } } +func TestResolveEnv_RejectsUnsupportedValue(t *testing.T) { + _, err := ResolveEnv("qa") + if err == nil { + t.Fatal("expected unsupported env to fail") + } + if !strings.Contains(err.Error(), "unsupported env") { + t.Fatalf("unexpected error: %v", err) + } +} + func TestBuildRuntime_RejectsUnsupportedEnv(t *testing.T) { _, err := buildRuntimeWithFactory(RuntimeOptions{ Env: "qa",