// 架构守护测试:用 AST 扫描 internal 下所有非测试代码的 import,强制分层依赖规则。 // // 目标分层(与实际依赖图一致,违规即测试失败): // // pkg/*、config、store ← 叶子层,不得依赖任何 internal 包 // channel / usage / passkey ← 只可依赖 store + pkg/* // proxy ← 只可依赖 store、channel、usage、proxy/*、pkg/* // api ← 不得直接 import channel/usage(必须经 app 持有) // app ← 组合根:channel、config、passkey、pkg/*、store、usage // cli ← 顶层装配,不限制 // // 本项目没有 arch-go 这类重型工具,一个 go/parser 扫描(毫秒级)足够守住红线; // 新增包时在这里登记规则,未登记的包默认禁止一切 internal 依赖(fail-closed)。 package internal_test import ( "go/parser" "go/token" "io/fs" "path/filepath" "sort" "strings" "testing" ) const modulePrefix = "github.com/openteam/server/internal/" // layerRules 相对 internal 的包目录 → 允许依赖的 internal 包前缀(空切片 = 禁止一切)。 // 子包继承最长前缀匹配的规则(如 proxy/convert 按 proxy 的规则校验)。 var layerRules = map[string][]string{ "config": {}, "store": {}, "pkg": {}, "channel": {"store", "pkg"}, "usage": {"store", "pkg"}, "passkey": {"store", "pkg"}, "proxy": {"store", "channel", "usage", "proxy", "pkg"}, "api": {"app", "proxy", "passkey", "store", "api", "pkg"}, "app": {"channel", "config", "passkey", "store", "usage", "pkg"}, "cli": nil, // nil = 顶层装配,不限制 } // ruleFor 取目录的生效规则:精确匹配优先,否则最长前缀;未登记的包 fail-closed。 func ruleFor(dir string) (allowed []string, unrestricted bool) { if r, ok := layerRules[dir]; ok { return r, r == nil } best := "" for k := range layerRules { if strings.HasPrefix(dir, k+"/") && len(k) > len(best) { best = k } } if best != "" { r := layerRules[best] return r, r == nil } return nil, false } func dependsOn(imported, allowed string) bool { return imported == allowed || strings.HasPrefix(imported, allowed+"/") } func TestInternalDependencyLayers(t *testing.T) { root := "." // 测试二进制的 cwd 即 server/internal var violations []string err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { name := d.Name() if name != "." && (strings.HasPrefix(name, ".") || name == "testdata" || name == "vendor") { return filepath.SkipDir } return nil } if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { return nil } dir := filepath.ToSlash(filepath.Dir(path)) if dir == "." { dir = "" } allowed, unrestricted := ruleFor(dir) if unrestricted { return nil } f, perr := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly) if perr != nil { t.Errorf("parse %s: %v", path, perr) return nil } for _, imp := range f.Imports { p := strings.Trim(imp.Path.Value, `"`) if !strings.HasPrefix(p, modulePrefix) { continue } target := strings.TrimPrefix(p, modulePrefix) ok := false for _, a := range allowed { if dependsOn(target, a) { ok = true break } } if !ok { violations = append(violations, path+" → "+p) } } return nil }) if err != nil { t.Fatal(err) } if len(violations) > 0 { sort.Strings(violations) t.Errorf("internal 分层依赖被破坏(%d 处):\n%s\n新增合法依赖请更新 arch_test.go 的 layerRules", len(violations), strings.Join(violations, "\n")) } }