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>
164 lines
3.9 KiB
Go
164 lines
3.9 KiB
Go
package sdlc
|
|
|
|
import "testing"
|
|
|
|
func TestAddTask(t *testing.T) {
|
|
tasks := AddTask(nil, "Create user model")
|
|
tasks = AddTask(tasks, "Add validation")
|
|
|
|
if len(tasks) != 2 {
|
|
t.Fatalf("len = %d, want 2", len(tasks))
|
|
}
|
|
if tasks[0].ID != "task-001" {
|
|
t.Errorf("tasks[0].ID = %q, want task-001", tasks[0].ID)
|
|
}
|
|
if tasks[1].ID != "task-002" {
|
|
t.Errorf("tasks[1].ID = %q, want task-002", tasks[1].ID)
|
|
}
|
|
if tasks[0].Status != TaskPending {
|
|
t.Errorf("tasks[0].Status = %q, want pending", tasks[0].Status)
|
|
}
|
|
}
|
|
|
|
func TestStartTask(t *testing.T) {
|
|
tasks := AddTask(nil, "Task 1")
|
|
|
|
tasks, err := StartTask(tasks, "task-001")
|
|
if err != nil {
|
|
t.Fatalf("StartTask: %v", err)
|
|
}
|
|
if tasks[0].Status != TaskInProgress {
|
|
t.Errorf("Status = %q, want in_progress", tasks[0].Status)
|
|
}
|
|
if tasks[0].StartedAt == nil {
|
|
t.Error("StartedAt is nil")
|
|
}
|
|
}
|
|
|
|
func TestStartTaskNotFound(t *testing.T) {
|
|
tasks := AddTask(nil, "Task 1")
|
|
_, err := StartTask(tasks, "task-999")
|
|
if err != ErrTaskNotFound {
|
|
t.Errorf("err = %v, want ErrTaskNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestStartTaskWrongStatus(t *testing.T) {
|
|
tasks := AddTask(nil, "Task 1")
|
|
tasks, _ = StartTask(tasks, "task-001")
|
|
tasks, _ = CompleteTask(tasks, "task-001")
|
|
|
|
_, err := StartTask(tasks, "task-001")
|
|
if err == nil {
|
|
t.Error("StartTask on complete task should fail")
|
|
}
|
|
}
|
|
|
|
func TestCompleteTask(t *testing.T) {
|
|
tasks := AddTask(nil, "Task 1")
|
|
tasks, _ = StartTask(tasks, "task-001")
|
|
|
|
tasks, err := CompleteTask(tasks, "task-001")
|
|
if err != nil {
|
|
t.Fatalf("CompleteTask: %v", err)
|
|
}
|
|
if tasks[0].Status != TaskComplete {
|
|
t.Errorf("Status = %q, want complete", tasks[0].Status)
|
|
}
|
|
if tasks[0].DoneAt == nil {
|
|
t.Error("DoneAt is nil")
|
|
}
|
|
}
|
|
|
|
func TestCompleteTaskWrongStatus(t *testing.T) {
|
|
tasks := AddTask(nil, "Task 1")
|
|
_, err := CompleteTask(tasks, "task-001")
|
|
if err == nil {
|
|
t.Error("CompleteTask on pending task should fail")
|
|
}
|
|
}
|
|
|
|
func TestBlockTask(t *testing.T) {
|
|
tasks := AddTask(nil, "Task 1")
|
|
tasks, err := BlockTask(tasks, "task-001")
|
|
if err != nil {
|
|
t.Fatalf("BlockTask: %v", err)
|
|
}
|
|
if tasks[0].Status != TaskBlocked {
|
|
t.Errorf("Status = %q, want blocked", tasks[0].Status)
|
|
}
|
|
}
|
|
|
|
func TestPendingTasks(t *testing.T) {
|
|
tasks := AddTask(nil, "Task 1")
|
|
tasks = AddTask(tasks, "Task 2")
|
|
tasks = AddTask(tasks, "Task 3")
|
|
tasks, _ = StartTask(tasks, "task-001")
|
|
tasks, _ = CompleteTask(tasks, "task-001")
|
|
|
|
pending := PendingTasks(tasks)
|
|
if len(pending) != 2 {
|
|
t.Errorf("PendingTasks len = %d, want 2", len(pending))
|
|
}
|
|
}
|
|
|
|
func TestNextTask(t *testing.T) {
|
|
tasks := AddTask(nil, "Task 1")
|
|
tasks = AddTask(tasks, "Task 2")
|
|
tasks, _ = StartTask(tasks, "task-001")
|
|
tasks, _ = CompleteTask(tasks, "task-001")
|
|
|
|
next := NextTask(tasks)
|
|
if next == nil {
|
|
t.Fatal("NextTask = nil, want task-002")
|
|
}
|
|
if next.ID != "task-002" {
|
|
t.Errorf("NextTask.ID = %q, want task-002", next.ID)
|
|
}
|
|
}
|
|
|
|
func TestAllTasksComplete(t *testing.T) {
|
|
if AllTasksComplete(nil) {
|
|
t.Error("AllTasksComplete(nil) = true, want false")
|
|
}
|
|
|
|
tasks := AddTask(nil, "Task 1")
|
|
tasks = AddTask(tasks, "Task 2")
|
|
|
|
if AllTasksComplete(tasks) {
|
|
t.Error("AllTasksComplete = true with pending tasks")
|
|
}
|
|
|
|
tasks, _ = StartTask(tasks, "task-001")
|
|
tasks, _ = CompleteTask(tasks, "task-001")
|
|
tasks, _ = StartTask(tasks, "task-002")
|
|
tasks, _ = CompleteTask(tasks, "task-002")
|
|
|
|
if !AllTasksComplete(tasks) {
|
|
t.Error("AllTasksComplete = false with all complete")
|
|
}
|
|
}
|
|
|
|
func TestSummarizeTasks(t *testing.T) {
|
|
tasks := AddTask(nil, "Task 1")
|
|
tasks = AddTask(tasks, "Task 2")
|
|
tasks = AddTask(tasks, "Task 3")
|
|
tasks, _ = StartTask(tasks, "task-001")
|
|
tasks, _ = CompleteTask(tasks, "task-001")
|
|
tasks, _ = StartTask(tasks, "task-002")
|
|
|
|
s := SummarizeTasks(tasks)
|
|
if s.Total != 3 {
|
|
t.Errorf("Total = %d, want 3", s.Total)
|
|
}
|
|
if s.Completed != 1 {
|
|
t.Errorf("Completed = %d, want 1", s.Completed)
|
|
}
|
|
if s.InProgress != 1 {
|
|
t.Errorf("InProgress = %d, want 1", s.InProgress)
|
|
}
|
|
if s.Pending != 1 {
|
|
t.Errorf("Pending = %d, want 1", s.Pending)
|
|
}
|
|
}
|