package service import ( "context" "errors" "testing" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" "github.com/orchard9/rdev/internal/sdlc" ) // mockGitCommitter implements PodGitCommitter for testing. type mockGitCommitter struct { result *GitCommitResult } func (m *mockGitCommitter) CommitAndPush(_ context.Context, _, _, _ string, _ bool) *GitCommitResult { if m.result != nil { return m.result } return &GitCommitResult{ HasChanges: true, CommitSHA: "abc123", FilesChanged: []string{"main.go"}, Pushed: true, } } // mockAgentRegistry implements port.CodeAgentRegistry for testing. type mockAgentRegistry struct { agent port.CodeAgent } func (r *mockAgentRegistry) Register(_ port.CodeAgent) {} func (r *mockAgentRegistry) Get(_ domain.AgentProvider) port.CodeAgent { return r.agent } func (r *mockAgentRegistry) Default() port.CodeAgent { return r.agent } func (r *mockAgentRegistry) DefaultProvider() domain.AgentProvider { return "" } func (r *mockAgentRegistry) SetDefault(_ domain.AgentProvider) error { return nil } func (r *mockAgentRegistry) Available() []domain.AgentProvider { return nil } func (r *mockAgentRegistry) AvailableAgents(_ context.Context) []port.CodeAgent { return nil } func (r *mockAgentRegistry) Count() int { return 0 } // mockCodeAgent implements port.CodeAgent for testing. type mockCodeAgent struct { result *domain.AgentResult err error } func (a *mockCodeAgent) Name() string { return "test-agent" } func (a *mockCodeAgent) Provider() domain.AgentProvider { return "test" } func (a *mockCodeAgent) Cancel(_ context.Context, _ string) error { return nil } func (a *mockCodeAgent) Capabilities() domain.AgentCapabilities { return domain.AgentCapabilities{} } func (a *mockCodeAgent) Available(_ context.Context) bool { return true } func (a *mockCodeAgent) Execute(_ context.Context, _ *domain.AgentRequest, handler domain.AgentEventHandler) (*domain.AgentResult, error) { if a.err != nil { return nil, a.err } if handler != nil { handler(domain.AgentEvent{ Type: domain.AgentEventOutput, Content: "agent output", }) } if a.result != nil { return a.result, nil } return &domain.AgentResult{}, nil } func newTestOrchestrator(exec *mockSDLCExecutor, repo *mockProjectRepo, registry port.CodeAgentRegistry, committer PodGitCommitter) *SDLCOrchestratorService { sdlcSvc := NewSDLCService(exec, repo) return NewSDLCOrchestratorService(sdlcSvc, registry, committer, repo) } func TestOrchestrator_ExecuteAction_Idle(t *testing.T) { exec := &mockSDLCExecutor{ getNextFn: func(_ context.Context, _, _ string) (*sdlc.Classification, error) { return &sdlc.Classification{ Action: sdlc.ActionIdle, Feature: "auth", Message: "Nothing to do", }, nil }, } repo := newMockProjectRepo(&domain.Project{ID: "proj", PodName: "pod"}) svc := newTestOrchestrator(exec, repo, nil, nil) result, err := svc.ExecuteAction(context.Background(), "proj", &ExecuteRequest{Feature: "auth"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !result.Success { t.Error("expected success") } if result.Action != sdlc.ActionIdle { t.Errorf("expected idle action, got %s", result.Action) } if result.Output != "Nothing to do" { t.Errorf("expected 'Nothing to do', got %s", result.Output) } } func TestOrchestrator_ExecuteAction_Transition(t *testing.T) { var transitioned bool exec := &mockSDLCExecutor{ getNextFn: func(_ context.Context, _, _ string) (*sdlc.Classification, error) { if transitioned { return &sdlc.Classification{Action: sdlc.ActionIdle, Feature: "auth"}, nil } return &sdlc.Classification{ Action: sdlc.ActionTransition, Feature: "auth", TransitionTo: sdlc.PhaseSpecified, }, nil }, transitionFeatureFn: func(_ context.Context, _, _ string, _ sdlc.FeaturePhase) error { transitioned = true return nil }, } repo := newMockProjectRepo(&domain.Project{ID: "proj", PodName: "pod"}) svc := newTestOrchestrator(exec, repo, nil, nil) result, err := svc.ExecuteAction(context.Background(), "proj", &ExecuteRequest{Feature: "auth"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !result.Success { t.Error("expected success") } if result.Action != sdlc.ActionTransition { t.Errorf("expected transition action, got %s", result.Action) } if !transitioned { t.Error("expected transition to have been called") } } func TestOrchestrator_ExecuteAction_AgentAction(t *testing.T) { exec := &mockSDLCExecutor{ getNextFn: func(_ context.Context, _, _ string) (*sdlc.Classification, error) { return &sdlc.Classification{ Action: sdlc.ActionCreateSpec, Feature: "auth", NextCommand: "create spec", }, nil }, } repo := newMockProjectRepo(&domain.Project{ID: "proj", PodName: "pod"}) agent := &mockCodeAgent{} registry := &mockAgentRegistry{agent: agent} svc := newTestOrchestrator(exec, repo, registry, nil) result, err := svc.ExecuteAction(context.Background(), "proj", &ExecuteRequest{Feature: "auth"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !result.Success { t.Error("expected success") } if result.Output != "agent output" { t.Errorf("expected 'agent output', got %s", result.Output) } } func TestOrchestrator_ExecuteAction_NoAgent(t *testing.T) { exec := &mockSDLCExecutor{ getNextFn: func(_ context.Context, _, _ string) (*sdlc.Classification, error) { return &sdlc.Classification{ Action: sdlc.ActionCreateSpec, Feature: "auth", }, nil }, } repo := newMockProjectRepo(&domain.Project{ID: "proj", PodName: "pod"}) registry := &mockAgentRegistry{agent: nil} svc := newTestOrchestrator(exec, repo, registry, nil) result, err := svc.ExecuteAction(context.Background(), "proj", &ExecuteRequest{Feature: "auth"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if result.Success { t.Error("expected failure (no agent)") } if result.Error == "" { t.Error("expected error message") } } func TestOrchestrator_ExecuteAction_ClassifyError(t *testing.T) { exec := &mockSDLCExecutor{ getNextFn: func(_ context.Context, _, _ string) (*sdlc.Classification, error) { return nil, sdlc.ErrFeatureNotFound }, } repo := newMockProjectRepo(&domain.Project{ID: "proj", PodName: "pod"}) svc := newTestOrchestrator(exec, repo, nil, nil) _, err := svc.ExecuteAction(context.Background(), "proj", &ExecuteRequest{Feature: "auth"}) if !errors.Is(err, sdlc.ErrFeatureNotFound) { t.Errorf("expected ErrFeatureNotFound, got %v", err) } } func TestOrchestrator_ResolveBlocker(t *testing.T) { var unblocked bool exec := &mockSDLCExecutor{ unblockFeatureFn: func(_ context.Context, _, _ string) error { unblocked = true return nil }, getNextFn: func(_ context.Context, _, _ string) (*sdlc.Classification, error) { return &sdlc.Classification{ Action: sdlc.ActionIdle, Feature: "auth", }, nil }, } repo := newMockProjectRepo(&domain.Project{ID: "proj", PodName: "pod"}) svc := newTestOrchestrator(exec, repo, nil, nil) result, err := svc.ResolveBlocker(context.Background(), "proj", &ResolveRequest{Feature: "auth"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !result.Success { t.Error("expected success") } if !unblocked { t.Error("expected unblock to have been called") } if result.Next == nil { t.Error("expected next classification") } } func TestOrchestrator_ResolveBlocker_Error(t *testing.T) { exec := &mockSDLCExecutor{ unblockFeatureFn: func(_ context.Context, _, _ string) error { return sdlc.ErrFeatureNotFound }, } repo := newMockProjectRepo(&domain.Project{ID: "proj", PodName: "pod"}) svc := newTestOrchestrator(exec, repo, nil, nil) _, err := svc.ResolveBlocker(context.Background(), "proj", &ResolveRequest{Feature: "auth"}) if !errors.Is(err, sdlc.ErrFeatureNotFound) { t.Errorf("expected ErrFeatureNotFound, got %v", err) } } func TestOrchestrator_CommitChanges(t *testing.T) { exec := &mockSDLCExecutor{} repo := newMockProjectRepo(&domain.Project{ID: "proj", PodName: "pod"}) committer := &mockGitCommitter{ result: &GitCommitResult{ HasChanges: true, CommitSHA: "def456", FilesChanged: []string{"auth.go", "auth_test.go"}, Pushed: true, }, } svc := newTestOrchestrator(exec, repo, nil, committer) result, err := svc.CommitChanges(context.Background(), "proj", &CommitRequest{ Feature: "auth", Message: "implement auth", Push: true, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if result.CommitSHA != "def456" { t.Errorf("expected SHA def456, got %s", result.CommitSHA) } if len(result.FilesChanged) != 2 { t.Errorf("expected 2 files changed, got %d", len(result.FilesChanged)) } if !result.Pushed { t.Error("expected pushed") } } func TestOrchestrator_CommitChanges_NoCommitter(t *testing.T) { exec := &mockSDLCExecutor{} repo := newMockProjectRepo(&domain.Project{ID: "proj", PodName: "pod"}) svc := newTestOrchestrator(exec, repo, nil, nil) // no git committer _, err := svc.CommitChanges(context.Background(), "proj", &CommitRequest{ Feature: "auth", Message: "commit", }) if err == nil { t.Fatal("expected error for nil committer") } } func TestOrchestrator_CommitChanges_ProjectNotFound(t *testing.T) { exec := &mockSDLCExecutor{} repo := newMockProjectRepo() // empty committer := &mockGitCommitter{} svc := newTestOrchestrator(exec, repo, nil, committer) _, err := svc.CommitChanges(context.Background(), "nonexistent", &CommitRequest{ Feature: "auth", Message: "commit", }) if !errors.Is(err, domain.ErrProjectNotFound) { t.Errorf("expected ErrProjectNotFound, got %v", err) } } func TestOrchestrator_CommitChanges_GitError(t *testing.T) { exec := &mockSDLCExecutor{} repo := newMockProjectRepo(&domain.Project{ID: "proj", PodName: "pod"}) committer := &mockGitCommitter{ result: &GitCommitResult{ Error: errors.New("push failed"), }, } svc := newTestOrchestrator(exec, repo, nil, committer) _, err := svc.CommitChanges(context.Background(), "proj", &CommitRequest{ Feature: "auth", Message: "commit", Push: true, }) if err == nil { t.Fatal("expected error for git failure") } }