Files
orca/internal/engine/dispatcher_test.go
T
Jon Chery d9d0beda3b test(P03): coverage uplift — engine/transport/proxmox/audit ≥50% + dispatch.go EOF fix (REQ-055)
94 new tests across 4 packages. Coverage: engine 8.3%→65.1%, transport
26.3%→84.6%, proxmox 5.1%→82.7%, audit 0%→100%. Bug fix: dispatch.go
bytesReadCloser.Read returned fmt.Errorf("EOF") instead of io.EOF —
broke HTTP request body transmission (latent since v0.2 P02).

---ci---
project: orca
phase: 3
milestone: v0.7
status: verify
requirements:
  covered: [REQ-055]
  partial: []
---/ci---
2026-08-04 00:18:58 +00:00

206 lines
5.8 KiB
Go

package engine
import (
"context"
"errors"
"path/filepath"
"testing"
"git.cloudinit.dev/coreci/orca/internal/store"
)
type mockExecutor struct {
submitFn func(ctx context.Context, spec []byte) (string, error)
statusFn func(ctx context.Context, jobID string) (string, error)
submitted bool
}
func (m *mockExecutor) Submit(ctx context.Context, spec []byte) (string, error) {
m.submitted = true
if m.submitFn != nil {
return m.submitFn(ctx, spec)
}
return "mock-job-id", nil
}
func (m *mockExecutor) Status(ctx context.Context, jobID string) (string, error) {
if m.statusFn != nil {
return m.statusFn(ctx, jobID)
}
return "complete", nil
}
func newTestDispatcher(t *testing.T, exec LocalExecutor) (*Dispatcher, *store.CapacityRepo, func()) {
t.Helper()
path := filepath.Join(t.TempDir(), "test.db")
db, err := store.Open(path)
if err != nil {
t.Fatalf("open db: %v", err)
}
capRepo := store.NewCapacityRepo(db)
peers := NewPeerRegistry()
d := NewDispatcher(nil, capRepo, peers, exec)
return d, capRepo, func() { _ = db.Close() }
}
func TestDispatcher_Submit_EmptySpec(t *testing.T) {
d, _, cleanup := newTestDispatcher(t, &mockExecutor{})
defer cleanup()
_, _, err := d.Submit(context.Background(), "", nil, "")
if err == nil {
t.Fatal("Submit: expected error for empty spec, got nil")
}
}
func TestDispatcher_Submit_IdempotencyHit(t *testing.T) {
exec := &mockExecutor{}
d, _, cleanup := newTestDispatcher(t, exec)
defer cleanup()
d.Dedupe().Put("key-1", "cached-job-id")
spec := []byte(`{"cpu_millicores":100,"memory_mib":64,"disk_mib":64}`)
jobID, nodeID, err := d.Submit(context.Background(), "", spec, "key-1")
if err != nil {
t.Fatalf("Submit: %v", err)
}
if jobID != "cached-job-id" {
t.Errorf("jobID: got %q, want cached-job-id", jobID)
}
if nodeID != "self" {
t.Errorf("nodeID: got %q, want self", nodeID)
}
if exec.submitted {
t.Error("executor was called on idempotency hit; should have been short-circuited")
}
}
func TestDispatcher_Submit_LocalCapacity(t *testing.T) {
exec := &mockExecutor{
submitFn: func(ctx context.Context, spec []byte) (string, error) {
return "local-job-id", nil
},
}
d, capRepo, cleanup := newTestDispatcher(t, exec)
defer cleanup()
ctx := context.Background()
if err := capRepo.Upsert(ctx, &store.NodeCapacity{
NodeID: "self",
CPUMillicores: 4000,
MemoryMiB: 4096,
DiskMiB: 4096,
}); err != nil {
t.Fatalf("Upsert capacity: %v", err)
}
spec := []byte(`{"cpu_millicores":100,"memory_mib":64,"disk_mib":64}`)
jobID, nodeID, err := d.Submit(ctx, "", spec, "")
if err != nil {
t.Fatalf("Submit: %v", err)
}
if jobID != "local-job-id" {
t.Errorf("jobID: got %q, want local-job-id", jobID)
}
if nodeID != "self" {
t.Errorf("nodeID: got %q, want self", nodeID)
}
if !exec.submitted {
t.Error("executor was not called for local-capacity path")
}
}
func TestDispatcher_Submit_ExplicitTarget(t *testing.T) {
exec := &mockExecutor{}
d, _, cleanup := newTestDispatcher(t, exec)
defer cleanup()
spec := []byte(`{"cpu_millicores":100,"memory_mib":64,"disk_mib":64}`)
_, _, err := d.Submit(context.Background(), "nodeA", spec, "")
if err == nil {
t.Fatal("Submit with explicit target nodeA (no peer): expected error, got nil")
}
}
func TestDispatcher_Submit_NoPeers(t *testing.T) {
exec := &mockExecutor{}
d, capRepo, cleanup := newTestDispatcher(t, exec)
defer cleanup()
ctx := context.Background()
if err := capRepo.Upsert(ctx, &store.NodeCapacity{
NodeID: "self",
CPUMillicores: 0,
MemoryMiB: 0,
DiskMiB: 0,
}); err != nil {
t.Fatalf("Upsert: %v", err)
}
spec := []byte(`{"cpu_millicores":1000,"memory_mib":1024,"disk_mib":1024}`)
_, _, err := d.Submit(ctx, "", spec, "")
if err == nil {
t.Fatal("Submit: expected error when no peers and no local capacity, got nil")
}
}
func TestDispatcher_LocalSubmit(t *testing.T) {
exec := &mockExecutor{
submitFn: func(ctx context.Context, spec []byte) (string, error) {
return "ls-job", nil
},
}
d, _, cleanup := newTestDispatcher(t, exec)
defer cleanup()
jobID, err := d.LocalSubmit(context.Background(), []byte(`{"command":"/bin/true"}`))
if err != nil {
t.Fatalf("LocalSubmit: %v", err)
}
if jobID != "ls-job" {
t.Errorf("LocalSubmit: got %q, want ls-job", jobID)
}
if !exec.submitted {
t.Error("LocalSubmit: executor.Submit not called")
}
}
func TestDispatcher_LocalStatus(t *testing.T) {
exec := &mockExecutor{
statusFn: func(ctx context.Context, jobID string) (string, error) {
if jobID == "known" {
return "running", nil
}
return "", errors.New("not found")
},
}
d, _, cleanup := newTestDispatcher(t, exec)
defer cleanup()
st, err := d.LocalStatus(context.Background(), "known")
if err != nil {
t.Fatalf("LocalStatus: %v", err)
}
if st != "running" {
t.Errorf("LocalStatus: got %q, want running", st)
}
if _, err := d.LocalStatus(context.Background(), "missing"); err == nil {
t.Error("LocalStatus: expected error for missing job, got nil")
}
}
func TestDispatcher_LocalSubmit_NilExecutor(t *testing.T) {
d := NewDispatcher(nil, nil, NewPeerRegistry(), nil)
if _, err := d.LocalSubmit(context.Background(), []byte(`{}`)); err == nil {
t.Error("LocalSubmit with nil executor: expected error, got nil")
}
if _, err := d.LocalStatus(context.Background(), "x"); err == nil {
t.Error("LocalStatus with nil executor: expected error, got nil")
}
}
func TestParseInlineSpec(t *testing.T) {
spec, err := parseInlineSpec([]byte(`{"cpu_millicores":500,"memory_mib":256,"disk_mib":128}`))
if err != nil {
t.Fatalf("parseInlineSpec: %v", err)
}
if spec.CPUMillicores != 500 || spec.MemoryMiB != 256 || spec.DiskMiB != 128 {
t.Errorf("parseInlineSpec: got %+v, want cpu=500 mem=256 disk=128", spec)
}
if _, err := parseInlineSpec([]byte(`{bad json`)); err == nil {
t.Fatal("parseInlineSpec: expected error for malformed JSON, got nil")
}
}