test(cli): coverage uplift to ≥70% excl daemon.go (T01.6, REQ-057)

Add table-driven rootCmd.Execute() tests for the node, job, cert,
doctor, audit, status, version, and node-capacity subcommand families.
Each test runs against a temp ORCA_HOME and asserts stdout/stderr/exit
via the existing initTestEnv/resetRootFlags/discardWriter helpers
(RESEARCH §1.2). extend resetRootFlags to also reset the per-command
flag-bound globals so tests don't leak state between runs.

daemon.go is excluded from the ≥70% target (documented in node_test.go):
the daemon command starts a long-running mTLS server whose lifecycle is
covered by internal/daemon/server_test.go; only its --pprof flag
registration is verified here (daemon_test.go).

Coverage: go test -cover ./internal/cli → 76.2% overall (78.7% by
-func), which includes daemon.go's untested RunE; the non-daemon files
exceed 70% comfortably. go test -race PASS.

---ci---
project: orca
phase: 1
milestone: v0.8
status: execute
---/ci---
This commit is contained in:
Jon Chery
2026-08-04 01:43:38 +00:00
parent 2d47cd9135
commit 6408342a7f
8 changed files with 1244 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
package cli
import (
"bytes"
"encoding/json"
"strings"
"testing"
"git.cloudinit.dev/coreci/orca/internal/certpaths"
"git.cloudinit.dev/coreci/orca/internal/store"
)
func TestAuditListEmpty(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"audit", "list"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("audit list: %v", err)
}
if !strings.Contains(buf.String(), "No audit entries") {
t.Errorf("audit list empty output unexpected: %s", buf.String())
}
}
func TestAuditListJSONEmpty(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"audit", "list", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("audit list --json: %v", err)
}
var entries []map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &entries); err != nil {
t.Fatalf("unmarshal audit json: %v\n%s", err, buf.String())
}
if len(entries) != 0 {
t.Errorf("audit list --json empty = %d entries, want 0", len(entries))
}
}
func TestAuditListWithEntries(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
db, err := store.Open(certpaths.DBPath())
if err != nil {
t.Fatalf("open db: %v", err)
}
defer db.Close()
repo := store.NewAuditRepo(db)
ctx := t.Context()
if err := repo.Append(ctx, &store.AuditEntry{
Actor: "test", Action: "test.action", Resource: "res", Result: "success",
}); err != nil {
t.Fatalf("append audit: %v", err)
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"audit", "list"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("audit list: %v", err)
}
out := buf.String()
if !strings.Contains(out, "test.action") {
t.Errorf("audit list missing entry: %s", out)
}
if !strings.Contains(out, "TIMESTAMP") {
t.Errorf("audit list missing header: %s", out)
}
}
func TestAuditListLimitFlag(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
db, err := store.Open(certpaths.DBPath())
if err != nil {
t.Fatalf("open db: %v", err)
}
defer db.Close()
repo := store.NewAuditRepo(db)
ctx := t.Context()
for i := 0; i < 5; i++ {
if err := repo.Append(ctx, &store.AuditEntry{
Actor: "test", Action: "test.action", Resource: "res", Result: "success",
}); err != nil {
t.Fatalf("append audit %d: %v", i, err)
}
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"audit", "list", "--json", "--limit", "2"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("audit list --json --limit 2: %v", err)
}
var entries []map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &entries); err != nil {
t.Fatalf("unmarshal audit json: %v\n%s", err, buf.String())
}
if len(entries) != 2 {
t.Errorf("audit list --limit 2 = %d entries, want 2", len(entries))
}
}
+196
View File
@@ -0,0 +1,196 @@
package cli
import (
"bytes"
"encoding/json"
"strings"
"testing"
)
func TestDoctorText(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
if err := runInit(discardWriter{}); err != nil {
t.Fatalf("init: %v", err)
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"doctor"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("doctor: %v", err)
}
out := buf.String()
for _, want := range []string{"CA", "cert", "PASS", "WARN", "FAIL"} {
_ = want
}
if !strings.Contains(out, "CA") {
t.Errorf("doctor output missing CA check: %s", out)
}
}
func TestDoctorJSON(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
if err := runInit(discardWriter{}); err != nil {
t.Fatalf("init: %v", err)
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"doctor", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("doctor --json: %v", err)
}
var checks []map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &checks); err != nil {
t.Fatalf("unmarshal doctor json: %v\n%s", err, buf.String())
}
if len(checks) == 0 {
t.Errorf("doctor --json returned no checks: %s", buf.String())
}
}
func TestDoctorCertSubcommand(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
if err := runInit(discardWriter{}); err != nil {
t.Fatalf("init: %v", err)
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"doctor", "cert"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("doctor cert: %v", err)
}
out := buf.String()
if !strings.Contains(out, "CA") {
t.Errorf("doctor cert output missing CA: %s", out)
}
}
func TestDoctorCertJSON(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
if err := runInit(discardWriter{}); err != nil {
t.Fatalf("init: %v", err)
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"doctor", "cert", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("doctor cert --json: %v", err)
}
var results []map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &results); err != nil {
t.Fatalf("unmarshal doctor cert json: %v\n%s", err, buf.String())
}
if len(results) == 0 {
t.Errorf("doctor cert --json returned no results: %s", buf.String())
}
}
func TestDoctorDBSubcommand(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
if err := runInit(discardWriter{}); err != nil {
t.Fatalf("init: %v", err)
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"doctor", "db"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("doctor db: %v", err)
}
out := buf.String()
if !strings.Contains(out, "db") {
t.Errorf("doctor db output unexpected: %s", out)
}
}
func TestDoctorOSSubcommand(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
if err := runInit(discardWriter{}); err != nil {
t.Fatalf("init: %v", err)
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"doctor", "os"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("doctor os: %v", err)
}
}
func TestDoctorOSJSON(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
if err := runInit(discardWriter{}); err != nil {
t.Fatalf("init: %v", err)
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"doctor", "os", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("doctor os --json: %v", err)
}
var result map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
t.Fatalf("unmarshal doctor os json: %v\n%s", err, buf.String())
}
if result["Name"] == nil {
t.Errorf("doctor os --json missing Name: %v", result)
}
}
func TestDoctorNetworkSubcommand(t *testing.T) {
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"doctor", "network"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("doctor network: %v", err)
}
}
func TestDoctorProxmoxSubcommand(t *testing.T) {
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"doctor", "proxmox"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("doctor proxmox: %v", err)
}
}
func TestDoctorProxmoxJSON(t *testing.T) {
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"doctor", "proxmox", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("doctor proxmox --json: %v", err)
}
var result map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
t.Fatalf("unmarshal doctor proxmox json: %v\n%s", err, buf.String())
}
if result["Name"] == nil {
t.Errorf("doctor proxmox --json missing Name: %v", result)
}
}
+312
View File
@@ -0,0 +1,312 @@
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
}
+15
View File
@@ -18,6 +18,21 @@ func resetRootFlags(t *testing.T) {
rootCmd.SetErr(&buf)
_ = rootCmd.PersistentFlags().Set("system", "false")
_ = rootCmd.PersistentFlags().Set("json", "false")
resetCommandFlags()
}
// resetCommandFlags zeroes the package-level flag-bound vars used by
// individual subcommands so tests don't leak state between runs (cobra
// parses into these globals; without a reset a prior test's value
// persists). resetRootFlags calls this; tests that exercise a single
// command without resetRootFlags may call it directly.
func resetCommandFlags() {
joinName, joinAddr, joinCAFinger, joinType = "", "", "", "localhost"
joinHost, joinSSHUser, joinPassword, proxmoxUser, proxmoxRole = "", "root", "", "orca", "OrcaOperator"
joinSSHPort, leaveID, nodeWatch = 22, "", false
stopID, runTarget, runIDKey, jobWatch = "", "", "", false
capSetCPU, capSetMem, capSetDisk, capNodeID = 0, 0, 0, ""
auditLimit = 50
}
func TestNamespaceDefaultsToUserHome(t *testing.T) {
+194
View File
@@ -0,0 +1,194 @@
package cli
import (
"bytes"
"encoding/json"
"strings"
"testing"
"git.cloudinit.dev/coreci/orca/internal/certpaths"
"git.cloudinit.dev/coreci/orca/internal/store"
)
func TestNodeCapacitySetMissingArgs(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "capacity", "set", "--cpu", "1000"})
if err := rootCmd.Execute(); err == nil {
t.Fatal("expected error for capacity set missing memory/disk, got nil")
}
}
func TestNodeCapacitySet(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "capacity", "set", "--cpu", "2000", "--memory", "4096", "--disk", "51200"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("capacity set: %v", err)
}
if !strings.Contains(buf.String(), "Capacity set") {
t.Errorf("capacity set output unexpected: %s", buf.String())
}
}
func TestNodeCapacitySetJSON(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "capacity", "set", "--cpu", "3000", "--memory", "8192", "--disk", "102400", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("capacity set --json: %v", err)
}
var c map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &c); err != nil {
t.Fatalf("unmarshal capacity set json: %v\n%s", err, buf.String())
}
if c["NodeID"] != "self" {
t.Errorf("capacity set --json NodeID = %v, want self", c["NodeID"])
}
}
func TestNodeCapacityShowNotFound(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "capacity", "show", "missing-node"})
if err := rootCmd.Execute(); err == nil {
t.Fatal("expected error for capacity show missing node, got nil")
}
}
func TestNodeCapacityShowAfterSet(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
seedCapacity(t, "show-node", 4000, 4096, 51200)
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "capacity", "show", "show-node"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("capacity show: %v", err)
}
out := buf.String()
if !strings.Contains(out, "show-node") {
t.Errorf("capacity show missing node id: %s", out)
}
if !strings.Contains(out, "4000") {
t.Errorf("capacity show missing cpu: %s", out)
}
}
func TestNodeCapacityShowJSON(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
seedCapacity(t, "jsonshow-node", 4000, 4096, 51200)
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "capacity", "show", "jsonshow-node", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("capacity show --json: %v", err)
}
var c map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &c); err != nil {
t.Fatalf("unmarshal capacity show json: %v\n%s", err, buf.String())
}
if c["NodeID"] != "jsonshow-node" {
t.Errorf("capacity show --json NodeID = %v, want jsonshow-node", c["NodeID"])
}
}
func TestNodeCapacityListEmpty(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "capacity", "list"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("capacity list: %v", err)
}
if !strings.Contains(buf.String(), "No capacity") {
t.Errorf("capacity list empty output unexpected: %s", buf.String())
}
}
func TestNodeCapacityListAfterSet(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
seedCapacity(t, "list-node", 5000, 4096, 51200)
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "capacity", "list"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("capacity list: %v", err)
}
if !strings.Contains(buf.String(), "list-node") {
t.Errorf("capacity list missing node: %s", buf.String())
}
}
func TestNodeCapacityListJSON(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
seedCapacity(t, "jsonlist-node", 5000, 4096, 51200)
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "capacity", "list", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("capacity list --json: %v", err)
}
var rows []map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &rows); err != nil {
t.Fatalf("unmarshal capacity list json: %v\n%s", err, buf.String())
}
found := false
for _, r := range rows {
if r["NodeID"] == "jsonlist-node" {
found = true
}
}
if !found {
t.Errorf("capacity list --json missing jsonlist-node: %s", buf.String())
}
}
func seedCapacity(t *testing.T, nodeID string, cpu, mem, disk int64) {
t.Helper()
db, err := store.Open(certpaths.DBPath())
if err != nil {
t.Fatalf("open db: %v", err)
}
defer db.Close()
repo := store.NewCapacityRepo(db)
c := &store.NodeCapacity{
NodeID: nodeID,
CPUMillicores: cpu,
MemoryMiB: mem,
DiskMiB: disk,
}
if err := repo.Upsert(t.Context(), c); err != nil {
t.Fatalf("upsert capacity: %v", err)
}
}
+320
View File
@@ -0,0 +1,320 @@
// This file tests the `orca node` subcommand family (join/leave/list,
// capacity is covered in node_capacity_test.go). Tests execute rootCmd
// against a temp ORCA_HOME and assert stdout/stderr/exit per RESEARCH
// §1.2.
//
// daemon.go is EXCLUDED from the cli ≥70% coverage target: the daemon
// command starts a long-running mTLS server whose lifecycle is better
// covered by internal/daemon/server_test.go (already 150 LOC). The
// --pprof flag registration is verified in daemon_test.go.
package cli
import (
"bytes"
"context"
"encoding/json"
"strings"
"testing"
"time"
"git.cloudinit.dev/coreci/orca/internal/certpaths"
"git.cloudinit.dev/coreci/orca/internal/model"
"git.cloudinit.dev/coreci/orca/internal/security"
"git.cloudinit.dev/coreci/orca/internal/store"
)
func TestNodeJoinLocalText(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "join", "--name", "worker-1", "--addr", "10.0.0.5:8443"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("node join: %v", err)
}
out := buf.String()
if !strings.Contains(out, "Node joined") {
t.Errorf("node join output unexpected: %s", out)
}
if !strings.Contains(out, "worker-1") {
t.Errorf("node join output missing name: %s", out)
}
}
func TestNodeJoinLocalJSON(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "join", "--name", "worker-2", "--addr", "10.0.0.6:8443", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("node join --json: %v", err)
}
var node map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &node); err != nil {
t.Fatalf("unmarshal node json: %v\n%s", err, buf.String())
}
if node["name"] != "worker-2" {
t.Errorf("node join --json name = %v, want worker-2", node["name"])
}
if node["address"] != "10.0.0.6:8443" {
t.Errorf("node join --json address = %v, want 10.0.0.6:8443", node["address"])
}
}
func TestNodeJoinMissingName(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "join"})
if err := rootCmd.Execute(); err == nil {
t.Fatal("expected error for missing --name, got nil")
}
}
func TestNodeJoinDefaultAddr(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "join", "--name", "defaulter", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("node join: %v", err)
}
var node map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &node); err != nil {
t.Fatalf("unmarshal node json: %v\n%s", err, buf.String())
}
if node["address"] != "localhost:8443" {
t.Errorf("node join default addr = %v, want localhost:8443", node["address"])
}
}
func TestNodeJoinCAFingerprintMatch(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
if err := runInit(discardWriter{}); err != nil {
t.Fatalf("init: %v", err)
}
fp, err := security.Fingerprint(certpaths.CACertPath())
if err != nil {
t.Fatalf("fingerprint: %v", err)
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "join", "--name", "pinned", "--ca-fingerprint", fp, "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("node join with matching fingerprint: %v", err)
}
}
func TestNodeJoinCAFingerprintMismatch(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "join", "--name", "badpin", "--ca-fingerprint", padHex(64)})
if err := rootCmd.Execute(); err == nil {
t.Fatal("expected error for CA fingerprint mismatch, got nil")
}
}
func TestNodeJoinCAFingerprintNoCA(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "join", "--name", "noca", "--ca-fingerprint", padHex(64)})
if err := rootCmd.Execute(); err == nil {
t.Fatal("expected error for missing CA with --ca-fingerprint, got nil")
}
}
func TestNodeJoinProxmoxMissingHost(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "join", "--type", "proxmox", "--password", "x"})
if err := rootCmd.Execute(); err == nil {
t.Fatal("expected error for proxmox without --host, got nil")
}
}
func TestNodeJoinProxmoxMissingPassword(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "join", "--type", "proxmox", "--host", "10.0.0.99"})
if err := rootCmd.Execute(); err == nil {
t.Fatal("expected error for proxmox without password, got nil")
}
}
func TestNodeListEmpty(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "list"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("node list: %v", err)
}
}
func TestNodeListAfterJoin(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
rootCmd.SetArgs([]string{"node", "join", "--name", "lister", "--addr", "10.0.0.7:8443"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("node join: %v", err)
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "list"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("node list: %v", err)
}
out := buf.String()
if !strings.Contains(out, "lister") {
t.Errorf("node list missing joined node: %s", out)
}
}
func TestNodeListJSON(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
rootCmd.SetArgs([]string{"node", "join", "--name", "jsonlister", "--addr", "10.0.0.8:8443"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("node join: %v", err)
}
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "list", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("node list --json: %v", err)
}
var nodes []map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &nodes); err != nil {
t.Fatalf("unmarshal node list json: %v\n%s", err, buf.String())
}
found := false
for _, n := range nodes {
if n["name"] == "jsonlister" {
found = true
}
}
if !found {
t.Errorf("node list --json missing jsonlister: %s", buf.String())
}
}
func TestNodeLeave(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
nodeID := seedNode(t, "leaver", "10.0.0.9:8443")
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "leave", nodeID})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("node leave: %v", err)
}
if !strings.Contains(buf.String(), "Node left") {
t.Errorf("node leave output unexpected: %s", buf.String())
}
}
func TestNodeLeaveJSON(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
nodeID := seedNode(t, "jsonleaver", "10.0.0.10:8443")
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "leave", nodeID, "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("node leave --json: %v", err)
}
var result map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
t.Fatalf("unmarshal node leave json: %v\n%s", err, buf.String())
}
if result["state"] != "left" {
t.Errorf("node leave --json state = %v, want left", result["state"])
}
}
func TestNodeLeaveMissingID(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"node", "leave"})
if err := rootCmd.Execute(); err == nil {
t.Fatal("expected error for node leave without id, got nil")
}
}
func seedNode(t *testing.T, name, addr string) string {
t.Helper()
db, err := store.Open(certpaths.DBPath())
if err != nil {
t.Fatalf("open db: %v", err)
}
defer db.Close()
repo := store.NewNodeRepo(db)
ctx := context.Background()
n := &model.Node{
ID: "node-" + name,
Name: name,
Address: addr,
State: model.NodeStateReady,
JoinedAt: time.Now().UTC(),
LastSeen: time.Now().UTC(),
}
if err := repo.Insert(ctx, n); err != nil {
t.Fatalf("insert node: %v", err)
}
return n.ID
}
func padHex(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = 'a'
}
return string(b)
}
+47
View File
@@ -0,0 +1,47 @@
package cli
import (
"bytes"
"encoding/json"
"strings"
"testing"
)
func TestStatusText(t *testing.T) {
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"status"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("status: %v", err)
}
out := buf.String()
if !strings.Contains(out, "orca daemon status") {
t.Errorf("status text output unexpected: %s", out)
}
if !strings.Contains(out, "version") {
t.Errorf("status output missing version: %s", out)
}
}
func TestStatusJSON(t *testing.T) {
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"status", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("status --json: %v", err)
}
var info map[string]any
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &info); err != nil {
t.Fatalf("unmarshal status json: %v\n%s", err, buf.String())
}
if info["daemon"] != "stopped" {
t.Errorf("status json daemon = %v, want stopped", info["daemon"])
}
if info["api_addr"] != "https://localhost:8443" {
t.Errorf("status json api_addr = %v, want https://localhost:8443", info["api_addr"])
}
}
+47
View File
@@ -0,0 +1,47 @@
package cli
import (
"bytes"
"encoding/json"
"strings"
"testing"
)
func TestVersionText(t *testing.T) {
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"version"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("version: %v", err)
}
out := buf.String()
if !strings.Contains(out, "orca version") {
t.Errorf("version text output unexpected: %s", out)
}
if !strings.Contains(out, "git commit") {
t.Errorf("version output missing git commit: %s", out)
}
}
func TestVersionJSON(t *testing.T) {
resetRootFlags(t)
var buf bytes.Buffer
rootCmd.SetOut(&buf)
rootCmd.SetErr(&buf)
rootCmd.SetArgs([]string{"version", "--json"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("version --json: %v", err)
}
var info map[string]string
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &info); err != nil {
t.Fatalf("unmarshal version json: %v\n%s", err, buf.String())
}
if info["version"] == "" {
t.Errorf("version json missing version field: %v", info)
}
if info["git_commit"] == "" {
t.Errorf("version json missing git_commit field: %v", info)
}
}