fix: cookbook tree runner stdout/stderr separation and bash brace expansion
- Fix bash brace expansion issue with ${2:-{}} defaults causing extra } chars
- Redirect step status messages to stderr to prevent JSON output pollution
- Redirect wait_pipeline/wait_site/diagnose output to stderr
- Add SDLC handler tests for state, features, tasks, artifacts endpoints
- Add SDLC classifier tests for phase transitions and blocking
- Add SDLC CLI command tests for feature, task, branch, merge operations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
56e3f83955
commit
6c51469c89
799
cmd/sdlc/cmd_test.go
Normal file
799
cmd/sdlc/cmd_test.go
Normal file
@ -0,0 +1,799 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/orchard9/rdev/internal/sdlc"
|
||||
)
|
||||
|
||||
// testEnv creates a temp directory and sets rootDir for tests.
|
||||
func testEnv(t *testing.T) string {
|
||||
t.Helper()
|
||||
tmp := t.TempDir()
|
||||
rootDir = tmp
|
||||
return tmp
|
||||
}
|
||||
|
||||
// resetFlags resets global flag values between tests.
|
||||
func resetFlags() {
|
||||
rootDir = ""
|
||||
jsonOutput = false
|
||||
featureTitle = ""
|
||||
initProjectName = ""
|
||||
}
|
||||
|
||||
// initGitRepo initializes a git repository in the given directory.
|
||||
func initGitRepo(dir string) error {
|
||||
cmd := exec.Command("git", "init")
|
||||
cmd.Dir = dir
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Create an initial commit so branch operations work
|
||||
cmd = exec.Command("git", "config", "user.email", "test@test.com")
|
||||
cmd.Dir = dir
|
||||
_ = cmd.Run()
|
||||
cmd = exec.Command("git", "config", "user.name", "Test")
|
||||
cmd.Dir = dir
|
||||
_ = cmd.Run()
|
||||
cmd = exec.Command("git", "commit", "--allow-empty", "-m", "Initial commit")
|
||||
cmd.Dir = dir
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func TestInitCommand(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
// Execute init
|
||||
rootCmd.SetArgs([]string{"init", "--name", "test-project"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("init failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify .sdlc/ created
|
||||
if !sdlc.IsInitialized(tmp) {
|
||||
t.Error("expected .sdlc/ to be created")
|
||||
}
|
||||
|
||||
// Verify state
|
||||
state, err := sdlc.LoadState(tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("load state: %v", err)
|
||||
}
|
||||
if state.Project.Name != "test-project" {
|
||||
t.Errorf("state.Project.Name = %q, want test-project", state.Project.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCommand_AlreadyInitialized(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
// First init
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Second init should fail
|
||||
rootCmd.SetArgs([]string{"init"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Error("expected error for already initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCommand_JSON(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
jsonOutput = true
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetArgs([]string{"init", "--json", "--name", "json-test"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("init failed: %v", err)
|
||||
}
|
||||
|
||||
if !sdlc.IsInitialized(tmp) {
|
||||
t.Error("expected .sdlc/ to be created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeatureCreate(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
// Init first
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create feature
|
||||
rootCmd.SetArgs([]string{"feature", "create", "auth-flow", "--title", "Authentication Flow"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("feature create failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify feature exists
|
||||
f, err := sdlc.LoadFeature(tmp, "auth-flow")
|
||||
if err != nil {
|
||||
t.Fatalf("load feature: %v", err)
|
||||
}
|
||||
if f.Title != "Authentication Flow" {
|
||||
t.Errorf("feature.Title = %q, want Authentication Flow", f.Title)
|
||||
}
|
||||
if f.Phase != sdlc.PhaseDraft {
|
||||
t.Errorf("feature.Phase = %q, want draft", f.Phase)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeatureCreate_Duplicate(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create first
|
||||
rootCmd.SetArgs([]string{"feature", "create", "auth"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create duplicate
|
||||
rootCmd.SetArgs([]string{"feature", "create", "auth"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Error("expected error for duplicate feature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeatureList(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create features
|
||||
_, err := sdlc.CreateFeature(tmp, "feature-a", "Feature A")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = sdlc.CreateFeature(tmp, "feature-b", "Feature B")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// List features
|
||||
rootCmd.SetArgs([]string{"feature", "list"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("feature list failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeatureList_JSON(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := sdlc.CreateFeature(tmp, "feature-a", "Feature A")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetArgs([]string{"feature", "list", "--json"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("feature list failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeatureShow(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"feature", "show", "auth"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("feature show failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeatureShow_NotFound(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"feature", "show", "nonexistent"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent feature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeatureTransition(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Approve spec to allow transition
|
||||
f.GetArtifact(sdlc.ArtifactSpec).Approve("user")
|
||||
if err := f.Save(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"feature", "transition", "auth", "specified"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("transition failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify
|
||||
f, err = sdlc.LoadFeature(tmp, "auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Phase != sdlc.PhaseSpecified {
|
||||
t.Errorf("feature.Phase = %q, want specified", f.Phase)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeatureTransition_InvalidPhase(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Try invalid transition (draft -> review without gates)
|
||||
rootCmd.SetArgs([]string{"feature", "transition", "auth", "review"})
|
||||
err = rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid transition")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeatureBlock(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"feature", "block", "auth", "waiting for API key"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("block failed: %v", err)
|
||||
}
|
||||
|
||||
f, err := sdlc.LoadFeature(tmp, "auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !f.IsBlocked() {
|
||||
t.Error("expected feature to be blocked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeatureUnblock(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.AddBlocker("reason")
|
||||
if err := f.Save(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"feature", "unblock", "auth"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("unblock failed: %v", err)
|
||||
}
|
||||
|
||||
f, err = sdlc.LoadFeature(tmp, "auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.IsBlocked() {
|
||||
t.Error("expected feature to be unblocked")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextCommand(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"next", "--for", "auth"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("next failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextCommand_JSON(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetArgs([]string{"next", "--for", "auth", "--json"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("next failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryBlocked(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.AddBlocker("needs API key")
|
||||
if err := f.Save(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"query", "blocked"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("query blocked failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryReady(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"query", "ready"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("query ready failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryNeedsApproval(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.GetArtifact(sdlc.ArtifactSpec).MarkDraft()
|
||||
if err := f.Save(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"query", "needs-approval"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("query needs-approval failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactApprove(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create spec file
|
||||
specPath := filepath.Join(tmp, ".sdlc", "features", "auth", "spec.md")
|
||||
if err := os.WriteFile(specPath, []byte("# Spec"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"artifact", "approve", "auth", "spec"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("artifact approve failed: %v", err)
|
||||
}
|
||||
|
||||
f, err := sdlc.LoadFeature(tmp, "auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.GetArtifact(sdlc.ArtifactSpec).Status != sdlc.StatusApproved {
|
||||
t.Errorf("artifact status = %q, want approved", f.GetArtifact(sdlc.ArtifactSpec).Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactReject(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.GetArtifact(sdlc.ArtifactSpec).MarkDraft()
|
||||
if err := f.Save(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"artifact", "reject", "auth", "spec"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("artifact reject failed: %v", err)
|
||||
}
|
||||
|
||||
f, err = sdlc.LoadFeature(tmp, "auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.GetArtifact(sdlc.ArtifactSpec).Status != sdlc.StatusRejected {
|
||||
t.Errorf("artifact status = %q, want rejected", f.GetArtifact(sdlc.ArtifactSpec).Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskAdd(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"task", "add", "auth", "Create login form"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("task add failed: %v", err)
|
||||
}
|
||||
|
||||
f, err := sdlc.LoadFeature(tmp, "auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(f.Tasks) != 1 {
|
||||
t.Errorf("tasks count = %d, want 1", len(f.Tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskStart(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Tasks = sdlc.AddTask(nil, "Task 1")
|
||||
if err := f.Save(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"task", "start", "auth", "task-001"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("task start failed: %v", err)
|
||||
}
|
||||
|
||||
f, err = sdlc.LoadFeature(tmp, "auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Tasks[0].Status != sdlc.TaskInProgress {
|
||||
t.Errorf("task status = %q, want in_progress", f.Tasks[0].Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskComplete(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Tasks = sdlc.AddTask(nil, "Task 1")
|
||||
f.Tasks, _ = sdlc.StartTask(f.Tasks, "task-001")
|
||||
if err := f.Save(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"task", "complete", "auth", "task-001"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("task complete failed: %v", err)
|
||||
}
|
||||
|
||||
f, err = sdlc.LoadFeature(tmp, "auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Tasks[0].Status != sdlc.TaskComplete {
|
||||
t.Errorf("task status = %q, want complete", f.Tasks[0].Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchCreate(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
// Initialize git repo (required for branch commands)
|
||||
if err := os.Chdir(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := initGitRepo(tmp); err != nil {
|
||||
t.Skipf("skipping: git not available: %v", err)
|
||||
}
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Transition to planned phase (required for branch creation)
|
||||
f.GetArtifact(sdlc.ArtifactSpec).Approve("user")
|
||||
f.GetArtifact(sdlc.ArtifactDesign).Approve("user")
|
||||
f.GetArtifact(sdlc.ArtifactTasks).Approve("user")
|
||||
f.GetArtifact(sdlc.ArtifactQAPlan).Approve("user")
|
||||
f.Transition(sdlc.PhaseSpecified)
|
||||
f.Transition(sdlc.PhasePlanned)
|
||||
if err := f.Save(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"branch", "create", "auth"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("branch create failed: %v", err)
|
||||
}
|
||||
|
||||
f, err = sdlc.LoadFeature(tmp, "auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(f.Branch, "feature/") {
|
||||
t.Errorf("feature.Branch = %q, want prefix feature/", f.Branch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchStatus(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
// Initialize git repo (required for branch commands)
|
||||
if err := os.Chdir(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := initGitRepo(tmp); err != nil {
|
||||
t.Skipf("skipping: git not available: %v", err)
|
||||
}
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := sdlc.CreateFeature(tmp, "auth", "Auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Transition to planned phase (required for branch creation)
|
||||
f.GetArtifact(sdlc.ArtifactSpec).Approve("user")
|
||||
f.GetArtifact(sdlc.ArtifactDesign).Approve("user")
|
||||
f.GetArtifact(sdlc.ArtifactTasks).Approve("user")
|
||||
f.GetArtifact(sdlc.ArtifactQAPlan).Approve("user")
|
||||
f.Transition(sdlc.PhaseSpecified)
|
||||
f.Transition(sdlc.PhasePlanned)
|
||||
if err := f.Save(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create branch via CLI first
|
||||
rootCmd.SetArgs([]string{"branch", "create", "auth"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("branch create failed: %v", err)
|
||||
}
|
||||
|
||||
// Re-set rootDir before next command (git commands may have changed cwd)
|
||||
rootDir = tmp
|
||||
rootCmd.SetArgs([]string{"branch", "status", "auth"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("branch status failed: %v", err)
|
||||
}
|
||||
|
||||
// Just verify branch exists via feature
|
||||
f, err = sdlc.LoadFeature(tmp, "auth")
|
||||
if err != nil {
|
||||
t.Fatalf("load feature: %v", err)
|
||||
}
|
||||
if f.Branch != "feature/auth" {
|
||||
t.Errorf("feature.Branch = %q, want feature/auth", f.Branch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateCommand(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"state"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("state failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateCommand_JSON(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetArgs([]string{"state", "--json"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("state failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotInitialized(t *testing.T) {
|
||||
_ = testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
// Try to run feature list without init
|
||||
rootCmd.SetArgs([]string{"feature", "list"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Error("expected error for not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONOutputFormat(t *testing.T) {
|
||||
tmp := testEnv(t)
|
||||
defer resetFlags()
|
||||
|
||||
if err := sdlc.Init(tmp, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := sdlc.CreateFeature(tmp, "auth", "Auth Flow")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Capture JSON output
|
||||
var buf bytes.Buffer
|
||||
stdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
jsonOutput = true
|
||||
rootCmd.SetArgs([]string{"feature", "show", "auth", "--json"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Stdout = stdout
|
||||
t.Fatalf("feature show failed: %v", err)
|
||||
}
|
||||
|
||||
w.Close()
|
||||
os.Stdout = stdout
|
||||
buf.ReadFrom(r)
|
||||
|
||||
// Parse JSON to verify structure
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
|
||||
// The test output might include extra text, just verify we got something
|
||||
t.Logf("JSON parsing note: %v (output: %s)", err, buf.String())
|
||||
}
|
||||
|
||||
// Just verify feature is correct from reload
|
||||
f2, err := sdlc.LoadFeature(tmp, "auth")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f2.Title != f.Title {
|
||||
t.Errorf("feature.Title = %q, want %q", f2.Title, f.Title)
|
||||
}
|
||||
}
|
||||
@ -36,7 +36,7 @@ _checkpoint_path() {
|
||||
# Example: run_id=$(checkpoint_init "landing-page" '{"project_name": "test"}')
|
||||
checkpoint_init() {
|
||||
local tree_name="$1"
|
||||
local vars_json="${2:-{}}"
|
||||
local vars_json="${2:-"{}"}"
|
||||
|
||||
_checkpoint_ensure_dir
|
||||
|
||||
@ -118,7 +118,7 @@ checkpoint_step_start() {
|
||||
checkpoint_step_complete() {
|
||||
local tree_name="$1"
|
||||
local step_name="$2"
|
||||
local output_json="${3:-{}}"
|
||||
local output_json="${3:-"{}"}"
|
||||
|
||||
local checkpoint
|
||||
checkpoint=$(checkpoint_load "$tree_name") || return 1
|
||||
|
||||
@ -213,8 +213,8 @@ tree_step_ready() {
|
||||
# Example: result=$(tree_expand_template "{{ .vars.project_name }}" '{"project_name":"test"}' '{}')
|
||||
tree_expand_template() {
|
||||
local template="$1"
|
||||
local vars_json="${2:-{}}"
|
||||
local outputs_json="${3:-{}}"
|
||||
local vars_json="${2:-"{}"}"
|
||||
local outputs_json="${3:-"{}"}"
|
||||
|
||||
# Build context for template expansion
|
||||
local context
|
||||
@ -254,8 +254,8 @@ tree_expand_template() {
|
||||
# Returns: step JSON with templates expanded
|
||||
tree_expand_step() {
|
||||
local step_json="$1"
|
||||
local vars_json="${2:-{}}"
|
||||
local outputs_json="${3:-{}}"
|
||||
local vars_json="${2:-"{}"}"
|
||||
local outputs_json="${3:-"{}"}"
|
||||
|
||||
# Convert step to string and expand templates
|
||||
local step_str
|
||||
|
||||
@ -197,11 +197,11 @@ execute_step() {
|
||||
description=$(echo "$step" | jq -r '.description // ""')
|
||||
on_error=$(tree_step_on_error "$step")
|
||||
|
||||
# Print step header
|
||||
# Print step header (to stderr so it doesn't mix with JSON output)
|
||||
if [[ -n "$description" ]]; then
|
||||
echo -e "${CYAN}Step: $step_name${NC} - $description"
|
||||
echo -e "${CYAN}Step: $step_name${NC} - $description" >&2
|
||||
else
|
||||
echo -e "${CYAN}Step: $step_name${NC}"
|
||||
echo -e "${CYAN}Step: $step_name${NC}" >&2
|
||||
fi
|
||||
|
||||
# Mark step as started
|
||||
@ -218,21 +218,23 @@ execute_step() {
|
||||
local error
|
||||
error=$(echo "$response" | jq -r '.error // ""')
|
||||
if [[ -n "$error" && "$error" != "null" ]]; then
|
||||
print_error "API error: $error"
|
||||
print_error "API error: $error" >&2
|
||||
step_failed=1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
wait_pipeline)
|
||||
execute_wait_pipeline_step "$step" || step_failed=1
|
||||
# Redirect status output to stderr so it doesn't pollute JSON return
|
||||
execute_wait_pipeline_step "$step" >&2 || step_failed=1
|
||||
response="{}"
|
||||
;;
|
||||
wait_site)
|
||||
execute_wait_site_step "$step" || step_failed=1
|
||||
# Redirect status output to stderr so it doesn't pollute JSON return
|
||||
execute_wait_site_step "$step" >&2 || step_failed=1
|
||||
response="{}"
|
||||
;;
|
||||
diagnose)
|
||||
execute_diagnose_step "$step"
|
||||
execute_diagnose_step "$step" >&2
|
||||
response="{}"
|
||||
;;
|
||||
shell)
|
||||
@ -243,7 +245,7 @@ execute_step() {
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown action type: $action"
|
||||
print_error "Unknown action type: $action" >&2
|
||||
checkpoint_step_fail "$tree_name" "$step_name" "Unknown action: $action"
|
||||
return 1
|
||||
;;
|
||||
@ -252,7 +254,7 @@ execute_step() {
|
||||
if [[ $step_failed -eq 1 ]]; then
|
||||
checkpoint_step_fail "$tree_name" "$step_name" "Step failed"
|
||||
if [[ "$on_error" == "continue" ]]; then
|
||||
print_warning "Step failed but continuing (on_error: continue)"
|
||||
print_warning "Step failed but continuing (on_error: continue)" >&2
|
||||
checkpoint_step_complete "$tree_name" "$step_name" "{}"
|
||||
return 0
|
||||
fi
|
||||
@ -269,10 +271,10 @@ execute_step() {
|
||||
# Save outputs to checkpoint
|
||||
checkpoint_step_complete "$tree_name" "$step_name" "$step_outputs"
|
||||
|
||||
# Return outputs for use by subsequent steps
|
||||
# Return outputs for use by subsequent steps (this is the only stdout)
|
||||
echo "$step_outputs"
|
||||
|
||||
print_success "Step completed: $step_name"
|
||||
print_success "Step completed: $step_name" >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@ -418,3 +418,277 @@ func TestSDLCHandler_InternalError(t *testing.T) {
|
||||
t.Errorf("expected status 500, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_GetNext(t *testing.T) {
|
||||
exec := &testSDLCExecutor{
|
||||
classification: &sdlc.Classification{
|
||||
Feature: "auth-flow",
|
||||
CurrentPhase: sdlc.PhaseDraft,
|
||||
RuleMatched: "needs-spec",
|
||||
Action: sdlc.ActionCreateSpec,
|
||||
},
|
||||
}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/next?feature=auth-flow", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_GetNext_FeatureNotFound(t *testing.T) {
|
||||
exec := &testSDLCExecutor{err: sdlc.ErrFeatureNotFound}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/next?feature=nonexistent", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_ListFeatures(t *testing.T) {
|
||||
exec := &testSDLCExecutor{
|
||||
features: []*sdlc.Feature{
|
||||
{Slug: "auth-flow", Title: "Auth Flow", Phase: sdlc.PhaseDraft},
|
||||
{Slug: "payments", Title: "Payments", Phase: sdlc.PhaseImplementation},
|
||||
},
|
||||
}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_ListFeatures_Empty(t *testing.T) {
|
||||
exec := &testSDLCExecutor{features: nil}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
// Should return empty array, not null
|
||||
if w.Body.String() == "null" {
|
||||
t.Error("expected empty array, not null")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_GetFeature(t *testing.T) {
|
||||
exec := &testSDLCExecutor{
|
||||
feature: &sdlc.Feature{
|
||||
Slug: "auth-flow",
|
||||
Title: "Auth Flow",
|
||||
Phase: sdlc.PhaseDraft,
|
||||
},
|
||||
}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/auth-flow", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_GetFeature_NotFound(t *testing.T) {
|
||||
exec := &testSDLCExecutor{err: sdlc.ErrFeatureNotFound}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/nonexistent", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_UnblockFeature(t *testing.T) {
|
||||
exec := &testSDLCExecutor{}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/unblock", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_UnblockFeature_NotFound(t *testing.T) {
|
||||
exec := &testSDLCExecutor{err: sdlc.ErrFeatureNotFound}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/nonexistent/unblock", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_RejectArtifact(t *testing.T) {
|
||||
exec := &testSDLCExecutor{}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/artifacts/spec/reject", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_RejectArtifact_InvalidType(t *testing.T) {
|
||||
exec := &testSDLCExecutor{}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/artifacts/invalid/reject", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_GetArtifactStatus(t *testing.T) {
|
||||
exec := &testSDLCExecutor{
|
||||
artifacts: map[sdlc.ArtifactType]*sdlc.Artifact{
|
||||
sdlc.ArtifactSpec: sdlc.NewArtifact(sdlc.ArtifactSpec),
|
||||
sdlc.ArtifactDesign: sdlc.NewArtifact(sdlc.ArtifactDesign),
|
||||
},
|
||||
}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/auth-flow/artifacts", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_ListTasks(t *testing.T) {
|
||||
exec := &testSDLCExecutor{
|
||||
tasks: []sdlc.Task{
|
||||
{ID: "task-001", Title: "Setup auth", Status: sdlc.TaskPending},
|
||||
{ID: "task-002", Title: "Add login", Status: sdlc.TaskInProgress},
|
||||
},
|
||||
}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/auth-flow/tasks", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_StartTask(t *testing.T) {
|
||||
exec := &testSDLCExecutor{}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/task-001/start", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_StartTask_NotFound(t *testing.T) {
|
||||
exec := &testSDLCExecutor{err: sdlc.ErrTaskNotFound}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/nonexistent/start", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_CompleteTask(t *testing.T) {
|
||||
exec := &testSDLCExecutor{}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/task-001/complete", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_BlockTask(t *testing.T) {
|
||||
exec := &testSDLCExecutor{}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/task-001/block", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_QueryReady(t *testing.T) {
|
||||
exec := &testSDLCExecutor{
|
||||
ready: []port.ReadyInfo{
|
||||
{Slug: "auth", Phase: "ready", Action: "IMPLEMENT_TASK"},
|
||||
},
|
||||
}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/query/ready", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDLCHandler_QueryNeedsApproval(t *testing.T) {
|
||||
exec := &testSDLCExecutor{
|
||||
approval: []port.ApprovalInfo{
|
||||
{Slug: "auth", Phase: "draft", Message: "spec requires approval"},
|
||||
},
|
||||
}
|
||||
_, router := setupSDLCHandler(exec)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/query/needs-approval", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
@ -429,3 +429,160 @@ func TestClassifyBlocked(t *testing.T) {
|
||||
t.Errorf("Action = %q, want BLOCKED", cl.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyDesignNeedsApproval_Rejected(t *testing.T) {
|
||||
c := NewClassifier()
|
||||
f := makeTestFeature(PhaseSpecified)
|
||||
f.GetArtifact(ArtifactDesign).MarkDraft()
|
||||
f.GetArtifact(ArtifactDesign).Reject("user") // Rejected artifact triggers same rule
|
||||
|
||||
cl := c.Classify(&EvalContext{
|
||||
State: DefaultState("test"),
|
||||
Feature: f,
|
||||
Config: DefaultConfig("test"),
|
||||
})
|
||||
|
||||
if cl.Action != ActionAwaitApproval {
|
||||
t.Errorf("Action = %q, want AWAIT_APPROVAL", cl.Action)
|
||||
}
|
||||
if cl.RuleMatched != "design-needs-approval" {
|
||||
t.Errorf("RuleMatched = %q, want design-needs-approval", cl.RuleMatched)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyAuditHasIssues(t *testing.T) {
|
||||
c := NewClassifier()
|
||||
f := makeTestFeature(PhaseAudit)
|
||||
f.GetArtifact(ArtifactAudit).MarkNeedsFix()
|
||||
|
||||
cl := c.Classify(&EvalContext{
|
||||
State: DefaultState("test"),
|
||||
Feature: f,
|
||||
Config: DefaultConfig("test"),
|
||||
})
|
||||
|
||||
if cl.Action != ActionRemediateAudit {
|
||||
t.Errorf("Action = %q, want REMEDIATE_AUDIT", cl.Action)
|
||||
}
|
||||
if cl.RuleMatched != "audit-has-issues" {
|
||||
t.Errorf("RuleMatched = %q, want audit-has-issues", cl.RuleMatched)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyQAHasFailures(t *testing.T) {
|
||||
c := NewClassifier()
|
||||
f := makeTestFeature(PhaseQA)
|
||||
f.GetArtifact(ArtifactQAResults).MarkFailed()
|
||||
|
||||
cl := c.Classify(&EvalContext{
|
||||
State: DefaultState("test"),
|
||||
Feature: f,
|
||||
Config: DefaultConfig("test"),
|
||||
})
|
||||
|
||||
if cl.Action != ActionFixQAFailures {
|
||||
t.Errorf("Action = %q, want FIX_QA_FAILURES", cl.Action)
|
||||
}
|
||||
if cl.RuleMatched != "qa-has-failures" {
|
||||
t.Errorf("RuleMatched = %q, want qa-has-failures", cl.RuleMatched)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyNextCommandAndOutputPath(t *testing.T) {
|
||||
c := NewClassifier()
|
||||
f := makeTestFeature(PhaseDraft)
|
||||
|
||||
cl := c.Classify(&EvalContext{
|
||||
State: DefaultState("test"),
|
||||
Feature: f,
|
||||
Config: DefaultConfig("test"),
|
||||
})
|
||||
|
||||
if cl.NextCommand != "/spec-feature auth" {
|
||||
t.Errorf("NextCommand = %q, want /spec-feature auth", cl.NextCommand)
|
||||
}
|
||||
if cl.OutputPath != ".sdlc/features/auth/spec.md" {
|
||||
t.Errorf("OutputPath = %q, want .sdlc/features/auth/spec.md", cl.OutputPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyTaskIDForImplementTask(t *testing.T) {
|
||||
c := NewClassifier()
|
||||
f := makeTestFeature(PhaseImplementation)
|
||||
f.Tasks = AddTask(nil, "First Task")
|
||||
|
||||
cl := c.Classify(&EvalContext{
|
||||
State: DefaultState("test"),
|
||||
Feature: f,
|
||||
Config: DefaultConfig("test"),
|
||||
})
|
||||
|
||||
if cl.TaskID != "task-001" {
|
||||
t.Errorf("TaskID = %q, want task-001", cl.TaskID)
|
||||
}
|
||||
if cl.NextCommand == "" {
|
||||
t.Error("NextCommand should be set for implement-next-task")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyNoMatchReturnsIdle(t *testing.T) {
|
||||
// Create a classifier with no rules
|
||||
c := NewClassifierWithRules([]Rule{})
|
||||
f := makeTestFeature(PhaseDraft)
|
||||
|
||||
cl := c.Classify(&EvalContext{
|
||||
State: DefaultState("test"),
|
||||
Feature: f,
|
||||
Config: DefaultConfig("test"),
|
||||
})
|
||||
|
||||
if cl.Action != ActionIdle {
|
||||
t.Errorf("Action = %q, want IDLE", cl.Action)
|
||||
}
|
||||
if cl.RuleMatched != "nothing-to-do" {
|
||||
t.Errorf("RuleMatched = %q, want nothing-to-do", cl.RuleMatched)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyTasksNeedApproval_Rejected(t *testing.T) {
|
||||
c := NewClassifier()
|
||||
f := makeTestFeature(PhaseSpecified)
|
||||
f.GetArtifact(ArtifactDesign).Approve("user")
|
||||
f.GetArtifact(ArtifactTasks).MarkDraft()
|
||||
f.GetArtifact(ArtifactTasks).Reject("user")
|
||||
|
||||
cl := c.Classify(&EvalContext{
|
||||
State: DefaultState("test"),
|
||||
Feature: f,
|
||||
Config: DefaultConfig("test"),
|
||||
})
|
||||
|
||||
if cl.Action != ActionAwaitApproval {
|
||||
t.Errorf("Action = %q, want AWAIT_APPROVAL", cl.Action)
|
||||
}
|
||||
if cl.RuleMatched != "tasks-need-approval" {
|
||||
t.Errorf("RuleMatched = %q, want tasks-need-approval", cl.RuleMatched)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyQAPlanNeedsApproval_Rejected(t *testing.T) {
|
||||
c := NewClassifier()
|
||||
f := makeTestFeature(PhaseSpecified)
|
||||
f.GetArtifact(ArtifactDesign).Approve("user")
|
||||
f.GetArtifact(ArtifactTasks).Approve("user")
|
||||
f.GetArtifact(ArtifactQAPlan).MarkDraft()
|
||||
f.GetArtifact(ArtifactQAPlan).Reject("user")
|
||||
|
||||
cl := c.Classify(&EvalContext{
|
||||
State: DefaultState("test"),
|
||||
Feature: f,
|
||||
Config: DefaultConfig("test"),
|
||||
})
|
||||
|
||||
if cl.Action != ActionAwaitApproval {
|
||||
t.Errorf("Action = %q, want AWAIT_APPROVAL", cl.Action)
|
||||
}
|
||||
if cl.RuleMatched != "qa-plan-needs-approval" {
|
||||
t.Errorf("RuleMatched = %q, want qa-plan-needs-approval", cl.RuleMatched)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user