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>
487 lines
14 KiB
Go
487 lines
14 KiB
Go
package main
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
)
|
||
|
||
// ─────────────────────────────────────────────
|
||
// loadSafetyRules
|
||
// ─────────────────────────────────────────────
|
||
|
||
func TestLoadSafetyRules_Missing(t *testing.T) {
|
||
dir := t.TempDir()
|
||
rules, err := loadSafetyRules(dir)
|
||
if err != nil {
|
||
t.Fatalf("loadSafetyRules() unexpected error for missing file: %v", err)
|
||
}
|
||
if rules != nil {
|
||
t.Fatalf("loadSafetyRules() want nil for missing file, got %+v", rules)
|
||
}
|
||
}
|
||
|
||
func TestLoadSafetyRules_ValidYAML(t *testing.T) {
|
||
dir := t.TempDir()
|
||
writeFile(t, dir, "safety-rules.yml", `
|
||
segmentation:
|
||
no_go_imports_from:
|
||
- otherplugin
|
||
migration_table_prefix: "myplugin_"
|
||
federation_name: myplugin
|
||
`)
|
||
rules, err := loadSafetyRules(dir)
|
||
if err != nil {
|
||
t.Fatalf("loadSafetyRules() unexpected error: %v", err)
|
||
}
|
||
if rules == nil || rules.Segmentation == nil {
|
||
t.Fatal("loadSafetyRules() returned nil rules or nil segmentation")
|
||
}
|
||
if len(rules.Segmentation.NoGoImportsFrom) != 1 || rules.Segmentation.NoGoImportsFrom[0] != "otherplugin" {
|
||
t.Fatalf("unexpected NoGoImportsFrom: %v", rules.Segmentation.NoGoImportsFrom)
|
||
}
|
||
if rules.Segmentation.MigrationTablePrefix != "myplugin_" {
|
||
t.Fatalf("unexpected MigrationTablePrefix: %q", rules.Segmentation.MigrationTablePrefix)
|
||
}
|
||
if rules.Segmentation.FederationName != "myplugin" {
|
||
t.Fatalf("unexpected FederationName: %q", rules.Segmentation.FederationName)
|
||
}
|
||
}
|
||
|
||
func TestLoadSafetyRules_InvalidYAML(t *testing.T) {
|
||
dir := t.TempDir()
|
||
writeFile(t, dir, "safety-rules.yml", `
|
||
segmentation:
|
||
no_go_imports_from: {bad yaml [
|
||
`)
|
||
_, err := loadSafetyRules(dir)
|
||
if err == nil {
|
||
t.Fatal("loadSafetyRules() expected error for invalid YAML, got nil")
|
||
}
|
||
}
|
||
|
||
func TestLoadSafetyRules_EmptyFile(t *testing.T) {
|
||
dir := t.TempDir()
|
||
writeFile(t, dir, "safety-rules.yml", ``)
|
||
rules, err := loadSafetyRules(dir)
|
||
if err != nil {
|
||
t.Fatalf("loadSafetyRules() unexpected error for empty file: %v", err)
|
||
}
|
||
// Empty YAML → non-nil SafetyRules with nil Segmentation
|
||
if rules != nil && rules.Segmentation != nil {
|
||
t.Fatalf("loadSafetyRules() want nil segmentation for empty file, got %+v", rules.Segmentation)
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// checkSegmentation – require_files rule
|
||
// ─────────────────────────────────────────────
|
||
|
||
func TestCheckSegmentation_RequireFiles_Present(t *testing.T) {
|
||
dir := t.TempDir()
|
||
writeFile(t, dir, "README.md", "# plugin")
|
||
|
||
rules := &SegmentationRules{
|
||
RequireFiles: []string{"README.md"},
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 0 {
|
||
t.Fatalf("expected 0 violations when required file exists, got %d: %#v", len(vs), vs)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_RequireFiles_Missing(t *testing.T) {
|
||
dir := t.TempDir()
|
||
// README.md does NOT exist
|
||
|
||
rules := &SegmentationRules{
|
||
RequireFiles: []string{"README.md"},
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 1 {
|
||
t.Fatalf("expected 1 violation for missing required file, got %d: %#v", len(vs), vs)
|
||
}
|
||
if vs[0].rule != "require_files" {
|
||
t.Fatalf("expected rule 'require_files', got %q", vs[0].rule)
|
||
}
|
||
if vs[0].pluginName != "myplugin" {
|
||
t.Fatalf("expected pluginName 'myplugin', got %q", vs[0].pluginName)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_RequireFiles_Multiple(t *testing.T) {
|
||
dir := t.TempDir()
|
||
writeFile(t, dir, "exists.md", "exists")
|
||
// "missing.md" does not exist
|
||
|
||
rules := &SegmentationRules{
|
||
RequireFiles: []string{"exists.md", "missing.md"},
|
||
}
|
||
vs := checkSegmentation(dir, "plugin", rules)
|
||
if len(vs) != 1 {
|
||
t.Fatalf("expected 1 violation for 1 missing file, got %d: %#v", len(vs), vs)
|
||
}
|
||
if vs[0].file != "missing.md" {
|
||
t.Fatalf("expected violation for missing.md, got %q", vs[0].file)
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// checkSegmentation – no_go_imports_from rule
|
||
// ─────────────────────────────────────────────
|
||
|
||
func TestCheckSegmentation_NoGoImports_Clean(t *testing.T) {
|
||
dir := t.TempDir()
|
||
writeFile(t, dir, "plugin.go", `package myplugin
|
||
|
||
import "fmt"
|
||
|
||
func Hello() string {
|
||
return fmt.Sprintf("hello")
|
||
}
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
NoGoImportsFrom: []string{"otherplugin"},
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 0 {
|
||
t.Fatalf("expected 0 violations for clean Go file, got %d: %#v", len(vs), vs)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_NoGoImports_Violation(t *testing.T) {
|
||
dir := t.TempDir()
|
||
writeFile(t, dir, "plugin.go", `package myplugin
|
||
|
||
import "git.example.com/block/ninja/internal/plugins/otherplugin"
|
||
|
||
func Hello() {
|
||
_ = otherplugin.SomeFunc()
|
||
}
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
NoGoImportsFrom: []string{"otherplugin"},
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) == 0 {
|
||
t.Fatal("expected violations for forbidden import, got 0")
|
||
}
|
||
found := false
|
||
for _, v := range vs {
|
||
if v.rule == "no_go_imports_from" {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("expected rule 'no_go_imports_from', got: %#v", vs)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_NoGoImports_TestFilesSkipped(t *testing.T) {
|
||
dir := t.TempDir()
|
||
// Test files should be skipped by the scanner
|
||
writeFile(t, dir, "plugin_test.go", `package myplugin
|
||
|
||
import "git.example.com/block/ninja/internal/plugins/otherplugin"
|
||
|
||
func TestSomething(t *testing.T) {
|
||
_ = otherplugin.SomeFunc()
|
||
}
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
NoGoImportsFrom: []string{"otherplugin"},
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 0 {
|
||
t.Fatalf("expected 0 violations for test file, got %d: %#v", len(vs), vs)
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// checkSegmentation – no_frontend_imports_from rule
|
||
// ─────────────────────────────────────────────
|
||
|
||
func TestCheckSegmentation_NoFrontendImports_NoWebDir(t *testing.T) {
|
||
dir := t.TempDir()
|
||
// No web/ directory — should return nothing
|
||
|
||
rules := &SegmentationRules{
|
||
NoFrontendImportsFrom: []string{"otherplugin"},
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 0 {
|
||
t.Fatalf("expected 0 violations when no web/ dir, got %d: %#v", len(vs), vs)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_NoFrontendImports_Clean(t *testing.T) {
|
||
dir := t.TempDir()
|
||
webDir := filepath.Join(dir, "web", "src")
|
||
if err := os.MkdirAll(webDir, 0755); err != nil {
|
||
t.Fatalf("mkdir web: %v", err)
|
||
}
|
||
writeFile(t, webDir, "component.tsx", `import React from 'react'
|
||
|
||
export function MyComponent() {
|
||
return <div>Hello</div>
|
||
}
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
NoFrontendImportsFrom: []string{"otherplugin"},
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 0 {
|
||
t.Fatalf("expected 0 violations for clean frontend, got %d: %#v", len(vs), vs)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_NoFrontendImports_Violation(t *testing.T) {
|
||
dir := t.TempDir()
|
||
webDir := filepath.Join(dir, "web", "src")
|
||
if err := os.MkdirAll(webDir, 0755); err != nil {
|
||
t.Fatalf("mkdir web: %v", err)
|
||
}
|
||
writeFile(t, webDir, "component.tsx", `import { SomeThing } from 'otherplugin/components'
|
||
|
||
export function MyComponent() {
|
||
return <SomeThing />
|
||
}
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
NoFrontendImportsFrom: []string{"otherplugin"},
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) == 0 {
|
||
t.Fatal("expected violations for forbidden frontend import, got 0")
|
||
}
|
||
found := false
|
||
for _, v := range vs {
|
||
if v.rule == "no_frontend_imports_from" {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("expected rule 'no_frontend_imports_from', got: %#v", vs)
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// checkSegmentation – migration_table_prefix rule
|
||
// ─────────────────────────────────────────────
|
||
|
||
func TestCheckSegmentation_MigrationFK_NoMigrationsDir(t *testing.T) {
|
||
dir := t.TempDir()
|
||
// No migrations/ directory — should skip gracefully
|
||
|
||
rules := &SegmentationRules{
|
||
MigrationTablePrefix: "myplugin_",
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 0 {
|
||
t.Fatalf("expected 0 violations when no migrations/ dir, got %d: %#v", len(vs), vs)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_MigrationFK_AllowsOwnPrefix(t *testing.T) {
|
||
dir := t.TempDir()
|
||
migrDir := filepath.Join(dir, "migrations")
|
||
if err := os.MkdirAll(migrDir, 0755); err != nil {
|
||
t.Fatalf("mkdir migrations: %v", err)
|
||
}
|
||
writeFile(t, migrDir, "001_create.sql", `
|
||
CREATE TABLE myplugin_items (
|
||
id BIGINT PRIMARY KEY,
|
||
FOREIGN KEY (owner_id) REFERENCES myplugin_owners(id)
|
||
);
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
MigrationTablePrefix: "myplugin_",
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 0 {
|
||
t.Fatalf("expected 0 violations for FK within own prefix, got %d: %#v", len(vs), vs)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_MigrationFK_AllowsPublicUsers(t *testing.T) {
|
||
dir := t.TempDir()
|
||
migrDir := filepath.Join(dir, "migrations")
|
||
if err := os.MkdirAll(migrDir, 0755); err != nil {
|
||
t.Fatalf("mkdir migrations: %v", err)
|
||
}
|
||
writeFile(t, migrDir, "001_create.sql", `
|
||
CREATE TABLE myplugin_posts (
|
||
id BIGINT PRIMARY KEY,
|
||
user_id BIGINT REFERENCES public_users(id)
|
||
);
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
MigrationTablePrefix: "myplugin_",
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 0 {
|
||
t.Fatalf("expected 0 violations for FK to public_users, got %d: %#v", len(vs), vs)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_MigrationFK_CrossPluginViolation(t *testing.T) {
|
||
dir := t.TempDir()
|
||
migrDir := filepath.Join(dir, "migrations")
|
||
if err := os.MkdirAll(migrDir, 0755); err != nil {
|
||
t.Fatalf("mkdir migrations: %v", err)
|
||
}
|
||
writeFile(t, migrDir, "001_create.sql", `
|
||
CREATE TABLE myplugin_items (
|
||
id BIGINT PRIMARY KEY,
|
||
other_id BIGINT REFERENCES otherplugin_widgets(id)
|
||
);
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
MigrationTablePrefix: "myplugin_",
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) == 0 {
|
||
t.Fatal("expected violations for cross-plugin FK reference, got 0")
|
||
}
|
||
found := false
|
||
for _, v := range vs {
|
||
if v.rule == "migration_table_prefix" {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("expected rule 'migration_table_prefix', got: %#v", vs)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_MigrationFK_SkipsDownFiles(t *testing.T) {
|
||
dir := t.TempDir()
|
||
migrDir := filepath.Join(dir, "migrations")
|
||
if err := os.MkdirAll(migrDir, 0755); err != nil {
|
||
t.Fatalf("mkdir migrations: %v", err)
|
||
}
|
||
// _down file should be skipped
|
||
writeFile(t, migrDir, "001_create_down.sql", `
|
||
DROP TABLE IF EXISTS myplugin_items;
|
||
ALTER TABLE otherplugin_widgets DROP CONSTRAINT fk_myplugin;
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
MigrationTablePrefix: "myplugin_",
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 0 {
|
||
t.Fatalf("expected 0 violations for _down migration, got %d: %#v", len(vs), vs)
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// checkSegmentation – federation_name rule
|
||
// ─────────────────────────────────────────────
|
||
|
||
func TestCheckSegmentation_FederationName_NoViteConfig(t *testing.T) {
|
||
dir := t.TempDir()
|
||
webDir := filepath.Join(dir, "web")
|
||
if err := os.MkdirAll(webDir, 0755); err != nil {
|
||
t.Fatalf("mkdir web: %v", err)
|
||
}
|
||
// No vite.config.ts — should skip (require_files handles this)
|
||
|
||
rules := &SegmentationRules{
|
||
FederationName: "myplugin",
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 0 {
|
||
t.Fatalf("expected 0 violations when vite.config.ts is absent, got %d: %#v", len(vs), vs)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_FederationName_Correct(t *testing.T) {
|
||
dir := t.TempDir()
|
||
webDir := filepath.Join(dir, "web")
|
||
if err := os.MkdirAll(webDir, 0755); err != nil {
|
||
t.Fatalf("mkdir web: %v", err)
|
||
}
|
||
writeFile(t, webDir, "vite.config.ts", `
|
||
import { defineConfig } from 'vite'
|
||
|
||
export default defineConfig({
|
||
plugins: [
|
||
federation({
|
||
name: "myplugin",
|
||
exposes: { './Editor': './src/Editor' },
|
||
}),
|
||
],
|
||
})
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
FederationName: "myplugin",
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) != 0 {
|
||
t.Fatalf("expected 0 violations for correct federation name, got %d: %#v", len(vs), vs)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_FederationName_Wrong(t *testing.T) {
|
||
dir := t.TempDir()
|
||
webDir := filepath.Join(dir, "web")
|
||
if err := os.MkdirAll(webDir, 0755); err != nil {
|
||
t.Fatalf("mkdir web: %v", err)
|
||
}
|
||
writeFile(t, webDir, "vite.config.ts", `
|
||
import { defineConfig } from 'vite'
|
||
|
||
export default defineConfig({
|
||
plugins: [
|
||
federation({
|
||
name: "wrongname",
|
||
}),
|
||
],
|
||
})
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
FederationName: "myplugin",
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) == 0 {
|
||
t.Fatal("expected violations for wrong federation name, got 0")
|
||
}
|
||
if vs[0].rule != "federation_name" {
|
||
t.Fatalf("expected rule 'federation_name', got %q", vs[0].rule)
|
||
}
|
||
}
|
||
|
||
func TestCheckSegmentation_FederationName_NoNameDecl(t *testing.T) {
|
||
dir := t.TempDir()
|
||
webDir := filepath.Join(dir, "web")
|
||
if err := os.MkdirAll(webDir, 0755); err != nil {
|
||
t.Fatalf("mkdir web: %v", err)
|
||
}
|
||
// vite.config.ts without a federation name: "..." declaration
|
||
writeFile(t, webDir, "vite.config.ts", `
|
||
export default {}
|
||
`)
|
||
|
||
rules := &SegmentationRules{
|
||
FederationName: "myplugin",
|
||
}
|
||
vs := checkSegmentation(dir, "myplugin", rules)
|
||
if len(vs) == 0 {
|
||
t.Fatal("expected violation when federation name declaration is absent, got 0")
|
||
}
|
||
if vs[0].rule != "federation_name" {
|
||
t.Fatalf("expected rule 'federation_name', got %q", vs[0].rule)
|
||
}
|
||
}
|