package worker import ( "context" "fmt" "sync" "time" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" "github.com/orchard9/rdev/internal/service" ) // ============================================================================= // Mock implementations for worker package tests // ============================================================================= type mockWorkQueue struct { mu sync.Mutex tasks map[string]*domain.WorkTask err error } func newMockWorkQueue() *mockWorkQueue { return &mockWorkQueue{tasks: make(map[string]*domain.WorkTask)} } func (m *mockWorkQueue) Enqueue(_ context.Context, task *domain.WorkTask) (string, error) { m.mu.Lock() defer m.mu.Unlock() 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(_ context.Context, workerID string) (*domain.WorkTask, error) { m.mu.Lock() defer m.mu.Unlock() if m.err != nil { return nil, m.err } for _, task := range m.tasks { if task.Status == domain.WorkTaskStatusPending { task.Status = domain.WorkTaskStatusRunning task.WorkerID = workerID now := time.Now() task.StartedAt = &now return task, nil } } return nil, nil } func (m *mockWorkQueue) Complete(_ context.Context, taskID string, result *domain.WorkResult) error { m.mu.Lock() defer m.mu.Unlock() task, ok := m.tasks[taskID] if !ok { return domain.ErrWorkTaskNotFound } task.Status = domain.WorkTaskStatusCompleted task.Result = result now := time.Now() task.CompletedAt = &now return nil } func (m *mockWorkQueue) Fail(_ context.Context, taskID string, errMsg string) error { return m.FailWithCode(context.Background(), taskID, errMsg, domain.WorkErrorCodeNone) } func (m *mockWorkQueue) FailWithCode(_ context.Context, taskID string, errMsg string, code domain.WorkErrorCode) error { m.mu.Lock() defer m.mu.Unlock() task, ok := m.tasks[taskID] if !ok { return domain.ErrWorkTaskNotFound } task.RetryCount++ if task.RetryCount >= task.MaxRetries { task.Status = domain.WorkTaskStatusFailed task.Error = errMsg task.ErrorCode = code } else { task.Status = domain.WorkTaskStatusPending task.WorkerID = "" } return nil } func (m *mockWorkQueue) Cancel(_ context.Context, taskID string) error { return nil } func (m *mockWorkQueue) GetTask(_ context.Context, taskID string) (*domain.WorkTask, error) { m.mu.Lock() defer m.mu.Unlock() task, ok := m.tasks[taskID] if !ok { return nil, domain.ErrWorkTaskNotFound } return task, nil } func (m *mockWorkQueue) List(_ context.Context, _ *domain.WorkTaskStatus, _ domain.WorkListOptions) (*domain.WorkListResult, error) { return &domain.WorkListResult{}, nil } func (m *mockWorkQueue) ListByProject(_ context.Context, _ string, _ *domain.WorkTaskStatus, _ domain.WorkListOptions) (*domain.WorkListResult, error) { return &domain.WorkListResult{}, nil } func (m *mockWorkQueue) GetStats(_ context.Context) (*domain.WorkQueueStats, error) { return &domain.WorkQueueStats{}, nil } func (m *mockWorkQueue) CleanupOld(_ context.Context, _ time.Duration) (int64, error) { return 0, nil } func (m *mockWorkQueue) RequeueStale(_ context.Context, _ time.Duration) (int64, error) { return 0, nil } func (m *mockWorkQueue) RequeueStaleWithIDs(_ context.Context, _ time.Duration) ([]string, error) { return nil, nil } type mockWorkerRegistry struct { mu sync.Mutex workers map[string]*domain.Worker err error } func newMockWorkerRegistry() *mockWorkerRegistry { return &mockWorkerRegistry{workers: make(map[string]*domain.Worker)} } func (m *mockWorkerRegistry) Register(_ context.Context, worker *domain.Worker) error { m.mu.Lock() defer m.mu.Unlock() if m.err != nil { return m.err } m.workers[worker.ID] = worker return nil } func (m *mockWorkerRegistry) Heartbeat(_ context.Context, workerID string) error { m.mu.Lock() defer m.mu.Unlock() w, ok := m.workers[workerID] if !ok { return domain.ErrWorkerNotFound } w.LastHeartbeat = time.Now() return nil } func (m *mockWorkerRegistry) UpdateStatus(_ context.Context, workerID string, status domain.WorkerStatus, taskID string) error { m.mu.Lock() defer m.mu.Unlock() w, ok := m.workers[workerID] if !ok { return domain.ErrWorkerNotFound } w.Status = status w.CurrentTask = taskID return nil } func (m *mockWorkerRegistry) Deregister(_ context.Context, workerID string) error { m.mu.Lock() defer m.mu.Unlock() delete(m.workers, workerID) return nil } func (m *mockWorkerRegistry) Get(_ context.Context, workerID string) (*domain.Worker, error) { m.mu.Lock() defer m.mu.Unlock() w, ok := m.workers[workerID] if !ok { return nil, domain.ErrWorkerNotFound } return w, nil } func (m *mockWorkerRegistry) List(_ context.Context, filter port.WorkerFilter) ([]*domain.Worker, error) { m.mu.Lock() defer m.mu.Unlock() var result []*domain.Worker for _, w := range m.workers { if filter.Status != nil && w.Status != *filter.Status { continue } result = append(result, w) } return result, nil } func (m *mockWorkerRegistry) MarkStaleOffline(_ context.Context, _ time.Duration) (int, error) { return 0, nil } type mockBuildAudit struct { mu sync.Mutex entries map[string]*domain.BuildAuditEntry } func newMockBuildAudit() *mockBuildAudit { return &mockBuildAudit{entries: make(map[string]*domain.BuildAuditEntry)} } func (m *mockBuildAudit) Record(_ context.Context, entry *domain.BuildAuditEntry) error { m.mu.Lock() defer m.mu.Unlock() m.entries[entry.TaskID] = entry return nil } func (m *mockBuildAudit) Update(_ context.Context, taskID string, result *domain.BuildResult) error { m.mu.Lock() defer m.mu.Unlock() entry, ok := m.entries[taskID] if !ok { return domain.ErrBuildNotFound } entry.Result = result if result.Success { entry.Status = domain.BuildStatusCompleted } else { entry.Status = domain.BuildStatusFailed } return nil } func (m *mockBuildAudit) UpdateStatus(_ context.Context, taskID string, status domain.BuildStatus, workerID string) error { m.mu.Lock() defer m.mu.Unlock() entry, ok := m.entries[taskID] if !ok { return domain.ErrBuildNotFound } entry.Status = status entry.WorkerID = workerID return nil } func (m *mockBuildAudit) Get(_ context.Context, taskID string) (*domain.BuildAuditEntry, error) { m.mu.Lock() defer m.mu.Unlock() entry, ok := m.entries[taskID] if !ok { return nil, domain.ErrBuildNotFound } return entry, nil } func (m *mockBuildAudit) List(_ context.Context, _ port.BuildAuditFilter) ([]*domain.BuildAuditEntry, error) { return nil, nil } type mockCodeAgent struct { result *domain.AgentResult err error } func (m *mockCodeAgent) Name() string { return "mock-agent" } func (m *mockCodeAgent) Provider() domain.AgentProvider { return "mock" } func (m *mockCodeAgent) Cancel(_ context.Context, _ string) error { return nil } func (m *mockCodeAgent) Capabilities() domain.AgentCapabilities { return domain.AgentCapabilities{Provider: "mock"} } func (m *mockCodeAgent) Available(_ context.Context) bool { return true } func (m *mockCodeAgent) Execute(_ context.Context, req *domain.AgentRequest, handler domain.AgentEventHandler) (*domain.AgentResult, error) { if handler != nil { handler(domain.AgentEvent{ Type: domain.AgentEventOutput, Content: "mock output for: " + req.Prompt, }) } if m.err != nil { return nil, m.err } return m.result, nil } type mockCodeAgentRegistry struct { agent port.CodeAgent } func (m *mockCodeAgentRegistry) Register(agent port.CodeAgent) { m.agent = agent } func (m *mockCodeAgentRegistry) Get(_ domain.AgentProvider) port.CodeAgent { return m.agent } func (m *mockCodeAgentRegistry) Default() port.CodeAgent { return m.agent } func (m *mockCodeAgentRegistry) DefaultProvider() domain.AgentProvider { return "mock" } func (m *mockCodeAgentRegistry) SetDefault(_ domain.AgentProvider) error { return nil } func (m *mockCodeAgentRegistry) Available() []domain.AgentProvider { return []domain.AgentProvider{"mock"} } func (m *mockCodeAgentRegistry) AvailableAgents(_ context.Context) []port.CodeAgent { return []port.CodeAgent{m.agent} } func (m *mockCodeAgentRegistry) Count() int { return 1 } // ============================================================================= // Mock CommandExecutor for verify tests // ============================================================================= type mockCommandExecutor struct { result *domain.CommandResult err error output []domain.OutputLine podExists bool podExistErr error } func newMockCommandExecutor() *mockCommandExecutor { return &mockCommandExecutor{ result: &domain.CommandResult{ ExitCode: 0, DurationMs: 100, }, podExists: true, } } func (m *mockCommandExecutor) Execute(_ context.Context, _ *domain.Command, _ string, handler domain.OutputHandler) (*domain.CommandResult, error) { if m.err != nil { return nil, m.err } // Deliver output lines to handler for _, line := range m.output { handler(line) } return m.result, nil } func (m *mockCommandExecutor) Cancel(_ context.Context, _ domain.CommandID) error { return nil } func (m *mockCommandExecutor) PodExists(_ context.Context, _ string) (bool, error) { return m.podExists, m.podExistErr } func (m *mockCommandExecutor) CheckConnection(_ context.Context) error { return nil } // ============================================================================= // Helper to build test dependencies // ============================================================================= type testDeps struct { queue *mockWorkQueue registry *mockWorkerRegistry audit *mockBuildAudit agent *mockCodeAgent workerSvc *service.WorkerService workSvc *service.WorkService buildExec *BuildExecutor } func newTestDeps() *testDeps { queue := newMockWorkQueue() registry := newMockWorkerRegistry() audit := newMockBuildAudit() agent := &mockCodeAgent{ result: &domain.AgentResult{ ExitCode: 0, DurationMs: 1000, }, } agentRegistry := &mockCodeAgentRegistry{agent: agent} workerSvc := service.NewWorkerService(registry, queue). WithBuildAudit(audit) workSvc := service.NewWorkService(queue) buildExec := NewBuildExecutor(agentRegistry, nil, nil, nil) return &testDeps{ queue: queue, registry: registry, audit: audit, agent: agent, workerSvc: workerSvc, workSvc: workSvc, buildExec: buildExec, } }