package cli import ( "bytes" "encoding/json" "os" "path/filepath" "strings" "testing" ) func writeMDSpec(t *testing.T, content string) string { t.Helper() dir := t.TempDir() p := filepath.Join(dir, "spec.md") if err := os.WriteFile(p, []byte(content), 0o644); err != nil { t.Fatalf("write spec: %v", err) } return p } func writeHCLSpec(t *testing.T, content string) string { t.Helper() dir := t.TempDir() p := filepath.Join(dir, "spec.hcl") if err := os.WriteFile(p, []byte(content), 0o644); err != nil { t.Fatalf("write spec: %v", err) } return p } const validJobMD = "---\n" + "kind: Job\n" + "name: my-job\n" + "runtime:\n" + " one_of: process\n" + " command: /bin/true\n" + "---\n" + "# My Job\n\nRuns /bin/true.\n" const validServiceMD = "---\n" + "kind: Service\n" + "name: web\n" + "count: 3\n" + "runtime:\n" + " one_of: process\n" + " command: /bin/http\n" + "ports:\n" + " - name: http\n" + " port: 8080\n" + "restart:\n" + " mode: service\n" + "update:\n" + " strategy: rolling\n" + " max_surge: 1\n" + "health:\n" + " check_type: http\n" + " interval: 5s\n" + "---\n" + "# Web service\n\nServes HTTP.\n" const invalidKindMD = "---\n" + "kind: CronJob\n" + "name: bad\n" + "---\nbody\n" const serviceMissingHealthMD = "---\n" + "kind: Service\n" + "name: web\n" + "count: 2\n" + "runtime:\n" + " one_of: process\n" + " command: /bin/http\n" + "ports:\n" + " - name: http\n" + " port: 8080\n" + "restart:\n" + " mode: service\n" + "update:\n" + " strategy: rolling\n" + "---\n" + "# web\n\nbody\n" const serviceMissingPortsMD = "---\n" + "kind: Service\n" + "name: web\n" + "runtime:\n" + " one_of: process\n" + " command: /bin/http\n" + "restart:\n" + " mode: service\n" + "update:\n" + " strategy: rolling\n" + "health:\n" + " check_type: http\n" + "---\nbody\n" const emptyBodyMD = "---\n" + "kind: Job\n" + "name: emptybody\n" + "runtime:\n" + " command: /bin/true\n" + "---\n" const badCELMD = "---\n" + "kind: Job\n" + "name: badcel\n" + "runtime:\n" + " command: /bin/true\n" + "constraints:\n" + " - 'node.role == \"web\"'\n" + " - 'region == (\"us\"'\n" + "---\nbody\n" func TestJobLintCmdRegistered(t *testing.T) { for _, c := range jobCmd.Commands() { if c.Name() == "lint" { return } } t.Fatal("job lint command not registered on jobCmd") } func TestJobLintValidSpec(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, validJobMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job lint valid: %v", err) } out := buf.String() if !strings.Contains(out, "0 error(s)") { t.Errorf("expected 0 errors, got: %s", out) } } func TestJobLintValidServiceSpec(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, validServiceMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job lint valid service: %v", err) } out := buf.String() if !strings.Contains(out, "0 error(s)") { t.Errorf("expected 0 errors, got: %s", out) } } func TestJobLintInvalidKind(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, invalidKindMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for invalid kind, got nil") } } func TestJobLintInvalidKindReportsError(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, invalidKindMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) _ = rootCmd.Execute() out := buf.String() if !strings.Contains(strings.ToLower(out), "cronjob") && !strings.Contains(strings.ToLower(out), "kind") { t.Errorf("expected error about kind, got: %s", out) } } func TestJobLintMissingRequiredField(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, serviceMissingPortsMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for missing ports, got nil") } out := buf.String() if !strings.Contains(strings.ToLower(out), "port") { t.Errorf("expected error mentioning ports, got: %s", out) } } func TestJobLintDeprecatedHCLWarning(t *testing.T) { resetRootFlags(t) spec := writeHCLSpec(t, trueJobSpec) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job lint hcl: %v", err) } out := buf.String() if !strings.Contains(out, "migration") && !strings.Contains(strings.ToLower(out), "legacy") && !strings.Contains(strings.ToLower(out), "hcl") { t.Errorf("expected migration/legacy warning, got: %s", out) } } func TestJobLintExplain(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, serviceMissingHealthMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec, "--explain"}) _ = rootCmd.Execute() out := buf.String() if !strings.Contains(out, "->") { t.Errorf("expected rationale lines with '->', got: %s", out) } } func TestJobLintFormatJSON(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, serviceMissingHealthMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec, "--format", "json"}) _ = rootCmd.Execute() out := buf.String() var findings []lintFinding if err := json.Unmarshal(bytes.TrimSpace([]byte(out)), &findings); err != nil { t.Fatalf("unmarshal json findings: %v\n%s", err, out) } if len(findings) == 0 { t.Fatalf("expected at least one finding") } foundHealth := false for _, f := range findings { if strings.Contains(strings.ToLower(f.Message), "health") { foundHealth = true } } if !foundHealth { t.Errorf("expected a health-related finding, got: %+v", findings) } } func TestJobLintMissingHealthCheckWarning(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, serviceMissingHealthMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) _ = rootCmd.Execute() out := buf.String() if !strings.Contains(strings.ToLower(out), "health") { t.Errorf("expected health-related warning, got: %s", out) } } func TestJobLintEmptyBodyWarning(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, emptyBodyMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job lint empty body (warnings only): %v", err) } out := buf.String() if !strings.Contains(strings.ToLower(out), "body") { t.Errorf("expected body warning, got: %s", out) } } func TestJobLintBadCEL(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, badCELMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for unbalanced CEL, got nil") } out := buf.String() if !strings.Contains(strings.ToLower(out), "cel") && !strings.Contains(strings.ToLower(out), "parenthes") { t.Errorf("expected CEL/parentheses error, got: %s", out) } } func TestJobLintMissingFile(t *testing.T) { resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", "/nonexistent/spec.md"}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for missing file, got nil") } } func TestJobLintDaemonSetValid(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, "---\n"+ "kind: DaemonSet\n"+ "name: log-shipper\n"+ "schedule:\n"+ " mode: every-node\n"+ "restart:\n"+ " mode: service\n"+ "runtime:\n"+ " one_of: process\n"+ " command: /usr/local/bin/log-shipper\n"+ "---\n# Log shipper\n\nRuns on every node.\n") var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job lint daemonset: %v\n%s", err, buf.String()) } out := buf.String() if !strings.Contains(out, "0 error(s)") { t.Errorf("expected 0 errors for valid DaemonSet, got: %s", out) } } func TestJobLintAdvisoryScheduleCron(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, "---\n"+ "kind: Job\n"+ "name: nightly\n"+ "schedule:\n"+ " cron: \"0 2 * * *\"\n"+ "runtime:\n"+ " one_of: process\n"+ " command: /bin/true\n"+ "---\n# Nightly\n\nBackup.\n") var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job lint: %v\n%s", err, buf.String()) } out := buf.String() if !strings.Contains(out, "schedule.cron' is not enforced") { t.Errorf("expected advisory warning for schedule.cron, got: %s", out) } if !strings.Contains(out, "0 error(s)") { t.Errorf("expected 0 errors, got: %s", out) } } func TestJobLintAdvisoryHealthUpdateAffinity(t *testing.T) { resetRootFlags(t) spec := writeMDSpec(t, validServiceMD) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "lint", spec}) _ = rootCmd.Execute() out := buf.String() // validServiceMD has health + update blocks; both are advisory. if !strings.Contains(out, "field 'health' is not enforced") { t.Errorf("expected advisory warning for health, got: %s", out) } if !strings.Contains(out, "field 'update' is not enforced") { t.Errorf("expected advisory warning for update, got: %s", out) } }