package quality import ( "testing" "time" ) func TestNewNarrativeGenerator(t *testing.T) { tests := []struct { name string targetScore int expected int }{ {"default target", 0, 95}, {"custom target", 85, 85}, {"negative target", -10, 95}, {"zero target", 0, 95}, {"high target", 100, 100}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gen := NewNarrativeGenerator(tt.targetScore) if gen.targetScore != tt.expected { t.Errorf("NewNarrativeGenerator() targetScore = %v, want %v", gen.targetScore, tt.expected) } }) } } func TestNarrativeGenerator_determinePhase(t *testing.T) { gen := NewNarrativeGenerator(95) tests := []struct { name string findings []Finding expected string }{ { name: "no open issues", findings: []Finding{{Status: StatusFixed}}, expected: "maintenance", }, { name: "critical phase with T4", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT4}, }, expected: "critical", }, { name: "debt reduction with many T3", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT3}, {Status: StatusOpen, Severity: SeverityT3}, {Status: StatusOpen, Severity: SeverityT3}, {Status: StatusOpen, Severity: SeverityT3}, {Status: StatusOpen, Severity: SeverityT3}, {Status: StatusOpen, Severity: SeverityT3}, }, expected: "debt_reduction", }, { name: "debt reduction with many open issues", findings: func() []Finding { var f []Finding for i := 0; i < 25; i++ { f = append(f, Finding{Status: StatusOpen, Severity: SeverityT2}) } return f }(), expected: "debt_reduction", }, { name: "cleanup phase", findings: func() []Finding { var f []Finding for i := 0; i < 10; i++ { f = append(f, Finding{Status: StatusOpen, Severity: SeverityT2}) } return f }(), expected: "cleanup", }, { name: "polish phase", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT2}, {Status: StatusOpen, Severity: SeverityT2}, }, expected: "polish", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { scorecard := &Scorecard{TotalScore: 100} phase := gen.determinePhase(tt.findings, scorecard) if phase != tt.expected { t.Errorf("determinePhase() = %v, want %v", phase, tt.expected) } }) } } func TestNarrativeGenerator_generateHeadline(t *testing.T) { gen := NewNarrativeGenerator(95) tests := []struct { name string phase string scorecard *Scorecard expected string }{ { name: "maintenance phase", phase: "maintenance", scorecard: &Scorecard{StrictScore: 50}, expected: "Codebase is healthy! Focus on preventing new debt.", }, { name: "critical phase", phase: "critical", scorecard: &Scorecard{StrictScore: 150}, expected: "Critical issues detected (150 strict score). Address T4 findings first.", }, { name: "debt reduction phase", phase: "debt_reduction", scorecard: &Scorecard{TotalScore: 200}, expected: "Significant technical debt (200 open issues). Systematic cleanup recommended.", }, { name: "cleanup phase", phase: "cleanup", scorecard: &Scorecard{TotalScore: 15}, expected: "Minor issues detected (15 open). Quick wins available.", }, { name: "polish phase", phase: "polish", scorecard: &Scorecard{TotalScore: 3}, expected: "Codebase in good shape (3 open issues).", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { headline := gen.generateHeadline(tt.phase, tt.scorecard) if headline != tt.expected { t.Errorf("generateHeadline() = %v, want %v", headline, tt.expected) } }) } } func TestNarrativeGenerator_classifyDimension(t *testing.T) { gen := NewNarrativeGenerator(95) tests := []struct { name string finding Finding expected Dimension }{ { name: "complexity", finding: Finding{Type: "complexity"}, expected: DimensionCodeQuality, }, { name: "complexity_ast", finding: Finding{Type: "complexity_ast"}, expected: DimensionCodeQuality, }, { name: "duplication", finding: Finding{Type: "duplication"}, expected: DimensionDuplication, }, { name: "dead_code", finding: Finding{Type: "dead_code"}, expected: DimensionFileHealth, }, { name: "security", finding: Finding{Type: "security"}, expected: DimensionSecurity, }, { name: "naming", finding: Finding{Type: "naming"}, expected: DimensionNamingQuality, }, { name: "import_cycle", finding: Finding{Type: "import_cycle"}, expected: DimensionAbstractionFit, }, { name: "unknown type", finding: Finding{Type: "unknown"}, expected: DimensionCodeQuality, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dimension := gen.classifyDimension(tt.finding) if dimension != tt.expected { t.Errorf("classifyDimension() = %v, want %v", dimension, tt.expected) } }) } } func TestNarrativeGenerator_generateActions(t *testing.T) { gen := NewNarrativeGenerator(95) tests := []struct { name string findings []Finding phase string expected []string }{ { name: "mixed severities", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT4}, {Status: StatusOpen, Severity: SeverityT3}, {Status: StatusOpen, Severity: SeverityT2}, {Status: StatusOpen, Severity: SeverityT1}, }, phase: "critical", expected: []string{ "Address 1 T4 (major refactor) issues - these require architectural changes", "Review 1 T3 (needs judgment) issues - decide if they need fixing", "Run auto-fixer for 1 T1 (auto-fixable) issues", "Quick manual fixes available for 1 T2 issues", }, }, { name: "no open issues", findings: []Finding{{Status: StatusFixed}}, phase: "maintenance", expected: []string{"No immediate actions required - maintain code quality"}, }, { name: "only T1 issues", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT1}, {Status: StatusOpen, Severity: SeverityT1}, }, phase: "polish", expected: []string{ "Run auto-fixer for 2 T1 (auto-fixable) issues", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actions := gen.generateActions(tt.findings, tt.phase) if len(actions) != len(tt.expected) { t.Errorf("generateActions() length = %v, want %v", len(actions), len(tt.expected)) } for i, action := range actions { if i < len(tt.expected) && action != tt.expected[i] { t.Errorf("generateActions()[%d] = %v, want %v", i, action, tt.expected[i]) } } }) } } func TestNarrativeGenerator_generateStrategy(t *testing.T) { gen := NewNarrativeGenerator(95) tests := []struct { name string findings []Finding expected string parallel bool }{ { name: "high auto-fixable coverage", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT1}, {Status: StatusOpen, Severity: SeverityT1}, {Status: StatusOpen, Severity: SeverityT2}, }, expected: "Use auto-fixers first, then address remaining issues manually", parallel: false, }, { name: "some auto-fixable", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT1}, {Status: StatusOpen, Severity: SeverityT3}, {Status: StatusOpen, Severity: SeverityT4}, }, expected: "Start with auto-fixers for quick wins, then prioritize by impact", parallel: false, }, { name: "no auto-fixable", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT3}, {Status: StatusOpen, Severity: SeverityT4}, }, expected: "Prioritize by severity and impact, starting with T4 issues", parallel: false, }, { name: "no findings", findings: []Finding{}, expected: "Prioritize by severity and impact, starting with T4 issues", parallel: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dimensions := &NarrativeDimensions{} strategy := gen.generateStrategy(tt.findings, dimensions) if strategy.FixerLeverage.Recommendation != tt.expected { t.Errorf("generateStrategy() recommendation = %v, want %v", strategy.FixerLeverage.Recommendation, tt.expected) } if strategy.CanParallelize != tt.parallel { t.Errorf("generateStrategy() CanParallelize = %v, want %v", strategy.CanParallelize, tt.parallel) } }) } } func TestNarrativeGenerator_generateHint(t *testing.T) { gen := NewNarrativeGenerator(95) tests := []struct { name string findings []Finding expected string }{ { name: "has T1 issues", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT1}, {Status: StatusOpen, Severity: SeverityT2}, }, expected: "T1 issues can be auto-fixed with 'devour quality fix'", }, { name: "has T4 issues but no T1", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT4}, {Status: StatusOpen, Severity: SeverityT3}, }, expected: "T4 issues require planning - consider creating a dedicated branch", }, { name: "no T1 or T4 issues", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT2}, {Status: StatusOpen, Severity: SeverityT3}, }, expected: "Focus on one category at a time for best results", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hint := gen.generateHint(tt.findings) if hint != tt.expected { t.Errorf("generateHint() = %v, want %v", hint, tt.expected) } }) } } func TestNarrativeGenerator_generateTools(t *testing.T) { gen := NewNarrativeGenerator(95) findings := []Finding{ {Status: StatusOpen, Severity: SeverityT1, Type: "dead_code"}, {Status: StatusOpen, Severity: SeverityT2, Type: "naming"}, } tools := gen.generateTools(findings) if tools.Plan.Command != "devour quality plan" { t.Errorf("generateTools() Plan.Command = %v, want %v", tools.Plan.Command, "devour quality plan") } if !tools.Badge.Generated { t.Error("generateTools() Badge.Generated should be true") } if len(tools.Fixers) != 1 { t.Errorf("generateTools() Fixers length = %v, want 1", len(tools.Fixers)) } } func TestNarrativeGenerator_analyzeDebt(t *testing.T) { gen := NewNarrativeGenerator(95) findings := []Finding{ {Status: StatusOpen, Severity: SeverityT4, Type: "security", Score: 10}, {Status: StatusWontfix, Severity: SeverityT2, Type: "naming", Score: 5}, {Status: StatusOpen, Severity: SeverityT3, Type: "complexity", Score: 8}, } scorecard := &Scorecard{StrictScore: 150} debt := gen.analyzeDebt(findings, scorecard) if debt.WontfixCount != 1 { t.Errorf("analyzeDebt() WontfixCount = %v, want 1", debt.WontfixCount) } if debt.OverallGap != 150.0 { t.Errorf("analyzeDebt() OverallGap = %v, want 150.0", debt.OverallGap) } if debt.WorstDimension != "Security" { t.Errorf("analyzeDebt() WorstDimension = %v, want Security", debt.WorstDimension) } } func TestNarrativeGenerator_calculateStrictTarget(t *testing.T) { gen := NewNarrativeGenerator(100) tests := []struct { name string scorecard *Scorecard expected string hasWarning bool }{ { name: "at target", scorecard: &Scorecard{StrictScore: 100}, expected: "at_target", hasWarning: false, }, { name: "near target", scorecard: &Scorecard{StrictScore: 85}, expected: "near_target", hasWarning: false, }, { name: "in progress", scorecard: &Scorecard{StrictScore: 60}, expected: "in_progress", hasWarning: true, }, { name: "needs work", scorecard: &Scorecard{StrictScore: 30}, expected: "needs_work", hasWarning: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { target := gen.calculateStrictTarget(tt.scorecard) if target.State != tt.expected { t.Errorf("calculateStrictTarget() State = %v, want %v", target.State, tt.expected) } if (target.Warning != nil) != tt.hasWarning { t.Errorf("calculateStrictTarget() Warning presence = %v, want %v", target.Warning != nil, tt.hasWarning) } }) } } func TestNarrativeGenerator_generateReminders(t *testing.T) { gen := NewNarrativeGenerator(95) tests := []struct { name string findings []Finding history []StateSnapshot expected []string }{ { name: "auto-fixable available", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT1}, {Status: StatusOpen, Severity: SeverityT1}, }, history: []StateSnapshot{}, expected: []string{ "2 auto-fixable issues available - use 'devour quality fix'", }, }, { name: "no progress", findings: []Finding{{Status: StatusOpen, Severity: SeverityT2}}, history: []StateSnapshot{{Findings: 1, Timestamp: time.Now()}}, expected: []string{ "No progress since last scan - consider tackling a specific category", }, }, { name: "no reminders", findings: []Finding{{Status: StatusOpen, Severity: SeverityT3}}, history: []StateSnapshot{}, expected: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reminders := gen.generateReminders(tt.findings, tt.history) if len(reminders) != len(tt.expected) { t.Errorf("generateReminders() length = %v, want %v", len(reminders), len(tt.expected)) } for i, reminder := range reminders { if i < len(tt.expected) && reminder != tt.expected[i] { t.Errorf("generateReminders()[%d] = %v, want %v", i, reminder, tt.expected[i]) } } }) } } func TestNarrativeGenerator_identifyRisks(t *testing.T) { gen := NewNarrativeGenerator(95) tests := []struct { name string findings []Finding history []StateSnapshot expected []string }{ { name: "high T4 count", findings: func() []Finding { var f []Finding for i := 0; i < 5; i++ { f = append(f, Finding{Status: StatusOpen, Severity: SeverityT4}) } return f }(), history: []StateSnapshot{}, expected: []string{ "High number of T4 issues (5) indicates architectural debt", }, }, { name: "upward trend", findings: func() []Finding { var f []Finding for i := 0; i < 25; i++ { f = append(f, Finding{Status: StatusOpen, Severity: SeverityT2}) } return f }(), history: []StateSnapshot{ {Findings: 10, Timestamp: time.Now().Add(-3 * time.Hour)}, {Findings: 12, Timestamp: time.Now().Add(-2 * time.Hour)}, {Findings: 15, Timestamp: time.Now().Add(-1 * time.Hour)}, }, expected: []string{ "Finding count is trending upward - debt is accumulating", }, }, { name: "no risks", findings: []Finding{{Status: StatusOpen, Severity: SeverityT2}}, history: []StateSnapshot{}, expected: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { risks := gen.identifyRisks(tt.findings, tt.history) if len(risks) != len(tt.expected) { t.Errorf("identifyRisks() length = %v, want %v", len(risks), len(tt.expected)) } for i, risk := range risks { if i < len(tt.expected) && risk != tt.expected[i] { t.Errorf("identifyRisks()[%d] = %v, want %v", i, risk, tt.expected[i]) } } }) } } func TestNarrativeGenerator_generateMilestone(t *testing.T) { gen := NewNarrativeGenerator(95) tests := []struct { name string phase string scorecard *Scorecard expected string }{ { name: "maintenance", phase: "maintenance", scorecard: &Scorecard{}, expected: "Maintain current quality level", }, { name: "critical", phase: "critical", scorecard: &Scorecard{}, expected: "Reduce T4 issues to zero", }, { name: "debt reduction", phase: "debt_reduction", scorecard: &Scorecard{}, expected: "Reduce strict score below 95", }, { name: "cleanup", phase: "cleanup", scorecard: &Scorecard{}, expected: "Clear all T1 and T2 issues", }, { name: "polish", phase: "polish", scorecard: &Scorecard{}, expected: "Continue quality improvement", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { milestone := gen.generateMilestone(tt.phase, tt.scorecard) if milestone != tt.expected { t.Errorf("generateMilestone() = %v, want %v", milestone, tt.expected) } }) } } func TestNarrativeGenerator_explainWhyNow(t *testing.T) { gen := NewNarrativeGenerator(95) tests := []struct { name string phase string findings []Finding expected string }{ { name: "has T4 issues", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT4}, }, expected: "T4 issues compound over time - addressing them early prevents architectural decay", }, { name: "many T1 issues", findings: func() []Finding { var f []Finding for i := 0; i < 6; i++ { f = append(f, Finding{Status: StatusOpen, Severity: SeverityT1}) } return f }(), expected: "Quick wins available - auto-fixers can clear low-hanging fruit in minutes", }, { name: "few T1 issues", findings: []Finding{ {Status: StatusOpen, Severity: SeverityT1}, {Status: StatusOpen, Severity: SeverityT2}, }, expected: "Consistent small improvements compound into significant quality gains", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { whyNow := gen.explainWhyNow(tt.phase, tt.findings) if whyNow != tt.expected { t.Errorf("explainWhyNow() = %v, want %v", whyNow, tt.expected) } }) } } func TestNarrativeGenerator_Generate(t *testing.T) { gen := NewNarrativeGenerator(95) findings := []Finding{ {Status: StatusOpen, Severity: SeverityT2, Type: "naming", Score: 5}, {Status: StatusOpen, Severity: SeverityT1, Type: "dead_code", Score: 3}, } scorecard := &Scorecard{ TotalScore: 8, StrictScore: 15, TargetScore: 95, LastScan: time.Now(), } history := []StateSnapshot{ {Findings: 10, Timestamp: time.Now().Add(-1 * time.Hour)}, } narrative := gen.Generate(findings, scorecard, history) if narrative.Phase == "" { t.Error("Generate() Phase should not be empty") } if narrative.Headline == "" { t.Error("Generate() Headline should not be empty") } if narrative.Dimensions == nil { t.Error("Generate() Dimensions should not be nil") } if len(narrative.Actions) == 0 { t.Error("Generate() Actions should not be empty") } if narrative.Strategy == nil { t.Error("Generate() Strategy should not be nil") } if narrative.Tools == nil { t.Error("Generate() Tools should not be nil") } if narrative.Debt == nil { t.Error("Generate() Debt should not be nil") } if narrative.Milestone == "" { t.Error("Generate() Milestone should not be empty") } if narrative.WhyNow == "" { t.Error("Generate() WhyNow should not be empty") } if narrative.StrictTarget == nil { t.Error("Generate() StrictTarget should not be nil") } }