Add branch lifecycle commands (branch, merge, archive) to the SDLC CLI. Introduce orchestrator handler and service for multi-step SDLC workflows. Expand skeleton template with 15 Claude commands covering the full feature lifecycle. Extend classifier rules, error types, and executor port for branch operations. Split rules.go and classifier_test.go to stay within 500-line limit. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
188 lines
4.3 KiB
Go
188 lines
4.3 KiB
Go
package sdlc
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func setupTestSDLC(t *testing.T) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
if err := Init(root, "test-project"); err != nil {
|
|
t.Fatalf("init failed: %v", err)
|
|
}
|
|
return root
|
|
}
|
|
|
|
func TestCreateBranch(t *testing.T) {
|
|
root := setupTestSDLC(t)
|
|
cfg := DefaultConfig("test")
|
|
|
|
// Create a feature first
|
|
_, err := CreateFeature(root, "auth-flow", "Auth Flow")
|
|
if err != nil {
|
|
t.Fatalf("create feature: %v", err)
|
|
}
|
|
|
|
manifest, err := CreateBranch(root, "auth-flow", cfg)
|
|
if err != nil {
|
|
t.Fatalf("create branch: %v", err)
|
|
}
|
|
|
|
if manifest.Name != "feature/auth-flow" {
|
|
t.Errorf("Name = %q, want feature/auth-flow", manifest.Name)
|
|
}
|
|
if manifest.Feature != "auth-flow" {
|
|
t.Errorf("Feature = %q, want auth-flow", manifest.Feature)
|
|
}
|
|
if manifest.BaseBranch != "main" {
|
|
t.Errorf("BaseBranch = %q, want main", manifest.BaseBranch)
|
|
}
|
|
|
|
// Verify feature was updated with branch reference
|
|
f, err := LoadFeature(root, "auth-flow")
|
|
if err != nil {
|
|
t.Fatalf("load feature: %v", err)
|
|
}
|
|
if f.Branch != "feature/auth-flow" {
|
|
t.Errorf("Feature.Branch = %q, want feature/auth-flow", f.Branch)
|
|
}
|
|
}
|
|
|
|
func TestCreateBranch_AlreadyExists(t *testing.T) {
|
|
root := setupTestSDLC(t)
|
|
cfg := DefaultConfig("test")
|
|
|
|
_, err := CreateFeature(root, "auth-flow", "Auth Flow")
|
|
if err != nil {
|
|
t.Fatalf("create feature: %v", err)
|
|
}
|
|
|
|
_, err = CreateBranch(root, "auth-flow", cfg)
|
|
if err != nil {
|
|
t.Fatalf("create branch: %v", err)
|
|
}
|
|
|
|
_, err = CreateBranch(root, "auth-flow", cfg)
|
|
if err != ErrBranchExists {
|
|
t.Errorf("err = %v, want ErrBranchExists", err)
|
|
}
|
|
}
|
|
|
|
func TestCreateBranch_FeatureNotFound(t *testing.T) {
|
|
root := setupTestSDLC(t)
|
|
cfg := DefaultConfig("test")
|
|
|
|
_, err := CreateBranch(root, "nonexistent", cfg)
|
|
if err != ErrFeatureNotFound {
|
|
t.Errorf("err = %v, want ErrFeatureNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadBranch(t *testing.T) {
|
|
root := setupTestSDLC(t)
|
|
cfg := DefaultConfig("test")
|
|
|
|
_, err := CreateFeature(root, "auth-flow", "Auth Flow")
|
|
if err != nil {
|
|
t.Fatalf("create feature: %v", err)
|
|
}
|
|
|
|
_, err = CreateBranch(root, "auth-flow", cfg)
|
|
if err != nil {
|
|
t.Fatalf("create branch: %v", err)
|
|
}
|
|
|
|
manifest, err := LoadBranch(root, "feature/auth-flow")
|
|
if err != nil {
|
|
t.Fatalf("load branch: %v", err)
|
|
}
|
|
if manifest.Name != "feature/auth-flow" {
|
|
t.Errorf("Name = %q, want feature/auth-flow", manifest.Name)
|
|
}
|
|
}
|
|
|
|
func TestLoadBranch_NotFound(t *testing.T) {
|
|
root := setupTestSDLC(t)
|
|
|
|
_, err := LoadBranch(root, "nonexistent")
|
|
if err != ErrBranchNotFound {
|
|
t.Errorf("err = %v, want ErrBranchNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestSaveBranch(t *testing.T) {
|
|
root := setupTestSDLC(t)
|
|
|
|
manifest := &BranchManifest{
|
|
Name: "feature/test",
|
|
Feature: "test",
|
|
BaseBranch: "main",
|
|
}
|
|
|
|
if err := SaveBranch(root, manifest); err != nil {
|
|
t.Fatalf("save branch: %v", err)
|
|
}
|
|
|
|
// Verify file exists
|
|
path := filepath.Join(root, SDLCDir, BranchesDir, "feature/test.yaml")
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
t.Error("branch manifest file not created")
|
|
}
|
|
}
|
|
|
|
func TestMergeChecklist_NotReady(t *testing.T) {
|
|
root := setupTestSDLC(t)
|
|
|
|
_, err := CreateFeature(root, "auth-flow", "Auth Flow")
|
|
if err != nil {
|
|
t.Fatalf("create feature: %v", err)
|
|
}
|
|
|
|
unmet, err := MergeChecklist(root, "auth-flow")
|
|
if err != nil {
|
|
t.Fatalf("merge checklist: %v", err)
|
|
}
|
|
|
|
if len(unmet) == 0 {
|
|
t.Error("expected unmet gates, got none")
|
|
}
|
|
}
|
|
|
|
func TestMergeChecklist_Ready(t *testing.T) {
|
|
root := setupTestSDLC(t)
|
|
|
|
f, err := CreateFeature(root, "auth-flow", "Auth Flow")
|
|
if err != nil {
|
|
t.Fatalf("create feature: %v", err)
|
|
}
|
|
|
|
// Set up all gates as passed and transition to merge phase
|
|
f.Phase = PhaseMerge
|
|
f.GetArtifact(ArtifactReview).MarkPassed()
|
|
f.GetArtifact(ArtifactAudit).MarkPassed()
|
|
f.GetArtifact(ArtifactQAResults).MarkPassed()
|
|
if err := f.Save(root); err != nil {
|
|
t.Fatalf("save feature: %v", err)
|
|
}
|
|
|
|
unmet, err := MergeChecklist(root, "auth-flow")
|
|
if err != nil {
|
|
t.Fatalf("merge checklist: %v", err)
|
|
}
|
|
|
|
if len(unmet) != 0 {
|
|
t.Errorf("expected 0 unmet gates, got %d: %v", len(unmet), unmet)
|
|
}
|
|
}
|
|
|
|
func TestMergeChecklist_FeatureNotFound(t *testing.T) {
|
|
root := setupTestSDLC(t)
|
|
|
|
_, err := MergeChecklist(root, "nonexistent")
|
|
if err != ErrFeatureNotFound {
|
|
t.Errorf("err = %v, want ErrFeatureNotFound", err)
|
|
}
|
|
}
|