- 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>
216 lines
5.0 KiB
Go
216 lines
5.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
)
|
|
|
|
func newTestWorkService() (*WorkService, *mockWorkQueue) {
|
|
q := newMockWorkQueue()
|
|
svc := NewWorkService(q, WorkServiceConfig{})
|
|
return svc, q
|
|
}
|
|
|
|
func TestWorkService_EnqueueTask(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
svc, q := newTestWorkService()
|
|
|
|
result, err := svc.EnqueueTask(context.Background(), EnqueueTaskRequest{
|
|
ProjectID: "myapp",
|
|
Type: domain.WorkTaskTypeBuild,
|
|
Priority: 1,
|
|
Spec: map[string]any{"branch": "main"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result.TaskID == "" {
|
|
t.Error("task ID should not be empty")
|
|
}
|
|
if result.StatusURL == "" {
|
|
t.Error("status URL should not be empty")
|
|
}
|
|
if len(q.tasks) != 1 {
|
|
t.Errorf("tasks in queue = %d, want 1", len(q.tasks))
|
|
}
|
|
})
|
|
|
|
t.Run("default max retries", func(t *testing.T) {
|
|
svc, q := newTestWorkService()
|
|
|
|
_, err := svc.EnqueueTask(context.Background(), EnqueueTaskRequest{
|
|
ProjectID: "myapp",
|
|
Type: domain.WorkTaskTypeBuild,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
for _, task := range q.tasks {
|
|
if task.MaxRetries != 3 {
|
|
t.Errorf("max retries = %d, want 3 (default)", task.MaxRetries)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("missing project id", func(t *testing.T) {
|
|
svc, _ := newTestWorkService()
|
|
|
|
_, err := svc.EnqueueTask(context.Background(), EnqueueTaskRequest{
|
|
Type: domain.WorkTaskTypeBuild,
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for missing project_id")
|
|
}
|
|
})
|
|
|
|
t.Run("missing type", func(t *testing.T) {
|
|
svc, _ := newTestWorkService()
|
|
|
|
_, err := svc.EnqueueTask(context.Background(), EnqueueTaskRequest{
|
|
ProjectID: "myapp",
|
|
})
|
|
if err == nil {
|
|
t.Error("expected error for missing type")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestWorkService_DequeueTask(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
svc, q := newTestWorkService()
|
|
|
|
// Enqueue a task first
|
|
q.tasks["task-1"] = &domain.WorkTask{
|
|
ID: "task-1",
|
|
ProjectID: "myapp",
|
|
Type: domain.WorkTaskTypeBuild,
|
|
Status: domain.WorkTaskStatusPending,
|
|
}
|
|
|
|
task, err := svc.DequeueTask(context.Background(), "worker-1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if task == nil {
|
|
t.Fatal("expected a task, got nil")
|
|
}
|
|
if task.ID != "task-1" {
|
|
t.Errorf("task ID = %q, want %q", task.ID, "task-1")
|
|
}
|
|
if task.Status != domain.WorkTaskStatusRunning {
|
|
t.Errorf("task status = %q, want %q", task.Status, domain.WorkTaskStatusRunning)
|
|
}
|
|
})
|
|
|
|
t.Run("empty queue", func(t *testing.T) {
|
|
svc, _ := newTestWorkService()
|
|
|
|
task, err := svc.DequeueTask(context.Background(), "worker-1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if task != nil {
|
|
t.Error("expected nil task for empty queue")
|
|
}
|
|
})
|
|
|
|
t.Run("missing worker id", func(t *testing.T) {
|
|
svc, _ := newTestWorkService()
|
|
|
|
_, err := svc.DequeueTask(context.Background(), "")
|
|
if err == nil {
|
|
t.Error("expected error for missing worker_id")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestWorkService_CompleteTask(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
svc, q := newTestWorkService()
|
|
|
|
q.tasks["task-1"] = &domain.WorkTask{
|
|
ID: "task-1",
|
|
ProjectID: "myapp",
|
|
Type: domain.WorkTaskTypeBuild,
|
|
Status: domain.WorkTaskStatusRunning,
|
|
}
|
|
|
|
result := &domain.WorkResult{Output: "ok"}
|
|
err := svc.CompleteTask(context.Background(), "task-1", result)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
task := q.tasks["task-1"]
|
|
if task.Status != domain.WorkTaskStatusCompleted {
|
|
t.Errorf("status = %q, want %q", task.Status, domain.WorkTaskStatusCompleted)
|
|
}
|
|
})
|
|
|
|
t.Run("task not found", func(t *testing.T) {
|
|
svc, _ := newTestWorkService()
|
|
|
|
err := svc.CompleteTask(context.Background(), "nonexistent", nil)
|
|
if err == nil {
|
|
t.Error("expected error for nonexistent task")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestWorkService_GetTask(t *testing.T) {
|
|
t.Run("found", func(t *testing.T) {
|
|
svc, q := newTestWorkService()
|
|
|
|
q.tasks["task-1"] = &domain.WorkTask{
|
|
ID: "task-1",
|
|
ProjectID: "myapp",
|
|
}
|
|
|
|
task, err := svc.GetTask(context.Background(), "task-1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if task.ID != "task-1" {
|
|
t.Errorf("task ID = %q, want %q", task.ID, "task-1")
|
|
}
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
svc, _ := newTestWorkService()
|
|
|
|
_, err := svc.GetTask(context.Background(), "missing")
|
|
if err == nil {
|
|
t.Error("expected error for missing task")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestWorkService_GetStats(t *testing.T) {
|
|
svc, _ := newTestWorkService()
|
|
|
|
stats, err := svc.GetStats(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if stats == nil {
|
|
t.Error("stats should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestWorkService_CancelTask(t *testing.T) {
|
|
svc, q := newTestWorkService()
|
|
|
|
q.tasks["task-1"] = &domain.WorkTask{
|
|
ID: "task-1",
|
|
ProjectID: "myapp",
|
|
Status: domain.WorkTaskStatusPending,
|
|
}
|
|
|
|
err := svc.CancelTask(context.Background(), "task-1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|