rdev/internal/adapter/kubernetes/sdlc_executor_test.go
jordan 425ef0f806 feat: add SDLC orchestration - library, CLI, and API integration
Implements deterministic feature lifecycle management for agent-driven
development. Agents use the CLI in pods; operators control via REST API.

Library (internal/sdlc/):
- Feature lifecycle with 10 phases (draft → released)
- Classifier engine with priority-ordered rules
- Artifact tracking with approval workflow
- Task management within features
- YAML-based state persistence

CLI (cmd/sdlc/):
- init, state, next, feature, artifact, task, query commands
- --json flag for machine-readable output
- Runs inside project pods

API (21 endpoints under /projects/{id}/sdlc/):
- State: GET /state, GET /next
- Features: CRUD + transition/block/unblock
- Artifacts: approve/reject per type
- Tasks: add/start/complete/block
- Queries: blocked/ready/needs-approval

Architecture:
- Port: SDLCExecutor interface (internal/port/)
- Adapter: kubectl exec into pods (internal/adapter/kubernetes/)
- Service: pod resolution + logging (internal/service/)
- Handlers: 5 files under 500-line limit (internal/handlers/)

Also includes template upgrades (chassis framework, UI components,
OpenAPI helpers, backend/frontend guides) and component improvements.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 09:57:05 -07:00

115 lines
2.7 KiB
Go

package kubernetes
import (
"errors"
"testing"
"github.com/orchard9/rdev/internal/sdlc"
)
func TestMapExecError(t *testing.T) {
exec := &SDLCExecutor{namespace: "test"}
baseErr := errors.New("exit status 1")
tests := []struct {
name string
stderr string
want error
wantMsg string
}{
{
name: "not initialized",
stderr: "Error: sdlc not initialized: run 'sdlc init'",
want: sdlc.ErrNotInitialized,
},
{
name: "feature not found",
stderr: "Error: feature not found",
want: sdlc.ErrFeatureNotFound,
},
{
name: "feature already exists",
stderr: "Error: feature already exists",
want: sdlc.ErrFeatureExists,
},
{
name: "invalid phase transition",
stderr: "Error: invalid phase transition: cannot move from draft to implementation (backward)",
want: sdlc.ErrInvalidTransition,
},
{
name: "invalid phase",
stderr: "Error: invalid phase: xyz",
want: sdlc.ErrInvalidPhase,
},
{
name: "task not found",
stderr: "Error: task not found",
want: sdlc.ErrTaskNotFound,
},
{
name: "artifact not found",
stderr: "Error: artifact not found",
want: sdlc.ErrArtifactNotFound,
},
{
name: "invalid slug",
stderr: "Error: invalid slug: must be lowercase alphanumeric with hyphens",
want: sdlc.ErrInvalidSlug,
},
{
name: "invalid artifact type",
stderr: "Error: invalid artifact type: foobar",
want: sdlc.ErrInvalidArtifact,
},
{
name: "unknown error with stderr",
stderr: "something unexpected happened",
wantMsg: "sdlc exec: something unexpected happened: exit status 1",
},
{
name: "unknown error without stderr",
stderr: "",
wantMsg: "sdlc exec: exit status 1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := exec.mapExecError(tt.stderr, baseErr)
if tt.want != nil {
if !errors.Is(got, tt.want) {
t.Errorf("mapExecError() = %v, want %v", got, tt.want)
}
} else if tt.wantMsg != "" {
if got.Error() != tt.wantMsg {
t.Errorf("mapExecError() message = %q, want %q", got.Error(), tt.wantMsg)
}
}
})
}
}
func TestMapExecError_WhitespaceHandling(t *testing.T) {
exec := &SDLCExecutor{namespace: "test"}
baseErr := errors.New("exit status 1")
// Stderr with leading/trailing whitespace
got := exec.mapExecError(" feature not found\n ", baseErr)
if !errors.Is(got, sdlc.ErrFeatureNotFound) {
t.Errorf("expected ErrFeatureNotFound, got %v", got)
}
}
func TestNewSDLCExecutor(t *testing.T) {
exec := NewSDLCExecutor(SDLCExecutorConfig{
Namespace: "rdev",
})
if exec.namespace != "rdev" {
t.Errorf("namespace = %q, want %q", exec.namespace, "rdev")
}
if exec.logger == nil {
t.Error("logger should not be nil")
}
}