check-safety/lint_test.go
Alex Dunmow 57bca429cc chore: follow CMS module rename in rule strings + test fixtures
CMS Go module renamed from git.dev.alexdunmow.com/block/ninja to
git.dev.alexdunmow.com/block/cms (along with the git remote rename
on gitea). All import paths, string-literal rules, test fixtures,
and doc references updated; (historical 'ninja-orchestrator' refs
preserved). go mod tidy regenerated checksums.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 13:55:47 +08:00

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/cms/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, "'", `'"'"'`) + "'"
}