rdev/internal/adapter/sdlc/worker_executor_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

270 lines
7.1 KiB
Go

package sdlc
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/orchard9/rdev/internal/domain"
)
// mockWorkQueue implements port.WorkQueue for testing.
type mockWorkQueue struct {
tasks map[string]*domain.WorkTask
err error
}
func newMockWorkQueue() *mockWorkQueue {
return &mockWorkQueue{tasks: make(map[string]*domain.WorkTask)}
}
func (m *mockWorkQueue) Enqueue(ctx context.Context, task *domain.WorkTask) (string, error) {
if m.err != nil {
return "", m.err
}
id := fmt.Sprintf("task-%d", len(m.tasks)+1)
task.ID = id
task.Status = domain.WorkTaskStatusPending
task.CreatedAt = time.Now()
m.tasks[id] = task
return id, nil
}
func (m *mockWorkQueue) Dequeue(ctx context.Context, workerID string) (*domain.WorkTask, error) {
return nil, nil
}
func (m *mockWorkQueue) Complete(ctx context.Context, taskID string, result *domain.WorkResult) error {
if m.err != nil {
return m.err
}
task, ok := m.tasks[taskID]
if !ok {
return domain.ErrWorkTaskNotFound
}
task.Status = domain.WorkTaskStatusCompleted
task.Result = result
return nil
}
func (m *mockWorkQueue) Fail(ctx context.Context, taskID string, errMsg string) error {
return m.FailWithCode(ctx, taskID, errMsg, domain.WorkErrorCodeNone)
}
func (m *mockWorkQueue) FailWithCode(ctx context.Context, taskID string, errMsg string, code domain.WorkErrorCode) error {
if m.err != nil {
return m.err
}
task, ok := m.tasks[taskID]
if !ok {
return domain.ErrWorkTaskNotFound
}
task.Status = domain.WorkTaskStatusFailed
task.Error = errMsg
task.ErrorCode = code
return nil
}
func (m *mockWorkQueue) Cancel(ctx context.Context, taskID string) error {
return nil
}
func (m *mockWorkQueue) GetTask(ctx context.Context, taskID string) (*domain.WorkTask, error) {
task, ok := m.tasks[taskID]
if !ok {
return nil, domain.ErrWorkTaskNotFound
}
return task, nil
}
func (m *mockWorkQueue) ListByProject(ctx context.Context, projectID string, status *domain.WorkTaskStatus, opts domain.WorkListOptions) (*domain.WorkListResult, error) {
return &domain.WorkListResult{}, nil
}
func (m *mockWorkQueue) GetStats(ctx context.Context) (*domain.WorkQueueStats, error) {
return &domain.WorkQueueStats{}, nil
}
func (m *mockWorkQueue) CleanupOld(ctx context.Context, olderThan time.Duration) (int64, error) {
return 0, nil
}
func (m *mockWorkQueue) RequeueStale(ctx context.Context, timeout time.Duration) (int64, error) {
return 0, nil
}
func (m *mockWorkQueue) RequeueStaleWithIDs(ctx context.Context, timeout time.Duration) ([]string, error) {
return nil, nil
}
func TestWorkerSDLCExecutor_EnqueueTask(t *testing.T) {
queue := newMockWorkQueue()
exec := NewWorkerSDLCExecutor(WorkerSDLCExecutorConfig{
WorkQueue: queue,
DB: nil, // No DB for this test
Timeout: 2 * time.Second,
})
// Test that enqueue builds the correct spec
spec := domain.SDLCTaskSpec{
Command: "feature-create",
Args: []string{"auth-flow", "--title", "Auth Flow"},
GitCloneURL: "https://git.example.com/owner/repo.git",
AutoCommit: true,
AutoPush: true,
}
// Start the completion goroutine before enqueueing
// Use a channel to synchronize
done := make(chan struct{})
go func() {
// Wait a bit for the task to be enqueued
time.Sleep(100 * time.Millisecond)
for i := 0; i < 10; i++ {
if len(queue.tasks) > 0 {
break
}
time.Sleep(50 * time.Millisecond)
}
for _, task := range queue.tasks {
task.Status = domain.WorkTaskStatusCompleted
featureJSON, _ := json.Marshal(map[string]string{
"slug": "auth-flow",
"title": "Auth Flow",
})
task.Result = &domain.WorkResult{Output: string(featureJSON)}
}
close(done)
}()
output, err := exec.enqueueAndWait(context.Background(), "project-1", spec)
<-done // Wait for completion goroutine
if err != nil {
t.Fatalf("enqueueAndWait() error = %v", err)
}
if output == "" {
t.Error("expected output")
}
// Verify task was enqueued with correct spec
if len(queue.tasks) != 1 {
t.Fatalf("expected 1 task, got %d", len(queue.tasks))
}
for _, task := range queue.tasks {
if task.Type != domain.WorkTaskTypeSDLC {
t.Errorf("got task type %q, want %q", task.Type, domain.WorkTaskTypeSDLC)
}
if cmd, _ := task.Spec["command"].(string); cmd != "feature-create" {
t.Errorf("got command %q, want %q", cmd, "feature-create")
}
}
}
func TestWorkerSDLCExecutor_Timeout(t *testing.T) {
queue := newMockWorkQueue()
exec := NewWorkerSDLCExecutor(WorkerSDLCExecutorConfig{
WorkQueue: queue,
DB: nil,
Timeout: 100 * time.Millisecond, // Short timeout
})
spec := domain.SDLCTaskSpec{
Command: "feature-create",
Args: []string{"auth-flow"},
GitCloneURL: "https://git.example.com/owner/repo.git",
}
// Don't complete the task - it should timeout
_, err := exec.enqueueAndWait(context.Background(), "project-1", spec)
if err == nil {
t.Error("expected timeout error")
}
}
func TestWorkerSDLCExecutor_TaskFailed(t *testing.T) {
queue := newMockWorkQueue()
exec := NewWorkerSDLCExecutor(WorkerSDLCExecutorConfig{
WorkQueue: queue,
DB: nil,
Timeout: 500 * time.Millisecond,
})
spec := domain.SDLCTaskSpec{
Command: "feature-create",
Args: []string{"auth-flow"},
GitCloneURL: "https://git.example.com/owner/repo.git",
}
// Simulate task failure
go func() {
time.Sleep(50 * time.Millisecond)
for _, task := range queue.tasks {
task.Status = domain.WorkTaskStatusFailed
task.Error = "sdlc command failed: feature already exists"
}
}()
_, err := exec.enqueueAndWait(context.Background(), "project-1", spec)
if err == nil {
t.Error("expected error for failed task")
}
}
func TestWorkerSDLCExecutor_ContextCancelled(t *testing.T) {
queue := newMockWorkQueue()
exec := NewWorkerSDLCExecutor(WorkerSDLCExecutorConfig{
WorkQueue: queue,
DB: nil,
Timeout: 5 * time.Second,
})
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
spec := domain.SDLCTaskSpec{
Command: "feature-create",
Args: []string{"auth-flow"},
GitCloneURL: "https://git.example.com/owner/repo.git",
}
_, err := exec.enqueueAndWait(ctx, "project-1", spec)
if err == nil {
t.Error("expected context cancelled error")
}
}
func TestWorkerSDLCExecutor_NoGitURL(t *testing.T) {
queue := newMockWorkQueue()
exec := NewWorkerSDLCExecutor(WorkerSDLCExecutorConfig{
WorkQueue: queue,
DB: nil, // No DB - can't get git URL
Timeout: 500 * time.Millisecond,
})
// This should fail because we can't get the git URL without a database
_, err := exec.GetState(context.Background(), "project-1")
if err == nil {
t.Error("expected error when DB is nil")
}
}
func TestWorkerSDLCExecutor_InterfaceCompliance(t *testing.T) {
// Verify that WorkerSDLCExecutor implements port.SDLCExecutor at compile time
// This is already done in the source file, but we test it here too
exec := NewWorkerSDLCExecutor(WorkerSDLCExecutorConfig{
WorkQueue: newMockWorkQueue(),
DB: (*sql.DB)(nil),
})
// Just ensure the executor exists and has the right methods
if exec == nil {
t.Error("expected non-nil executor")
}
}