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>
115 lines
2.7 KiB
Go
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")
|
|
}
|
|
}
|