Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

301 lines
9.0 KiB
Go

package review
import (
"os"
"path/filepath"
"testing"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/platform"
)
// isolateGlobalConfig points platform.ConfigDir() at a fresh, empty
// tempdir for the duration of a test so the global rule layer never
// reads the developer's real ~/.gortex/config.yaml. It returns the
// resolved config dir (already created) so callers can plant a global
// config.yaml when they want to exercise that layer.
func isolateGlobalConfig(t *testing.T) string {
t.Helper()
root := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", root)
dir := platform.ConfigDir()
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir config dir: %v", err)
}
return dir
}
// TestReviewConfigRoundTrip asserts config.Load round-trips a `review:`
// block carrying every field: the rule list, the gating thresholds, the
// depth-selection bounds, and post.allow_public.
func TestReviewConfigRoundTrip(t *testing.T) {
yaml := `
review:
rules:
- name: tests
path: "**/*_test.go"
severity: warning
rulepack: test
disabled: false
- name: auth
path: "internal/auth/**"
severity: error
rulepack: security
disabled: true
min_confidence: 0.75
min_severity: warning
categories:
- nil-deref
- injection
max_findings: 25
quick_max_lines: 40
deep_min_lines: 400
deep_min_files: 12
post:
allow_public: true
`
dir := t.TempDir()
path := filepath.Join(dir, ".gortex.yaml")
if err := os.WriteFile(path, []byte(yaml), 0o644); err != nil {
t.Fatalf("write config: %v", err)
}
cfg, err := config.Load(path)
if err != nil {
t.Fatalf("config.Load: %v", err)
}
r := cfg.Review
if len(r.Rules) != 2 {
t.Fatalf("rules: got %d want 2", len(r.Rules))
}
if r.Rules[0].Name != "tests" || r.Rules[0].Path != "**/*_test.go" ||
r.Rules[0].Severity != "warning" || r.Rules[0].Rulepack != "test" || r.Rules[0].Disabled {
t.Errorf("rule[0] mismatch: %+v", r.Rules[0])
}
if r.Rules[1].Name != "auth" || r.Rules[1].Path != "internal/auth/**" ||
r.Rules[1].Severity != "error" || r.Rules[1].Rulepack != "security" || !r.Rules[1].Disabled {
t.Errorf("rule[1] mismatch: %+v", r.Rules[1])
}
if r.MinConfidence != 0.75 {
t.Errorf("min_confidence: got %v want 0.75", r.MinConfidence)
}
if r.MinSeverity != "warning" {
t.Errorf("min_severity: got %q want warning", r.MinSeverity)
}
if len(r.Categories) != 2 || r.Categories[0] != "nil-deref" || r.Categories[1] != "injection" {
t.Errorf("categories: got %v", r.Categories)
}
if r.MaxFindings != 25 {
t.Errorf("max_findings: got %d want 25", r.MaxFindings)
}
if r.QuickMaxLines != 40 {
t.Errorf("quick_max_lines: got %d want 40", r.QuickMaxLines)
}
if r.DeepMinLines != 400 {
t.Errorf("deep_min_lines: got %d want 400", r.DeepMinLines)
}
if r.DeepMinFiles != 12 {
t.Errorf("deep_min_files: got %d want 12", r.DeepMinFiles)
}
if !r.Post.AllowPublic {
t.Errorf("post.allow_public: got false want true")
}
}
// TestRuleResolverDefaultsOnly asserts that with no config files present
// the embedded defaults resolve: a test file selects the test rulepack
// (ahead of the `**` catch-all), a non-test file falls to general, and
// `**` matches arbitrarily nested paths.
func TestRuleResolverDefaultsOnly(t *testing.T) {
isolateGlobalConfig(t)
repoRoot := t.TempDir() // empty repo — no .gortex.yaml, no .gortex/review.yaml
rr, err := NewRuleResolver("", repoRoot)
if err != nil {
t.Fatalf("NewRuleResolver: %v", err)
}
// `**/*_test.go` wins over `**` for a test file, at any depth.
for _, p := range []string{"foo_test.go", "internal/pkg/foo_test.go", "a/b/c/d_test.go"} {
got, ok := rr.RuleFor(p)
if !ok {
t.Fatalf("RuleFor(%q): no match", p)
}
if got.Rulepack != "test" {
t.Errorf("RuleFor(%q): rulepack=%q want test", p, got.Rulepack)
}
}
// Non-test files fall through to the `**` general catch-all, nested too.
for _, p := range []string{"main.go", "internal/auth/login.go", "a/b/c/d/e/f.go"} {
got, ok := rr.RuleFor(p)
if !ok {
t.Fatalf("RuleFor(%q): no match (** must match nested paths)", p)
}
if got.Rulepack != "general" {
t.Errorf("RuleFor(%q): rulepack=%q want general", p, got.Rulepack)
}
}
}
// TestRuleResolverLayerPrecedence asserts the four layers merge in
// precedence order: a custom rule shadows a project rule for the same
// glob, and the project layer in turn shadows the global layer.
func TestRuleResolverLayerPrecedence(t *testing.T) {
globalDir := isolateGlobalConfig(t)
// Global layer: config.yaml with a rule for *.go.
globalYAML := `
review:
rules:
- name: global-go
path: "**/*.go"
rulepack: global-pack
`
if err := os.WriteFile(filepath.Join(globalDir, "config.yaml"), []byte(globalYAML), 0o644); err != nil {
t.Fatalf("write global config: %v", err)
}
// Project layer: .gortex.yaml shadows the global rule for *.go.
repoRoot := t.TempDir()
projectYAML := `
review:
rules:
- name: project-go
path: "**/*.go"
rulepack: project-pack
`
if err := os.WriteFile(filepath.Join(repoRoot, ".gortex.yaml"), []byte(projectYAML), 0o644); err != nil {
t.Fatalf("write project config: %v", err)
}
// Custom layer (--rules): shadows everything for the same glob.
customDir := t.TempDir()
customPath := filepath.Join(customDir, "review.yaml")
customYAML := `
rules:
- name: custom-go
path: "**/*.go"
rulepack: custom-pack
`
if err := os.WriteFile(customPath, []byte(customYAML), 0o644); err != nil {
t.Fatalf("write custom config: %v", err)
}
// All four layers present → custom wins.
rr, err := NewRuleResolver(customPath, repoRoot)
if err != nil {
t.Fatalf("NewRuleResolver: %v", err)
}
got, ok := rr.RuleFor("internal/x.go")
if !ok {
t.Fatalf("RuleFor: no match")
}
if got.Name != "custom-go" || got.Rulepack != "custom-pack" {
t.Errorf("custom should shadow project+global: got %+v", got)
}
// Drop the custom layer → project wins over global.
rr2, err := NewRuleResolver("", repoRoot)
if err != nil {
t.Fatalf("NewRuleResolver (no custom): %v", err)
}
got2, ok := rr2.RuleFor("internal/x.go")
if !ok {
t.Fatalf("RuleFor (no custom): no match")
}
if got2.Name != "project-go" || got2.Rulepack != "project-pack" {
t.Errorf("project should shadow global: got %+v", got2)
}
}
// TestRuleResolverRepoLocalCustom asserts the repo-local
// .gortex/review.yaml is picked up as the custom layer when no explicit
// --rules path is given.
func TestRuleResolverRepoLocalCustom(t *testing.T) {
isolateGlobalConfig(t)
repoRoot := t.TempDir()
if err := os.MkdirAll(filepath.Join(repoRoot, ".gortex"), 0o755); err != nil {
t.Fatalf("mkdir .gortex: %v", err)
}
repoLocalYAML := `
rules:
- name: repo-local-go
path: "**/*.go"
rulepack: repo-local-pack
`
if err := os.WriteFile(filepath.Join(repoRoot, ".gortex", "review.yaml"), []byte(repoLocalYAML), 0o644); err != nil {
t.Fatalf("write repo-local review.yaml: %v", err)
}
rr, err := NewRuleResolver("", repoRoot)
if err != nil {
t.Fatalf("NewRuleResolver: %v", err)
}
got, ok := rr.RuleFor("internal/x.go")
if !ok {
t.Fatalf("RuleFor: no match")
}
if got.Name != "repo-local-go" || got.Rulepack != "repo-local-pack" {
t.Errorf("repo-local .gortex/review.yaml should win: got %+v", got)
}
}
// TestRuleResolverDisabledSkipped asserts a Disabled rule is skipped
// during resolution, so a lower-precedence rule (or the embedded
// catch-all) governs the file instead.
func TestRuleResolverDisabledSkipped(t *testing.T) {
isolateGlobalConfig(t)
repoRoot := t.TempDir()
// A disabled custom rule for *.go must NOT shadow anything. A second,
// enabled custom rule for the same glob proves the disabled one was
// skipped (the enabled one wins) rather than the disabled one matching.
customDir := t.TempDir()
customPath := filepath.Join(customDir, "review.yaml")
customYAML := `
rules:
- name: disabled-go
path: "**/*.go"
rulepack: disabled-pack
disabled: true
- name: enabled-go
path: "**/*.go"
rulepack: enabled-pack
`
if err := os.WriteFile(customPath, []byte(customYAML), 0o644); err != nil {
t.Fatalf("write custom config: %v", err)
}
rr, err := NewRuleResolver(customPath, repoRoot)
if err != nil {
t.Fatalf("NewRuleResolver: %v", err)
}
got, ok := rr.RuleFor("internal/x.go")
if !ok {
t.Fatalf("RuleFor: no match")
}
if got.Name == "disabled-go" {
t.Fatalf("disabled rule must be skipped, but it matched: %+v", got)
}
if got.Name != "enabled-go" || got.Rulepack != "enabled-pack" {
t.Errorf("enabled rule should win after disabled skipped: got %+v", got)
}
}
// TestDefaultReviewRules asserts the embedded layer is the documented
// pair: a `**/*_test.go` test rulepack ahead of a `**` general catch-all.
func TestDefaultReviewRules(t *testing.T) {
rules := defaultReviewRules()
if len(rules) != 2 {
t.Fatalf("defaultReviewRules: got %d want 2", len(rules))
}
if rules[0].Path != "**/*_test.go" || rules[0].Rulepack != "test" {
t.Errorf("rule[0]: got %+v want test/**/*_test.go", rules[0])
}
if rules[1].Path != "**" || rules[1].Rulepack != "general" {
t.Errorf("rule[1]: got %+v want general/**", rules[1])
}
}