rdev/internal/domain/component_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

183 lines
4.8 KiB
Go

package domain
import "testing"
func TestIsValidComponentType(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{"service", "service", true},
{"worker", "worker", true},
{"app-astro", "app-astro", true},
{"app-react", "app-react", true},
{"app-nextjs", "app-nextjs", true},
{"cli", "cli", true},
{"invalid", "invalid", false},
{"empty", "", false},
{"uppercase", "SERVICE", false},
{"partial", "serv", false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := IsValidComponentType(tc.input)
if result != tc.expected {
t.Errorf("IsValidComponentType(%q) = %v, want %v", tc.input, result, tc.expected)
}
})
}
}
func TestComponentType_DestDir(t *testing.T) {
tests := []struct {
name string
componentType ComponentType
expected string
}{
{"service", ComponentTypeService, "services"},
{"worker", ComponentTypeWorker, "workers"},
{"app-astro", ComponentTypeAppAstro, "apps"},
{"app-react", ComponentTypeAppReact, "apps"},
{"app-nextjs", ComponentTypeAppNextJS, "apps"},
{"cli", ComponentTypeCLI, "cli"},
{"unknown", ComponentType("unknown"), ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := tc.componentType.DestDir()
if result != tc.expected {
t.Errorf("%s.DestDir() = %q, want %q", tc.componentType, result, tc.expected)
}
})
}
}
func TestComponentType_StartingPort(t *testing.T) {
tests := []struct {
name string
componentType ComponentType
expected int
}{
{"service", ComponentTypeService, 8001},
{"worker", ComponentTypeWorker, 0},
{"app-astro", ComponentTypeAppAstro, 3001},
{"app-react", ComponentTypeAppReact, 3001},
{"app-nextjs", ComponentTypeAppNextJS, 3001},
{"cli", ComponentTypeCLI, 0},
{"unknown", ComponentType("unknown"), 0},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := tc.componentType.StartingPort()
if result != tc.expected {
t.Errorf("%s.StartingPort() = %d, want %d", tc.componentType, result, tc.expected)
}
})
}
}
func TestComponentType_NeedsPort(t *testing.T) {
tests := []struct {
name string
componentType ComponentType
expected bool
}{
{"service", ComponentTypeService, true},
{"worker", ComponentTypeWorker, false},
{"app-astro", ComponentTypeAppAstro, true},
{"app-react", ComponentTypeAppReact, true},
{"app-nextjs", ComponentTypeAppNextJS, true},
{"cli", ComponentTypeCLI, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := tc.componentType.NeedsPort()
if result != tc.expected {
t.Errorf("%s.NeedsPort() = %v, want %v", tc.componentType, result, tc.expected)
}
})
}
}
func TestComponentType_IsGoComponent(t *testing.T) {
tests := []struct {
name string
componentType ComponentType
expected bool
}{
{"service", ComponentTypeService, true},
{"worker", ComponentTypeWorker, true},
{"app-astro", ComponentTypeAppAstro, false},
{"app-react", ComponentTypeAppReact, false},
{"app-nextjs", ComponentTypeAppNextJS, false},
{"cli", ComponentTypeCLI, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := tc.componentType.IsGoComponent()
if result != tc.expected {
t.Errorf("%s.IsGoComponent() = %v, want %v", tc.componentType, result, tc.expected)
}
})
}
}
func TestValidateComponentName(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"valid simple", "auth", false},
{"valid with dash", "auth-api", false},
{"valid with numbers", "api2", false},
{"valid complex", "user-service-v2", false},
{"empty", "", true},
{"starts with number", "2api", true},
{"starts with dash", "-api", true},
{"uppercase", "Auth", true},
{"mixed case", "authApi", true},
{"underscore", "auth_api", true},
{"space", "auth api", true},
{"special char", "auth@api", true},
{"too long", "a" + string(make([]byte, 63)), true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := ValidateComponentName(tc.input)
if (err != nil) != tc.wantErr {
t.Errorf("ValidateComponentName(%q) error = %v, wantErr %v", tc.input, err, tc.wantErr)
}
})
}
}
func TestValidComponentTypes(t *testing.T) {
// Ensure all valid types are in the slice
expected := []ComponentType{
ComponentTypeService,
ComponentTypeWorker,
ComponentTypeAppAstro,
ComponentTypeAppReact,
ComponentTypeAppNextJS,
ComponentTypeCLI,
}
if len(ValidComponentTypes) != len(expected) {
t.Errorf("ValidComponentTypes has %d types, want %d", len(ValidComponentTypes), len(expected))
}
for i, ct := range ValidComponentTypes {
if ct != expected[i] {
t.Errorf("ValidComponentTypes[%d] = %s, want %s", i, ct, expected[i])
}
}
}