From 4399d693de9c9cf5d152990b2caeae52a7915d1b Mon Sep 17 00:00:00 2001 From: Will-thom <116388885+Will-thom@users.noreply.github.com> Date: Sun, 31 May 2026 01:35:19 -0300 Subject: [PATCH] test: guard local-first core analysis --- docs/permissions.md | 6 ++ docs/testing.md | 4 +- pkg/prmaven/local_first_test.go | 109 ++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 pkg/prmaven/local_first_test.go diff --git a/docs/permissions.md b/docs/permissions.md index d6099a0..edab243 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -62,6 +62,12 @@ Enabled public-repository security controls: - Secret scanning push protection. - Dependabot security updates. +Core analyzer guardrails: + +- `pkg/prmaven` tests exercise analysis without GitHub or GitLab provider tokens. +- Production core code must not import network/provider clients. +- Production core code must not read provider environment variables. + Expected empty surfaces during stabilization: - GitHub Actions secrets. diff --git a/docs/testing.md b/docs/testing.md index 755f824..624a82e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -51,6 +51,8 @@ Package: `pkg/prmaven`. Coverage includes: +- local-first regression guards that exercise core analysis without provider tokens; +- static guards against core network/provider-client imports and provider environment reads; - Maven module discovery from `pom.xml`; - Surefire report parsing; - Failsafe report parsing; @@ -114,7 +116,7 @@ GitHub Actions runs: - coverage generation on Linux; - a minimum total coverage gate of 70%. -The CI workflow is intentionally dependency-light. Core tests do not require Maven, network services, GitHub tokens, Docker, or external APIs. +The CI workflow is intentionally dependency-light. Core tests do not require Maven, network services, GitHub tokens, Docker, or external APIs. The `pkg/prmaven` local-first guard fails if production core code starts importing network/provider clients or reading provider environment variables. ## Contributor Expectations diff --git a/pkg/prmaven/local_first_test.go b/pkg/prmaven/local_first_test.go new file mode 100644 index 0000000..f51094e --- /dev/null +++ b/pkg/prmaven/local_first_test.go @@ -0,0 +1,109 @@ +package prmaven + +import ( + "go/ast" + "go/parser" + "go/token" + "io/fs" + "path/filepath" + "strconv" + "strings" + "testing" +) + +func TestAnalyzeDoesNotRequireProviderEnvironment(t *testing.T) { + for _, name := range []string{ + "GITHUB_TOKEN", + "GH_TOKEN", + "GITHUB_ACTIONS", + "GITLAB_TOKEN", + "GITLAB_PRIVATE_TOKEN", + "CI_JOB_TOKEN", + } { + t.Setenv(name, "") + } + + report, err := Analyze(Options{ProjectDir: "../../demo/multi-module-failure"}) + if err != nil { + t.Fatal(err) + } + if report.Summary.FindingCount != 2 { + t.Fatalf("finding count = %d, want 2", report.Summary.FindingCount) + } +} + +func TestCorePackageDoesNotImportNetworkOrProviderClients(t *testing.T) { + forbiddenImports := map[string]bool{ + "net": true, + "net/http": true, + "net/rpc": true, + "golang.org/x/oauth2": true, + "github.com/google/go-github": true, + "github.com/xanzy/go-gitlab": true, + "gitlab.com/gitlab-org/api/client-go": true, + } + + forEachProductionFile(t, func(path string, file *ast.File) { + for _, spec := range file.Imports { + importPath, err := strconv.Unquote(spec.Path.Value) + if err != nil { + t.Fatalf("unquote import in %s: %v", path, err) + } + if forbiddenImports[importPath] { + t.Fatalf("%s imports %q; core analysis must stay provider-agnostic and no-network", path, importPath) + } + } + }) +} + +func TestCorePackageDoesNotReadProviderEnvironment(t *testing.T) { + forbiddenSelectors := map[string]bool{ + "Getenv": true, + "LookupEnv": true, + "Environ": true, + "ExpandEnv": true, + } + + forEachProductionFile(t, func(path string, file *ast.File) { + ast.Inspect(file, func(node ast.Node) bool { + selector, ok := node.(*ast.SelectorExpr) + if !ok || !forbiddenSelectors[selector.Sel.Name] { + return true + } + ident, ok := selector.X.(*ast.Ident) + if ok && ident.Name == "os" { + t.Fatalf("%s reads environment through os.%s; core analysis must not require provider tokens", path, selector.Sel.Name) + } + return true + }) + }) +} + +func forEachProductionFile(t *testing.T, visit func(path string, file *ast.File)) { + t.Helper() + + fileSet := token.NewFileSet() + err := filepath.WalkDir(".", func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + if entry.Name() == "testdata" { + return filepath.SkipDir + } + return nil + } + if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") { + return nil + } + file, err := parser.ParseFile(fileSet, path, nil, 0) + if err != nil { + return err + } + visit(path, file) + return nil + }) + if err != nil { + t.Fatal(err) + } +}