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) } }