package sdlc import "testing" func makeTestFeature(phase FeaturePhase) *Feature { f := &Feature{ Slug: "auth", Title: "Auth", Phase: phase, Artifacts: map[ArtifactType]*Artifact{ ArtifactSpec: NewArtifact(ArtifactSpec), ArtifactDesign: NewArtifact(ArtifactDesign), ArtifactTasks: NewArtifact(ArtifactTasks), ArtifactQAPlan: NewArtifact(ArtifactQAPlan), ArtifactReview: NewArtifact(ArtifactReview), ArtifactAudit: NewArtifact(ArtifactAudit), ArtifactQAResults: NewArtifact(ArtifactQAResults), }, } return f } func TestClassifyDraftNeedsSpec(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseDraft) cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionCreateSpec { t.Errorf("Action = %q, want CREATE_SPEC", cl.Action) } if cl.RuleMatched != "needs-spec" { t.Errorf("RuleMatched = %q, want needs-spec", cl.RuleMatched) } } func TestClassifyDraftSpecDraftNeedsApproval(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseDraft) f.GetArtifact(ArtifactSpec).MarkDraft() 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 != "spec-needs-approval" { t.Errorf("RuleMatched = %q, want spec-needs-approval", cl.RuleMatched) } } func TestClassifyDraftSpecApprovedTransition(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseDraft) f.GetArtifact(ArtifactSpec).Approve("user") cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionTransition { t.Errorf("Action = %q, want TRANSITION", cl.Action) } if cl.TransitionTo != PhaseSpecified { t.Errorf("TransitionTo = %q, want specified", cl.TransitionTo) } } func TestClassifySpecifiedNeedsDesign(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseSpecified) cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionCreateDesign { t.Errorf("Action = %q, want CREATE_DESIGN", cl.Action) } } func TestClassifySpecifiedNeedsTasks(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseSpecified) f.GetArtifact(ArtifactDesign).Approve("user") cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionCreateTasks { t.Errorf("Action = %q, want CREATE_TASKS", cl.Action) } } func TestClassifySpecifiedNeedsQAPlan(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseSpecified) f.GetArtifact(ArtifactDesign).Approve("user") f.GetArtifact(ArtifactTasks).Approve("user") cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionCreateQAPlan { t.Errorf("Action = %q, want CREATE_QA_PLAN", cl.Action) } } func TestClassifySpecifiedPlanningComplete(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseSpecified) f.GetArtifact(ArtifactDesign).Approve("user") f.GetArtifact(ArtifactTasks).Approve("user") f.GetArtifact(ArtifactQAPlan).Approve("user") cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionTransition { t.Errorf("Action = %q, want TRANSITION", cl.Action) } if cl.TransitionTo != PhasePlanned { t.Errorf("TransitionTo = %q, want planned", cl.TransitionTo) } } func TestClassifyPlannedNeedsBranch(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhasePlanned) cfg := DefaultConfig("test") // DefaultConfig has RequireBranch: true, feature has no branch cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: cfg, }) if cl.Action != ActionCreateBranch { t.Errorf("Action = %q, want CREATE_BRANCH", cl.Action) } if cl.RuleMatched != "needs-branch" { t.Errorf("RuleMatched = %q, want needs-branch", cl.RuleMatched) } } func TestClassifyPlannedBranchNotRequired(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhasePlanned) cfg := DefaultConfig("test") cfg.Compliance.RequireBranch = false cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: cfg, }) // Should skip branch rule and go to ready-to-implement if cl.Action != ActionTransition { t.Errorf("Action = %q, want TRANSITION", cl.Action) } if cl.TransitionTo != PhaseReady { t.Errorf("TransitionTo = %q, want ready", cl.TransitionTo) } } func TestClassifyPlannedBranchAlreadyExists(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhasePlanned) f.Branch = "feature/auth" // Branch already set cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) // Should skip branch rule and go to ready-to-implement if cl.Action != ActionTransition { t.Errorf("Action = %q, want TRANSITION", cl.Action) } if cl.TransitionTo != PhaseReady { t.Errorf("TransitionTo = %q, want ready", cl.TransitionTo) } } func TestClassifyPlannedTransitionsToReady(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhasePlanned) f.Branch = "feature/auth" // Branch exists, so needs-branch rule won't fire cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionTransition { t.Errorf("Action = %q, want TRANSITION", cl.Action) } if cl.TransitionTo != PhaseReady { t.Errorf("TransitionTo = %q, want ready", cl.TransitionTo) } } func TestClassifyImplementationNextTask(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseImplementation) f.Tasks = AddTask(nil, "Task 1") f.Tasks = AddTask(f.Tasks, "Task 2") cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionImplementTask { t.Errorf("Action = %q, want IMPLEMENT_TASK", cl.Action) } if cl.TaskID != "task-001" { t.Errorf("TaskID = %q, want task-001", cl.TaskID) } } func TestClassifyImplementationComplete(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseImplementation) f.Tasks = AddTask(nil, "Task 1") f.Tasks, _ = StartTask(f.Tasks, "task-001") f.Tasks, _ = CompleteTask(f.Tasks, "task-001") cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionTransition { t.Errorf("Action = %q, want TRANSITION", cl.Action) } if cl.TransitionTo != PhaseReview { t.Errorf("TransitionTo = %q, want review", cl.TransitionTo) } } func TestClassifyReviewNeeded(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseReview) cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionReviewCode { t.Errorf("Action = %q, want REVIEW_CODE", cl.Action) } } func TestClassifyReviewNeedsFix(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseReview) f.GetArtifact(ArtifactReview).MarkNeedsFix() cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionFixReviewIssues { t.Errorf("Action = %q, want FIX_REVIEW_ISSUES", cl.Action) } } func TestClassifyReviewPassed(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseReview) f.GetArtifact(ArtifactReview).MarkPassed() cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionTransition { t.Errorf("Action = %q, want TRANSITION", cl.Action) } if cl.TransitionTo != PhaseAudit { t.Errorf("TransitionTo = %q, want audit", cl.TransitionTo) } } func TestClassifyAuditNeeded(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseAudit) cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionAuditCode { t.Errorf("Action = %q, want AUDIT_CODE", cl.Action) } } func TestClassifyAuditPassed(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseAudit) f.GetArtifact(ArtifactAudit).MarkPassed() cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionTransition { t.Errorf("Action = %q, want TRANSITION", cl.Action) } if cl.TransitionTo != PhaseQA { t.Errorf("TransitionTo = %q, want qa", cl.TransitionTo) } } func TestClassifyQANeeded(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseQA) cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionRunQA { t.Errorf("Action = %q, want RUN_QA", cl.Action) } } func TestClassifyQAPassed(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseQA) f.GetArtifact(ArtifactQAResults).MarkPassed() cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionTransition { t.Errorf("Action = %q, want TRANSITION", cl.Action) } if cl.TransitionTo != PhaseMerge { t.Errorf("TransitionTo = %q, want merge", cl.TransitionTo) } } func TestClassifyMerge(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseMerge) cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionMergeFeature { t.Errorf("Action = %q, want MERGE_FEATURE", cl.Action) } } func TestClassifyArchive(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseReleased) cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionArchive { t.Errorf("Action = %q, want ARCHIVE", cl.Action) } } func TestClassifyBlocked(t *testing.T) { c := NewClassifier() f := makeTestFeature(PhaseDraft) f.AddBlocker("depends on payments") cl := c.Classify(&EvalContext{ State: DefaultState("test"), Feature: f, Config: DefaultConfig("test"), }) if cl.Action != ActionBlocked { t.Errorf("Action = %q, want BLOCKED", cl.Action) } }