rdev/internal/service/worker_service_test.go
jordan bc47e426b0 feat: Add CI pipeline proxy, DNS alias management, and worker executor system
- Add ListPipelines/GetPipeline to CIProvider port with Woodpecker adapter
- Add DNS alias endpoints: GET/POST/DELETE /projects/{id}/domains
- Implement worker executor daemon, build executor, and git operations
- Add build service, worker service, and build audit tracking
- Add worker registry with PostgreSQL adapter and migration
- Add multi-provider code agent interface (Claude Code + OpenCode)
- Add create-and-build combo endpoint
- Update landing-page cookbook to reflect all gaps closed
- Fix tech debt: unified validation, auth scopes, error wrapping, slog patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 21:05:28 -07:00

329 lines
8.7 KiB
Go

package service
import (
"context"
"testing"
"time"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/port"
)
func TestWorkerService_Register(t *testing.T) {
ctx := context.Background()
t.Run("registers valid worker", func(t *testing.T) {
registry := newMockWorkerRegistry()
queue := newMockWorkQueue()
svc := NewWorkerService(registry, queue, nil)
err := svc.Register(ctx, &domain.Worker{
ID: "worker-1",
Hostname: "host-1",
Capabilities: []string{"build"},
Version: "1.0.0",
})
if err != nil {
t.Fatalf("Register() error = %v", err)
}
w := registry.workers["worker-1"]
if w == nil {
t.Fatal("worker not found in registry")
}
if w.Status != domain.WorkerStatusIdle {
t.Errorf("got status %q, want %q", w.Status, domain.WorkerStatusIdle)
}
if w.RegisteredAt.IsZero() {
t.Error("expected registered_at to be set")
}
})
t.Run("validates worker", func(t *testing.T) {
registry := newMockWorkerRegistry()
queue := newMockWorkQueue()
svc := NewWorkerService(registry, queue, nil)
err := svc.Register(ctx, &domain.Worker{})
if err == nil {
t.Error("expected validation error for empty worker")
}
})
}
func TestWorkerService_Heartbeat(t *testing.T) {
ctx := context.Background()
t.Run("updates heartbeat", func(t *testing.T) {
registry := newMockWorkerRegistry()
registry.workers["worker-1"] = &domain.Worker{
ID: "worker-1",
Hostname: "host-1",
Status: domain.WorkerStatusIdle,
LastHeartbeat: time.Now().Add(-30 * time.Second),
}
svc := NewWorkerService(registry, newMockWorkQueue(), nil)
err := svc.Heartbeat(ctx, "worker-1")
if err != nil {
t.Fatalf("Heartbeat() error = %v", err)
}
// Heartbeat should be recent
w := registry.workers["worker-1"]
if time.Since(w.LastHeartbeat) > time.Second {
t.Error("expected heartbeat to be updated to now")
}
})
t.Run("returns error for nonexistent worker", func(t *testing.T) {
registry := newMockWorkerRegistry()
svc := NewWorkerService(registry, newMockWorkQueue(), nil)
err := svc.Heartbeat(ctx, "nonexistent")
if err == nil {
t.Error("expected error for nonexistent worker")
}
})
}
func TestWorkerService_Deregister(t *testing.T) {
ctx := context.Background()
t.Run("deregisters worker", func(t *testing.T) {
registry := newMockWorkerRegistry()
registry.workers["worker-1"] = &domain.Worker{
ID: "worker-1",
Hostname: "host-1",
Status: domain.WorkerStatusIdle,
}
svc := NewWorkerService(registry, newMockWorkQueue(), nil)
err := svc.Deregister(ctx, "worker-1")
if err != nil {
t.Fatalf("Deregister() error = %v", err)
}
if _, ok := registry.workers["worker-1"]; ok {
t.Error("worker should be removed from registry")
}
})
}
func TestWorkerService_ClaimTask(t *testing.T) {
ctx := context.Background()
t.Run("claims task and marks worker busy", func(t *testing.T) {
registry := newMockWorkerRegistry()
registry.workers["worker-1"] = &domain.Worker{
ID: "worker-1",
Hostname: "host-1",
Status: domain.WorkerStatusIdle,
}
queue := newMockWorkQueue()
queue.tasks["task-1"] = &domain.WorkTask{
ID: "task-1",
ProjectID: "project-1",
Type: domain.WorkTaskTypeBuild,
Status: domain.WorkTaskStatusPending,
CreatedAt: time.Now(),
}
svc := NewWorkerService(registry, queue, nil)
task, err := svc.ClaimTask(ctx, "worker-1")
if err != nil {
t.Fatalf("ClaimTask() error = %v", err)
}
if task == nil {
t.Fatal("expected task to be returned")
}
if task.ID != "task-1" {
t.Errorf("got task ID %q, want %q", task.ID, "task-1")
}
// Worker should be busy with the task
w := registry.workers["worker-1"]
if w.Status != domain.WorkerStatusBusy {
t.Errorf("got status %q, want %q", w.Status, domain.WorkerStatusBusy)
}
if w.CurrentTask != "task-1" {
t.Errorf("got current_task %q, want %q", w.CurrentTask, "task-1")
}
})
t.Run("returns nil when no tasks available", func(t *testing.T) {
registry := newMockWorkerRegistry()
registry.workers["worker-1"] = &domain.Worker{
ID: "worker-1",
Hostname: "host-1",
Status: domain.WorkerStatusIdle,
}
queue := newMockWorkQueue()
svc := NewWorkerService(registry, queue, nil)
task, err := svc.ClaimTask(ctx, "worker-1")
if err != nil {
t.Fatalf("ClaimTask() error = %v", err)
}
if task != nil {
t.Error("expected nil task when queue is empty")
}
})
}
func TestWorkerService_CompleteTask(t *testing.T) {
ctx := context.Background()
t.Run("completes task and returns worker to idle", func(t *testing.T) {
registry := newMockWorkerRegistry()
registry.workers["worker-1"] = &domain.Worker{
ID: "worker-1",
Hostname: "host-1",
Status: domain.WorkerStatusBusy,
CurrentTask: "task-1",
}
queue := newMockWorkQueue()
queue.tasks["task-1"] = &domain.WorkTask{
ID: "task-1",
ProjectID: "project-1",
Type: domain.WorkTaskTypeBuild,
Status: domain.WorkTaskStatusRunning,
WorkerID: "worker-1",
}
svc := NewWorkerService(registry, queue, nil)
err := svc.CompleteTask(ctx, "worker-1", "task-1", &domain.BuildResult{
Success: true,
CommitSHA: "abc123",
DurationMs: 5000,
})
if err != nil {
t.Fatalf("CompleteTask() error = %v", err)
}
// Task should be completed
task := queue.tasks["task-1"]
if task.Status != domain.WorkTaskStatusCompleted {
t.Errorf("got task status %q, want %q", task.Status, domain.WorkTaskStatusCompleted)
}
// Worker should be idle
w := registry.workers["worker-1"]
if w.Status != domain.WorkerStatusIdle {
t.Errorf("got worker status %q, want %q", w.Status, domain.WorkerStatusIdle)
}
if w.CurrentTask != "" {
t.Errorf("got current_task %q, want empty", w.CurrentTask)
}
})
t.Run("handles nil result", func(t *testing.T) {
registry := newMockWorkerRegistry()
registry.workers["worker-1"] = &domain.Worker{
ID: "worker-1",
Hostname: "host-1",
Status: domain.WorkerStatusBusy,
CurrentTask: "task-1",
}
queue := newMockWorkQueue()
queue.tasks["task-1"] = &domain.WorkTask{
ID: "task-1",
Status: domain.WorkTaskStatusRunning,
WorkerID: "worker-1",
}
svc := NewWorkerService(registry, queue, nil)
err := svc.CompleteTask(ctx, "worker-1", "task-1", nil)
if err != nil {
t.Fatalf("CompleteTask(nil result) error = %v", err)
}
// Worker should be idle
w := registry.workers["worker-1"]
if w.Status != domain.WorkerStatusIdle {
t.Errorf("got worker status %q, want %q", w.Status, domain.WorkerStatusIdle)
}
})
}
func TestWorkerService_ListWorkers(t *testing.T) {
ctx := context.Background()
registry := newMockWorkerRegistry()
registry.workers["worker-1"] = &domain.Worker{ID: "worker-1", Status: domain.WorkerStatusIdle}
registry.workers["worker-2"] = &domain.Worker{ID: "worker-2", Status: domain.WorkerStatusBusy}
registry.workers["worker-3"] = &domain.Worker{ID: "worker-3", Status: domain.WorkerStatusIdle}
svc := NewWorkerService(registry, newMockWorkQueue(), nil)
t.Run("lists all workers", func(t *testing.T) {
workers, err := svc.ListWorkers(ctx, port.WorkerFilter{})
if err != nil {
t.Fatalf("ListWorkers() error = %v", err)
}
if len(workers) != 3 {
t.Errorf("got %d workers, want 3", len(workers))
}
})
t.Run("filters by status", func(t *testing.T) {
idle := domain.WorkerStatusIdle
workers, err := svc.ListWorkers(ctx, port.WorkerFilter{Status: &idle})
if err != nil {
t.Fatalf("ListWorkers() error = %v", err)
}
if len(workers) != 2 {
t.Errorf("got %d idle workers, want 2", len(workers))
}
})
}
func TestWorkerService_DrainWorker(t *testing.T) {
ctx := context.Background()
t.Run("drains worker", func(t *testing.T) {
registry := newMockWorkerRegistry()
registry.workers["worker-1"] = &domain.Worker{
ID: "worker-1",
Hostname: "host-1",
Status: domain.WorkerStatusBusy,
CurrentTask: "task-1",
}
svc := NewWorkerService(registry, newMockWorkQueue(), nil)
err := svc.DrainWorker(ctx, "worker-1")
if err != nil {
t.Fatalf("DrainWorker() error = %v", err)
}
w := registry.workers["worker-1"]
if w.Status != domain.WorkerStatusDraining {
t.Errorf("got status %q, want %q", w.Status, domain.WorkerStatusDraining)
}
// Should preserve current task
if w.CurrentTask != "task-1" {
t.Errorf("got current_task %q, want %q", w.CurrentTask, "task-1")
}
})
t.Run("returns error for nonexistent worker", func(t *testing.T) {
registry := newMockWorkerRegistry()
svc := NewWorkerService(registry, newMockWorkQueue(), nil)
err := svc.DrainWorker(ctx, "nonexistent")
if err == nil {
t.Error("expected error for nonexistent worker")
}
})
}