package web import ( "context" "os" "regexp" "strings" "testing" ) // TestAggHelpersPropagateErrors 是 D1 的行为守卫:四个「重算聚合 / 写标签」的辅助函数 // 必须把数据库错误返回给调用方。 // // 它们原先的签名是 `(isHome, isWork bool)` / 无返回值,Scan 的错误被直接丢掉—— // 于是「查询炸了」和「查到了 false/false」在调用方看来一模一样:到访会被写得没有归属活动, // 计数会静默地停在旧值上。用一个已取消的 context 就能逼出这一层错误。 func TestAggHelpersPropagateErrors(t *testing.T) { f := newMovementFixture(t) // 已取消的 context:任何一次 Query/Exec 都会立刻以 context.Canceled 返回。 ctx, cancel := context.WithCancel(f.ctx) cancel() if err := f.srv.refreshDatasetAgg(ctx, f.pg, f.dsID); err == nil { t.Error("refreshDatasetAgg 吞掉了错误:ctx 已取消却返回 nil") } if err := f.srv.refreshPlaceAgg(ctx, f.pg, 1); err == nil { t.Error("refreshPlaceAgg 吞掉了错误:ctx 已取消却返回 nil") } if err := f.srv.saveVisitTags(ctx, f.pg, f.dsID, 1, nil); err == nil { t.Error("saveVisitTags 吞掉了错误:ctx 已取消却返回 nil") } if _, _, err := f.srv.activityFlags(ctx, f.pg, 1); err == nil { t.Error("activityFlags 吞掉了错误:ctx 已取消却返回 nil") } } // TestNoAggCallSiteSwallowsError 是 D1 的静态守卫:这三个辅助函数「返回 error」只有在 // 每个调用点都接住它时才有意义。裸调用(语句形式、丢掉返回值)会随第一个新加的调用点回归, // 而且编译器不会拦——这里按行扫描源码,把裸调用钉死。 func TestNoAggCallSiteSwallowsError(t *testing.T) { // 只匹配「整行就是一次调用」的裸语句,合法写法前面一定还有 `if err := `、`return `、 // `logAggFailure(` 等前缀,因此不会被误伤。 bare := regexp.MustCompile(`^\s*s\.(refreshDatasetAgg|refreshPlaceAgg|saveVisitTags)\(`) for _, name := range []string{ "handlers_edit.go", "handlers_photos.go", "fillup.go", } { path := pkgFile(t, name) data, err := os.ReadFile(path) if err != nil { t.Fatalf("读 %s 失败: %v", path, err) } for i, line := range strings.Split(string(data), "\n") { if bare.MatchString(line) { t.Errorf("%s:%d 裸调用吞掉了 error:%s", name, i+1, strings.TrimSpace(line)) } } } } // TestAggHelpersHaveErrorReturn 钉住签名本身:如果谁把它们改回无返回值, // 上面两条用例会失去意义(编译期就不会再有 error 可断言)。 func TestAggHelpersHaveErrorReturn(t *testing.T) { data, err := os.ReadFile(pkgFile(t, "handlers_edit.go")) if err != nil { t.Fatalf("读 handlers_edit.go 失败: %v", err) } src := string(data) for _, sig := range []string{ "func (s *Server) saveVisitTags(ctx context.Context, ex execer, datasetID int64, visitSrc int, tagIDs []string) error", "func (s *Server) refreshPlaceAgg(ctx context.Context, ex execer, placeID int64) error", "func (s *Server) refreshDatasetAgg(ctx context.Context, ex execer, datasetID int64) error", } { if !strings.Contains(src, sig) { t.Errorf("签名被改掉了(不再返回 error):%s", sig) } } }