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>
281 lines
6.8 KiB
Go
281 lines
6.8 KiB
Go
package main
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// todoRoot is a convenience helper for these tests.
|
|
func todoRoot(root string) []todoScanRoot {
|
|
return []todoScanRoot{{root: root}}
|
|
}
|
|
|
|
func TestCheckTODOs_GoFileFlagsTODO(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "service.go", `package service
|
|
|
|
// TODO: implement retry logic
|
|
func Retry() {}
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected TODO violation in .go file")
|
|
}
|
|
if vs[0].file != "service.go" {
|
|
t.Fatalf("expected file 'service.go', got %q", vs[0].file)
|
|
}
|
|
if vs[0].line != 3 {
|
|
t.Fatalf("expected line 3, got %d", vs[0].line)
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_TSFileFlagsTODO(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "utils.ts", `export function foo() {
|
|
// TODO: add error handling
|
|
return null
|
|
}
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected TODO violation in .ts file")
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_TSXFileFlagsTODO(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "component.tsx", `export function Comp() {
|
|
// TODO(alex): fix this
|
|
return <div />
|
|
}
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected TODO violation in .tsx file")
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_HTMLFileFlagsTODO(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "page.html", `<!-- TODO: update hero copy -->
|
|
<h1>Hello</h1>
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected TODO violation in .html file")
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_TemplFileFlagsTODO(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "block.templ", `package block
|
|
|
|
templ Hero() {
|
|
// TODO: make dynamic
|
|
<h1>Static</h1>
|
|
}
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected TODO violation in .templ file")
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_SQLFileFlagsTODO(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "queries.sql", `-- TODO: add index
|
|
SELECT * FROM users;
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected TODO violation in .sql file")
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_ScssFileFlagsTODO(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "styles.scss", `// TODO: update colours
|
|
.btn { color: red; }
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected TODO violation in .scss file")
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_CaseInsensitive(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "service.go", `package service
|
|
|
|
// todo: lowercase todo should also match
|
|
func f() {}
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected TODO violation for lowercase 'todo'")
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_WithParens(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "service.go", `package service
|
|
|
|
// TODO(alex): with author attribution
|
|
func f() {}
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected TODO violation for 'TODO(alex)' form")
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_CleanFile_NoViolation(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "clean.go", `package service
|
|
|
|
// This is a regular comment about the implementation.
|
|
func DoWork() error {
|
|
return nil
|
|
}
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected no violations for clean file, got %d", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_TestGoFile_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "service_test.go", `package service
|
|
|
|
// TODO: add more test cases
|
|
func TestFoo(t *testing.T) {}
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected _test.go files to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_TestTSFile_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "utils.test.ts", `// TODO: more tests
|
|
describe("foo", () => {})
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected .test.ts files to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_TestTSXFile_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "comp.test.tsx", `// TODO: add render tests
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected .test.tsx files to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_SpecTSFile_Skipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "utils.spec.ts", `// TODO: cover edge cases
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected .spec.ts files to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_NonCodeFile_Skipped(t *testing.T) {
|
|
// .md files are not in todoFileExts — should be skipped
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "README.md", `# Project
|
|
|
|
TODO: finish the docs
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected .md files to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_NodeModulesSkipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
nmDir := filepath.Join(dir, "node_modules", "some-lib")
|
|
writeTestFile(t, filepath.Join(nmDir, "index.ts"), `// TODO: vendor file — should be skipped
|
|
`, 0644)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected node_modules to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_VendorDirSkipped(t *testing.T) {
|
|
dir := t.TempDir()
|
|
vendorDir := filepath.Join(dir, "vendor", "somelib")
|
|
writeTestFile(t, filepath.Join(vendorDir, "lib.go"), `package lib
|
|
// TODO: vendor should be skipped
|
|
`, 0644)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected vendor/ to be skipped, got %d violations", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_EmptyRootSkipped(t *testing.T) {
|
|
// An empty root string should be skipped without panicking
|
|
vs := checkTODOs([]todoScanRoot{{root: ""}})
|
|
if len(vs) != 0 {
|
|
t.Fatalf("expected no violations for empty root, got %d", len(vs))
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_DisplayPrefix(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeFile(t, dir, "service.go", `package service
|
|
// TODO: remove this
|
|
func f() {}
|
|
`)
|
|
vs := checkTODOs([]todoScanRoot{{root: dir, displayPrefix: "backend"}})
|
|
if len(vs) == 0 {
|
|
t.Fatal("expected TODO violation")
|
|
}
|
|
if vs[0].file != "backend/service.go" {
|
|
t.Fatalf("expected prefixed path 'backend/service.go', got %q", vs[0].file)
|
|
}
|
|
}
|
|
|
|
func TestCheckTODOs_MultipleFiles_SortedByFileAndLine(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// b.go has one TODO on line 2
|
|
writeFile(t, dir, "b.go", `package p
|
|
// TODO: b
|
|
func b() {}
|
|
`)
|
|
// a.go has two TODOs — line 3 and line 5
|
|
writeFile(t, dir, "a.go", `package p
|
|
func a() {
|
|
// TODO: first
|
|
x := 1
|
|
// TODO: second
|
|
_ = x
|
|
}
|
|
`)
|
|
vs := checkTODOs(todoRoot(dir))
|
|
if len(vs) != 3 {
|
|
t.Fatalf("expected 3 violations, got %d: %#v", len(vs), vs)
|
|
}
|
|
// Sorted: a.go first (alphabetically before b.go)
|
|
if vs[0].file != "a.go" {
|
|
t.Fatalf("first violation should be in a.go, got %q", vs[0].file)
|
|
}
|
|
if vs[0].line > vs[1].line {
|
|
t.Fatalf("violations in same file should be sorted by line")
|
|
}
|
|
if vs[2].file != "b.go" {
|
|
t.Fatalf("last violation should be in b.go, got %q", vs[2].file)
|
|
}
|
|
}
|