Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions internal/compare/compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,119 @@ func TestBuildSummaryNewContextAppeared(t *testing.T) {
}
}

// ── fixSteps ──────────────────────────────────────────────────────────────────

func TestFixStepsNilAnalysis(t *testing.T) {
got := fixSteps(nil)
if got != nil {
t.Errorf("expected nil for nil analysis, got %v", got)
}
}

func TestFixStepsNilArtifact(t *testing.T) {
a := makeAnalysis("docker-auth", "Docker auth", 0.9, nil)
got := fixSteps(a)
if got != nil {
t.Errorf("expected nil when Artifact is nil, got %v", got)
}
}

func TestFixStepsWithArtifact(t *testing.T) {
a := makeAnalysis("docker-auth", "Docker auth", 0.9, nil)
a.Artifact = &model.FailureArtifact{
FixSteps: []string{"step 1", "step 2"},
}
got := fixSteps(a)
if len(got) != 2 || got[0] != "step 1" || got[1] != "step 2" {
t.Errorf("expected [step 1, step 2], got %v", got)
}
}

func TestFixStepsReturnsIndependentCopy(t *testing.T) {
a := makeAnalysis("docker-auth", "Docker auth", 0.9, nil)
a.Artifact = &model.FailureArtifact{
FixSteps: []string{"step 1"},
}
got := fixSteps(a)
got[0] = "mutated"
if a.Artifact.FixSteps[0] != "step 1" {
t.Error("fixSteps should return an independent copy")
}
}

// ── dominantSignals ───────────────────────────────────────────────────────────

func TestDominantSignalsNilAnalysis(t *testing.T) {
got := dominantSignals(nil)
if got != nil {
t.Errorf("expected nil for nil analysis, got %v", got)
}
}

func TestDominantSignalsNilArtifact(t *testing.T) {
a := makeAnalysis("docker-auth", "Docker auth", 0.9, nil)
got := dominantSignals(a)
if got != nil {
t.Errorf("expected nil when Artifact is nil, got %v", got)
}
}

func TestDominantSignalsWithArtifact(t *testing.T) {
a := makeAnalysis("docker-auth", "Docker auth", 0.9, nil)
a.Artifact = &model.FailureArtifact{
DominantSignals: []string{"signal-A", "signal-B"},
}
got := dominantSignals(a)
if len(got) != 2 || got[0] != "signal-A" || got[1] != "signal-B" {
t.Errorf("expected [signal-A, signal-B], got %v", got)
}
}

func TestDominantSignalsReturnsIndependentCopy(t *testing.T) {
a := makeAnalysis("docker-auth", "Docker auth", 0.9, nil)
a.Artifact = &model.FailureArtifact{
DominantSignals: []string{"signal-A"},
}
got := dominantSignals(a)
got[0] = "mutated"
if a.Artifact.DominantSignals[0] != "signal-A" {
t.Error("dominantSignals should return an independent copy")
}
}

// ── fixSteps and dominantSignals propagate via Build ─────────────────────────

func TestBuildFixStepsDelta(t *testing.T) {
left := makeAnalysis("docker-auth", "Docker auth", 0.9, nil)
left.Artifact = &model.FailureArtifact{FixSteps: []string{"old fix"}}
right := makeAnalysis("docker-auth", "Docker auth", 0.9, nil)
right.Artifact = &model.FailureArtifact{FixSteps: []string{"new fix"}}
report := Build(left, right)
if len(report.FixSteps.Added) != 1 || report.FixSteps.Added[0] != "new fix" {
t.Errorf("expected FixSteps.Added=[new fix], got %v", report.FixSteps.Added)
}
if len(report.FixSteps.Removed) != 1 || report.FixSteps.Removed[0] != "old fix" {
t.Errorf("expected FixSteps.Removed=[old fix], got %v", report.FixSteps.Removed)
}
if !report.Changed {
t.Error("expected Changed=true when fix steps differ")
}
}

func TestBuildDominantSignalsDelta(t *testing.T) {
left := makeAnalysis("docker-auth", "Docker auth", 0.9, nil)
left.Artifact = &model.FailureArtifact{DominantSignals: []string{"sig-A"}}
right := makeAnalysis("docker-auth", "Docker auth", 0.9, nil)
right.Artifact = &model.FailureArtifact{DominantSignals: []string{"sig-B"}}
report := Build(left, right)
if len(report.DominantSignals.Added) != 1 || report.DominantSignals.Added[0] != "sig-B" {
t.Errorf("expected DominantSignals.Added=[sig-B], got %v", report.DominantSignals.Added)
}
if len(report.DominantSignals.Removed) != 1 || report.DominantSignals.Removed[0] != "sig-A" {
t.Errorf("expected DominantSignals.Removed=[sig-A], got %v", report.DominantSignals.Removed)
}
}

// ── diffStrings ───────────────────────────────────────────────────────────────

func TestDiffStringsEmptyInputs(t *testing.T) {
Expand Down
153 changes: 153 additions & 0 deletions internal/coverage/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"path/filepath"
"strings"
"testing"

"faultline/internal/model"
)

func TestBuildCountsFixtureExpectationsAndNegativeAssertions(t *testing.T) {
Expand Down Expand Up @@ -405,3 +407,154 @@ func TestFixtureLayoutForRootWithChildFixturesDir(t *testing.T) {
t.Errorf("Layout.Root = %q, want %q", layout.Root, parent)
}
}

// ── falsePositiveRisk ─────────────────────────────────────────────────────────

func TestFalsePositiveRiskHighSharedPatterns(t *testing.T) {
pb := model.Playbook{ID: "test"}
if got := falsePositiveRisk(pb, 7); got != "high" {
t.Errorf("expected high for 7 shared patterns, got %q", got)
}
if got := falsePositiveRisk(pb, 10); got != "high" {
t.Errorf("expected high for 10 shared patterns, got %q", got)
}
}

func TestFalsePositiveRiskMediumSharedPatterns(t *testing.T) {
pb := model.Playbook{ID: "test"}
if got := falsePositiveRisk(pb, 3); got != "medium" {
t.Errorf("expected medium for 3 shared patterns, got %q", got)
}
if got := falsePositiveRisk(pb, 6); got != "medium" {
t.Errorf("expected medium for 6 shared patterns, got %q", got)
}
}

func TestFalsePositiveRiskMediumSilentFailureCategory(t *testing.T) {
pb := model.Playbook{ID: "test", Category: "silent_failure"}
if got := falsePositiveRisk(pb, 0); got != "medium" {
t.Errorf("expected medium for silent_failure category, got %q", got)
}
}

func TestFalsePositiveRiskLow(t *testing.T) {
pb := model.Playbook{ID: "test", Category: "build"}
if got := falsePositiveRisk(pb, 0); got != "low" {
t.Errorf("expected low for 0 shared patterns (non-silent), got %q", got)
}
if got := falsePositiveRisk(pb, 2); got != "low" {
t.Errorf("expected low for 2 shared patterns, got %q", got)
}
}

// ── falseNegativeRisk ─────────────────────────────────────────────────────────

func TestFalseNegativeRiskSourceDetector(t *testing.T) {
pb := model.Playbook{ID: "test", Detector: "source"}
if got := falseNegativeRisk(pb, 10); got != "medium" {
t.Errorf("expected medium for source detector, got %q", got)
}
}

func TestFalseNegativeRiskHighFewFixtures(t *testing.T) {
pb := model.Playbook{ID: "test", Detector: "log"}
if got := falseNegativeRisk(pb, 0); got != "high" {
t.Errorf("expected high for 0 positive fixtures, got %q", got)
}
if got := falseNegativeRisk(pb, 1); got != "high" {
t.Errorf("expected high for 1 positive fixture, got %q", got)
}
}

func TestFalseNegativeRiskMediumTwoFixtures(t *testing.T) {
pb := model.Playbook{ID: "test", Detector: "log"}
if got := falseNegativeRisk(pb, 2); got != "medium" {
t.Errorf("expected medium for 2 positive fixtures, got %q", got)
}
}

func TestFalseNegativeRiskLowManyFixtures(t *testing.T) {
pb := model.Playbook{ID: "test", Detector: "log"}
if got := falseNegativeRisk(pb, 3); got != "low" {
t.Errorf("expected low for 3+ positive fixtures, got %q", got)
}
if got := falseNegativeRisk(pb, 10); got != "low" {
t.Errorf("expected low for 10 positive fixtures, got %q", got)
}
}

// ── robustnessScore ───────────────────────────────────────────────────────────

func TestRobustnessScoreBaselineNoBonus(t *testing.T) {
got := robustnessScore("low", "low", 0, 0, 0)
// base=86, no penalty for low/low, no bonuses
if got != 86 {
t.Errorf("expected 86 for low/low with no bonus, got %d", got)
}
}

func TestRobustnessScoreHighFPPenalty(t *testing.T) {
got := robustnessScore("high", "low", 0, 0, 0)
// 86 - 16 = 70
if got != 70 {
t.Errorf("expected 70 for high FP risk, got %d", got)
}
}

func TestRobustnessScoreMediumFNPenalty(t *testing.T) {
got := robustnessScore("low", "medium", 0, 0, 0)
// 86 - 8 = 78
if got != 78 {
t.Errorf("expected 78 for medium FN risk, got %d", got)
}
}

func TestRobustnessScorePositiveFixturesBonus(t *testing.T) {
got3 := robustnessScore("low", "low", 3, 0, 0)
// 86 + 3 = 89
if got3 != 89 {
t.Errorf("expected 89 for 3 positive fixtures, got %d", got3)
}
got5 := robustnessScore("low", "low", 5, 0, 0)
// 86 + 6 = 92
if got5 != 92 {
t.Errorf("expected 92 for 5 positive fixtures, got %d", got5)
}
}

func TestRobustnessScoreNegativeAssertionBonus(t *testing.T) {
got := robustnessScore("low", "low", 0, 3, 0)
// 86 + 3 = 89
if got != 89 {
t.Errorf("expected 89 for 3 negative assertions, got %d", got)
}
}

func TestRobustnessScoreStrictTop1Bonus(t *testing.T) {
got := robustnessScore("low", "low", 0, 0, 1)
// 86 + 2 = 88
if got != 88 {
t.Errorf("expected 88 for strict top-1, got %d", got)
}
}

func TestRobustnessScoreClampedAtMax(t *testing.T) {
// All bonuses: 86 + 6 + 3 + 2 = 97 → clamped to 95
got := robustnessScore("low", "low", 5, 3, 1)
if got != 95 {
t.Errorf("expected 95 (clamped max), got %d", got)
}
}

func TestRobustnessScoreClampedAtMin(t *testing.T) {
// 86 - 16 - 16 = 54; min is 45
got := robustnessScore("high", "high", 0, 0, 0)
if got != 54 {
t.Errorf("expected 54 for double high penalty, got %d", got)
}
// Force below min: theoretical
got2 := robustnessScore("high", "high", 0, 0, 0)
if got2 < 45 {
t.Errorf("expected minimum 45, got %d", got2)
}
}
Loading
Loading