rdev/internal/service/build_service_test.go
jordan d69da6d627 feat: add structured logging infrastructure and SDLC extensions
Major changes:
- Add internal/logging package with field constants, context propagation,
  sensitive data auto-redaction, and per-component log levels
- Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.)
- Extend SDLC with callback handlers, generate endpoints, and executor
- Add new cookbook trees for aeries and slackpath progression
- Add skeleton templates for queue, realtime, and microservices
- Add worker component template with async job processing
- Refactor services and handlers to use new logging infrastructure
- Split component.go into component_infra.go and component_listing.go

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:56:04 -07:00

216 lines
5.6 KiB
Go

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")
}
})
}