rdev/internal/sdlc/classifier_test.go
jordan f22b220c6d feat: add SDLC branch management, merge, archive, and orchestrator APIs
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>
2026-02-02 12:30:03 -07:00

432 lines
10 KiB
Go

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