package postgres import ( "context" "database/sql" "testing" "time" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" "github.com/orchard9/rdev/internal/testutil" ) func cleanupTestBuildAudit(t *testing.T, db *sql.DB) { t.Helper() _, err := db.Exec("DELETE FROM build_audit WHERE project_id LIKE 'test-%'") if err != nil { t.Logf("cleanup test build_audit: %v", err) } } func TestBuildAuditRepository_Record(t *testing.T) { db := testutil.TestDB(t) t.Cleanup(func() { cleanupTestBuildAudit(t, db) }) repo := NewBuildAuditRepository(db) ctx := context.Background() t.Run("records new audit entry", func(t *testing.T) { entry := &domain.BuildAuditEntry{ TaskID: "test-task-audit-1", ProjectID: "test-project-1", Spec: domain.BuildSpec{ Prompt: "Build a landing page", Template: "nextjs", }, Status: domain.BuildStatusPending, StartedAt: time.Now(), } err := repo.Record(ctx, entry) if err != nil { t.Fatalf("Record() error = %v", err) } // Verify it was stored got, err := repo.Get(ctx, "test-task-audit-1") if err != nil { t.Fatalf("Get() error = %v", err) } if got.ProjectID != "test-project-1" { t.Errorf("got project_id %q, want %q", got.ProjectID, "test-project-1") } if got.Spec.Prompt != "Build a landing page" { t.Errorf("got prompt %q, want %q", got.Spec.Prompt, "Build a landing page") } if got.Status != domain.BuildStatusPending { t.Errorf("got status %q, want %q", got.Status, domain.BuildStatusPending) } }) t.Run("records entry with worker ID", func(t *testing.T) { entry := &domain.BuildAuditEntry{ TaskID: "test-task-audit-2", ProjectID: "test-project-1", WorkerID: "worker-1", Spec: domain.BuildSpec{ Prompt: "Run tests", }, Status: domain.BuildStatusRunning, StartedAt: time.Now(), } err := repo.Record(ctx, entry) if err != nil { t.Fatalf("Record() error = %v", err) } got, err := repo.Get(ctx, "test-task-audit-2") if err != nil { t.Fatalf("Get() error = %v", err) } if got.WorkerID != "worker-1" { t.Errorf("got worker_id %q, want %q", got.WorkerID, "worker-1") } }) } func TestBuildAuditRepository_Update(t *testing.T) { db := testutil.TestDB(t) t.Cleanup(func() { cleanupTestBuildAudit(t, db) }) repo := NewBuildAuditRepository(db) ctx := context.Background() // Create initial entry entry := &domain.BuildAuditEntry{ TaskID: "test-task-update-1", ProjectID: "test-project-1", Spec: domain.BuildSpec{Prompt: "Build"}, Status: domain.BuildStatusPending, StartedAt: time.Now(), } if err := repo.Record(ctx, entry); err != nil { t.Fatalf("Record() error = %v", err) } t.Run("updates with success result", func(t *testing.T) { result := &domain.BuildResult{ Success: true, Output: "Build successful", CommitSHA: "abc123", FilesChanged: []string{"index.html", "style.css"}, DurationMs: 5000, } err := repo.Update(ctx, "test-task-update-1", result) if err != nil { t.Fatalf("Update() error = %v", err) } got, err := repo.Get(ctx, "test-task-update-1") if err != nil { t.Fatalf("Get() error = %v", err) } if got.Status != domain.BuildStatusCompleted { t.Errorf("got status %q, want %q", got.Status, domain.BuildStatusCompleted) } if got.Result == nil { t.Fatal("expected result to be set") } if !got.Result.Success { t.Error("expected result.Success = true") } if got.Result.CommitSHA != "abc123" { t.Errorf("got commit_sha %q, want %q", got.Result.CommitSHA, "abc123") } if got.CompletedAt == nil { t.Error("expected completed_at to be set") } }) t.Run("updates with failure result", func(t *testing.T) { // Create a new entry entry := &domain.BuildAuditEntry{ TaskID: "test-task-update-2", ProjectID: "test-project-1", Spec: domain.BuildSpec{Prompt: "Build"}, Status: domain.BuildStatusPending, StartedAt: time.Now(), } if err := repo.Record(ctx, entry); err != nil { t.Fatalf("Record() error = %v", err) } result := &domain.BuildResult{ Success: false, Error: "compilation error", } err := repo.Update(ctx, "test-task-update-2", result) if err != nil { t.Fatalf("Update() error = %v", err) } got, err := repo.Get(ctx, "test-task-update-2") if err != nil { t.Fatalf("Get() error = %v", err) } if got.Status != domain.BuildStatusFailed { t.Errorf("got status %q, want %q", got.Status, domain.BuildStatusFailed) } }) t.Run("returns error for nonexistent task", func(t *testing.T) { result := &domain.BuildResult{Success: true} err := repo.Update(ctx, "test-task-nonexistent", result) if err == nil { t.Error("expected error for nonexistent task") } }) } func TestBuildAuditRepository_UpdateStatus(t *testing.T) { db := testutil.TestDB(t) t.Cleanup(func() { cleanupTestBuildAudit(t, db) }) repo := NewBuildAuditRepository(db) ctx := context.Background() // Create initial entry entry := &domain.BuildAuditEntry{ TaskID: "test-task-status-1", ProjectID: "test-project-1", Spec: domain.BuildSpec{Prompt: "Build"}, Status: domain.BuildStatusPending, StartedAt: time.Now(), } if err := repo.Record(ctx, entry); err != nil { t.Fatalf("Record() error = %v", err) } t.Run("updates status and worker ID", func(t *testing.T) { err := repo.UpdateStatus(ctx, "test-task-status-1", domain.BuildStatusRunning, "worker-123") if err != nil { t.Fatalf("UpdateStatus() error = %v", err) } got, err := repo.Get(ctx, "test-task-status-1") if err != nil { t.Fatalf("Get() error = %v", err) } if got.Status != domain.BuildStatusRunning { t.Errorf("got status %q, want %q", got.Status, domain.BuildStatusRunning) } if got.WorkerID != "worker-123" { t.Errorf("got worker_id %q, want %q", got.WorkerID, "worker-123") } }) t.Run("updates status with empty worker ID", func(t *testing.T) { // Create another entry entry := &domain.BuildAuditEntry{ TaskID: "test-task-status-2", ProjectID: "test-project-1", WorkerID: "old-worker", Spec: domain.BuildSpec{Prompt: "Build"}, Status: domain.BuildStatusRunning, StartedAt: time.Now(), } if err := repo.Record(ctx, entry); err != nil { t.Fatalf("Record() error = %v", err) } err := repo.UpdateStatus(ctx, "test-task-status-2", domain.BuildStatusCompleted, "") if err != nil { t.Fatalf("UpdateStatus() error = %v", err) } got, err := repo.Get(ctx, "test-task-status-2") if err != nil { t.Fatalf("Get() error = %v", err) } if got.Status != domain.BuildStatusCompleted { t.Errorf("got status %q, want %q", got.Status, domain.BuildStatusCompleted) } // WorkerID should be cleared when empty string is passed if got.WorkerID != "" { t.Errorf("got worker_id %q, want empty", got.WorkerID) } }) t.Run("returns error for nonexistent task", func(t *testing.T) { err := repo.UpdateStatus(ctx, "test-task-nonexistent", domain.BuildStatusRunning, "worker-1") if err == nil { t.Error("expected error for nonexistent task") } }) } func TestBuildAuditRepository_Get(t *testing.T) { db := testutil.TestDB(t) t.Cleanup(func() { cleanupTestBuildAudit(t, db) }) repo := NewBuildAuditRepository(db) ctx := context.Background() t.Run("returns error for nonexistent entry", func(t *testing.T) { _, err := repo.Get(ctx, "test-task-nonexistent") if err == nil { t.Error("expected error for nonexistent entry") } }) } func TestBuildAuditRepository_List(t *testing.T) { db := testutil.TestDB(t) t.Cleanup(func() { cleanupTestBuildAudit(t, db) }) repo := NewBuildAuditRepository(db) ctx := context.Background() // Create entries for different projects entries := []*domain.BuildAuditEntry{ {TaskID: "test-task-list-1", ProjectID: "test-project-a", Spec: domain.BuildSpec{Prompt: "Build 1"}, Status: domain.BuildStatusCompleted, StartedAt: time.Now()}, {TaskID: "test-task-list-2", ProjectID: "test-project-a", Spec: domain.BuildSpec{Prompt: "Build 2"}, Status: domain.BuildStatusFailed, StartedAt: time.Now()}, {TaskID: "test-task-list-3", ProjectID: "test-project-b", Spec: domain.BuildSpec{Prompt: "Build 3"}, Status: domain.BuildStatusPending, StartedAt: time.Now()}, } for _, e := range entries { if err := repo.Record(ctx, e); err != nil { t.Fatalf("Record() error = %v", err) } } t.Run("filters by project", func(t *testing.T) { got, err := repo.List(ctx, port.BuildAuditFilter{ ProjectID: "test-project-a", }) if err != nil { t.Fatalf("List() error = %v", err) } if len(got) != 2 { t.Errorf("got %d entries, want 2", len(got)) } }) t.Run("filters by status", func(t *testing.T) { completed := domain.BuildStatusCompleted got, err := repo.List(ctx, port.BuildAuditFilter{ Status: &completed, }) if err != nil { t.Fatalf("List() error = %v", err) } for _, e := range got { if e.Status != domain.BuildStatusCompleted { t.Errorf("got status %q, want only completed", e.Status) } } }) t.Run("respects limit", func(t *testing.T) { got, err := repo.List(ctx, port.BuildAuditFilter{ Limit: 1, }) if err != nil { t.Fatalf("List() error = %v", err) } if len(got) > 1 { t.Errorf("got %d entries, want at most 1", len(got)) } }) }