package workflow import ( "strings" "testing" ) func TestComputeSignals_EmptyInput(t *testing.T) { if got := ComputeSignals(nil); len(got) != 0 { t.Errorf("nil input: got %v, want empty", got) } if got := ComputeSignals([]string{}); len(got) != 0 { t.Errorf("empty slice: got %v, want empty", got) } if got := ComputeSignals([]string{"", " "}); len(got) != 0 { t.Errorf("blank lines: got %v, want empty", got) } } func TestComputeSignals_BelowThresholdIsDropped(t *testing.T) { lines := []string{ "abc1234 fix: one", "abc1235 fix: two", // only two "fix:" — below threshold of 3 "abc1236 docs: unrelated", } got := ComputeSignals(lines) if len(got) != 0 { t.Errorf("below-threshold counts must drop, got %v", got) } } func TestComputeSignals_AtAndAboveThreshold(t *testing.T) { lines := []string{ "abc1234 fix: one", "abc1235 fix: two", "abc1236 fix: three", // hits threshold } got := ComputeSignals(lines) if len(got) != 1 || got[0].Key != "fix" || got[0].Count != 3 || got[0].Kind != SignalCommitType { t.Errorf("threshold hit: got %v, want one fix=3", got) } } func TestComputeSignals_OrderingCountDescThenKeyAsc(t *testing.T) { lines := []string{ // 4 fix "a1 fix: a", "a2 fix: b", "a3 fix: c", "a4 fix: d", // 3 chore "b1 chore: a", "b2 chore: b", "b3 chore: c", // 3 docs "c1 docs: a", "c2 docs: b", "c3 docs: c", } got := ComputeSignals(lines) if len(got) != 3 { t.Fatalf("expected 3 signals, got %v", got) } if got[0].Key != "fix" || got[0].Count != 4 { t.Errorf("rank 1: got %v, want fix=4", got[0]) } // Tie at count=3: alphabetical → chore before docs. if got[1].Key != "chore" || got[2].Key != "docs" { t.Errorf("tie-break order: got %v, want chore then docs", got) } } func TestComputeSignals_ConventionalShapes(t *testing.T) { lines := []string{ "a1 feat: one", "a2 feat(ui): two", // scope → still counts as feat "a3 feat(api)!: three", // breaking → still counts as feat "a4 not a commit subject", "a5 RANDOM CAPITALS: ignored", } got := ComputeSignals(lines) if len(got) != 1 || got[0].Key != "feat" || got[0].Count != 3 { t.Errorf("conventional shapes: got %v, want feat=3", got) } } func TestComputeSignals_NonConventionalIgnored(t *testing.T) { lines := []string{ "a1 fix something", "a2 fix something else", "a3 fix one more thing", // no colon — not a conventional subject } got := ComputeSignals(lines) if len(got) != 0 { t.Errorf("non-conventional must be ignored, got %v", got) } } func TestProposeCandidates_TitleAndReason(t *testing.T) { signals := []Signal{ {Kind: SignalCommitType, Key: "fix", Count: 5}, } got := ProposeCandidates(signals) if len(got) != 1 { t.Fatalf("expected 1 candidate, got %v", got) } if got[0].Title != "fix-workflow" { t.Errorf("title: got %q, want fix-workflow", got[0].Title) } if !workflowNameRE.MatchString(got[0].Title) { t.Errorf("title %q must satisfy workflowNameRE", got[0].Title) } if !strings.Contains(got[0].Reason, "fix") || !strings.Contains(got[0].Reason, "5") { t.Errorf("reason should mention type+count: got %q", got[0].Reason) } if len(got[0].Signals) != 1 || got[0].Signals[0].Key != "fix" { t.Errorf("signals should carry source signal, got %v", got[0].Signals) } } func TestProposeCandidates_CapAtFive(t *testing.T) { signals := []Signal{ {Kind: SignalCommitType, Key: "a", Count: 10}, {Kind: SignalCommitType, Key: "b", Count: 9}, {Kind: SignalCommitType, Key: "c", Count: 8}, {Kind: SignalCommitType, Key: "d", Count: 7}, {Kind: SignalCommitType, Key: "e", Count: 6}, {Kind: SignalCommitType, Key: "f", Count: 5}, // 6th — must drop } got := ProposeCandidates(signals) if len(got) != 5 { t.Fatalf("cap not enforced, got %d candidates", len(got)) } // The 6th signal ("f") must NOT appear. for _, c := range got { if c.Title == "f-workflow" { t.Errorf("6th candidate leaked past the cap") } } } func TestProposeCandidates_UnknownKindIgnored(t *testing.T) { signals := []Signal{ {Kind: "future-file-touch", Key: "x", Count: 99}, {Kind: SignalCommitType, Key: "fix", Count: 3}, } got := ProposeCandidates(signals) if len(got) != 1 || got[0].Title != "fix-workflow" { t.Errorf("unknown signal kind must be ignored, got %v", got) } } func TestComputeSignals_HandlesLogOneline(t *testing.T) { // Realistic git log --oneline shape: " " log := strings.Join([]string{ "a0cf4fb docs: formal versioning policy", "2a29a6b chore: gitignore the local roadmap file", "4bfd48a docs: cross-repo nav design", "0fcfcae docs: cross-project fingerprint", "ef1f084 feat: built, installable, validated", }, "\n") got := ComputeSignals(splitLines(log)) // 3 docs (≥ threshold), 1 chore, 1 feat → only docs survives. if len(got) != 1 || got[0].Key != "docs" || got[0].Count != 3 { t.Errorf("realistic log: got %v, want docs=3", got) } }