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