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>
335 lines
9.7 KiB
Go
335 lines
9.7 KiB
Go
package main
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// ─── checkComingSoon ──────────────────────────────────────────────────────
|
|
|
|
func TestCheckComingSoon_ComingSoon_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "hero.tsx", `export function Hero() {
|
|
return <div>Coming Soon</div>
|
|
}
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected violation for 'Coming Soon'")
|
|
}
|
|
if vs[0].file != "hero.tsx" {
|
|
t.Fatalf("expected file 'hero.tsx', got %q", vs[0].file)
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_UnderConstruction_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "page.tsx", `export function Page() {
|
|
return <p>Under Construction</p>
|
|
}
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected violation for 'Under Construction'")
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_WorkInProgress_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "dash.tsx", `export function Dash() {
|
|
return <span>Work in Progress</span>
|
|
}
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected violation for 'Work in Progress'")
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_NotYetAvailable_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "feature.tsx", `export function Feature() {
|
|
return <p>Not yet available</p>
|
|
}
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected violation for 'Not yet available'")
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_AvailableSoon_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "feature.tsx", `export function Feature() {
|
|
return <p>Available soon</p>
|
|
}
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected violation for 'Available soon'")
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_ComingLater_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "feature.tsx", `export function Feature() {
|
|
return <p>Coming later</p>
|
|
}
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected violation for 'Coming later'")
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_CaseInsensitive(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "feature.tsx", `export function Feature() {
|
|
return <p>COMING SOON</p>
|
|
}
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected case-insensitive match for 'COMING SOON'")
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_CommentLine_NotFlagged(t *testing.T) {
|
|
// isCommentLine() skips lines starting with //, *, /*
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "feature.tsx", `// TODO: coming soon section
|
|
/* coming soon banner */
|
|
export function Feature() {
|
|
return <div>Real content</div>
|
|
}
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected comment lines to be skipped, got %d violations: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_GoFile_Flagged(t *testing.T) {
|
|
// checkComingSoon uses todoFileExts which includes .go
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "handler.go", `package handler
|
|
|
|
const pageTitle = "Coming Soon"
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected violation for 'Coming Soon' in .go file")
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_TestTSXFile_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "comp.test.tsx", `// test: coming soon banner
|
|
describe("coming soon", () => {})
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected .test.tsx to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_TestTSFile_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "page.test.ts", `describe("coming soon", () => {})
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected .test.ts to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_NonCodeFile_Skipped(t *testing.T) {
|
|
// .md not in todoFileExts
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "ROADMAP.md", `## Coming Soon
|
|
|
|
- Feature A
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected .md file to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_AllowedFile_NotFlagged(t *testing.T) {
|
|
// comingSoonAllowedFiles contains specific web/src relative paths;
|
|
// checkComingSoon strips to relative from webSrcDir — so the file's relPath
|
|
// must match the allowlist key exactly.
|
|
dir := t.TempDir()
|
|
// "components/pages/system-pages-list.tsx" is an allowed file
|
|
allowedDir := filepath.Join(dir, "components", "pages")
|
|
writeTestFile(t, filepath.Join(allowedDir, "system-pages-list.tsx"), `export function SystemPages() {
|
|
return <li>Coming Soon</li>
|
|
}
|
|
`, 0644)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected allowed file to be exempt, got %d violations: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_NodeModules_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
nmDir := filepath.Join(dir, "node_modules", "some-lib")
|
|
writeTestFile(t, filepath.Join(nmDir, "index.ts"), `export const msg = "Coming Soon"
|
|
`, 0644)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected node_modules to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckComingSoon_SortedByFileAndLine(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "z.tsx", `export function Z() {
|
|
return <p>Coming Soon</p>
|
|
}
|
|
`)
|
|
writeFile(t, dir, "a.tsx", `export function A() {
|
|
return <div>Available soon</div>
|
|
}
|
|
`)
|
|
vs := checkComingSoon(dir)
|
|
if len(vs) < 2 {
|
|
t.Fatalf("expected at least 2 violations, got %d", len(vs))
|
|
}
|
|
if vs[0].file > vs[1].file {
|
|
t.Fatalf("violations should be sorted by file alphabetically, got %q then %q", vs[0].file, vs[1].file)
|
|
}
|
|
}
|
|
|
|
// ─── checkPlaceholderLanguage ─────────────────────────────────────────────
|
|
|
|
func scanRoots(root string) []todoScanRoot {
|
|
return []todoScanRoot{{root: root}}
|
|
}
|
|
|
|
func TestCheckPlaceholderLanguage_StringLiteralWithPlaceholder_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// A string literal containing lowercase "placeholder" as meaningful content (not an attribute).
|
|
// Note: rePlaceholderStringLiteral is case-sensitive — "placeholder" (lowercase) matches;
|
|
// "Placeholder" (initial cap) does NOT match.
|
|
writeFile(t, dir, "comp.tsx", `export function Comp() {
|
|
const label = "placeholder content here"
|
|
return <div>{label}</div>
|
|
}
|
|
`)
|
|
vs := checkPlaceholderLanguage(scanRoots(dir))
|
|
if len(vs) == 0 {
|
|
t.Fatalf("expected placeholder language violation for lowercase 'placeholder' string literal, got none")
|
|
}
|
|
}
|
|
|
|
func TestCheckPlaceholderLanguage_HTMLAttribute_NotFlagged(t *testing.T) {
|
|
// placeholder= is an HTML attribute — explicitly excluded
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "input.tsx", `export function Input() {
|
|
return <input placeholder="Enter your name" />
|
|
}
|
|
`)
|
|
vs := checkPlaceholderLanguage(scanRoots(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected HTML placeholder= attribute to be skipped, got %d violations: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckPlaceholderLanguage_GoFile_Flagged(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "handler.go", `package handler
|
|
|
|
// This is a "Placeholder feature"
|
|
func PlaceholderHandler() {}
|
|
`)
|
|
vs := checkPlaceholderLanguage(scanRoots(dir))
|
|
if len(vs) == 0 {
|
|
t.Fatalf("expected placeholder violation in .go file")
|
|
}
|
|
}
|
|
|
|
func TestCheckPlaceholderLanguage_TestGoFile_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "handler_test.go", `package handler
|
|
// "Placeholder content"
|
|
func TestFoo(t *testing.T) {}
|
|
`)
|
|
vs := checkPlaceholderLanguage(scanRoots(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected _test.go to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckPlaceholderLanguage_NonGoTSXFile_Skipped(t *testing.T) {
|
|
// checkPlaceholderLanguage only scans .go and .tsx
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "content.html", `<div>"Placeholder content"</div>
|
|
`)
|
|
vs := checkPlaceholderLanguage(scanRoots(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected .html file to be skipped by checkPlaceholderLanguage, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckPlaceholderLanguage_EmptyRoot_Skipped(t *testing.T) {
|
|
vs := checkPlaceholderLanguage([]todoScanRoot{{root: ""}})
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected no violations for empty root, got %d", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckPlaceholderLanguage_LiteralJustPlaceholder_NotFlagged(t *testing.T) {
|
|
// The word "placeholder" alone (without additional content) is excluded
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "comp.tsx", `export function Comp() {
|
|
const type = "placeholder"
|
|
return <div data-type={type} />
|
|
}
|
|
`)
|
|
vs := checkPlaceholderLanguage(scanRoots(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("bare 'placeholder' string should not be flagged, got %d violations: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckPlaceholderLanguage_PropertyAccess_NotFlagged(t *testing.T) {
|
|
// .placeholder (CSS property access) should not be flagged
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "styles.tsx", `const styles = {
|
|
color: input.placeholder,
|
|
}
|
|
`)
|
|
vs := checkPlaceholderLanguage(scanRoots(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected .placeholder property access to be skipped, got %d violations: %#v", len(vs), vs)
|
|
}
|
|
}
|
|
|
|
func TestCheckPlaceholderLanguage_DisplayPrefix(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// rePlaceholderStringLiteral is case-sensitive — use lowercase "placeholder"
|
|
writeFile(t, dir, "comp.tsx", `const x = "placeholder content here"
|
|
`)
|
|
vs := checkPlaceholderLanguage([]todoScanRoot{{root: dir, displayPrefix: "web/src"}})
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected violation")
|
|
}
|
|
if vs[0].file != "web/src/comp.tsx" {
|
|
t.Fatalf("expected prefixed path 'web/src/comp.tsx', got %q", vs[0].file)
|
|
}
|
|
}
|
|
|
|
func TestCheckPlaceholderLanguage_TestTSXFile_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "comp.test.tsx", `// "Placeholder content"
|
|
describe("test", () => {})
|
|
`)
|
|
vs := checkPlaceholderLanguage(scanRoots(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected .test.tsx to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|