Files
orca/internal/cli/job_lint_test.go
T
Jon Chery 020aa01623 feat(P11,P12): orca job lint (REQ-084) + orca job verify (dry-run txn)
P11: orca job lint <spec.md> — schema/CEL/body/migration/best-practice
checks; --explain, --format json; exit 0/1 by errors found.
P12: orca job verify <spec.md> — dry-run txn (render + stage + verify
without apply); reports planned allocs/files/units; no side effects;
--namespace, --json.

---ci---
project: orca
phase: 11
milestone: v0.11
status: execute
---/ci---
2026-08-07 07:29:44 +00:00

311 lines
7.6 KiB
Go

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")
}
}