Static safety/lint runner for the BlockNinja codebase. ~25 invariant
checks across Go and frontend sources. Was at git.dev.alexdunmow.com:block/ninja
in backend/cmd/check-safety/ until the 2026-06-06 consolidation moved
the BlockNinja repos under a shared ~/src/blockninja/ parent.
This repo is the standalone extraction:
- Own go.mod (git.dev.alexdunmow.com/block/check-safety, go 1.26.4)
- Vendored internal/{helpers,theme} from CMS (Go's internal/ rule
blocks cross-module imports; vendoring is the workaround)
- CLI contract unchanged: `check-safety <target-dir> [--flags]`
- CMS Makefile shells into ../check-safety for safety-check /
install-safety-checker targets
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
371 lines
11 KiB
Go
371 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// ─────────────────────────────────────────────
|
|
// usesCoreInternalLayout
|
|
// ─────────────────────────────────────────────
|
|
|
|
// Guard fires when internal/services/ exists.
|
|
func TestUsesCoreInternalLayout_TrueWhenServicesExists(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(root, "internal", "services"), 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
if !usesCoreInternalLayout(root) {
|
|
t.Fatal("expected true when internal/services/ exists, got false")
|
|
}
|
|
}
|
|
|
|
// Guard fires when internal/handlers/ exists.
|
|
func TestUsesCoreInternalLayout_TrueWhenHandlersExists(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(root, "internal", "handlers"), 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
if !usesCoreInternalLayout(root) {
|
|
t.Fatal("expected true when internal/handlers/ exists, got false")
|
|
}
|
|
}
|
|
|
|
// Guard fires when BOTH directories exist.
|
|
func TestUsesCoreInternalLayout_TrueWhenBothExist(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(root, "internal", "services"), 0755); err != nil {
|
|
t.Fatalf("mkdir services: %v", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(root, "internal", "handlers"), 0755); err != nil {
|
|
t.Fatalf("mkdir handlers: %v", err)
|
|
}
|
|
|
|
if !usesCoreInternalLayout(root) {
|
|
t.Fatal("expected true when both internal/services/ and internal/handlers/ exist")
|
|
}
|
|
}
|
|
|
|
// Guard returns false when neither directory exists.
|
|
func TestUsesCoreInternalLayout_FalseWhenNeitherExists(t *testing.T) {
|
|
root := t.TempDir()
|
|
// No internal/ tree at all — plugin-style flat layout.
|
|
|
|
if usesCoreInternalLayout(root) {
|
|
t.Fatal("expected false for empty root (no internal/{services,handlers}), got true")
|
|
}
|
|
}
|
|
|
|
// Only the exact subdirectories count — internal/models/ alone is not enough.
|
|
func TestUsesCoreInternalLayout_FalseForOtherInternalDirs(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(root, "internal", "models"), 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
if usesCoreInternalLayout(root) {
|
|
t.Fatal("expected false when only internal/models/ exists (not services/ or handlers/)")
|
|
}
|
|
}
|
|
|
|
// A file named "services" is not a directory — should not satisfy the guard.
|
|
func TestUsesCoreInternalLayout_FalseWhenServicesIsAFile(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(root, "internal"), 0755); err != nil {
|
|
t.Fatalf("mkdir internal: %v", err)
|
|
}
|
|
// Create a regular file called "services" (not a directory).
|
|
if err := os.WriteFile(filepath.Join(root, "internal", "services"), []byte("oops"), 0o644); err != nil {
|
|
t.Fatalf("write file: %v", err)
|
|
}
|
|
|
|
if usesCoreInternalLayout(root) {
|
|
t.Fatal("expected false when internal/services is a regular file, not a directory")
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// checkServiceStructure
|
|
// ─────────────────────────────────────────────
|
|
|
|
// Helper: builds a minimal fixture with internal/services/ (so the guard passes)
|
|
// then writes a .go file at the given relative path inside root.
|
|
func buildStructureFixture(t *testing.T, relGoPath, content string) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
|
|
// Satisfy the guard: create internal/services/
|
|
if err := os.MkdirAll(filepath.Join(root, "internal", "services"), 0755); err != nil {
|
|
t.Fatalf("mkdir internal/services: %v", err)
|
|
}
|
|
|
|
// Write the target Go file (create parent dirs as needed).
|
|
abs := filepath.Join(root, filepath.FromSlash(relGoPath))
|
|
if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", filepath.Dir(abs), err)
|
|
}
|
|
if err := os.WriteFile(abs, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", relGoPath, err)
|
|
}
|
|
|
|
return root
|
|
}
|
|
|
|
// A type ending with "Service" at the root (outside internal/) must be flagged.
|
|
func TestCheckServiceStructure_FlagsServiceTypeAtRoot(t *testing.T) {
|
|
root := buildStructureFixture(t, "myservice.go", `package main
|
|
|
|
type UserService struct{}
|
|
`)
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected violation for UserService outside internal/, got 0")
|
|
}
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "service-location" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected rule='service-location', got violations: %#v", vs)
|
|
}
|
|
}
|
|
|
|
// A type ending with "Handler" at the root (outside internal/) must be flagged.
|
|
func TestCheckServiceStructure_FlagsHandlerTypeAtRoot(t *testing.T) {
|
|
root := buildStructureFixture(t, "handler.go", `package main
|
|
|
|
type PageHandler struct{}
|
|
`)
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected violation for PageHandler outside internal/, got 0")
|
|
}
|
|
found := false
|
|
for _, v := range vs {
|
|
if v.rule == "handler-location" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected rule='handler-location', got violations: %#v", vs)
|
|
}
|
|
}
|
|
|
|
// A file under internal/services/ is SKIPPED entirely — even a type there generates no violation.
|
|
func TestCheckServiceStructure_SkipsInternalServicesDir(t *testing.T) {
|
|
root := buildStructureFixture(t, "internal/services/user_service.go", `package services
|
|
|
|
type UserService struct{}
|
|
`)
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected 0 violations for type in internal/services/, got %d: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
// A file under internal/handlers/ is SKIPPED entirely.
|
|
func TestCheckServiceStructure_SkipsInternalHandlersDir(t *testing.T) {
|
|
root := t.TempDir()
|
|
// Provide internal/handlers/ as the guard directory.
|
|
if err := os.MkdirAll(filepath.Join(root, "internal", "handlers"), 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
abs := filepath.Join(root, "internal", "handlers", "page.go")
|
|
if err := os.WriteFile(abs, []byte(`package handlers
|
|
|
|
type PageHandler struct{}
|
|
`), 0o644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected 0 violations for type in internal/handlers/, got %d: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
// A type under internal/ (but NOT under internal/services/ or internal/handlers/)
|
|
// is walked and the name check runs — however, since the relPath starts with
|
|
// "internal/", the condition `!strings.HasPrefix(relPath, "internal/")` is false
|
|
// and no violation is produced. Verify the no-violation outcome.
|
|
func TestCheckServiceStructure_NoViolationForTypeInOtherInternalSubdir(t *testing.T) {
|
|
root := buildStructureFixture(t, "internal/middleware/auth_service.go", `package middleware
|
|
|
|
type AuthService struct{}
|
|
`)
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected 0 violations for type in internal/middleware/ (relPath starts with internal/), got %d: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
// cmd/ files are skipped entirely — no violation even for types named *Service.
|
|
func TestCheckServiceStructure_SkipsCmdDir(t *testing.T) {
|
|
root := buildStructureFixture(t, "cmd/server/main.go", `package main
|
|
|
|
type ServerService struct{}
|
|
`)
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected 0 violations for type in cmd/ (skipped), got %d: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
// Test files (_test.go) are always skipped.
|
|
func TestCheckServiceStructure_SkipsTestFiles(t *testing.T) {
|
|
root := buildStructureFixture(t, "myservice_test.go", `package main
|
|
|
|
type FakeUserService struct{}
|
|
`)
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected 0 violations for type in _test.go (skipped), got %d: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
// Generated files (.connect.go, .pb.go, .gen.go) are skipped.
|
|
func TestCheckServiceStructure_SkipsGeneratedFiles(t *testing.T) {
|
|
root := buildStructureFixture(t, "rpc.pb.go", `package main
|
|
|
|
type RPCService struct{}
|
|
`)
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected 0 violations for .pb.go generated file, got %d: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
// api/ prefix files are skipped.
|
|
func TestCheckServiceStructure_SkipsAPIDir(t *testing.T) {
|
|
root := buildStructureFixture(t, "api/v1/handler.go", `package v1
|
|
|
|
type PageHandler struct{}
|
|
`)
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected 0 violations for type in api/ (skipped), got %d: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
// When guard returns false (no internal layout), checkServiceStructure is a no-op.
|
|
func TestCheckServiceStructure_NoOpWhenGuardFalse(t *testing.T) {
|
|
// Root with NO internal/services or internal/handlers — guard will return false.
|
|
root := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(root, "svc.go"), []byte(`package main
|
|
|
|
type UserService struct{}
|
|
`), 0o644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected 0 violations (guard=false, check skipped), got %d: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
// Clean case: valid placement — type in internal/services/ and internal/handlers/,
|
|
// nothing misplaced anywhere. Zero violations.
|
|
func TestCheckServiceStructure_CleanRepo(t *testing.T) {
|
|
root := t.TempDir()
|
|
// Satisfy guard.
|
|
if err := os.MkdirAll(filepath.Join(root, "internal", "services"), 0755); err != nil {
|
|
t.Fatalf("mkdir services: %v", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(root, "internal", "handlers"), 0755); err != nil {
|
|
t.Fatalf("mkdir handlers: %v", err)
|
|
}
|
|
|
|
// Correctly placed service.
|
|
if err := os.WriteFile(filepath.Join(root, "internal", "services", "user.go"), []byte(`package services
|
|
|
|
type UserService struct {
|
|
db interface{}
|
|
}
|
|
`), 0o644); err != nil {
|
|
t.Fatalf("write user.go: %v", err)
|
|
}
|
|
|
|
// Correctly placed handler.
|
|
if err := os.WriteFile(filepath.Join(root, "internal", "handlers", "page.go"), []byte(`package handlers
|
|
|
|
type PageHandler struct {
|
|
svc interface{}
|
|
}
|
|
`), 0o644); err != nil {
|
|
t.Fatalf("write page.go: %v", err)
|
|
}
|
|
|
|
// A non-service/handler type at root — fine.
|
|
if err := os.WriteFile(filepath.Join(root, "config.go"), []byte(`package main
|
|
|
|
type Config struct {
|
|
Debug bool
|
|
}
|
|
`), 0o644); err != nil {
|
|
t.Fatalf("write config.go: %v", err)
|
|
}
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected 0 violations for clean repo layout, got %d: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
// Snippet field encodes the type name in the expected format.
|
|
func TestCheckServiceStructure_ViolationSnippetFormat(t *testing.T) {
|
|
root := buildStructureFixture(t, "billing.go", `package main
|
|
|
|
type BillingService struct{}
|
|
`)
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected at least one violation, got 0")
|
|
}
|
|
v := vs[0]
|
|
if v.snippet != "type BillingService — should be in internal/services/" {
|
|
t.Fatalf("unexpected snippet: %q", v.snippet)
|
|
}
|
|
if v.line == 0 {
|
|
t.Error("expected non-zero line number in violation")
|
|
}
|
|
}
|
|
|
|
// Multiple misplaced types in one file each produce their own violation.
|
|
func TestCheckServiceStructure_MultipleViolationsInOneFile(t *testing.T) {
|
|
root := buildStructureFixture(t, "mixed.go", `package main
|
|
|
|
type OrderService struct{}
|
|
type OrderHandler struct{}
|
|
`)
|
|
|
|
vs := checkServiceStructure(root)
|
|
if len(vs) < 2 {
|
|
t.Fatalf("expected at least 2 violations (1 service + 1 handler), got %d: %#v", len(vs), vs)
|
|
}
|
|
rules := map[string]bool{}
|
|
for _, v := range vs {
|
|
rules[v.rule] = true
|
|
}
|
|
if !rules["service-location"] {
|
|
t.Error("expected 'service-location' violation")
|
|
}
|
|
if !rules["handler-location"] {
|
|
t.Error("expected 'handler-location' violation")
|
|
}
|
|
}
|