package sdlc import ( "errors" "testing" ) func setupInitializedRoot(t *testing.T) string { t.Helper() root := t.TempDir() if err := Init(root, "test-project"); err != nil { t.Fatalf("Init: %v", err) } return root } func TestCreateFeature(t *testing.T) { root := setupInitializedRoot(t) f, err := CreateFeature(root, "auth", "User Authentication") if err != nil { t.Fatalf("CreateFeature: %v", err) } if f.Slug != "auth" { t.Errorf("Slug = %q, want auth", f.Slug) } if f.Title != "User Authentication" { t.Errorf("Title = %q, want User Authentication", f.Title) } if f.Phase != PhaseDraft { t.Errorf("Phase = %q, want draft", f.Phase) } if len(f.PhaseHistory) != 1 { t.Errorf("PhaseHistory len = %d, want 1", len(f.PhaseHistory)) } if len(f.Artifacts) != 7 { t.Errorf("Artifacts len = %d, want 7", len(f.Artifacts)) } } func TestCreateFeatureDuplicate(t *testing.T) { root := setupInitializedRoot(t) if _, err := CreateFeature(root, "auth", "Auth"); err != nil { t.Fatalf("CreateFeature: %v", err) } _, err := CreateFeature(root, "auth", "Auth Again") if !errors.Is(err, ErrFeatureExists) { t.Errorf("err = %v, want ErrFeatureExists", err) } } func TestCreateFeatureInvalidSlug(t *testing.T) { root := setupInitializedRoot(t) _, err := CreateFeature(root, "INVALID", "Bad Slug") if !errors.Is(err, ErrInvalidSlug) { t.Errorf("err = %v, want ErrInvalidSlug", err) } } func TestLoadFeature(t *testing.T) { root := setupInitializedRoot(t) if _, err := CreateFeature(root, "auth", "Auth"); err != nil { t.Fatalf("CreateFeature: %v", err) } loaded, err := LoadFeature(root, "auth") if err != nil { t.Fatalf("LoadFeature: %v", err) } if loaded.Slug != "auth" { t.Errorf("Slug = %q, want auth", loaded.Slug) } if loaded.Phase != PhaseDraft { t.Errorf("Phase = %q, want draft", loaded.Phase) } } func TestLoadFeatureNotFound(t *testing.T) { root := setupInitializedRoot(t) _, err := LoadFeature(root, "nonexistent") if !errors.Is(err, ErrFeatureNotFound) { t.Errorf("err = %v, want ErrFeatureNotFound", err) } } func TestListFeatures(t *testing.T) { root := setupInitializedRoot(t) if _, err := CreateFeature(root, "auth", "Auth"); err != nil { t.Fatal(err) } if _, err := CreateFeature(root, "payments", "Payments"); err != nil { t.Fatal(err) } features, err := ListFeatures(root) if err != nil { t.Fatalf("ListFeatures: %v", err) } if len(features) != 2 { t.Errorf("ListFeatures len = %d, want 2", len(features)) } } func TestCanTransitionTo(t *testing.T) { cfg := DefaultConfig("test") f := &Feature{ Phase: PhaseDraft, Artifacts: map[ArtifactType]*Artifact{ ArtifactSpec: {Status: StatusApproved}, }, } // Valid: draft -> specified (spec is approved) if err := f.CanTransitionTo(PhaseSpecified, cfg); err != nil { t.Errorf("CanTransitionTo(specified) = %v, want nil", err) } // Invalid: draft -> planned (skip) if err := f.CanTransitionTo(PhasePlanned, cfg); err == nil { t.Error("CanTransitionTo(planned) = nil, want error (skip)") } // Idempotent: draft -> draft (allowed, returns nil) if err := f.CanTransitionTo(PhaseDraft, cfg); err != nil { t.Errorf("CanTransitionTo(draft) = %v, want nil (idempotent)", err) } // Invalid phase if err := f.CanTransitionTo("bogus", cfg); err == nil { t.Error("CanTransitionTo(bogus) = nil, want error") } // Without config, artifact checks are skipped bare := &Feature{Phase: PhaseDraft} if err := bare.CanTransitionTo(PhaseSpecified, nil); err != nil { t.Errorf("CanTransitionTo(specified) without config = %v, want nil", err) } } func TestTransition(t *testing.T) { f := &Feature{ Phase: PhaseDraft, PhaseHistory: []PhaseTransition{ {Phase: PhaseDraft}, }, } if err := f.Transition(PhaseSpecified); err != nil { t.Fatalf("Transition: %v", err) } if f.Phase != PhaseSpecified { t.Errorf("Phase = %q, want specified", f.Phase) } if len(f.PhaseHistory) != 2 { t.Fatalf("PhaseHistory len = %d, want 2", len(f.PhaseHistory)) } if f.PhaseHistory[0].Exited == nil { t.Error("PhaseHistory[0].Exited is nil, want set") } if f.PhaseHistory[1].Phase != PhaseSpecified { t.Errorf("PhaseHistory[1].Phase = %q, want specified", f.PhaseHistory[1].Phase) } } func TestFeatureManifestRoundTrip(t *testing.T) { root := setupInitializedRoot(t) f, err := CreateFeature(root, "auth", "Auth") if err != nil { t.Fatal(err) } // Modify and save f.Branch = "feature/auth" if err := f.Transition(PhaseSpecified); err != nil { t.Fatal(err) } f.GetArtifact(ArtifactSpec).Approve("user") f.Tasks = AddTask(nil, "Create user model") f.UpdateTaskSummary() if err := f.Save(root); err != nil { t.Fatalf("Save: %v", err) } loaded, err := LoadFeature(root, "auth") if err != nil { t.Fatalf("LoadFeature: %v", err) } if loaded.Branch != "feature/auth" { t.Errorf("Branch = %q, want feature/auth", loaded.Branch) } if loaded.Phase != PhaseSpecified { t.Errorf("Phase = %q, want specified", loaded.Phase) } if loaded.GetArtifact(ArtifactSpec).Status != StatusApproved { t.Errorf("Spec status = %q, want approved", loaded.GetArtifact(ArtifactSpec).Status) } if len(loaded.Tasks) != 1 { t.Errorf("Tasks len = %d, want 1", len(loaded.Tasks)) } } func TestCanTransitionToRequiredArtifacts(t *testing.T) { cfg := DefaultConfig("test") // draft -> specified requires spec to be approved f := &Feature{ Phase: PhaseDraft, Artifacts: map[ArtifactType]*Artifact{ ArtifactSpec: {Status: StatusPending}, }, } err := f.CanTransitionTo(PhaseSpecified, cfg) if err == nil { t.Error("CanTransitionTo(specified) should fail with unapproved spec") } // Approve spec - now draft -> specified should work f.Artifacts[ArtifactSpec].Approve("user") err = f.CanTransitionTo(PhaseSpecified, cfg) if err != nil { t.Errorf("CanTransitionTo(specified) with approved spec: %v", err) } // specified -> planned requires spec, design, tasks, qa_plan f.Phase = PhaseSpecified err = f.CanTransitionTo(PhasePlanned, cfg) if err == nil { t.Error("CanTransitionTo(planned) should fail with missing design/tasks/qa_plan") } } func TestCanTransitionToBlockersPrevent(t *testing.T) { cfg := DefaultConfig("test") f := &Feature{ Phase: PhaseDraft, Blockers: []string{"dependency on payments"}, } err := f.CanTransitionTo(PhaseSpecified, cfg) if err == nil { t.Error("CanTransitionTo should fail when feature has blockers") } } func TestDeleteFeature(t *testing.T) { root := setupInitializedRoot(t) if _, err := CreateFeature(root, "auth", "Auth"); err != nil { t.Fatal(err) } if err := DeleteFeature(root, "auth"); err != nil { t.Fatalf("DeleteFeature: %v", err) } _, err := LoadFeature(root, "auth") if err != ErrFeatureNotFound { t.Errorf("LoadFeature after delete: %v, want ErrFeatureNotFound", err) } } func TestDeleteFeatureNotFound(t *testing.T) { root := setupInitializedRoot(t) if err := DeleteFeature(root, "nonexistent"); err != ErrFeatureNotFound { t.Errorf("DeleteFeature = %v, want ErrFeatureNotFound", err) } } func TestArtifactFileExists(t *testing.T) { root := setupInitializedRoot(t) f, err := CreateFeature(root, "auth", "Auth") if err != nil { t.Fatal(err) } // No spec file exists yet if f.ArtifactFileExists(root, ArtifactSpec) { t.Error("ArtifactFileExists(spec) = true before file creation") } } func TestBlockers(t *testing.T) { f := &Feature{} if f.IsBlocked() { t.Error("IsBlocked = true, want false") } f.AddBlocker("dependency on auth") if !f.IsBlocked() { t.Error("IsBlocked = false, want true") } f.ClearBlockers() if f.IsBlocked() { t.Error("IsBlocked = true after clear, want false") } }