package cmd import ( "context" "os" "testing" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcron" "github.com/gogf/gf/v2/os/gfile" "tool-api/internal/logic" ) // ============================================================================ // 启动期(cmd.Main.Func)两条易静默失效/直接崩的路径,各自固化一个用例。 // // 背景:这两条都是「本地跑起来才发现」的问题,都不会被业务单测覆盖 —— // ① cron pattern 写错 → AddSingleton 返回 error 被 WARNING 吞掉,任务静默不注册; // ② 静态目录不存在 → AddStaticPath 调 Logger().Fatalf → os.Exit(1),进程直接死。 // ============================================================================ // TestCronPatternsAreValid 固化启动期两个定时任务必须注册成功。 // // GoFrame gcron 的 pattern 是 **6 段**(秒 分 时 日 月 周),不是标准 crontab 的 5 段。 // 本项目曾把「笔记提醒扫描」写成 5 段 "*/1 * * * *": // AddSingleton 返回 error → 被启动日志里的 WARNING 吞掉 → 服务照常启动、无人察觉, // 但该任务从未注册、笔记定时提醒功能完全失效。 func TestCronPatternsAreValid(t *testing.T) { patterns := map[string]string{ "member-order-sweep": cronPatternSweepOrders, "note-remind-scan": cronPatternScanRemind, "coupon-lock-release": cronPatternReleaseCoupons, "order-close-pending": cronPatternCloseOrders, } for name, pattern := range patterns { name, pattern := name, pattern t.Run(name, func(t *testing.T) { if _, err := gcron.AddSingleton( context.Background(), pattern, func(ctx context.Context) {}, name, ); err != nil { t.Fatalf("cron 任务 %s 注册失败,pattern=%q:%v\n"+ "(gcron 需要 6 段「秒 分 时 日 月 周」;标准 5 段需前置 '#')", name, pattern, err) } gcron.Remove(name) }) } } // TestFivePartCronPatternIsRejected 反证上面的用例不是空转: // 标准 5 段 crontab 写法确实会被 gcron 拒绝。 // 若哪天这条断言失效(gcron 升级后支持了 5 段),说明本文件的假设已过期,需重新核对。 func TestFivePartCronPatternIsRejected(t *testing.T) { const bad = "*/1 * * * *" if _, err := gcron.AddSingleton( context.Background(), bad, func(ctx context.Context) {}, "probe-bad-5part", ); err == nil { t.Fatalf("5 段 pattern %q 竟被 gcron 接受,本文件假设已失效,请重新核对 gcron 行为", bad) } gcron.Remove("probe-bad-5part") } // TestQrCodeStaticPathBootOrder 固化「静态目录必须先存在」这一启动约束。 // // 机理(已读 gf 源码确认,net/ghttp/ghttp_server_config_static.go:93): // // if p, err := gfile.Search(path); err != nil { // s.Logger().Fatalf(ctx, `AddStaticPath failed: %+v`, err) // } // // 是 Fatalf → os.Exit(1),**不是 panic、无法 recover**。 // 而 manifest/qrcode 原先只在「生成小程序码」时才惰性创建 → // 全新环境首次启动必然崩在注册静态路由这一步(先有鸡先有蛋的死锁)。 // 修法:cmd.go 先 gfile.Mkdir(logic.QrCodeDir) 再 AddStaticPath。 func TestQrCodeStaticPathBootOrder(t *testing.T) { chdirRepoRoot(t) // ① 目录不存在时 gfile.Search 会报错(这就是上面 Fatalf 的触发条件)。 // 用 Search 探测而不真的调 AddStaticPath —— 后者会直接 os.Exit(1) 杀掉测试进程。 t.Run("missing_dir_makes_gf_search_fail", func(t *testing.T) { if _, err := gfile.Search("manifest/__definitely_absent__"); err == nil { t.Error("gfile.Search 对不存在的目录竟返回成功,本用例假设已失效(gf 行为可能已变更)") } }) // ② 预创建目录后,AddStaticPath 可以正常注册(即 cmd.go 的修复路径)。 // AddStaticPath 无返回值,能走到下一行即为通过。 t.Run("precreated_dir_registers_ok", func(t *testing.T) { if err := gfile.Mkdir(logic.QrCodeDir); err != nil { t.Fatalf("gfile.Mkdir(%q) 失败:%v", logic.QrCodeDir, err) } if !gfile.Exists(logic.QrCodeDir) { t.Fatalf("Mkdir 之后目录仍不存在:%q", logic.QrCodeDir) } g.Server("static-path-probe").AddStaticPath("/qrcode", logic.QrCodeDir) }) } // chdirRepoRoot 把工作目录切到仓库根目录。 // go test 的 CWD 是「包目录」(internal/cmd),直接用相对路径会跑偏到 internal/cmd/manifest/qrcode, // 与被测代码在真实运行时的 CWD(仓库根)不一致。 func chdirRepoRoot(t *testing.T) { t.Helper() old, err := os.Getwd() if err != nil { t.Fatalf("获取当前工作目录失败:%v", err) } t.Cleanup(func() { _ = os.Chdir(old) }) dir := old for i := 0; i < 8; i++ { if gfile.Exists(gfile.Join(dir, "go.mod")) { if err = os.Chdir(dir); err != nil { t.Fatalf("切换到仓库根目录 %s 失败:%v", dir, err) } return } dir = gfile.Dir(dir) } t.Fatal("向上 8 层未找到 go.mod,无法定位仓库根目录") }