diff --git a/.gitignore b/.gitignore index 456778e..554f315 100644 --- a/.gitignore +++ b/.gitignore @@ -11,16 +11,12 @@ bin/ .qwen/skills/ # Local certs (generated) -config/certs/*.pem -config/certs/*.zip +configs/certs/*.pem +configs/certs/*.zip # Local config (may contain secrets) -config/config.yaml -config/local/ -config/bvt/ -config/test/ -config/prod/ -config/integration/ +# NOTE: environment configs are now tracked; only config.yaml (potential secrets) is ignored +configs/config.yaml # Dependencies vendor/ diff --git a/README.md b/README.md index b12384e..8d90af7 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,12 @@ kratos-develop-template/ │ ├── middleware/ # 中间件 │ └── conf/ # 配置定义 (protobuf 生成) │ └── conf.proto # 配置结构体定义 -├── config/ -│ └── config.yaml # 运行时配置 +├── configs/ # 多环境配置 +│ ├── config.yaml # 运行时配置(不提交,可能含密钥) +│ ├── config.example.yaml # 示例配置 +│ ├── local/ # 本地开发 +│ ├── prod/ # 生产环境 +│ └── integration/ # 集成测试 ├── third_party/ # 第三方 proto 依赖 ├── go.mod ├── go.sum @@ -94,12 +98,32 @@ go build -o bin/server ./cmd/server/ # Linux / macOS make generate && make wire && make build -# 运行(默认 MySQL/Redis/Kafka/RabbitMQ 均为 enabled:false,无需依赖即可启动) -./bin/server -conf=./config +# 使用 kratos 工具运行(推荐,自动处理工作目录和热重载) +kratos run -- -conf ./configs -e local +kratos run -- -conf ./configs -e prod + +# 直接运行二进制 +./bin/server -conf=./configs + +# 指定环境配置(-e 或 --env,加载 configs/{env}/config.yaml) +./bin/server -e local +./bin/server -e prod # CLI 定时任务 go build -o bin/task ./cmd/task/ -./bin/task savePageDataCron -conf=./config + +# 交互命令(cobra CLI,支持 -conf/-c 指定配置目录,-e 指定环境) +./bin/task help # 查看帮助 +./bin/task # 默认执行 savePageData +./bin/task savePageData # 爬数据入库(单次) +./bin/task savePageData -e local # 使用 local 环境配置 +./bin/task savePageDataCron # 爬数据入库(单次运行) +./bin/task savePageDataCron --daemon # 按 cron 循环运行 +./bin/task savePageDataCron -e prod --daemon # 生产环境循环爬取 + +# 使用 kratos run 运行 task +kratos run -- -conf ./configs -e local savePageData +kratos run -- -conf ./configs -e prod savePageDataCron --daemon ``` 默认 HTTP 端口 `8989`(与原 Gin 项目一致),Hello 接口:`GET http://127.0.0.1:8989/test` @@ -108,7 +132,7 @@ go build -o bin/task ./cmd/task/ ```bash docker compose up -d -cp config/config.example.yaml config/config.yaml # 按需修改 enabled: true +cp configs/config.example.yaml configs/config.yaml # 按需修改 enabled: true ``` ### 6. OpenAPI @@ -140,7 +164,7 @@ $env:INTEGRATION_LEAVE_UP = "1" powershell -File scripts/integration-test.ps1 ``` -集成测试使用独立配置目录 `config/integration/`(`kafka` / `rabbitmq` 均为 `enabled: true`),构建标签为 `integration`,日常 `go test ./...` 不会执行。 +集成测试使用独立配置目录 `configs/integration/`(`kafka` / `rabbitmq` 均为 `enabled: true`),构建标签为 `integration`,日常 `go test ./...` 不会执行。 ### 8. 部署 @@ -158,7 +182,29 @@ docker run -p 8000:8000 -p 9000:9000 kratos-develop-template:latest ## 配置说明 -编辑 `config/config.yaml`: +### 环境配置 + +项目支持多环境配置,通过 `-e` 或 `--env` 指定: + +| 环境 | 配置目录 | 说明 | +|------|----------|------| +| `local` | `configs/local/` | 本地开发(默认) | +| `bvt` | `configs/bvt/` | 构建验证测试 | +| `test` | `configs/test/` | 测试环境 | +| `prod` | `configs/prod/` | 生产环境 | +| `integration` | `configs/integration/` | 集成测试(kafka/rabbitmq enabled) | + +```bash +# 加载指定环境配置 +./bin/server -e local +./bin/server -e prod +./bin/task -e local savePageData +./bin/task -c ./configs -e test savePageDataCron --daemon +``` + +### 配置项示例 + +编辑 `configs/config.yaml` 或对应环境的配置: ```yaml server: diff --git a/cmd/server/main.go b/cmd/server/main.go index 2c3e6bd..05c81eb 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -33,8 +33,8 @@ var ( ) func init() { - flag.StringVar(&Flagconfig, "conf", "./config", "config directory, eg: -conf ./config") - flag.StringVar(&Flagenv, "e", "", "env shorthand: local, prod, integration ??config/{env}") + flag.StringVar(&Flagconfig, "conf", "./configs", "config directory, eg: -conf ./configs") + flag.StringVar(&Flagenv, "e", "", "env shorthand: local, prod, integration → config/{env}") } func newApp(logger log.Logger, hs *http.Server, gs *grpc.Server) *kratos.App { diff --git a/cmd/task/main.go b/cmd/task/main.go index dc3c3a8..6d8441f 100644 --- a/cmd/task/main.go +++ b/cmd/task/main.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "os" @@ -14,46 +13,83 @@ import ( "github.com/go-kratos/kratos/v2/log" ) -var ( - flagconf string - flagenv string -) - func init() { - flag.StringVar(&flagconf, "conf", "./config", "config directory, eg: -conf ./config") - flag.StringVar(&flagenv, "e", "", "env shorthand (Gin parity): local, prod, integration ??config/{env}") + // Register flags on rootCmd so cobra recognizes them + rootCmd.PersistentFlags().StringVarP(&flagconf, "conf", "c", "./configs", "config directory, eg: -conf ./configs") + rootCmd.PersistentFlags().StringVarP(&flagenv, "e", "e", "", "env shorthand (Gin parity): local, prod, integration → config/{env}") rootCmd.PersistentFlags().StringVar(&flagenv, "env", "", "alias for -e") + + // Register subcommands + rootCmd.AddCommand(newSavePageDataCmd()) + rootCmd.AddCommand(newSavePageDataCronCmd()) } func main() { - flag.Parse() + // Normalize go-style flags to cobra format before cobra parses. + // Cobra treats -conf as -c + "onfig", so we convert -conf → --conf upfront. + args := make([]string, 0, len(os.Args)-1) + for i := 1; i < len(os.Args); i++ { + arg := os.Args[i] + if arg == "-conf" { + arg = "--conf" + } + args = append(args, arg) + } + + // If no subcommand provided, default to savePageData + hasSubcommand := false + for _, arg := range args { + if arg == "savePageData" || arg == "savePageDataCron" || arg == "completion" || arg == "help" { + hasSubcommand = true + break + } + } + if !hasSubcommand { + args = append(args, "savePageData") + } + + os.Args = append([]string{os.Args[0]}, args...) + + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +// loadBootstrap loads config and returns bootstrap + cleanup function. +// Subcommands should call this in their RunE. +func loadBootstrap() (*conf.Bootstrap, func(), error) { confDir := confpath.Resolve(flagconf, flagenv) if flagenv != "" { - fmt.Printf("using config env %q ??%s\n", flagenv, confDir) + fmt.Printf("using config env %q → %s\n", flagenv, confDir) } logger := log.NewStdLogger(os.Stdout) c := config.New(config.WithSource(file.NewSource(confDir))) - defer c.Close() if err := c.Load(); err != nil { - panic(err) + c.Close() + return nil, nil, err } var bc conf.Bootstrap if err := c.Scan(&bc); err != nil { - panic(err) + c.Close() + return nil, nil, err } cleanup, err := initTaskApp(bc.GetData(), logger) if err != nil { - panic(err) + c.Close() + return nil, nil, err } - defer cleanup() - - crawlerUC := biz.NewCrawlerUsecase(bc.GetApp()) - rootCmd.AddCommand(newSavePageDataCmd(crawlerUC)) - rootCmd.AddCommand(newSavePageDataCronCmd(crawlerUC)) - if err := rootCmd.Execute(); err != nil { - os.Exit(1) + fullCleanup := func() { + cleanup() + c.Close() } + + return &bc, fullCleanup, nil +} + +// createCrawlerUsecase is called by subcommands after loading config. +func createCrawlerUsecase(bc *conf.Bootstrap) *biz.CrawlerUsecase { + return biz.NewCrawlerUsecase(bc.GetApp()) } diff --git a/cmd/task/output/outputGetAllTagsData.dat b/cmd/task/output/outputGetAllTagsData.dat new file mode 100644 index 0000000..7781def --- /dev/null +++ b/cmd/task/output/outputGetAllTagsData.dat @@ -0,0 +1 @@ +complete the task GetAllTagsData at 2026-06-04T23:52:58+08:00 diff --git a/cmd/task/root.go b/cmd/task/root.go index d775674..f7f4e80 100644 --- a/cmd/task/root.go +++ b/cmd/task/root.go @@ -1,11 +1,14 @@ package main -import ( - "github.com/spf13/cobra" -) +import "github.com/spf13/cobra" + +var ( + flagconf string + flagenv string -var rootCmd = &cobra.Command{ - Use: "kratos-task", - Short: "Kratos develop template CLI tasks", - Long: "Scheduled / batch tasks migrated from gin-develop-template.", -} + rootCmd = &cobra.Command{ + Use: "kratos-task", + Short: "Kratos develop template CLI tasks", + Long: "Scheduled / batch tasks migrated from gin-develop-template.", + } +) diff --git a/cmd/task/save_page_data.go b/cmd/task/save_page_data.go index a173667..adb399e 100644 --- a/cmd/task/save_page_data.go +++ b/cmd/task/save_page_data.go @@ -1,17 +1,22 @@ package main import ( - "github.com/JasonMetal/kratos-develop-template/internal/biz" - "github.com/spf13/cobra" ) -func newSavePageDataCmd(uc *biz.CrawlerUsecase) *cobra.Command { +func newSavePageDataCmd() *cobra.Command { return &cobra.Command{ Use: "savePageData", Short: "爬数据入库(单次 GetAllTagsData)", Long: "与 gin-develop-template 的 savePageData 一致:只跑 GetAllTagsData,不执行 shell/cron", RunE: func(cmd *cobra.Command, args []string) error { + bc, cleanup, err := loadBootstrap() + if err != nil { + return err + } + defer cleanup() + + uc := createCrawlerUsecase(bc) return uc.RunSavePageData(cmd.Context()) }, } diff --git a/cmd/task/save_page_data_cron.go b/cmd/task/save_page_data_cron.go index ef07706..9fe9c28 100644 --- a/cmd/task/save_page_data_cron.go +++ b/cmd/task/save_page_data_cron.go @@ -6,18 +6,23 @@ import ( "os/signal" "syscall" - "github.com/JasonMetal/kratos-develop-template/internal/biz" - "github.com/spf13/cobra" ) -func newSavePageDataCronCmd(uc *biz.CrawlerUsecase) *cobra.Command { +func newSavePageDataCronCmd() *cobra.Command { var daemon bool cmd := &cobra.Command{ Use: "savePageDataCron", Short: "爬数据入库", Long: "抓取并入库(单次运行)。--daemon 与 gin crawlerLogic 一样按 cron 循环", RunE: func(cmd *cobra.Command, args []string) error { + bc, cleanup, err := loadBootstrap() + if err != nil { + return err + } + defer cleanup() + + uc := createCrawlerUsecase(bc) if daemon { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() diff --git a/configs/bvt/config.yaml b/configs/bvt/config.yaml new file mode 100644 index 0000000..e1e903a --- /dev/null +++ b/configs/bvt/config.yaml @@ -0,0 +1,52 @@ +# BVT env (Gin: -e bvt → config/bvt/) +# Run: go run ./cmd/server/ -e bvt + +server: + http: + addr: 0.0.0.0:8989 + timeout: 15s + grpc: + addr: 0.0.0.0:50052 + timeout: 15s +data: + database: + enabled: false + driver: mysql + source: root:root@tcp(127.0.0.1:3306)/gin_db?charset=utf8&parseTime=True&loc=Local&timeout=10s + max_idle_conns: 10 + max_open_conns: 100 + conn_max_lifetime: 3600s + redis: + enabled: false + addr: 127.0.0.1:6379 + password: "" + db: 0 + dial_timeout_seconds: 5 + read_timeout_seconds: 3 + write_timeout_seconds: 3 + pool_size: 50 + min_idle_conns: 10 + kafka: + enabled: false + brokers: + - 127.0.0.1:9092 + rabbitmq: + enabled: false + url: amqp://guest:guest@127.0.0.1:5672/ + exchange: bvt_exchange + exchange_type: direct + routing_key: bvt_routing_key + durable: true +app: + auth: + domain_mark: "" + auth_token: "foobar386cb6d14ab7b5f6738f7fc1704cto" + private_key_path: config/certs/privateKey.pem + public_key_path: config/certs/publicKey.pem + rsa: + certs_dir: config/certs + key_bits: 2048 + crawler: + shell_script: scripts/crawler.sh + output_dir: output + api_host: http://127.0.0.1:8888 diff --git a/config/config.example.yaml b/configs/config.example.yaml similarity index 100% rename from config/config.example.yaml rename to configs/config.example.yaml diff --git a/configs/integration/config.yaml b/configs/integration/config.yaml new file mode 100644 index 0000000..e16ec8c --- /dev/null +++ b/configs/integration/config.yaml @@ -0,0 +1,34 @@ +# Integration test profile — isolated from config/config.yaml +# go test -tags=integration ./test/integration/... +server: + http: + addr: 0.0.0.0:8989 + timeout: 30s + grpc: + addr: 0.0.0.0:50052 + timeout: 30s +data: + database: + enabled: false + redis: + enabled: false + kafka: + enabled: true + brokers: + - 127.0.0.1:9092 + rabbitmq: + enabled: true + url: amqp://guest:guest@127.0.0.1:5672/ + exchange: kratos_test_exchange + exchange_type: direct + routing_key: kratos_test_routing + durable: true +app: + auth: + domain_mark: "" + auth_token: "integration-test-token" + private_key_path: config/certs/privateKey.pem + public_key_path: config/certs/publicKey.pem + rsa: + certs_dir: config/certs + key_bits: 2048 diff --git a/configs/local/config.yaml b/configs/local/config.yaml new file mode 100644 index 0000000..faf54d7 --- /dev/null +++ b/configs/local/config.yaml @@ -0,0 +1,56 @@ +# Local dev (Gin: -e local → config/local/) +# Run: go run ./cmd/server/ -e local +# go run ./cmd/task/ -e local savePageData + +server: + http: + addr: 0.0.0.0:8989 + timeout: 10s + grpc: + addr: 0.0.0.0:50052 + timeout: 10s +data: + database: + enabled: false + driver: mysql + source: root:root@tcp(127.0.0.1:3306)/gin_db?charset=utf8&parseTime=True&loc=Local&timeout=10s + max_idle_conns: 10 + max_open_conns: 100 + conn_max_lifetime: 3600s + redis: + enabled: false + addr: 127.0.0.1:6379 + password: "" + db: 0 + dial_timeout_seconds: 5 + read_timeout_seconds: 3 + write_timeout_seconds: 3 + pool_size: 50 + min_idle_conns: 10 + kafka: + enabled: false + brokers: + - 127.0.0.1:9092 + username: "" + password: "" + algorithm: "" + rabbitmq: + enabled: false + url: amqp://guest:guest@127.0.0.1:5672/ + exchange: my_exchange + exchange_type: direct + routing_key: my_routing_key + durable: true +app: + auth: + domain_mark: "" + auth_token: "foobar386cb6d14ab7b5f6738f7fc1704cto" + private_key_path: config/certs/privateKey.pem + public_key_path: config/certs/publicKey.pem + rsa: + certs_dir: config/certs + key_bits: 2048 + crawler: + shell_script: scripts/crawler.sh + output_dir: output + api_host: http://127.0.0.1:8888 diff --git a/configs/prod/config.yaml b/configs/prod/config.yaml new file mode 100644 index 0000000..fb68fcf --- /dev/null +++ b/configs/prod/config.yaml @@ -0,0 +1,55 @@ +# Production example (Gin: -e prod → config/prod/) +# Run: go run ./cmd/server/ -e prod + +server: + http: + addr: 0.0.0.0:8989 + timeout: 30s + grpc: + addr: 0.0.0.0:50052 + timeout: 30s +data: + database: + enabled: true + driver: mysql + source: user:pass@tcp(mysql:3306)/app_db?charset=utf8mb4&parseTime=True&loc=Local + max_idle_conns: 10 + max_open_conns: 100 + conn_max_lifetime: 3600s + redis: + enabled: true + addr: redis:6379 + password: "" + db: 0 + dial_timeout_seconds: 5 + read_timeout_seconds: 3 + write_timeout_seconds: 3 + pool_size: 50 + min_idle_conns: 10 + kafka: + enabled: true + brokers: + - kafka:9092 + username: "" + password: "" + algorithm: "" + rabbitmq: + enabled: true + url: amqp://guest:guest@rabbitmq:5672/ + exchange: my_exchange + exchange_type: direct + routing_key: my_routing_key + durable: true +app: + auth: + domain_mark: "" + auth_token: "change-me-in-production" + private_key_path: config/certs/privateKey.pem + public_key_path: config/certs/publicKey.pem + rsa: + certs_dir: config/certs + key_bits: 2048 + crawler: + shell_script: scripts/crawler.sh + output_dir: /var/data/crawler + api_host: http://127.0.0.1:8888 diff --git a/configs/test/config.yaml b/configs/test/config.yaml new file mode 100644 index 0000000..37f8e5e --- /dev/null +++ b/configs/test/config.yaml @@ -0,0 +1,55 @@ +# Test env (Gin: -e test → config/test/) +# Run: go run ./cmd/server/ -e test + +server: + http: + addr: 0.0.0.0:8989 + timeout: 10s + grpc: + addr: 0.0.0.0:50052 + timeout: 10s +data: + database: + enabled: false + driver: mysql + source: root:root@tcp(127.0.0.1:3306)/gin_db?charset=utf8&parseTime=True&loc=Local&timeout=10s + max_idle_conns: 10 + max_open_conns: 100 + conn_max_lifetime: 3600s + redis: + enabled: false + addr: 127.0.0.1:6379 + password: "" + db: 0 + dial_timeout_seconds: 5 + read_timeout_seconds: 3 + write_timeout_seconds: 3 + pool_size: 50 + min_idle_conns: 10 + kafka: + enabled: false + brokers: + - 127.0.0.1:9092 + username: "" + password: "" + algorithm: "" + rabbitmq: + enabled: false + url: amqp://guest:guest@127.0.0.1:5672/ + exchange: my_exchange + exchange_type: direct + routing_key: my_routing_key + durable: true +app: + auth: + domain_mark: "" + auth_token: "foobar386cb6d14ab7b5f6738f7fc1704cto" + private_key_path: config/certs/privateKey.pem + public_key_path: config/certs/publicKey.pem + rsa: + certs_dir: config/certs + key_bits: 2048 + crawler: + shell_script: scripts/crawler.sh + output_dir: output + api_host: http://127.0.0.1:8888 diff --git a/internal/pkg/confpath/confpath.go b/internal/pkg/confpath/confpath.go index eecdeb4..5d4f891 100644 --- a/internal/pkg/confpath/confpath.go +++ b/internal/pkg/confpath/confpath.go @@ -1,14 +1,115 @@ package confpath -import "path/filepath" +import ( + "os" + "path/filepath" + "runtime" + "strings" +) // Resolve picks the config directory (Gin -e local|prod parity via -conf or -e). +// Always returns an absolute path — safe for kratos run which changes working directory. func Resolve(confDir, env string) string { + // Normalize legacy "config" → "configs" + if confDir != "" { + confDir = normalizeConfigDirName(confDir) + } + if env != "" { - return filepath.Join("config", env) + // If confDir was provided, join env with it + if confDir != "" { + return makeAbsolute(filepath.Join(confDir, env)) + } + // Default: configs/{env} relative to project root + return makeAbsolute(filepath.Join("configs", env)) } if confDir == "" { - return "./config" + return makeAbsolute("./configs") + } + return makeAbsolute(confDir) +} + +// normalizeConfigDirName replaces legacy "config" with "configs". +func normalizeConfigDirName(dir string) string { + base := filepath.Base(dir) + if strings.EqualFold(base, "config") { + return filepath.Join(filepath.Dir(dir), "configs") + } + return dir +} + +// makeAbsolute converts a relative path to absolute by finding the project root (go.mod). +func makeAbsolute(path string) string { + if filepath.IsAbs(path) { + return path + } + // Try cwd first + if _, err := os.Stat(path); err == nil { + abs, err := filepath.Abs(path) + if err == nil { + return abs + } + } + // Fallback: find project root by searching for go.mod + root := findProjectRoot() + if root != "" { + candidate := filepath.Join(root, path) + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + // Try from the source file location (this package is in internal/pkg/confpath) + srcRoot := findProjectRootFromSource() + if srcRoot != "" { + candidate := filepath.Join(srcRoot, path) + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + // Last resort: just make it absolute from cwd + abs, _ := filepath.Abs(path) + return abs +} + +// findProjectRoot searches upward for go.mod (project root). +func findProjectRoot() string { + dir, err := os.Getwd() + if err != nil { + return "" + } + return searchUpForGoMod(dir) +} + +// searchUpForGoMod walks up the directory tree looking for go.mod. +func searchUpForGoMod(dir string) string { + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir || strings.HasSuffix(dir, string(filepath.Separator)) { + break + } + dir = parent + } + return "" +} + +// findProjectRootFromSource uses the location of this source file to find project root. +// This works even when the working directory is completely different. +func findProjectRootFromSource() string { + _, filename, _, ok := runtime.Caller(0) + if !ok { + return "" + } + // This file is at internal/pkg/confpath/confpath.go + // Go up 4 levels to project root + dir := filepath.Dir(filename) // internal/pkg/confpath + dir = filepath.Dir(dir) // internal/pkg + dir = filepath.Dir(dir) // internal + dir = filepath.Dir(dir) // project root + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir } - return confDir + return "" } diff --git a/internal/pkg/confpath/confpath_test.go b/internal/pkg/confpath/confpath_test.go index 7f02fc0..baabcad 100644 --- a/internal/pkg/confpath/confpath_test.go +++ b/internal/pkg/confpath/confpath_test.go @@ -1,16 +1,76 @@ package confpath import ( + "os" "path/filepath" + "strings" "testing" ) func TestResolve(t *testing.T) { - want := filepath.Join("config", "local") - if got := Resolve("./config", "local"); got != want { + // Resolve now returns absolute paths + // Find project root (where go.mod is) + projectRoot := findProjectRoot() + if projectRoot == "" { + t.Fatal("could not find project root") + } + + want := filepath.Join(projectRoot, "configs", "local") + if got := Resolve("./configs", "local"); got != want { + t.Fatalf("got %q want %q", got, want) + } + + // Absolute path should remain absolute + got := Resolve("", "") + if !filepath.IsAbs(got) { + t.Fatalf("expected absolute path, got %q", got) + } + + // Empty inputs should return absolute configs path + wantConfigs := filepath.Join(projectRoot, "configs") + if got := Resolve("", ""); got != wantConfigs { + t.Fatalf("got %q want %q", got, wantConfigs) + } +} + +func TestResolveEnv(t *testing.T) { + projectRoot := findProjectRoot() + if projectRoot == "" { + t.Fatal("could not find project root") + } + + // Test env-based resolution + want := filepath.Join(projectRoot, "configs", "prod") + if got := Resolve("", "prod"); got != want { t.Fatalf("got %q want %q", got, want) } - if got := Resolve("/etc/app", ""); got != "/etc/app" { - t.Fatalf("got %q", got) +} + +func TestMakeAbsolute(t *testing.T) { + // Already absolute paths should be unchanged + absPath := "D:\\etc\\app" + if got := makeAbsolute(absPath); got != absPath { + t.Fatalf("got %q want %s", got, absPath) + } + + // Relative paths should become absolute + got := makeAbsolute("./configs") + if !filepath.IsAbs(got) { + t.Fatalf("expected absolute path, got %q", got) + } + if !strings.HasSuffix(got, "configs") { + t.Fatalf("expected path to end with 'configs', got %q", got) + } +} + +func TestFindProjectRoot(t *testing.T) { + root := findProjectRoot() + if root == "" { + t.Fatal("expected to find project root") + } + + // Verify go.mod exists in returned directory + if _, err := os.Stat(filepath.Join(root, "go.mod")); err != nil { + t.Fatalf("go.mod not found in %s", root) } } diff --git a/internal/server/ping_http.go b/internal/server/ping_http.go index adf487d..32f456c 100644 --- a/internal/server/ping_http.go +++ b/internal/server/ping_http.go @@ -13,6 +13,6 @@ func RegisterPing(srv *khttp.Server) { w := ctx.Response() w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) - return json.NewEncoder(w).Encode("Health check for Gin framework:init pong") + return json.NewEncoder(w).Encode("Health check for Kratos framework: pong") }) } diff --git a/output/outputGetAllTagsData.dat b/output/outputGetAllTagsData.dat new file mode 100644 index 0000000..84bd0c4 --- /dev/null +++ b/output/outputGetAllTagsData.dat @@ -0,0 +1 @@ +complete GetAllTagsData at 2026-06-04T23:53:26+08:00 diff --git a/test/e2e/ping_test.go b/test/e2e/ping_test.go index 522b7f1..24ffe6a 100644 --- a/test/e2e/ping_test.go +++ b/test/e2e/ping_test.go @@ -33,7 +33,7 @@ func TestPingRoute(t *testing.T) { if err := json.Unmarshal(body, &s); err != nil { t.Fatalf("body %q: %v", body, err) } - want := "Health check for Gin framework:init pong" + want := "Health check for Kratos framework: pong" if s != want { t.Fatalf("got %q want %q", s, want) }