package cli import ( "bytes" "encoding/json" "os" "path/filepath" "strings" "testing" "git.cloudinit.dev/coreci/orca/internal/certpaths" "git.cloudinit.dev/coreci/orca/internal/model" "git.cloudinit.dev/coreci/orca/internal/store" ) func writeJobSpec(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 trueJobSpec = `job "true" {} task "t" { command = "/bin/true" } ` const falseJobSpec = `job "false" {} task "t" { command = "/bin/false" } ` func TestJobRunComplete(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() spec := writeJobSpec(t, trueJobSpec) resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "run", spec}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job run: %v", err) } if !strings.Contains(buf.String(), "Job complete") { t.Errorf("job run output unexpected: %s", buf.String()) } } func TestJobRunCompleteJSON(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() spec := writeJobSpec(t, trueJobSpec) resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "run", spec, "--json"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job run --json: %v", err) } var result map[string]any if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil { t.Fatalf("unmarshal job run json: %v\n%s", err, buf.String()) } if result["status"] != "complete" { t.Errorf("job run --json status = %v, want complete", result["status"]) } } func TestJobRunFailed(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() spec := writeJobSpec(t, falseJobSpec) resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "run", spec}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for failing job, got nil") } } func TestJobRunFailedJSON(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() spec := writeJobSpec(t, falseJobSpec) resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "run", spec, "--json"}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for failing job --json, got nil") } if !strings.Contains(buf.String(), "failed") { t.Errorf("job run --json failed output unexpected: %s", buf.String()) } } func TestJobRunMissingSpecFile(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "run", "/nonexistent/spec.hcl"}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for missing spec file, got nil") } } func TestJobListEmpty(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "list"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job list: %v", err) } if !strings.Contains(buf.String(), "No jobs") { t.Errorf("job list empty output unexpected: %s", buf.String()) } } func TestJobListJSONEmpty(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "list", "--json"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job list --json: %v", err) } var jobs []map[string]any if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &jobs); err != nil { t.Fatalf("unmarshal job list json: %v\n%s", err, buf.String()) } if len(jobs) != 0 { t.Errorf("job list --json empty = %d jobs, want 0", len(jobs)) } } func TestJobListAfterRun(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() spec := writeJobSpec(t, trueJobSpec) resetRootFlags(t) rootCmd.SetArgs([]string{"job", "run", spec}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job run: %v", err) } resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "list"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job list: %v", err) } out := buf.String() if !strings.Contains(out, "true") { t.Errorf("job list missing job name: %s", out) } } func TestJobStop(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() jobID := seedJob(t, "stopper", model.JobStatusRunning) resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "stop", jobID}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job stop: %v", err) } if !strings.Contains(buf.String(), "Job stopped") { t.Errorf("job stop output unexpected: %s", buf.String()) } } func TestJobStopJSON(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() jobID := seedJob(t, "jsonstopper", model.JobStatusRunning) resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "stop", jobID, "--json"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job stop --json: %v", err) } var result map[string]any if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil { t.Fatalf("unmarshal job stop json: %v\n%s", err, buf.String()) } if result["status"] != "stopped" { t.Errorf("job stop --json status = %v, want stopped", result["status"]) } } func TestJobStopNotFound(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "stop", "nonexistent-id"}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for job stop not found, got nil") } } func TestJobStopMissingID(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "stop"}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for job stop without id, got nil") } } func TestJobLogsEmpty(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() jobID := seedJob(t, "logger", model.JobStatusComplete) resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "logs", jobID}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job logs: %v", err) } if !strings.Contains(buf.String(), "No tasks") { t.Errorf("job logs empty output unexpected: %s", buf.String()) } } func TestJobLogsJSONEmpty(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() jobID := seedJob(t, "jsonlogger", model.JobStatusComplete) resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "logs", jobID, "--json"}) if err := rootCmd.Execute(); err != nil { t.Fatalf("job logs --json: %v", err) } var tasks []map[string]any if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &tasks); err != nil { t.Fatalf("unmarshal job logs json: %v\n%s", err, buf.String()) } if len(tasks) != 0 { t.Errorf("job logs --json empty = %d tasks, want 0", len(tasks)) } } func TestJobLogsMissingID(t *testing.T) { _, cleanup := initTestEnv(t) defer cleanup() resetRootFlags(t) var buf bytes.Buffer rootCmd.SetOut(&buf) rootCmd.SetErr(&buf) rootCmd.SetArgs([]string{"job", "logs"}) if err := rootCmd.Execute(); err == nil { t.Fatal("expected error for job logs without id, got nil") } } func seedJob(t *testing.T, name string, status model.JobStatus) string { t.Helper() db, err := store.Open(certpaths.DBPath()) if err != nil { t.Fatalf("open db: %v", err) } defer db.Close() repo := store.NewJobRepo(db) j := &model.Job{ ID: "job-" + name, Name: name, Spec: "spec.hcl", Status: status, } if err := repo.Insert(t.Context(), j); err != nil { t.Fatalf("insert job: %v", err) } return j.ID }