df58bc25a3
---ci--- project: orca phase: 3 milestone: v0.3 status: complete requirements: covered: [REQ-022, REQ-030, REQ-032] partial: [] ---/ci--- v0.3 milestone merged to main. Includes all v0.2 work (P08-P10) that was previously on the milestone branch but not yet merged to main, plus the v0.3 completion work (iter.Seq streaming + doctor network/db). v0.2 phases included: P08 (mTLS), P09 (scheduling), P10 (security scan). v0.3 phases: P0 (pre-execution), P1 (iter.Seq streaming), P2 (doctor), P3 (final review+ship). Total: 40 requirements, all complete. No new go.mod dependencies. Full test suite passes under -race. gofmt + go vet clean.
288 lines
7.3 KiB
Go
288 lines
7.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/model"
|
|
"git.cloudinit.dev/coreci/orca/internal/store"
|
|
)
|
|
|
|
func TestWatchJobs_JSONStreaming(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "orca.db")
|
|
db, err := store.Open(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
repo := store.NewJobRepo(db)
|
|
bgCtx := context.Background()
|
|
_ = repo.Insert(bgCtx, &model.Job{ID: "seed-job", Name: "seed", Spec: "t", Status: model.JobStatusPending})
|
|
|
|
t.Setenv("ORCA_DB", dbPath)
|
|
|
|
jsonOutput = true
|
|
t.Cleanup(func() { jsonOutput = false })
|
|
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
t.Cleanup(func() { rootCmd.SetOut(os.Stdout); rootCmd.SetErr(os.Stderr) })
|
|
|
|
ctx, cancel := context.WithCancel(bgCtx)
|
|
|
|
done := make(chan error, 1)
|
|
go func() { done <- watchJobsCtx(rootCmd, ctx) }()
|
|
|
|
// First yield is immediate (G-002); wait for it.
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
_ = repo.Insert(bgCtx, &model.Job{ID: "watch-job", Name: "watch", Spec: "t", Status: model.JobStatusPending})
|
|
|
|
// Wait for at least one ticker interval (default 1s) to capture the change.
|
|
time.Sleep(1100 * time.Millisecond)
|
|
cancel()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("watchJobsCtx did not return within 2s after cancel")
|
|
}
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, `"event":"init"`) {
|
|
t.Errorf("expected init event, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "watch-job") {
|
|
t.Errorf("expected watch-job in output, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestWatchJobs_TableRefresh(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "orca.db")
|
|
db, err := store.Open(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
repo := store.NewJobRepo(db)
|
|
bgCtx := context.Background()
|
|
_ = repo.Insert(bgCtx, &model.Job{ID: "seed-job", Name: "seed", Spec: "t", Status: model.JobStatusPending})
|
|
|
|
t.Setenv("ORCA_DB", dbPath)
|
|
|
|
jsonOutput = false
|
|
t.Cleanup(func() { jsonOutput = false })
|
|
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
t.Cleanup(func() { rootCmd.SetOut(os.Stdout); rootCmd.SetErr(os.Stderr) })
|
|
|
|
ctx, cancel := context.WithCancel(bgCtx)
|
|
|
|
done := make(chan error, 1)
|
|
go func() { done <- watchJobsCtx(rootCmd, ctx) }()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
_ = repo.Insert(bgCtx, &model.Job{ID: "table-job", Name: "table", Spec: "t", Status: model.JobStatusPending})
|
|
|
|
time.Sleep(1100 * time.Millisecond)
|
|
cancel()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("watchJobsCtx did not return within 2s after cancel")
|
|
}
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, "\033[2J\033[H") {
|
|
t.Errorf("expected clear-screen escape in table watch output, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "table-job") {
|
|
t.Errorf("expected table-job in output, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestWatchNodes_JSONStreaming(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "orca.db")
|
|
db, err := store.Open(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
repo := store.NewNodeRepo(db)
|
|
bgCtx := context.Background()
|
|
_ = repo.Insert(bgCtx, &model.Node{
|
|
ID: "seed-node", Name: "seed", Address: "addr",
|
|
State: model.NodeStateReady, JoinedAt: time.Now().UTC(), LastSeen: time.Now().UTC(),
|
|
})
|
|
|
|
t.Setenv("ORCA_DB", dbPath)
|
|
|
|
jsonOutput = true
|
|
t.Cleanup(func() { jsonOutput = false })
|
|
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
t.Cleanup(func() { rootCmd.SetOut(os.Stdout); rootCmd.SetErr(os.Stderr) })
|
|
|
|
ctx, cancel := context.WithCancel(bgCtx)
|
|
|
|
done := make(chan error, 1)
|
|
go func() { done <- watchNodesCtx(rootCmd, ctx) }()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
_ = repo.Insert(bgCtx, &model.Node{
|
|
ID: "watch-node", Name: "watch", Address: "addr2",
|
|
State: model.NodeStateReady, JoinedAt: time.Now().UTC(), LastSeen: time.Now().UTC(),
|
|
})
|
|
|
|
time.Sleep(1100 * time.Millisecond)
|
|
cancel()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("watchNodesCtx did not return within 2s after cancel")
|
|
}
|
|
|
|
output := buf.String()
|
|
initFound := false
|
|
watchNodeFound := false
|
|
for _, line := range strings.Split(output, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var event map[string]any
|
|
if err := json.Unmarshal([]byte(line), &event); err != nil {
|
|
continue
|
|
}
|
|
if event["event"] == "init" {
|
|
initFound = true
|
|
if node, ok := event["node"].(map[string]any); ok {
|
|
if node["id"] == "watch-node" {
|
|
watchNodeFound = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !initFound {
|
|
t.Errorf("expected init event in JSON stream, got: %s", output)
|
|
}
|
|
if !watchNodeFound {
|
|
t.Errorf("expected watch-node in JSON stream, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestWatchNodes_TableRefresh(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "orca.db")
|
|
db, err := store.Open(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
repo := store.NewNodeRepo(db)
|
|
bgCtx := context.Background()
|
|
_ = repo.Insert(bgCtx, &model.Node{
|
|
ID: "seed-node", Name: "seed", Address: "addr",
|
|
State: model.NodeStateReady, JoinedAt: time.Now().UTC(), LastSeen: time.Now().UTC(),
|
|
})
|
|
|
|
t.Setenv("ORCA_DB", dbPath)
|
|
|
|
jsonOutput = false
|
|
t.Cleanup(func() { jsonOutput = false })
|
|
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
t.Cleanup(func() { rootCmd.SetOut(os.Stdout); rootCmd.SetErr(os.Stderr) })
|
|
|
|
ctx, cancel := context.WithCancel(bgCtx)
|
|
|
|
done := make(chan error, 1)
|
|
go func() { done <- watchNodesCtx(rootCmd, ctx) }()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
_ = repo.Insert(bgCtx, &model.Node{
|
|
ID: "table-node", Name: "table", Address: "addr2",
|
|
State: model.NodeStateReady, JoinedAt: time.Now().UTC(), LastSeen: time.Now().UTC(),
|
|
})
|
|
|
|
time.Sleep(1100 * time.Millisecond)
|
|
cancel()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("watchNodesCtx did not return within 2s after cancel")
|
|
}
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, "\033[2J\033[H") {
|
|
t.Errorf("expected clear-screen escape in table watch output, got: %s", output)
|
|
}
|
|
if !strings.Contains(output, "table-node") {
|
|
t.Errorf("expected table-node in output, got: %s", output)
|
|
}
|
|
}
|
|
|
|
func TestRenderJobTable(t *testing.T) {
|
|
jobs := []*model.Job{
|
|
{ID: "j1", Name: "alpha", Status: "running", ExitCode: 0},
|
|
{ID: "j2", Name: "beta", Status: "done", ExitCode: 0},
|
|
}
|
|
out := renderJobTable(jobs)
|
|
if !strings.Contains(out, "j1") || !strings.Contains(out, "alpha") {
|
|
t.Errorf("renderJobTable missing job 1: %s", out)
|
|
}
|
|
if !strings.Contains(out, "j2") || !strings.Contains(out, "beta") {
|
|
t.Errorf("renderJobTable missing job 2: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestRenderJobTableEmpty(t *testing.T) {
|
|
out := renderJobTable(nil)
|
|
if !strings.Contains(out, "No jobs") {
|
|
t.Errorf("expected empty message, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestRenderNodeTable(t *testing.T) {
|
|
nodes := []*model.Node{
|
|
{ID: "n1", Name: "alpha", Address: "localhost:8443", State: "ready"},
|
|
}
|
|
out := renderNodeTable(nodes)
|
|
if !strings.Contains(out, "n1") || !strings.Contains(out, "alpha") {
|
|
t.Errorf("renderNodeTable missing node: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestRenderNodeTableEmpty(t *testing.T) {
|
|
out := renderNodeTable(nil)
|
|
if !strings.Contains(out, "No nodes") {
|
|
t.Errorf("expected empty message, got: %s", out)
|
|
}
|
|
}
|