chore: configure gin mode
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2bdad13e5c
commit
ab4f905725
@@ -46,6 +46,8 @@ process:
|
||||
opencodeCommand: "opencode"
|
||||
file:
|
||||
maxWriteBytes: 1048576
|
||||
gin:
|
||||
mode: "release"
|
||||
log:
|
||||
level: "info"
|
||||
format: "json"
|
||||
@@ -59,6 +61,7 @@ log:
|
||||
| `CODESPACE_WORKSPACE_ROOT` | Workspace storage root | `./workspaces` |
|
||||
| `CODESPACE_OPENCODE_COMMAND` | OpenCode binary path | `opencode` |
|
||||
| `CODESPACE_FILE_MAX_WRITE_BYTES` | Max bytes per file write | `1048576` |
|
||||
| `CODESPACE_GIN_MODE` | Gin mode (`debug`, `release`, `test`) | `release` |
|
||||
| `CODESPACE_READ_TIMEOUT` | HTTP read timeout | `15s` |
|
||||
| `CODESPACE_WRITE_TIMEOUT` | HTTP write timeout | `15s` |
|
||||
| `CODESPACE_IDLE_TIMEOUT` | HTTP idle timeout | `60s` |
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ func main() {
|
||||
fileSvc := service.NewFileService(workspaces, cfg.File.MaxWriteBytes)
|
||||
processSvc := service.NewProcessService(workspaces, processes, lg)
|
||||
|
||||
router := api.NewRouter(workspaceSvc, fileSvc, processSvc, lg)
|
||||
router := api.NewRouter(workspaceSvc, fileSvc, processSvc, lg, cfg.Gin.Mode)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: cfg.Server.Addr,
|
||||
|
||||
@@ -10,6 +10,8 @@ process:
|
||||
opencodeCommand: "opencode"
|
||||
file:
|
||||
maxWriteBytes: 1048576
|
||||
gin:
|
||||
mode: "release"
|
||||
log:
|
||||
level: "info"
|
||||
format: "json"
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
# Gin Mode Config Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Move Gin mode selection out of `internal/api.NewRouter` hardcoding and into project configuration.
|
||||
|
||||
**Architecture:** Add a dedicated `GinConfig` to `pkg/config`, validate Gin mode during `config.Load`, pass `cfg.Gin.Mode` from `cmd/server/main.go` into `api.NewRouter`, and use that value in `gin.SetMode`. Router tests should pass `gin.TestMode` explicitly.
|
||||
|
||||
**Tech Stack:** Go 1.25, Gin, existing YAML config via `gopkg.in/yaml.v3`, existing Gin + slog backend.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Do not hardcode `gin.SetMode(gin.ReleaseMode)` in router code.
|
||||
- Add a dedicated `gin.mode` config block; do not put Gin mode under `server`.
|
||||
- Supported Gin modes are exactly `debug`, `release`, and `test`.
|
||||
- Default Gin mode is `release`.
|
||||
- `CODESPACE_GIN_MODE` must override YAML/default config.
|
||||
- Invalid Gin mode must fail during `config.Load`.
|
||||
- Do not change API paths, methods, response JSON, middleware behavior, service interfaces, or logging policy except for passing Gin mode into router construction.
|
||||
- All application logs must continue to use `log/slog`; do not add `fmt.Print*`, `log.Printf`, `gin.Default`, or `gin.Logger`.
|
||||
- Do not add dependencies.
|
||||
- Code generation/refactoring must be delegated to Codex MCP using exactly `model: "kimi/kimi-k2.7-code"`, `sandbox: "danger-full-access"`, and `approval-policy: "on-failure"`.
|
||||
- Do not commit unless the user explicitly asks.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### Modify
|
||||
|
||||
- `configs/config.yaml` — add `gin.mode: "release"`.
|
||||
- `pkg/config/config.go` — add `GinConfig`, default, YAML/env load, validation.
|
||||
- `pkg/config/config_test.go` — add default/YAML/env/invalid mode tests.
|
||||
- `internal/api/router.go` — change `NewRouter` signature to accept `ginMode string` and call `gin.SetMode(ginMode)`.
|
||||
- `internal/api/router_test.go` — pass `gin.TestMode` into `NewRouter`.
|
||||
- `cmd/server/main.go` — pass `cfg.Gin.Mode` into `api.NewRouter`.
|
||||
- `README.md` — document `gin.mode` and `CODESPACE_GIN_MODE`.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add Gin mode config and validation
|
||||
|
||||
**Files:**
|
||||
- Modify: `pkg/config/config.go`
|
||||
- Modify: `pkg/config/config_test.go`
|
||||
- Modify: `configs/config.yaml`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `type GinConfig struct { Mode string `yaml:"mode"` }`.
|
||||
- Produces: `Config.Gin GinConfig`.
|
||||
- Produces: `CODESPACE_GIN_MODE` env override.
|
||||
- Produces: config-load validation for `debug|release|test`.
|
||||
|
||||
- [ ] **Step 1: Add failing config tests**
|
||||
|
||||
Add tests to `pkg/config/config_test.go`:
|
||||
|
||||
```go
|
||||
func TestDefaultIncludesGinConfig(t *testing.T) {
|
||||
cfg := Default()
|
||||
if cfg.Gin.Mode != "release" {
|
||||
t.Fatalf("Gin.Mode = %q, want release", cfg.Gin.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadParsesGinConfigFromYAML(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
data := []byte(`gin:
|
||||
mode: "debug"
|
||||
`)
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
if cfg.Gin.Mode != "debug" {
|
||||
t.Fatalf("Gin.Mode = %q, want debug", cfg.Gin.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAppliesGinModeEnvironmentOverride(t *testing.T) {
|
||||
t.Setenv("CODESPACE_GIN_MODE", "test")
|
||||
cfg, err := Load(filepath.Join(t.TempDir(), "missing.yaml"))
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
if cfg.Gin.Mode != "test" {
|
||||
t.Fatalf("Gin.Mode = %q, want test", cfg.Gin.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidGinMode(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
data := []byte(`gin:
|
||||
mode: "production"
|
||||
`)
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := Load(path); err == nil {
|
||||
t.Fatal("expected invalid gin mode error")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Implement config fields and validation**
|
||||
|
||||
Update `pkg/config/config.go`:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Workspace WorkspaceConfig `yaml:"workspace"`
|
||||
Process ProcessConfig `yaml:"process"`
|
||||
File FileConfig `yaml:"file"`
|
||||
Log LogConfig `yaml:"log"`
|
||||
Gin GinConfig `yaml:"gin"`
|
||||
}
|
||||
|
||||
type GinConfig struct {
|
||||
Mode string `yaml:"mode"`
|
||||
}
|
||||
```
|
||||
|
||||
Update `rawConfig` with `Gin GinConfig`.
|
||||
|
||||
Default:
|
||||
|
||||
```go
|
||||
Gin: GinConfig{Mode: "release"},
|
||||
```
|
||||
|
||||
Raw/env handling:
|
||||
|
||||
```go
|
||||
if raw.Gin.Mode != "" { cfg.Gin.Mode = raw.Gin.Mode }
|
||||
if v := os.Getenv("CODESPACE_GIN_MODE"); v != "" { cfg.Gin.Mode = v }
|
||||
```
|
||||
|
||||
Add validation after env overrides, before returning from `Load`:
|
||||
|
||||
```go
|
||||
func validateGinMode(mode string) error {
|
||||
switch mode {
|
||||
case "debug", "release", "test":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid gin mode %q", mode)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Call it from `Load`:
|
||||
|
||||
```go
|
||||
if err := validateGinMode(cfg.Gin.Mode); err != nil { return Config{}, err }
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update config YAML**
|
||||
|
||||
Add to `configs/config.yaml`:
|
||||
|
||||
```yaml
|
||||
gin:
|
||||
mode: "release"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify config package**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
gofmt -w pkg/config
|
||||
go test ./pkg/config
|
||||
```
|
||||
|
||||
Expected: config tests pass.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Pass Gin mode into router
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/api/router.go`
|
||||
- Modify: `internal/api/router_test.go`
|
||||
- Modify: `cmd/server/main.go`
|
||||
|
||||
**Interfaces:**
|
||||
- Changes: `api.NewRouter(workspaces, files, processes, lg, ginMode string) *gin.Engine`.
|
||||
- Consumes: `cfg.Gin.Mode` from main.
|
||||
- Tests consume: `gin.TestMode`.
|
||||
|
||||
- [ ] **Step 1: Update router signature**
|
||||
|
||||
Modify `internal/api/router.go`:
|
||||
|
||||
```go
|
||||
func NewRouter(workspaces *service.WorkspaceService, files *service.FileService, processes *service.ProcessService, lg *slog.Logger, ginMode string) *gin.Engine {
|
||||
gin.SetMode(ginMode)
|
||||
r := gin.New()
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Remove `gin.SetMode(gin.ReleaseMode)`.
|
||||
|
||||
- [ ] **Step 2: Update main wiring**
|
||||
|
||||
Modify `cmd/server/main.go`:
|
||||
|
||||
```go
|
||||
router := api.NewRouter(workspaceSvc, fileSvc, processSvc, lg, cfg.Gin.Mode)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update router tests**
|
||||
|
||||
Modify `internal/api/router_test.go`:
|
||||
|
||||
```go
|
||||
return NewRouter(wsSvc, fileSvc, procSvc, nil, gin.TestMode)
|
||||
```
|
||||
|
||||
Add Gin import if needed:
|
||||
|
||||
```go
|
||||
"github.com/gin-gonic/gin"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify API and server compile**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
gofmt -w cmd/server internal/api
|
||||
go test ./internal/api ./cmd/server
|
||||
```
|
||||
|
||||
Expected: tests/compile pass.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Documentation and final validation
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Documents `gin.mode` and `CODESPACE_GIN_MODE`.
|
||||
|
||||
- [ ] **Step 1: Update README**
|
||||
|
||||
Add a short config note:
|
||||
|
||||
```markdown
|
||||
### Gin mode
|
||||
|
||||
Gin mode is configured through `config.yaml`:
|
||||
|
||||
```yaml
|
||||
gin:
|
||||
mode: "release"
|
||||
```
|
||||
|
||||
Supported values: `debug`, `release`, `test`.
|
||||
|
||||
Environment override:
|
||||
|
||||
- `CODESPACE_GIN_MODE`
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run final validation**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
gofmt -w cmd internal pkg
|
||||
go mod tidy
|
||||
go test ./...
|
||||
grep -R "gin.SetMode(gin.ReleaseMode)" cmd internal pkg || true
|
||||
grep -R "gin\.Default\|gin\.Logger" cmd internal pkg || true
|
||||
grep -R "log\.Printf\|log\.Println\|log\.Fatal\|fmt\.Print" cmd internal pkg || true
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- Tests pass.
|
||||
- No hardcoded `gin.SetMode(gin.ReleaseMode)`.
|
||||
- No `gin.Default` / `gin.Logger`.
|
||||
- No disallowed runtime logging.
|
||||
|
||||
- [ ] **Step 3: Server smoke test**
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
(go run ./cmd/server > /tmp/codespace-server.log 2>&1 & pid=$!; \
|
||||
for i in $(seq 1 30); do \
|
||||
if curl -fsS http://localhost:8080/healthz; then \
|
||||
kill $pid; wait $pid 2>/dev/null || true; exit 0; \
|
||||
fi; \
|
||||
sleep 0.2; \
|
||||
done; \
|
||||
kill $pid 2>/dev/null || true; \
|
||||
wait $pid 2>/dev/null || true; \
|
||||
cat /tmp/codespace-server.log; exit 1)
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```json
|
||||
{"status":"ok"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Codex Execution Prompt
|
||||
|
||||
Use this exact structure when dispatching Codex:
|
||||
|
||||
```text
|
||||
## Context
|
||||
- Project root: /Users/taochen/llm/codespace
|
||||
- Current branch: feat/gin-mode-config
|
||||
- Go baseline: 1.25
|
||||
- Existing backend uses Gin, explicit http.Server, slog logging, config.yaml.
|
||||
- Design spec: docs/superpowers/specs/2026-07-02-gin-mode-config-design.md
|
||||
- Implementation plan: docs/superpowers/plans/2026-07-02-gin-mode-config.md
|
||||
|
||||
## Task
|
||||
Move Gin mode from hardcoded `gin.SetMode(gin.ReleaseMode)` into config.
|
||||
Steps:
|
||||
1. Add `gin.mode` to config.yaml.
|
||||
2. Add `config.GinConfig` with default `release`.
|
||||
3. Support `CODESPACE_GIN_MODE` override.
|
||||
4. Validate allowed modes: `debug`, `release`, `test` during `config.Load`.
|
||||
5. Change `api.NewRouter` to accept `ginMode string` and call `gin.SetMode(ginMode)`.
|
||||
6. Pass `cfg.Gin.Mode` from main and `gin.TestMode` from tests.
|
||||
7. Update README and run validation.
|
||||
|
||||
## Constraints
|
||||
- Do not change API paths, methods, responses, services, middleware behavior, or logging strategy.
|
||||
- Do not add dependencies.
|
||||
- Do not use gin.Default or gin.Logger.
|
||||
- Do not add disallowed runtime logging.
|
||||
- Do not commit.
|
||||
|
||||
## Acceptance
|
||||
- gofmt -w cmd internal pkg succeeds.
|
||||
- go mod tidy succeeds.
|
||||
- go test ./... passes.
|
||||
- grep for `gin.SetMode(gin.ReleaseMode)` returns no output.
|
||||
- go run ./cmd/server starts and /healthz returns {"status":"ok"}.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: Plan covers config field, default, YAML/env load, validation, router signature, main wiring, tests, docs, and validation.
|
||||
- Placeholder scan: No TBD/TODO/ambiguous implementation steps remain.
|
||||
- Type consistency: `GinConfig`, `Config.Gin`, `cfg.Gin.Mode`, and `api.NewRouter(..., ginMode string)` are consistent.
|
||||
- Scope check: Plan is limited to Gin mode configuration. It does not change APIs, middleware behavior, services, logging strategy, or dependencies.
|
||||
@@ -0,0 +1,156 @@
|
||||
# Gin Mode Config Design
|
||||
|
||||
日期:2026-07-02
|
||||
|
||||
## 目标
|
||||
|
||||
移除 `internal/api.NewRouter` 中硬编码的:
|
||||
|
||||
```go
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
```
|
||||
|
||||
改为通过项目配置控制 Gin mode。
|
||||
|
||||
## 当前问题
|
||||
|
||||
当前 `NewRouter` 写死 release mode。这个做法有两个问题:
|
||||
|
||||
1. 开发、测试、生产不能用同一套配置机制控制 Gin mode。
|
||||
2. Gin mode 是运行配置,不应该藏在 router 代码里。
|
||||
|
||||
## 非目标
|
||||
|
||||
本次不做:
|
||||
|
||||
- 不改变 API 路径、method、响应结构。
|
||||
- 不改变 Gin middleware。
|
||||
- 不改变 slog 日志策略。
|
||||
- 不引入新依赖。
|
||||
- 不实现 auth/CORS/WebSocket/Git/LSP/Agent。
|
||||
|
||||
## 配置设计
|
||||
|
||||
`configs/config.yaml` 增加:
|
||||
|
||||
```yaml
|
||||
gin:
|
||||
mode: "release"
|
||||
```
|
||||
|
||||
`pkg/config` 增加:
|
||||
|
||||
```go
|
||||
type GinConfig struct {
|
||||
Mode string `yaml:"mode"`
|
||||
}
|
||||
```
|
||||
|
||||
`Config` 增加:
|
||||
|
||||
```go
|
||||
Gin GinConfig `yaml:"gin"`
|
||||
```
|
||||
|
||||
默认值:
|
||||
|
||||
```text
|
||||
release
|
||||
```
|
||||
|
||||
环境变量覆盖:
|
||||
|
||||
```text
|
||||
CODESPACE_GIN_MODE
|
||||
```
|
||||
|
||||
合法值:
|
||||
|
||||
- `debug`
|
||||
- `release`
|
||||
- `test`
|
||||
|
||||
非法值在 `config.Load` 阶段返回错误。不要等到 router 初始化才失败。
|
||||
|
||||
## Router 设计
|
||||
|
||||
`api.NewRouter` 签名改为:
|
||||
|
||||
```go
|
||||
func NewRouter(
|
||||
workspaces *service.WorkspaceService,
|
||||
files *service.FileService,
|
||||
processes *service.ProcessService,
|
||||
lg *slog.Logger,
|
||||
ginMode string,
|
||||
) *gin.Engine
|
||||
```
|
||||
|
||||
内部:
|
||||
|
||||
```go
|
||||
gin.SetMode(ginMode)
|
||||
r := gin.New()
|
||||
```
|
||||
|
||||
`NewRouter` 不负责校验 mode。校验属于 config 层。
|
||||
|
||||
## main.go 设计
|
||||
|
||||
```go
|
||||
router := api.NewRouter(workspaceSvc, fileSvc, processSvc, lg, cfg.Gin.Mode)
|
||||
```
|
||||
|
||||
## 测试设计
|
||||
|
||||
### config tests
|
||||
|
||||
覆盖:
|
||||
|
||||
- 默认 mode 是 `release`
|
||||
- YAML 可设置 `debug`
|
||||
- env `CODESPACE_GIN_MODE` 可覆盖为 `test`
|
||||
- 非法 mode 返回错误
|
||||
|
||||
### router tests
|
||||
|
||||
所有测试调用:
|
||||
|
||||
```go
|
||||
NewRouter(wsSvc, fileSvc, procSvc, nil, gin.TestMode)
|
||||
```
|
||||
|
||||
这样测试明确使用 Gin test mode。
|
||||
|
||||
### validation
|
||||
|
||||
必须通过:
|
||||
|
||||
```sh
|
||||
gofmt -w cmd internal pkg
|
||||
go test ./...
|
||||
grep -R "gin.SetMode(gin.ReleaseMode)" cmd internal pkg || true
|
||||
grep -R "gin\.Default\|gin\.Logger" cmd internal pkg || true
|
||||
```
|
||||
|
||||
预期:
|
||||
|
||||
- 不再出现 `gin.SetMode(gin.ReleaseMode)`
|
||||
- 不出现 `gin.Default` / `gin.Logger`
|
||||
|
||||
## 风险与处理
|
||||
|
||||
- 风险:Gin mode 是全局状态,测试互相污染。处理:router tests 统一传 `gin.TestMode`。
|
||||
- 风险:非法 mode 到运行时才失败。处理:config.Load 阶段校验。
|
||||
- 风险:把 Gin mode 塞进 `server` 配置造成语义混乱。处理:独立 `gin` 配置块。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- `configs/config.yaml` 有 `gin.mode`。
|
||||
- `config.Default()` 返回 `Gin.Mode == "release"`。
|
||||
- `CODESPACE_GIN_MODE` 可覆盖 mode。
|
||||
- 非法 mode 让 `config.Load` 返回错误。
|
||||
- `NewRouter` 不再硬编码 `gin.ReleaseMode`。
|
||||
- `main.go` 从 config 传入 Gin mode。
|
||||
- `go test ./...` 通过。
|
||||
- server smoke test 通过。
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
)
|
||||
|
||||
// NewRouter builds a Gin engine with all API routes registered.
|
||||
func NewRouter(workspaces *service.WorkspaceService, files *service.FileService, processes *service.ProcessService, lg *slog.Logger) *gin.Engine {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
func NewRouter(workspaces *service.WorkspaceService, files *service.FileService, processes *service.ProcessService, lg *slog.Logger, ginMode string) *gin.Engine {
|
||||
gin.SetMode(ginMode)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(requestLogger(lg))
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"codespace/internal/process"
|
||||
"codespace/internal/service"
|
||||
"codespace/internal/workspace"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func setupTestRouter(t *testing.T) http.Handler {
|
||||
@@ -25,7 +27,7 @@ func setupTestRouterWithMaxWriteBytes(t *testing.T, maxWriteBytes int64) http.Ha
|
||||
wsSvc := service.NewWorkspaceService(wsMgr, procMgr, nil)
|
||||
fileSvc := service.NewFileService(wsMgr, maxWriteBytes)
|
||||
procSvc := service.NewProcessService(wsMgr, procMgr, nil)
|
||||
return NewRouter(wsSvc, fileSvc, procSvc, nil)
|
||||
return NewRouter(wsSvc, fileSvc, procSvc, nil, gin.TestMode)
|
||||
}
|
||||
|
||||
func createWorkspaceForTest(t *testing.T, router http.Handler, id string) {
|
||||
|
||||
@@ -15,6 +15,7 @@ type Config struct {
|
||||
Workspace WorkspaceConfig `yaml:"workspace"`
|
||||
Process ProcessConfig `yaml:"process"`
|
||||
File FileConfig `yaml:"file"`
|
||||
Gin GinConfig `yaml:"gin"`
|
||||
Log LogConfig `yaml:"log"`
|
||||
}
|
||||
|
||||
@@ -42,6 +43,11 @@ type FileConfig struct {
|
||||
MaxWriteBytes int64 `yaml:"maxWriteBytes"`
|
||||
}
|
||||
|
||||
// GinConfig holds Gin framework runtime settings.
|
||||
type GinConfig struct {
|
||||
Mode string `yaml:"mode"`
|
||||
}
|
||||
|
||||
// LogConfig holds structured logging settings.
|
||||
type LogConfig struct {
|
||||
Level string `yaml:"level"`
|
||||
@@ -55,6 +61,7 @@ type rawConfig struct {
|
||||
Workspace WorkspaceConfig `yaml:"workspace"`
|
||||
Process ProcessConfig `yaml:"process"`
|
||||
File FileConfig `yaml:"file"`
|
||||
Gin GinConfig `yaml:"gin"`
|
||||
Log LogConfig `yaml:"log"`
|
||||
}
|
||||
|
||||
@@ -79,6 +86,7 @@ func Default() Config {
|
||||
Workspace: WorkspaceConfig{Root: "./workspaces"},
|
||||
Process: ProcessConfig{OpenCodeCommand: "opencode"},
|
||||
File: FileConfig{MaxWriteBytes: 1 << 20},
|
||||
Gin: GinConfig{Mode: "release"},
|
||||
Log: LogConfig{Level: "info", Format: "json"},
|
||||
}
|
||||
}
|
||||
@@ -104,6 +112,9 @@ func Load(path string) (Config, error) {
|
||||
if err := applyEnv(&cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := validateGinMode(cfg.Gin.Mode); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -132,6 +143,9 @@ func applyRaw(cfg *Config, raw rawConfig) error {
|
||||
if raw.File.MaxWriteBytes != 0 {
|
||||
cfg.File.MaxWriteBytes = raw.File.MaxWriteBytes
|
||||
}
|
||||
if raw.Gin.Mode != "" {
|
||||
cfg.Gin.Mode = raw.Gin.Mode
|
||||
}
|
||||
if raw.Log.Level != "" {
|
||||
cfg.Log.Level = raw.Log.Level
|
||||
}
|
||||
@@ -192,6 +206,9 @@ func applyEnv(cfg *Config) error {
|
||||
}
|
||||
cfg.File.MaxWriteBytes = n
|
||||
}
|
||||
if v := os.Getenv("CODESPACE_GIN_MODE"); v != "" {
|
||||
cfg.Gin.Mode = v
|
||||
}
|
||||
if v := os.Getenv("CODESPACE_LOG_LEVEL"); v != "" {
|
||||
cfg.Log.Level = v
|
||||
}
|
||||
@@ -200,3 +217,12 @@ func applyEnv(cfg *Config) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateGinMode(mode string) error {
|
||||
switch mode {
|
||||
case "debug", "release", "test":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid gin mode %q: must be one of debug, release, test", mode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,3 +179,54 @@ func TestLoadAppliesFileEnvironmentOverride(t *testing.T) {
|
||||
t.Fatalf("got %d", cfg.File.MaxWriteBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultIncludesGinConfig(t *testing.T) {
|
||||
cfg := Default()
|
||||
if cfg.Gin.Mode != "release" {
|
||||
t.Fatalf("Gin.Mode = %q, want release", cfg.Gin.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadParsesGinConfigFromYAML(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
data := []byte(`gin:
|
||||
mode: "debug"
|
||||
`)
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
if cfg.Gin.Mode != "debug" {
|
||||
t.Fatalf("Gin.Mode = %q, want debug", cfg.Gin.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAppliesGinModeEnvironmentOverride(t *testing.T) {
|
||||
t.Setenv("CODESPACE_GIN_MODE", "test")
|
||||
cfg, err := Load(filepath.Join(t.TempDir(), "missing.yaml"))
|
||||
if err != nil {
|
||||
t.Fatalf("Load returned error: %v", err)
|
||||
}
|
||||
if cfg.Gin.Mode != "test" {
|
||||
t.Fatalf("Gin.Mode = %q, want test", cfg.Gin.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidGinMode(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
data := []byte(`gin:
|
||||
mode: "verbose"
|
||||
`)
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := Load(path); err == nil {
|
||||
t.Fatal("expected invalid gin mode error")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user