package service import ( "context" "fmt" "testing" "github.com/orchard9/rdev/internal/domain" ) func TestBuildService_StartBuild(t *testing.T) { ctx := context.Background() t.Run("enqueues build successfully", func(t *testing.T) { queue := newMockWorkQueue() audit := newMockBuildAudit() svc := NewBuildService(queue, audit) taskID, err := svc.StartBuild(ctx, "project-1", domain.BuildSpec{ Prompt: "Build a landing page", Template: "nextjs", }) if err != nil { t.Fatalf("StartBuild() error = %v", err) } if taskID == "" { t.Error("expected non-empty task ID") } // Verify task was enqueued if len(queue.tasks) != 1 { t.Errorf("expected 1 task in queue, got %d", len(queue.tasks)) } task := queue.tasks[taskID] if task.ProjectID != "project-1" { t.Errorf("got project_id %q, want %q", task.ProjectID, "project-1") } if task.Type != domain.WorkTaskTypeBuild { t.Errorf("got type %q, want %q", task.Type, domain.WorkTaskTypeBuild) } // Verify audit was recorded if len(audit.entries) != 1 { t.Errorf("expected 1 audit entry, got %d", len(audit.entries)) } }) t.Run("validates prompt required", func(t *testing.T) { queue := newMockWorkQueue() audit := newMockBuildAudit() svc := NewBuildService(queue, audit) _, err := svc.StartBuild(ctx, "project-1", domain.BuildSpec{}) if err == nil { t.Error("expected error for empty prompt") } }) t.Run("validates project ID required", func(t *testing.T) { queue := newMockWorkQueue() audit := newMockBuildAudit() svc := NewBuildService(queue, audit) _, err := svc.StartBuild(ctx, "", domain.BuildSpec{Prompt: "Build"}) if err == nil { t.Error("expected error for empty project ID") } }) t.Run("includes variables in spec", func(t *testing.T) { queue := newMockWorkQueue() audit := newMockBuildAudit() svc := NewBuildService(queue, audit) taskID, err := svc.StartBuild(ctx, "project-1", domain.BuildSpec{ Prompt: "Build", Variables: map[string]string{ "name": "My App", "color": "blue", }, }) if err != nil { t.Fatalf("StartBuild() error = %v", err) } task := queue.tasks[taskID] vars, ok := task.Spec["variables"].(map[string]string) if !ok { t.Fatal("expected variables in task spec") } if vars["name"] != "My App" { t.Errorf("got variable name %q, want %q", vars["name"], "My App") } }) t.Run("continues if audit fails", func(t *testing.T) { queue := newMockWorkQueue() audit := newMockBuildAudit() audit.err = fmt.Errorf("db connection failed") svc := NewBuildService(queue, audit) taskID, err := svc.StartBuild(ctx, "project-1", domain.BuildSpec{ Prompt: "Build", }) if err != nil { t.Fatalf("StartBuild() should succeed even if audit fails, got error = %v", err) } if taskID == "" { t.Error("expected non-empty task ID") } }) } func TestBuildService_GetBuildStatus(t *testing.T) { ctx := context.Background() t.Run("returns existing entry", func(t *testing.T) { audit := newMockBuildAudit() audit.entries["task-1"] = &domain.BuildAuditEntry{ TaskID: "task-1", ProjectID: "project-1", Status: domain.BuildStatusRunning, } svc := NewBuildService(newMockWorkQueue(), audit) entry, err := svc.GetBuildStatus(ctx, "task-1") if err != nil { t.Fatalf("GetBuildStatus() error = %v", err) } if entry.Status != domain.BuildStatusRunning { t.Errorf("got status %q, want %q", entry.Status, domain.BuildStatusRunning) } }) t.Run("returns error for nonexistent entry", func(t *testing.T) { audit := newMockBuildAudit() svc := NewBuildService(newMockWorkQueue(), audit) _, err := svc.GetBuildStatus(ctx, "nonexistent") if err == nil { t.Error("expected error for nonexistent entry") } }) } func TestBuildService_ListBuilds(t *testing.T) { ctx := context.Background() audit := newMockBuildAudit() audit.entries["task-1"] = &domain.BuildAuditEntry{ TaskID: "task-1", ProjectID: "project-a", Status: domain.BuildStatusCompleted, } audit.entries["task-2"] = &domain.BuildAuditEntry{ TaskID: "task-2", ProjectID: "project-a", Status: domain.BuildStatusFailed, } audit.entries["task-3"] = &domain.BuildAuditEntry{ TaskID: "task-3", ProjectID: "project-b", Status: domain.BuildStatusPending, } svc := NewBuildService(newMockWorkQueue(), audit) t.Run("lists builds for project", func(t *testing.T) { entries, err := svc.ListBuilds(ctx, "project-a", 50) if err != nil { t.Fatalf("ListBuilds() error = %v", err) } if len(entries) != 2 { t.Errorf("got %d entries, want 2", len(entries)) } }) t.Run("uses default limit", func(t *testing.T) { entries, err := svc.ListBuilds(ctx, "project-a", 0) if err != nil { t.Fatalf("ListBuilds() error = %v", err) } if len(entries) != 2 { t.Errorf("got %d entries, want 2", len(entries)) } }) } func TestBuildService_CompleteBuild(t *testing.T) { ctx := context.Background() t.Run("updates audit on completion", func(t *testing.T) { audit := newMockBuildAudit() audit.entries["task-1"] = &domain.BuildAuditEntry{ TaskID: "task-1", ProjectID: "project-1", Status: domain.BuildStatusRunning, } svc := NewBuildService(newMockWorkQueue(), audit) err := svc.CompleteBuild(ctx, "task-1", &domain.BuildResult{ Success: true, CommitSHA: "abc123", DurationMs: 5000, }) if err != nil { t.Fatalf("CompleteBuild() error = %v", err) } entry := audit.entries["task-1"] if entry.Status != domain.BuildStatusCompleted { t.Errorf("got status %q, want %q", entry.Status, domain.BuildStatusCompleted) } if entry.Result == nil { t.Fatal("expected result to be set") } if !entry.Result.Success { t.Error("expected result.Success = true") } }) }