Files
certctl/internal/ciparity/surface_parity_test.go
T
shankar0123 b4378942fc fix(ciparity): drop unused methodPathRe regex (golangci-lint cleanup)
golangci-lint v2.11.4 surfaced one finding against the bundle's new
code: 'var methodPathRe is unused' in
internal/ciparity/surface_parity_test.go:46.

The regex was leftover scaffolding from when I drafted the file as a
package-router test before moving it into the stdlib-only ciparity
package. The router-route scanner in this package uses its own
inline regex (registerRe + muxHandleRe via scanRouterRoutes) and
never reads methodPathRe.

Verified clean against the two bundle packages:
  - golangci-lint run --timeout 5m ./internal/ciparity/... ./internal/config/... → 0 issues
  - gofmt -l → no output
  - go vet → clean
  - go test -short -count=1 → ciparity 0.017s, config 0.727s

Audit-Closes: post-v2.1.0-anti-rot/item-2
2026-05-12 14:25:37 +00:00

279 lines
9.2 KiB
Go

package ciparity
// surface_parity_test.go — per post-v2.1.0 anti-rot item 2 (Auditable
// Codebase Bundle).
//
// Three tests, all stdlib-only:
//
// 1. TestSurfaceParity_MCPToolCatalogue (HARD GATE)
// Every MCP tool name matches the `certctl_<word>(_<word>)*`
// convention; no duplicates across files; total count ≥
// mcpBaselineFloor. Catches accidental tool deletions + naming-
// convention drift.
//
// 2. TestSurfaceParity_CLICommandCatalogue (INFORMATIONAL — t.Log only)
// Walks cmd/cli/main.go's switch-case dispatcher. Per frozen
// decision 0.9, warn-only until the CLI surface stabilizes.
//
// 3. TestSurfaceParity_OpenAPI_MCPHeuristicCoverage (INFORMATIONAL)
// Reports the fraction of OpenAPI operations whose path tokens
// overlap with any MCP tool name token. Trend metric, not a gate.
//
// 4. TestSurfaceParity_Summary (INFORMATIONAL)
// One-glance summary of the four surface counts.
//
// All file paths are resolved relative to the repo root, which this
// test discovers by walking up from $PWD until it finds go.mod. Keeps
// the test runnable from any working directory.
import (
"errors"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
)
// mcpBaselineFloor — see header doc. Bump when a deletion is
// deliberate; the diff captures the change.
const mcpBaselineFloor = 150
var (
mcpToolNameRe = regexp.MustCompile(`^certctl_[a-z][a-z0-9_]*[a-z0-9]$`)
mcpNameDeclRe = regexp.MustCompile(`Name:\s*"(certctl_[a-z0-9_]+)"`)
openapiPathRe = regexp.MustCompile(`^ (/[^:]+):\s*$`)
openapiVerbRe = regexp.MustCompile(`^ (get|post|put|delete|patch|options|head):\s*$`)
caseLiteralRe = regexp.MustCompile(`case\s+"([a-z\-]+)":`)
)
// mcpToolFiles lists the (non-test) Go files expected to register
// MCP tools.
func mcpToolFiles(repo string) []string {
base := filepath.Join(repo, "internal", "mcp")
return []string{
filepath.Join(base, "tools.go"),
filepath.Join(base, "tools_audit_fix.go"),
filepath.Join(base, "tools_auth.go"),
filepath.Join(base, "tools_auth_bundle2.go"),
filepath.Join(base, "tools_est.go"),
}
}
func findRepoRoot(t *testing.T) string {
t.Helper()
wd, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
cur := wd
for {
if _, err := os.Stat(filepath.Join(cur, "go.mod")); err == nil {
return cur
}
parent := filepath.Dir(cur)
if parent == cur {
t.Fatalf("no go.mod found from %s upward", wd)
}
cur = parent
}
}
// readFileOrSkip reads a file; on ENOENT, calls t.Skipf rather than
// failing — useful for files that may be renamed during refactors.
func readFileOrFail(t *testing.T, p string) []byte {
t.Helper()
body, err := os.ReadFile(p)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
t.Fatalf("expected file missing: %s (refactor? — update the test)", p)
}
t.Fatalf("read %s: %v", p, err)
}
return body
}
// TestSurfaceParity_MCPToolCatalogue is a HARD gate.
//
// Asserts:
//
// - Every MCP tool name conforms to certctl_<word>(_<word>)*.
// - No tool name is registered in more than one tools*.go file.
// - Total tools ≥ mcpBaselineFloor.
func TestSurfaceParity_MCPToolCatalogue(t *testing.T) {
repo := findRepoRoot(t)
names := map[string]string{}
for _, path := range mcpToolFiles(repo) {
body := readFileOrFail(t, path)
base := filepath.Base(path)
for _, m := range mcpNameDeclRe.FindAllStringSubmatch(string(body), -1) {
name := m[1]
if !mcpToolNameRe.MatchString(name) {
t.Errorf("MCP tool name %q in %s does not match certctl_<word>(_<word>)* — fix the name or relax the convention deliberately",
name, base)
continue
}
if other, dup := names[name]; dup {
t.Errorf("MCP tool name %q duplicated: first in %s, again in %s — pick a unique name",
name, filepath.Base(other), base)
continue
}
names[name] = path
}
}
if len(names) < mcpBaselineFloor {
t.Errorf("MCP tool count regressed: %d found, baseline floor %d. "+
"If the deletion is intentional, lower mcpBaselineFloor in this test in the SAME commit. "+
"If accidental, restore the deleted tools.",
len(names), mcpBaselineFloor)
}
t.Logf("MCP tool catalogue: %d tools (baseline floor %d)", len(names), mcpBaselineFloor)
}
// TestSurfaceParity_CLICommandCatalogue — informational only.
//
// Walks cmd/cli/main.go's switch-case dispatcher and reports the
// distinct verbs handled. Returns success regardless of contents.
// Promoted to fail-on-miss when the CLI surface stabilizes.
func TestSurfaceParity_CLICommandCatalogue(t *testing.T) {
repo := findRepoRoot(t)
body := readFileOrFail(t, filepath.Join(repo, "cmd", "cli", "main.go"))
verbs := map[string]struct{}{}
for _, m := range caseLiteralRe.FindAllStringSubmatch(string(body), -1) {
verbs[m[1]] = struct{}{}
}
if len(verbs) == 0 {
t.Fatal("CLI scanner found zero verbs — likely a refactor; update the test")
}
out := make([]string, 0, len(verbs))
for v := range verbs {
out = append(out, v)
}
sort.Strings(out)
t.Logf("CLI verb catalogue (%d distinct case literals; informational only per frozen decision 0.9):\n %s",
len(out), strings.Join(out, ", "))
}
// scanOpenAPIOps walks openapi.yaml's paths block and returns every
// (METHOD, PATH) tuple. Mirrors the parser used by
// internal/api/router/openapi_parity_test.go::scanOpenAPIOperations.
func scanOpenAPIOps(t *testing.T, path string) []string {
t.Helper()
body := readFileOrFail(t, path)
var out []string
inPaths := false
currentPath := ""
for _, line := range strings.Split(string(body), "\n") {
if strings.HasPrefix(line, "paths:") {
inPaths = true
continue
}
if inPaths && line != "" && !strings.HasPrefix(line, " ") {
inPaths = false
continue
}
if !inPaths {
continue
}
if m := openapiPathRe.FindStringSubmatch(line); m != nil {
currentPath = m[1]
continue
}
if m := openapiVerbRe.FindStringSubmatch(line); m != nil && currentPath != "" {
out = append(out, strings.ToUpper(m[1])+" "+currentPath)
}
}
return out
}
// scanRouterRoutes scans internal/api/router/router.go for route
// registrations. Uses a regex (not go/ast) to keep this package
// stdlib-only; the router.go file is large but the patterns we care
// about are stable enough that regex is sufficient.
func scanRouterRoutes(t *testing.T, path string) []string {
t.Helper()
body := readFileOrFail(t, path)
// Match r.Register("METHOD /path", ...) and r.mux.Handle("METHOD /path", ...).
registerRe := regexp.MustCompile(`r\.Register\(\s*"([A-Z]+ /[^"]+)"`)
muxHandleRe := regexp.MustCompile(`r\.mux\.Handle\(\s*"([A-Z]+ /[^"]+)"`)
seen := map[string]struct{}{}
for _, m := range registerRe.FindAllStringSubmatch(string(body), -1) {
seen[m[1]] = struct{}{}
}
for _, m := range muxHandleRe.FindAllStringSubmatch(string(body), -1) {
seen[m[1]] = struct{}{}
}
out := make([]string, 0, len(seen))
for s := range seen {
out = append(out, s)
}
return out
}
// TestSurfaceParity_OpenAPI_MCPHeuristicCoverage — informational.
//
// For each OpenAPI operation, splits the path into tokens; if any
// token appears in any MCP tool name's token set, the op is "covered."
// This is a heuristic — the actual semantic map (operationId →
// MCP tool) is not declared in the source, so we approximate.
func TestSurfaceParity_OpenAPI_MCPHeuristicCoverage(t *testing.T) {
repo := findRepoRoot(t)
specOps := scanOpenAPIOps(t, filepath.Join(repo, "api", "openapi.yaml"))
mcpTokens := map[string]struct{}{}
for _, path := range mcpToolFiles(repo) {
body := readFileOrFail(t, path)
for _, m := range mcpNameDeclRe.FindAllStringSubmatch(string(body), -1) {
for _, tok := range strings.Split(strings.TrimPrefix(m[1], "certctl_"), "_") {
mcpTokens[tok] = struct{}{}
}
}
}
covered := 0
for _, op := range specOps {
parts := strings.Split(op, " ")
if len(parts) != 2 {
continue
}
segs := strings.FieldsFunc(parts[1], func(r rune) bool {
return r == '/' || r == '{' || r == '}' || r == '-'
})
hit := false
for _, s := range segs {
if _, ok := mcpTokens[strings.ToLower(s)]; ok {
hit = true
break
}
}
if hit {
covered++
}
}
if len(specOps) == 0 {
t.Fatal("openapi.yaml scan returned zero operations — fix the test")
}
pct := (covered * 100) / len(specOps)
t.Logf("OpenAPI↔MCP heuristic coverage (informational): %d of %d ops (%d%%) share at least one path token with an MCP tool name",
covered, len(specOps), pct)
}
// TestSurfaceParity_Summary prints the four surface counts side-by-side.
func TestSurfaceParity_Summary(t *testing.T) {
repo := findRepoRoot(t)
routes := scanRouterRoutes(t, filepath.Join(repo, "internal", "api", "router", "router.go"))
specOps := scanOpenAPIOps(t, filepath.Join(repo, "api", "openapi.yaml"))
mcpCount := 0
for _, path := range mcpToolFiles(repo) {
body := readFileOrFail(t, path)
mcpCount += len(mcpNameDeclRe.FindAllStringSubmatch(string(body), -1))
}
cliBody := readFileOrFail(t, filepath.Join(repo, "cmd", "cli", "main.go"))
cliCount := len(caseLiteralRe.FindAllStringSubmatch(string(cliBody), -1))
t.Logf("Surface parity summary (informational):\n"+
" router routes : %d\n"+
" OpenAPI ops : %d\n"+
" MCP tools : %d (floor %d)\n"+
" CLI verbs : %d (warn-only — frozen decision 0.9)",
len(routes), len(specOps), mcpCount, mcpBaselineFloor, cliCount)
}