rdev/internal/service/sdlc_orchestrator_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

334 lines
10 KiB
Go

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