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>
718 lines
25 KiB
Go
718 lines
25 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCheckSafetySkipsRPCCheckForPluginWithoutRPCs(t *testing.T) {
|
|
pluginRoot := t.TempDir()
|
|
sdkVersion, err := currentCMSCoreSDKVersion()
|
|
if err != nil {
|
|
t.Fatalf("currentCMSCoreSDKVersion: %v", err)
|
|
}
|
|
writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "main.go"), "package example\n", 0644)
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("getwd: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command("go", "run", ".", pluginRoot)
|
|
cmd.Dir = cwd
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("go run check-safety: %v\n%s", err, output)
|
|
}
|
|
|
|
got := string(output)
|
|
if !strings.Contains(got, "=== Check 2: RPC methods registered in RBAC interceptor ===") {
|
|
t.Fatalf("output missing Check 2 header:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "SKIP: no proto-backed RPC procedures found in scanned target(s)") {
|
|
t.Fatalf("output missing no-RPC plugin skip:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestCheckSafetyTypechecksExternalPluginFrontendWithWorkspaceDeps(t *testing.T) {
|
|
pluginRoot := t.TempDir()
|
|
pluginWebDir := filepath.Join(pluginRoot, "web")
|
|
sdkVersion, err := currentCMSCoreSDKVersion()
|
|
if err != nil {
|
|
t.Fatalf("currentCMSCoreSDKVersion: %v", err)
|
|
}
|
|
realRepoRoot := blockNinjaRepoRoot()
|
|
relESLintConfig, err := filepath.Rel(pluginWebDir, filepath.Join(realRepoRoot, "web", "eslint.config.js"))
|
|
if err != nil {
|
|
t.Fatalf("rel eslint config: %v", err)
|
|
}
|
|
|
|
writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "main.go"), "package example\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginWebDir, "package.json"), "{\n \"name\": \"external-plugin-test\",\n \"private\": true,\n \"type\": \"module\",\n \"dependencies\": {\n \"@block-ninja/ui\": \"workspace:*\"\n }\n}\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginWebDir, "tsconfig.json"), "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n \"jsx\": \"react-jsx\",\n \"strict\": true,\n \"skipLibCheck\": true,\n \"noEmit\": true\n },\n \"include\": [\"*.tsx\", \"*.ts\"]\n}\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginWebDir, "eslint.config.js"), "import config from "+strconv.Quote(filepath.ToSlash(relESLintConfig))+"\n\nexport default config\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginWebDir, "editor.tsx"), "import { Separator } from '@block-ninja/ui'\n\nexport function Editor() {\n\treturn <Separator />\n}\n", 0644)
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("getwd: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command("go", "run", ".", pluginRoot)
|
|
cmd.Dir = cwd
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("go run check-safety: %v\n%s", err, output)
|
|
}
|
|
|
|
got := string(output)
|
|
if !strings.Contains(got, "=== Check 4: Frontend auto-format, lint, and typecheck ===") {
|
|
t.Fatalf("output missing Check 4 header:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "OK: Prettier, ESLint, and TypeScript checks clean for 1 frontend target") {
|
|
t.Fatalf("output missing successful frontend typecheck result:\n%s", got)
|
|
}
|
|
if strings.Contains(got, "Cannot find module '@block-ninja/ui'") {
|
|
t.Fatalf("output still shows unresolved workspace dependency:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestCheckSafetyFailsStandalonePluginImportsFromBlockNinjaCMS(t *testing.T) {
|
|
pluginRoot := t.TempDir()
|
|
writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "main.go"), `package example
|
|
|
|
import "git.dev.alexdunmow.com/block/ninja/internal/helpers"
|
|
|
|
func Example() string {
|
|
return helpers.Slugify("danger")
|
|
}
|
|
`, 0644)
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("getwd: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command("go", "run", ".", pluginRoot)
|
|
cmd.Dir = cwd
|
|
output, err := cmd.CombinedOutput()
|
|
if err == nil {
|
|
t.Fatalf("expected go run check-safety to fail\n%s", output)
|
|
}
|
|
|
|
got := string(output)
|
|
if !strings.Contains(got, "=== Check 2c: Standalone plugin SDK import boundaries ===") {
|
|
t.Fatalf("output missing Check 2c header:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "imports BlockNinja CMS package") {
|
|
t.Fatalf("output missing forbidden BlockNinja import message:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "use git.dev.alexdunmow.com/block/core") {
|
|
t.Fatalf("output missing remediation text:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestCheckSafetyFailsStandalonePluginReplaceDirective(t *testing.T) {
|
|
pluginRoot := t.TempDir()
|
|
sdkVersion, err := currentCMSCoreSDKVersion()
|
|
if err != nil {
|
|
t.Fatalf("currentCMSCoreSDKVersion: %v", err)
|
|
}
|
|
writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n\nreplace git.dev.alexdunmow.com/block/core => ../block-core\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "main.go"), "package example\n", 0644)
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("getwd: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command("go", "run", ".", pluginRoot)
|
|
cmd.Dir = cwd
|
|
output, err := cmd.CombinedOutput()
|
|
if err == nil {
|
|
t.Fatalf("expected go run check-safety to fail\n%s", output)
|
|
}
|
|
|
|
got := string(output)
|
|
if !strings.Contains(got, "no replace directives") {
|
|
t.Fatalf("output missing no replace guidance:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "replace directive present") {
|
|
t.Fatalf("output missing replace violation detail:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestCheckSafetyFailsStandalonePluginSQLCUUIDStringOverrides(t *testing.T) {
|
|
pluginRoot := t.TempDir()
|
|
sdkVersion, err := currentCMSCoreSDKVersion()
|
|
if err != nil {
|
|
t.Fatalf("currentCMSCoreSDKVersion: %v", err)
|
|
}
|
|
writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "main.go"), "package example\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "sqlc.yaml"), `version: "2"
|
|
sql:
|
|
- engine: "postgresql"
|
|
gen:
|
|
go:
|
|
overrides:
|
|
- db_type: "uuid"
|
|
go_type: "string"
|
|
- db_type: "uuid"
|
|
nullable: true
|
|
go_type:
|
|
type: "string"
|
|
pointer: true
|
|
`, 0644)
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("getwd: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command("go", "run", ".", pluginRoot)
|
|
cmd.Dir = cwd
|
|
output, err := cmd.CombinedOutput()
|
|
if err == nil {
|
|
t.Fatalf("expected go run check-safety to fail\n%s", output)
|
|
}
|
|
|
|
got := string(output)
|
|
if !strings.Contains(got, "=== Check 2d: sqlc UUID overrides in plugins and cmd ===") {
|
|
t.Fatalf("output missing Check 2d header:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "github.com/google/uuid.UUID") {
|
|
t.Fatalf("output missing UUID remediation:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "github.com/google/uuid.NullUUID") {
|
|
t.Fatalf("output missing NullUUID remediation:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestCheckSafetyWarnsOnStandalonePluginGoAnyUsage(t *testing.T) {
|
|
pluginRoot := t.TempDir()
|
|
sdkVersion, err := currentCMSCoreSDKVersion()
|
|
if err != nil {
|
|
t.Fatalf("currentCMSCoreSDKVersion: %v", err)
|
|
}
|
|
writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "main.go"), `package example
|
|
|
|
type Payload struct {
|
|
Value any
|
|
}
|
|
`, 0644)
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("getwd: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command("go", "run", ".", pluginRoot)
|
|
cmd.Dir = cwd
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("expected go run check-safety to pass with warning: %v\n%s", err, output)
|
|
}
|
|
|
|
got := string(output)
|
|
if !strings.Contains(got, "=== Check 2e: Warn on any usage in Go and TypeScript ===") {
|
|
t.Fatalf("output missing Check 2e header:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "WARN:") || !strings.Contains(got, "[go]") {
|
|
t.Fatalf("output missing Go any warning:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestCheckSafetyFailsStandalonePluginSQLCCompile(t *testing.T) {
|
|
pluginRoot := t.TempDir()
|
|
sdkVersion, err := currentCMSCoreSDKVersion()
|
|
if err != nil {
|
|
t.Fatalf("currentCMSCoreSDKVersion: %v", err)
|
|
}
|
|
writeTestFile(t, filepath.Join(pluginRoot, "plugin.mod"), "[plugin]\nname = \"example\"\nversion = \"1.0.0\"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "go.mod"), "module example.com/plugin\n\ngo 1.26.2\n\nrequire git.dev.alexdunmow.com/block/core "+sdkVersion+"\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "main.go"), "package example\n", 0644)
|
|
writeTestFile(t, filepath.Join(pluginRoot, "sqlc.yaml"), `version: "2"
|
|
sql:
|
|
- engine: "postgresql"
|
|
queries: "sql/queries"
|
|
schema: "sql/schema.sql"
|
|
gen:
|
|
go:
|
|
package: "db"
|
|
out: "db"
|
|
`, 0644)
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("getwd: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command("go", "run", ".", pluginRoot)
|
|
cmd.Dir = cwd
|
|
output, err := cmd.CombinedOutput()
|
|
if err == nil {
|
|
t.Fatalf("expected go run check-safety to fail\n%s", output)
|
|
}
|
|
|
|
got := string(output)
|
|
if !strings.Contains(got, "=== Check 2f: sqlc compile and buf generate ===") {
|
|
t.Fatalf("output missing Check 2f header:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "[sqlc compile]") {
|
|
t.Fatalf("output missing sqlc compile stage:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestRunFrontendAutoFixAndLintFailsTypeScriptTargetsAtTSC(t *testing.T) {
|
|
repoRoot, sourceDir := setupFrontendLintTestPackage(t, frontendLintTestOptions{
|
|
withTSConfig: true,
|
|
tscScript: "#!/bin/sh\n" +
|
|
"echo \"src/index.ts(1,1): error TS2322: Type 'string' is not assignable to type 'number'.\" >&2\n" +
|
|
"exit 1\n",
|
|
})
|
|
|
|
completed, skipped, failures, err := runFrontendAutoFixAndLint(repoRoot, []frontendScanTarget{
|
|
{dir: sourceDir, label: "frontend/src"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("runFrontendAutoFixAndLint() error = %v", err)
|
|
}
|
|
if len(completed) != 0 {
|
|
t.Fatalf("completed = %v, want none", completed)
|
|
}
|
|
if len(skipped) != 0 {
|
|
t.Fatalf("skipped = %v, want none", skipped)
|
|
}
|
|
if len(failures) != 1 {
|
|
t.Fatalf("failures = %d, want 1", len(failures))
|
|
}
|
|
if failures[0].stage != "tsc --noEmit" {
|
|
t.Fatalf("failure stage = %q, want %q", failures[0].stage, "tsc --noEmit")
|
|
}
|
|
if !strings.Contains(failures[0].output, "TS2322") {
|
|
t.Fatalf("failure output = %q, want TS2322", failures[0].output)
|
|
}
|
|
}
|
|
|
|
func TestRunFrontendAutoFixAndLintFailsWhenTypeScriptTargetHasNoTSConfig(t *testing.T) {
|
|
repoRoot, sourceDir := setupFrontendLintTestPackage(t, frontendLintTestOptions{})
|
|
|
|
completed, skipped, failures, err := runFrontendAutoFixAndLint(repoRoot, []frontendScanTarget{
|
|
{dir: sourceDir, label: "frontend/src"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("runFrontendAutoFixAndLint() error = %v", err)
|
|
}
|
|
if len(completed) != 0 {
|
|
t.Fatalf("completed = %v, want none", completed)
|
|
}
|
|
if len(skipped) != 0 {
|
|
t.Fatalf("skipped = %v, want none", skipped)
|
|
}
|
|
if len(failures) != 1 {
|
|
t.Fatalf("failures = %d, want 1", len(failures))
|
|
}
|
|
if failures[0].stage != "tsc --noEmit" {
|
|
t.Fatalf("failure stage = %q, want %q", failures[0].stage, "tsc --noEmit")
|
|
}
|
|
if !strings.Contains(failures[0].output, "no tsconfig.json was located") {
|
|
t.Fatalf("failure output = %q, want missing tsconfig message", failures[0].output)
|
|
}
|
|
}
|
|
|
|
func TestRunFrontendAutoFixAndLintRunsTSCBeforePrettierAndESLint(t *testing.T) {
|
|
repoRoot, sourceDir := setupFrontendLintTestPackage(t, frontendLintTestOptions{
|
|
withTSConfig: true,
|
|
tscScript: "#!/bin/sh\n" +
|
|
"echo \"src/index.ts(1,1): error TS9999: forced type failure.\" >&2\n" +
|
|
"exit 1\n",
|
|
})
|
|
|
|
packageRoot := filepath.Dir(sourceDir)
|
|
prettierMarker := filepath.Join(packageRoot, "prettier-ran")
|
|
eslintMarker := filepath.Join(packageRoot, "eslint-ran")
|
|
writeExecutable(t, filepath.Join(packageRoot, "node_modules", ".bin", "prettier"), "#!/bin/sh\n"+
|
|
"echo ran > "+shellQuote(prettierMarker)+"\n"+
|
|
"exit 0\n")
|
|
writeExecutable(t, filepath.Join(packageRoot, "node_modules", ".bin", "eslint"), "#!/bin/sh\n"+
|
|
"echo ran > "+shellQuote(eslintMarker)+"\n"+
|
|
"exit 0\n")
|
|
|
|
_, _, failures, err := runFrontendAutoFixAndLint(repoRoot, []frontendScanTarget{
|
|
{dir: sourceDir, label: "frontend/src"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("runFrontendAutoFixAndLint() error = %v", err)
|
|
}
|
|
if len(failures) != 1 {
|
|
t.Fatalf("failures = %d, want 1", len(failures))
|
|
}
|
|
if failures[0].stage != "tsc --noEmit" {
|
|
t.Fatalf("failure stage = %q, want %q", failures[0].stage, "tsc --noEmit")
|
|
}
|
|
if _, err := os.Stat(prettierMarker); !os.IsNotExist(err) {
|
|
t.Fatalf("prettier marker exists, want TypeScript failure to short-circuit before prettier")
|
|
}
|
|
if _, err := os.Stat(eslintMarker); !os.IsNotExist(err) {
|
|
t.Fatalf("eslint marker exists, want TypeScript failure to short-circuit before eslint")
|
|
}
|
|
}
|
|
|
|
func TestRunFrontendAutoFixAndLintUsesSyntheticWorkspaceForPluginTargets(t *testing.T) {
|
|
repoRoot, sourceDir := setupFrontendLintTestPackage(t, frontendLintTestOptions{
|
|
withTSConfig: true,
|
|
})
|
|
|
|
hostNodeModules := filepath.Join(repoRoot, "web", "node_modules")
|
|
if err := os.MkdirAll(hostNodeModules, 0755); err != nil {
|
|
t.Fatalf("mkdir host node_modules: %v", err)
|
|
}
|
|
hostBinDir := filepath.Join(hostNodeModules, ".bin")
|
|
if err := os.MkdirAll(hostBinDir, 0755); err != nil {
|
|
t.Fatalf("mkdir host bin dir: %v", err)
|
|
}
|
|
writeExecutable(t, filepath.Join(hostBinDir, "tsc"), "#!/bin/sh\n"+
|
|
"if [ ! -e node_modules/react ]; then echo \"missing plugin dep\" >&2; exit 1; fi\n"+
|
|
"if [ ! -e node_modules/@block-ninja/api ]; then echo \"missing blockninja api\" >&2; exit 1; fi\n"+
|
|
"if [ ! -e package.json ]; then echo \"missing package\" >&2; exit 1; fi\n"+
|
|
"exit 0\n")
|
|
writeExecutable(t, filepath.Join(hostBinDir, "prettier"), "#!/bin/sh\nexit 0\n")
|
|
writeExecutable(t, filepath.Join(hostBinDir, "eslint"), "#!/bin/sh\nexit 0\n")
|
|
if err := os.MkdirAll(filepath.Join(hostNodeModules, "@block-ninja", "api"), 0755); err != nil {
|
|
t.Fatalf("mkdir host @block-ninja/api: %v", err)
|
|
}
|
|
|
|
packageRoot := filepath.Dir(sourceDir)
|
|
if err := os.MkdirAll(filepath.Join(packageRoot, "node_modules"), 0755); err != nil {
|
|
t.Fatalf("mkdir local node_modules: %v", err)
|
|
}
|
|
writeExecutable(t, filepath.Join(packageRoot, "node_modules", "react"), "#!/bin/sh\nexit 0\n")
|
|
|
|
completed, skipped, failures, err := runFrontendAutoFixAndLint(repoRoot, []frontendScanTarget{
|
|
{dir: sourceDir, label: "plugin/web/src", pluginRules: true},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("runFrontendAutoFixAndLint() error = %v", err)
|
|
}
|
|
if len(skipped) != 0 {
|
|
t.Fatalf("skipped = %v, want none", skipped)
|
|
}
|
|
if len(failures) != 0 {
|
|
t.Fatalf("failures = %v, want none", failures)
|
|
}
|
|
if len(completed) != 1 {
|
|
t.Fatalf("completed = %v, want one completed target", completed)
|
|
}
|
|
}
|
|
|
|
func TestRunFrontendAutoFixAndLintCreatesDefaultConfigsForManagedStandaloneTargets(t *testing.T) {
|
|
repoRoot, sourceDir := setupFrontendLintTestPackage(t, frontendLintTestOptions{
|
|
withTSConfig: true,
|
|
})
|
|
packageRoot := filepath.Dir(sourceDir)
|
|
|
|
if err := os.Remove(filepath.Join(packageRoot, "eslint.config.js")); err != nil {
|
|
t.Fatalf("remove eslint config: %v", err)
|
|
}
|
|
|
|
completed, skipped, failures, err := runFrontendAutoFixAndLint(repoRoot, []frontendScanTarget{
|
|
{dir: sourceDir, label: "managed/frontend/src", managedConfigs: true},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("runFrontendAutoFixAndLint() error = %v", err)
|
|
}
|
|
if len(skipped) != 0 {
|
|
t.Fatalf("skipped = %v, want none", skipped)
|
|
}
|
|
if len(failures) != 0 {
|
|
t.Fatalf("failures = %v, want none", failures)
|
|
}
|
|
if len(completed) != 1 {
|
|
t.Fatalf("completed = %v, want one completed target", completed)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(packageRoot, "eslint.config.js")); err != nil {
|
|
t.Fatalf("expected generated eslint config: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(packageRoot, "prettier.config.cjs")); err != nil {
|
|
t.Fatalf("expected generated prettier config: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunFrontendAutoFixAndLintOmitsExtForFlatConfigTargets(t *testing.T) {
|
|
repoRoot, sourceDir := setupFrontendLintTestPackage(t, frontendLintTestOptions{
|
|
withTSConfig: true,
|
|
tscScript: "#!/bin/sh\nexit 0\n",
|
|
})
|
|
packageRoot := filepath.Dir(sourceDir)
|
|
writeExecutable(t, filepath.Join(packageRoot, "node_modules", ".bin", "eslint"), "#!/bin/sh\n"+
|
|
"for arg in \"$@\"; do\n"+
|
|
" if [ \"$arg\" = \"--ext\" ]; then\n"+
|
|
" echo \"unexpected --ext\" >&2\n"+
|
|
" exit 1\n"+
|
|
" fi\n"+
|
|
"done\n"+
|
|
"exit 0\n")
|
|
|
|
completed, skipped, failures, err := runFrontendAutoFixAndLint(repoRoot, []frontendScanTarget{
|
|
{dir: sourceDir, label: "frontend/src"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("runFrontendAutoFixAndLint() error = %v", err)
|
|
}
|
|
if len(skipped) != 0 {
|
|
t.Fatalf("skipped = %v, want none", skipped)
|
|
}
|
|
if len(failures) != 0 {
|
|
t.Fatalf("failures = %v, want none", failures)
|
|
}
|
|
if len(completed) != 1 {
|
|
t.Fatalf("completed = %v, want one completed target", completed)
|
|
}
|
|
}
|
|
|
|
func TestRunFrontendAutoFixAndLintPrefersWorkspaceESLintForManagedTargets(t *testing.T) {
|
|
repoRoot, sourceDir := setupFrontendLintTestPackage(t, frontendLintTestOptions{
|
|
withTSConfig: true,
|
|
tscScript: "#!/bin/sh\nexit 0\n",
|
|
})
|
|
packageRoot := filepath.Dir(sourceDir)
|
|
|
|
if err := os.Remove(filepath.Join(packageRoot, "eslint.config.js")); err != nil {
|
|
t.Fatalf("remove eslint config: %v", err)
|
|
}
|
|
|
|
writeExecutable(t, filepath.Join(packageRoot, "node_modules", ".bin", "eslint"), "#!/bin/sh\n"+
|
|
"echo local-eslint-should-not-run >&2\n"+
|
|
"exit 1\n")
|
|
|
|
workspaceBinDir := filepath.Join(repoRoot, "web", "node_modules", ".bin")
|
|
if err := os.MkdirAll(workspaceBinDir, 0755); err != nil {
|
|
t.Fatalf("mkdir workspace bin dir: %v", err)
|
|
}
|
|
writeExecutable(t, filepath.Join(workspaceBinDir, "eslint"), "#!/bin/sh\nexit 0\n")
|
|
|
|
completed, skipped, failures, err := runFrontendAutoFixAndLint(repoRoot, []frontendScanTarget{
|
|
{dir: sourceDir, label: "managed/frontend/src", managedConfigs: true},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("runFrontendAutoFixAndLint() error = %v", err)
|
|
}
|
|
if len(skipped) != 0 {
|
|
t.Fatalf("skipped = %v, want none", skipped)
|
|
}
|
|
if len(failures) != 0 {
|
|
t.Fatalf("failures = %v, want none", failures)
|
|
}
|
|
if len(completed) != 1 {
|
|
t.Fatalf("completed = %v, want one completed target", completed)
|
|
}
|
|
}
|
|
|
|
func TestRunStrictGoLintFailsCompileCheck(t *testing.T) {
|
|
_, moduleRoot := setupGoLintTestModule(t)
|
|
vetMarker := filepath.Join(t.TempDir(), "vet-ran")
|
|
|
|
goBin := writeExecutableFile(t, filepath.Join(t.TempDir(), "go"), "#!/bin/sh\n"+
|
|
"case \"$1\" in\n"+
|
|
" fix)\n"+
|
|
" exit 0\n"+
|
|
" ;;\n"+
|
|
" test)\n"+
|
|
" echo \"compile failure: undefined: brokenSymbol\" >&2\n"+
|
|
" exit 1\n"+
|
|
" ;;\n"+
|
|
" vet)\n"+
|
|
" echo ran > "+shellQuote(vetMarker)+"\n"+
|
|
" exit 0\n"+
|
|
" ;;\n"+
|
|
"esac\n"+
|
|
"exit 0\n")
|
|
lintBin := writeExecutableFile(t, filepath.Join(t.TempDir(), "golangci-lint"), "#!/bin/sh\nexit 0\n")
|
|
|
|
completed, skipped, failures, err := runStrictGoLintWithBinaries([]goLintTarget{
|
|
{label: "backend", moduleRoot: moduleRoot},
|
|
}, nil, goBin, lintBin)
|
|
if err != nil {
|
|
t.Fatalf("runStrictGoLintWithBinaries() error = %v", err)
|
|
}
|
|
if len(completed) != 0 {
|
|
t.Fatalf("completed = %v, want none", completed)
|
|
}
|
|
if len(skipped) != 0 {
|
|
t.Fatalf("skipped = %v, want none", skipped)
|
|
}
|
|
if len(failures) != 1 {
|
|
t.Fatalf("failures = %d, want 1", len(failures))
|
|
}
|
|
if failures[0].stage != "go test -run=^$ ./..." {
|
|
t.Fatalf("failure stage = %q, want compile stage", failures[0].stage)
|
|
}
|
|
if !strings.Contains(failures[0].output, "brokenSymbol") {
|
|
t.Fatalf("failure output = %q, want compile error", failures[0].output)
|
|
}
|
|
if _, err := os.Stat(vetMarker); !os.IsNotExist(err) {
|
|
t.Fatalf("vet marker exists, want compile failure to stop before go vet")
|
|
}
|
|
}
|
|
|
|
func TestRunStrictGoLintRunsCompileBeforeVetAndFinalLint(t *testing.T) {
|
|
_, moduleRoot := setupGoLintTestModule(t)
|
|
logPath := filepath.Join(t.TempDir(), "go-lint-order.log")
|
|
|
|
goBin := writeExecutableFile(t, filepath.Join(t.TempDir(), "go"), "#!/bin/sh\n"+
|
|
"echo go-$1 >> "+shellQuote(logPath)+"\n"+
|
|
"exit 0\n")
|
|
lintBin := writeExecutableFile(t, filepath.Join(t.TempDir(), "golangci-lint"), "#!/bin/sh\n"+
|
|
"if [ \"$2\" = \"--fix\" ]; then\n"+
|
|
" echo golangci-fix >> "+shellQuote(logPath)+"\n"+
|
|
" exit 0\n"+
|
|
"fi\n"+
|
|
"echo golangci >> "+shellQuote(logPath)+"\n"+
|
|
"exit 0\n")
|
|
|
|
completed, skipped, failures, err := runStrictGoLintWithBinaries([]goLintTarget{
|
|
{label: "backend", moduleRoot: moduleRoot},
|
|
}, nil, goBin, lintBin)
|
|
if err != nil {
|
|
t.Fatalf("runStrictGoLintWithBinaries() error = %v", err)
|
|
}
|
|
if len(skipped) != 0 {
|
|
t.Fatalf("skipped = %v, want none", skipped)
|
|
}
|
|
if len(failures) != 0 {
|
|
t.Fatalf("failures = %v, want none", failures)
|
|
}
|
|
if len(completed) != 1 {
|
|
t.Fatalf("completed = %v, want one completed target", completed)
|
|
}
|
|
|
|
content, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatalf("read log: %v", err)
|
|
}
|
|
got := strings.Fields(string(content))
|
|
want := []string{"go-fix", "golangci-fix", "go-test", "go-vet", "golangci"}
|
|
if strings.Join(got, ",") != strings.Join(want, ",") {
|
|
t.Fatalf("command order = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRunGoTestTargetsRunsFullPackageTests(t *testing.T) {
|
|
moduleRoot := t.TempDir()
|
|
logPath := filepath.Join(t.TempDir(), "go-test.log")
|
|
goBin := writeExecutableFile(t, filepath.Join(t.TempDir(), "go"), "#!/bin/sh\n"+
|
|
"pwd > "+shellQuote(logPath)+"\n"+
|
|
"printf ' %s' \"$@\" >> "+shellQuote(logPath)+"\n"+
|
|
"exit 0\n")
|
|
|
|
completed, failures, err := runGoTestTargetsWithBinary([]goTestTarget{
|
|
{label: "orchestrator/backend", moduleRoot: moduleRoot, args: []string{"test", "./..."}},
|
|
}, goBin)
|
|
if err != nil {
|
|
t.Fatalf("runGoTestTargetsWithBinary() error = %v", err)
|
|
}
|
|
if len(failures) != 0 {
|
|
t.Fatalf("failures = %v, want none", failures)
|
|
}
|
|
if len(completed) != 1 || completed[0] != "orchestrator/backend [go test ./...]" {
|
|
t.Fatalf("completed = %v, want orchestrator/backend go test", completed)
|
|
}
|
|
|
|
content, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatalf("read log: %v", err)
|
|
}
|
|
if !strings.Contains(string(content), moduleRoot+"\n test ./...") {
|
|
t.Fatalf("go command log = %q, want module root and full test args", content)
|
|
}
|
|
}
|
|
|
|
type frontendLintTestOptions struct {
|
|
withTSConfig bool
|
|
tscScript string
|
|
}
|
|
|
|
func setupFrontendLintTestPackage(t *testing.T, opts frontendLintTestOptions) (repoRoot string, sourceDir string) {
|
|
t.Helper()
|
|
|
|
repoRoot = t.TempDir()
|
|
packageRoot := filepath.Join(repoRoot, "frontend")
|
|
sourceDir = filepath.Join(packageRoot, "src")
|
|
binDir := filepath.Join(packageRoot, "node_modules", ".bin")
|
|
|
|
for _, dir := range []string{sourceDir, binDir} {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", dir, err)
|
|
}
|
|
}
|
|
|
|
writeTestFile(t, filepath.Join(packageRoot, "package.json"), "{\n \"name\": \"frontend-test\"\n}\n", 0644)
|
|
writeTestFile(t, filepath.Join(packageRoot, "eslint.config.js"), "export default []\n", 0644)
|
|
writeTestFile(t, filepath.Join(sourceDir, "index.ts"), "export const value: number = 1\n", 0644)
|
|
writeExecutable(t, filepath.Join(binDir, "prettier"), "#!/bin/sh\nexit 0\n")
|
|
writeExecutable(t, filepath.Join(binDir, "eslint"), "#!/bin/sh\nexit 0\n")
|
|
|
|
if opts.withTSConfig {
|
|
writeTestFile(t, filepath.Join(packageRoot, "tsconfig.json"), "{\n \"compilerOptions\": {\n \"target\": \"ES2020\"\n },\n \"include\": [\"src/**/*\"]\n}\n", 0644)
|
|
}
|
|
if opts.tscScript != "" {
|
|
writeExecutable(t, filepath.Join(binDir, "tsc"), opts.tscScript)
|
|
}
|
|
|
|
return repoRoot, sourceDir
|
|
}
|
|
|
|
func setupGoLintTestModule(t *testing.T) (repoRoot string, moduleRoot string) {
|
|
t.Helper()
|
|
|
|
repoRoot = t.TempDir()
|
|
moduleRoot = filepath.Join(repoRoot, "backend")
|
|
if err := os.MkdirAll(moduleRoot, 0755); err != nil {
|
|
t.Fatalf("mkdir module root: %v", err)
|
|
}
|
|
|
|
writeTestFile(t, filepath.Join(moduleRoot, "go.mod"), "module example.com/checksafetytest\n\ngo 1.24.0\n", 0644)
|
|
writeTestFile(t, filepath.Join(moduleRoot, "main.go"), "package main\n\nfunc main() {}\n", 0644)
|
|
return repoRoot, moduleRoot
|
|
}
|
|
|
|
func writeExecutable(t *testing.T, path string, content string) {
|
|
t.Helper()
|
|
writeTestFile(t, path, content, 0755)
|
|
}
|
|
|
|
func writeExecutableFile(t *testing.T, path string, content string) string {
|
|
t.Helper()
|
|
writeExecutable(t, path, content)
|
|
return path
|
|
}
|
|
|
|
func writeTestFile(t *testing.T, path string, content string, mode os.FileMode) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(content), mode); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func shellQuote(path string) string {
|
|
return "'" + strings.ReplaceAll(path, "'", `'"'"'`) + "'"
|
|
}
|