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>
284 lines
7.5 KiB
Go
284 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// minimalValidPreset returns a presets.json that satisfies the strict decoder
|
|
// (all known fields, correct types, required id+name).
|
|
const minimalValidPresetJSON = `[
|
|
{
|
|
"id": "test-preset",
|
|
"name": "Test Preset",
|
|
"description": "A minimal test preset",
|
|
"theme": {
|
|
"lightColors": {
|
|
"background": "0 0% 100%",
|
|
"foreground": "240 10% 3.9%",
|
|
"card": "0 0% 100%",
|
|
"cardForeground": "240 10% 3.9%",
|
|
"popover": "0 0% 100%",
|
|
"popoverForeground": "240 10% 3.9%",
|
|
"primary": "240 5.9% 10%",
|
|
"primaryForeground": "0 0% 98%",
|
|
"secondary": "240 4.8% 95.9%",
|
|
"secondaryForeground": "240 5.9% 10%",
|
|
"muted": "240 4.8% 95.9%",
|
|
"mutedForeground": "240 3.8% 46.1%",
|
|
"accent": "240 4.8% 95.9%",
|
|
"accentForeground": "240 5.9% 10%",
|
|
"destructive": "0 84.2% 60.2%",
|
|
"destructiveForeground": "0 0% 98%",
|
|
"border": "240 5.9% 90%",
|
|
"input": "240 5.9% 90%",
|
|
"ring": "240 5.9% 10%"
|
|
},
|
|
"darkColors": {
|
|
"background": "240 10% 3.9%",
|
|
"foreground": "0 0% 98%",
|
|
"card": "240 10% 3.9%",
|
|
"cardForeground": "0 0% 98%",
|
|
"popover": "240 10% 3.9%",
|
|
"popoverForeground": "0 0% 98%",
|
|
"primary": "0 0% 98%",
|
|
"primaryForeground": "240 5.9% 10%",
|
|
"secondary": "240 3.7% 15.9%",
|
|
"secondaryForeground": "0 0% 98%",
|
|
"muted": "240 3.7% 15.9%",
|
|
"mutedForeground": "240 5% 64.9%",
|
|
"accent": "240 3.7% 15.9%",
|
|
"accentForeground": "0 0% 98%",
|
|
"destructive": "0 62.8% 30.6%",
|
|
"destructiveForeground": "0 0% 98%",
|
|
"border": "240 3.7% 15.9%",
|
|
"input": "240 3.7% 15.9%",
|
|
"ring": "240 4.9% 83.9%"
|
|
},
|
|
"typography": {
|
|
"fontHeading": "Inter",
|
|
"fontBody": "Inter",
|
|
"fontMono": "JetBrains Mono",
|
|
"fontSizeBase": "1rem",
|
|
"lineHeightBase": "1.5",
|
|
"fontWeightBase": "400"
|
|
},
|
|
"spacing": {
|
|
"radius": "0.5rem",
|
|
"buttonRadius": "0.5rem",
|
|
"cardRadius": "0.5rem",
|
|
"inputRadius": "0.5rem",
|
|
"pillButtons": false,
|
|
"borderWidth": "1px",
|
|
"containerWidth": "1280px",
|
|
"spacingScale": "1"
|
|
},
|
|
"effects": {
|
|
"shadowIntensity": 20,
|
|
"shadowColor": "220 3% 15%",
|
|
"shadowBlur": "8px",
|
|
"elevationPreset": "soft",
|
|
"transitionSpeed": "200ms",
|
|
"reduceMotion": false,
|
|
"hoverIntensity": "medium"
|
|
},
|
|
"mode": "light"
|
|
}
|
|
}
|
|
]`
|
|
|
|
func TestCheckPresets_ValidFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "presets.json", minimalValidPresetJSON)
|
|
|
|
violations := checkPresets(dir)
|
|
if len(violations) != 0 {
|
|
t.Fatalf("checkPresets() returned %d violations for valid presets.json, want 0: %#v", len(violations), violations)
|
|
}
|
|
}
|
|
|
|
func TestCheckPresets_EmptyArray(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "presets.json", `[]`)
|
|
|
|
violations := checkPresets(dir)
|
|
if len(violations) != 0 {
|
|
t.Fatalf("checkPresets() returned %d violations for empty array, want 0", len(violations))
|
|
}
|
|
}
|
|
|
|
func TestCheckPresets_MissingID(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// Preset with no "id" field (empty string after decode)
|
|
writeFile(t, dir, "presets.json", `[
|
|
{
|
|
"id": "",
|
|
"name": "My Preset",
|
|
"theme": {}
|
|
}
|
|
]`)
|
|
|
|
violations := checkPresets(dir)
|
|
if len(violations) == 0 {
|
|
t.Fatal("checkPresets() returned 0 violations for preset with empty id, want >= 1")
|
|
}
|
|
found := false
|
|
for _, v := range violations {
|
|
if containsStr(v.message, "missing \"id\"") {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected 'missing id' violation, got: %#v", violations)
|
|
}
|
|
}
|
|
|
|
func TestCheckPresets_MissingName(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "presets.json", `[
|
|
{
|
|
"id": "my-id",
|
|
"name": "",
|
|
"theme": {}
|
|
}
|
|
]`)
|
|
|
|
violations := checkPresets(dir)
|
|
if len(violations) == 0 {
|
|
t.Fatal("checkPresets() returned 0 violations for preset with empty name, want >= 1")
|
|
}
|
|
found := false
|
|
for _, v := range violations {
|
|
if containsStr(v.message, "missing \"name\"") {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected 'missing name' violation, got: %#v", violations)
|
|
}
|
|
}
|
|
|
|
func TestCheckPresets_BothIDAndNameMissing(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "presets.json", `[
|
|
{
|
|
"id": "",
|
|
"name": "",
|
|
"theme": {}
|
|
}
|
|
]`)
|
|
|
|
violations := checkPresets(dir)
|
|
if len(violations) < 2 {
|
|
t.Fatalf("checkPresets() returned %d violations, want at least 2 (id+name): %#v", len(violations), violations)
|
|
}
|
|
}
|
|
|
|
func TestCheckPresets_InvalidJSON(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "presets.json", `{ this is not valid json`)
|
|
|
|
violations := checkPresets(dir)
|
|
if len(violations) == 0 {
|
|
t.Fatal("checkPresets() returned 0 violations for invalid JSON, want >= 1")
|
|
}
|
|
found := false
|
|
for _, v := range violations {
|
|
if containsStr(v.message, "invalid JSON") {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected 'invalid JSON' violation, got: %#v", violations)
|
|
}
|
|
}
|
|
|
|
func TestCheckPresets_UnknownField(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// Valid except for an extra unknown field — triggers "stale preset data" path
|
|
writeFile(t, dir, "presets.json", `[
|
|
{
|
|
"id": "test",
|
|
"name": "Test",
|
|
"unknownExtraField": "oops",
|
|
"theme": {}
|
|
}
|
|
]`)
|
|
|
|
violations := checkPresets(dir)
|
|
if len(violations) == 0 {
|
|
t.Fatal("checkPresets() returned 0 violations for unknown field, want >= 1")
|
|
}
|
|
found := false
|
|
for _, v := range violations {
|
|
if containsStr(v.message, "unknown fields") || containsStr(v.message, "stale preset data") {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected 'unknown fields' violation, got: %#v", violations)
|
|
}
|
|
}
|
|
|
|
func TestCheckPresets_NoPresetsFileReturnsClean(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// No presets.json at all — should return 0 violations
|
|
violations := checkPresets(dir)
|
|
if len(violations) != 0 {
|
|
t.Fatalf("checkPresets() returned %d violations for dir without presets.json, want 0", len(violations))
|
|
}
|
|
}
|
|
|
|
func TestFindPresetsFiles_SkipsVendorAndWeb(t *testing.T) {
|
|
root := t.TempDir()
|
|
|
|
// Legitimate presets.json at root level
|
|
writeFile(t, root, "presets.json", `[]`)
|
|
|
|
// Should be skipped (vendor/)
|
|
vendorDir := filepath.Join(root, "vendor", "somelib")
|
|
if err := os.MkdirAll(vendorDir, 0755); err != nil {
|
|
t.Fatalf("mkdir vendor: %v", err)
|
|
}
|
|
writeFile(t, vendorDir, "presets.json", `[]`)
|
|
|
|
// Should be skipped (web/)
|
|
webDir := filepath.Join(root, "web", "src")
|
|
if err := os.MkdirAll(webDir, 0755); err != nil {
|
|
t.Fatalf("mkdir web: %v", err)
|
|
}
|
|
writeFile(t, webDir, "presets.json", `[]`)
|
|
|
|
files := findPresetsFiles(root)
|
|
if len(files) != 1 {
|
|
t.Fatalf("findPresetsFiles() returned %d files, want 1 (only root-level): %v", len(files), files)
|
|
}
|
|
}
|
|
|
|
func TestFindPresetsFiles_FindsInSubdir(t *testing.T) {
|
|
root := t.TempDir()
|
|
pluginDir := filepath.Join(root, "internal", "plugins", "myplugin")
|
|
if err := os.MkdirAll(pluginDir, 0755); err != nil {
|
|
t.Fatalf("mkdir plugin: %v", err)
|
|
}
|
|
writeFile(t, pluginDir, "presets.json", `[]`)
|
|
|
|
files := findPresetsFiles(root)
|
|
if len(files) != 1 {
|
|
t.Fatalf("findPresetsFiles() returned %d files, want 1: %v", len(files), files)
|
|
}
|
|
}
|
|
|
|
// containsStr is a helper to test substring containment.
|
|
func containsStr(s, substr string) bool {
|
|
return len(s) > 0 && len(substr) > 0 && func() bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}()
|
|
}
|