processor/history_blame_test.go GO 287 lines View on github.com → Search inside
1// SPDX-License-Identifier: MIT23package processor45import (6	"strings"7	"testing"8)910func TestApplyDiffToBlameNewFile(t *testing.T) {11	got := applyDiffToBlame(nil, 3, []LineRange{{Start: 1, Count: 3}}, nil, 7)12	want := []authorID{7, 7, 7}13	if !equalIDs(got, want) {14		t.Errorf("new file blame = %v, want %v", got, want)15	}16}1718func TestApplyDiffToBlameAppend(t *testing.T) {19	prev := []authorID{1, 1}20	got := applyDiffToBlame(prev, 4, []LineRange{{Start: 3, Count: 2}}, nil, 9)21	want := []authorID{1, 1, 9, 9}22	if !equalIDs(got, want) {23		t.Errorf("append blame = %v, want %v", got, want)24	}25}2627func TestApplyDiffToBlamePureDelete(t *testing.T) {28	prev := []authorID{1, 1, 1, 1}29	got := applyDiffToBlame(prev, 2, nil, []LineRange{{Start: 2, Count: 2}}, 9)30	want := []authorID{1, 1}31	if !equalIDs(got, want) {32		t.Errorf("delete blame = %v, want %v", got, want)33	}34}3536func TestApplyDiffToBlameReplaceMiddle(t *testing.T) {37	// 5 lines from author 1; replace line 3 with two lines from author 2.38	prev := []authorID{1, 1, 1, 1, 1}39	got := applyDiffToBlame(prev, 6,40		[]LineRange{{Start: 3, Count: 2}},41		[]LineRange{{Start: 3, Count: 1}},42		2)43	want := []authorID{1, 1, 2, 2, 1, 1}44	if !equalIDs(got, want) {45		t.Errorf("replace blame = %v, want %v", got, want)46	}47}4849func TestApplyDiffToBlamePadsToNewLines(t *testing.T) {50	// Diff arithmetic disagrees with newLines — defensive pad with sentinel.51	got := applyDiffToBlame(nil, 4, nil, nil, 9)52	want := []authorID{0, 0, 0, 0}53	if !equalIDs(got, want) {54		t.Errorf("pad blame = %v, want %v", got, want)55	}56}5758func TestAuthorRegistryInternsCanonical(t *testing.T) {59	mm := parseMailmap([]byte("Alice <alice@example.com> <a@example.com>\n"))60	r := newAuthorRegistry(mm)61	id1 := r.intern("Alice", "a@example.com")62	id2 := r.intern("Alice", "alice@example.com")63	if id1 != id2 {64		t.Errorf("mailmap-folded identities should collapse: %d vs %d", id1, id2)65	}66	if r.record(id1).Email != "alice@example.com" {67		t.Errorf("canonical email = %q, want alice@example.com", r.record(id1).Email)68	}69}7071func TestAuthorRegistrySentinelReserved(t *testing.T) {72	r := newAuthorRegistry(nil)73	id := r.intern("Bob", "bob@example.com")74	if id == sentinelAuthorID {75		t.Errorf("real author should not be assigned sentinel ID")76	}77}7879func TestParseMailmapNameOnly(t *testing.T) {80	m := parseMailmap([]byte("Proper Name <commit@example.com>\n"))81	name, email := m.Resolve("Other Name", "commit@example.com")82	if name != "Proper Name" {83		t.Errorf("name = %q, want Proper Name", name)84	}85	if email != "commit@example.com" {86		t.Errorf("email = %q, want commit@example.com", email)87	}88}8990func TestParseMailmapEmailReplacement(t *testing.T) {91	m := parseMailmap([]byte("<proper@example.com> <commit@example.com>\n"))92	name, email := m.Resolve("Commit Name", "commit@example.com")93	if email != "proper@example.com" {94		t.Errorf("email = %q, want proper@example.com", email)95	}96	if name != "Commit Name" {97		t.Errorf("name = %q, want unchanged Commit Name", name)98	}99}100101func TestParseMailmapNameAndEmailReplacement(t *testing.T) {102	m := parseMailmap([]byte("Proper <proper@example.com> Commit <commit@example.com>\n"))103	// Should only match when commit name AND commit email both match.104	name, email := m.Resolve("Commit", "commit@example.com")105	if name != "Proper" || email != "proper@example.com" {106		t.Errorf("got (%q,%q), want (Proper, proper@example.com)", name, email)107	}108	// Different commit name → no match.109	name2, email2 := m.Resolve("Other", "commit@example.com")110	if name2 != "Other" || email2 != "commit@example.com" {111		t.Errorf("got (%q,%q), want unchanged", name2, email2)112	}113}114115func TestParseMailmapSkipsCommentsAndBlanks(t *testing.T) {116	body := "# comment\n\nAlice <a@x>  # trailing comment\n"117	m := parseMailmap([]byte(body))118	if len(m.byEmail) != 1 {119		t.Errorf("byEmail entries = %d, want 1", len(m.byEmail))120	}121}122123func TestMailmapResolveNilSafe(t *testing.T) {124	var m *mailmap125	n, e := m.Resolve("Bob", "b@x")126	if n != "Bob" || e != "b@x" {127		t.Errorf("nil mailmap should be no-op, got (%q,%q)", n, e)128	}129}130131func TestParseMailmapLineForms(t *testing.T) {132	cases := []struct {133		in      string134		properN string135		properE string136		commitN string137		commitE string138		ok      bool139	}{140		{"Proper Name <c@x>", "Proper Name", "", "", "c@x", true},141		{"<p@x> <c@x>", "", "p@x", "", "c@x", true},142		{"Proper Name <p@x> <c@x>", "Proper Name", "p@x", "", "c@x", true},143		{"Proper Name <p@x> Commit Name <c@x>", "Proper Name", "p@x", "Commit Name", "c@x", true},144		{"no brackets here", "", "", "", "", false},145	}146	for _, c := range cases {147		got, ok := parseMailmapLine(c.in)148		if ok != c.ok {149			t.Errorf("parseMailmapLine(%q) ok = %v, want %v", c.in, ok, c.ok)150			continue151		}152		if !ok {153			continue154		}155		if got.properName != c.properN || got.properEmail != c.properE ||156			got.commitName != c.commitN || got.commitEmail != c.commitE {157			t.Errorf("parseMailmapLine(%q) = %+v, want (%q,%q,%q,%q)",158				c.in, got, c.properN, c.properE, c.commitN, c.commitE)159		}160	}161}162163func TestParseMailmapTrimsWhitespace(t *testing.T) {164	m := parseMailmap([]byte("  Proper Name   <c@x>  \n"))165	if e, ok := m.byEmail["c@x"]; !ok || e.Name != "Proper Name" {166		t.Errorf("byEmail[c@x] = %+v, want Proper Name", e)167	}168}169170// TestAuthorRegistryFoldsByNameAndDomain documents the canonical scc-repo171// case: the same human committing from a personal address and a172// github-noreply address. The two have different domains so under the173// strict (name, domain) rule they intentionally stay split — folding here174// would require the looser name-only mode which is not the default.175func TestAuthorRegistryFoldsByNameAndDomain(t *testing.T) {176	r := newAuthorRegistryWithFold(nil, true)177	id1 := r.intern("Ben Boyter", "ben@boyter.org")178	id2 := r.intern("Ben Boyter", "boyter@users.noreply.github.com")179	if id1 == id2 {180		t.Errorf("different domains should not fold under strict (name,domain): %d == %d", id1, id2)181	}182}183184func TestAuthorRegistryFoldsSameDomainDifferentEmail(t *testing.T) {185	r := newAuthorRegistryWithFold(nil, true)186	id1 := r.intern("Alice", "alice@x.com")187	id2 := r.intern("Alice", "alice.smith@x.com")188	if id1 != id2 {189		t.Errorf("same (name,domain) should fold: %d vs %d", id1, id2)190	}191}192193func TestAuthorRegistryDoesNotFoldDifferentDomain(t *testing.T) {194	r := newAuthorRegistryWithFold(nil, true)195	id1 := r.intern("Daniel", "d@a.com")196	id2 := r.intern("Daniel", "d@b.com")197	if id1 == id2 {198		t.Errorf("different domains should stay split: %d == %d", id1, id2)199	}200}201202func TestAuthorRegistrySkipsGenericNames(t *testing.T) {203	r := newAuthorRegistryWithFold(nil, true)204	id1 := r.intern("root", "root@host-a")205	id2 := r.intern("root", "root@host-b")206	if id1 == id2 {207		t.Errorf("generic name 'root' must not fold across hosts: %d == %d", id1, id2)208	}209	// Even within the same domain, the skip list keeps them split — the210	// name is too generic to assume identity.211	id3 := r.intern("root", "root@host-a")212	if id3 != id1 {213		t.Errorf("identical (name,email) for skipped names should still dedupe by primary key: %d vs %d", id3, id1)214	}215}216217func TestAuthorRegistryFoldsBots(t *testing.T) {218	r := newAuthorRegistryWithFold(nil, true)219	id1 := r.intern("dependabot[bot]", "x@y.com")220	id2 := r.intern("dependabot[bot]", "x@y.com")221	if id1 != id2 {222		t.Errorf("identical bot identities should collapse: %d vs %d", id1, id2)223	}224}225226func TestAuthorRegistryMailmapBeatsFold(t *testing.T) {227	// Mailmap maps A→C and B→D. Even though A and B share a name+domain228	// after the (unrelated) folding key would suggest, the mailmap routes229	// them to distinct canonical identities and they must stay split.230	mm := parseMailmap([]byte(231		"Carol <carol@example.com> Sam <a@example.com>\n" +232			"Dave <dave@example.com> Sam <b@example.com>\n"))233	r := newAuthorRegistryWithFold(mm, true)234	id1 := r.intern("Sam", "a@example.com")235	id2 := r.intern("Sam", "b@example.com")236	if id1 == id2 {237		t.Errorf("mailmap-distinct identities must not fold: %d == %d", id1, id2)238	}239}240241func TestAuthorRegistryFoldDisabled(t *testing.T) {242	r := newAuthorRegistryWithFold(nil, false)243	id1 := r.intern("Alice", "alice@x.com")244	id2 := r.intern("Alice", "alice.smith@x.com")245	if id1 == id2 {246		t.Errorf("with fold disabled, distinct emails must stay split: %d == %d", id1, id2)247	}248}249250func TestEmailDomain(t *testing.T) {251	cases := []struct{ in, want string }{252		{"a@b.com", "b.com"},253		{"A@B.COM", "b.com"},254		{"a@b@c.com", "c.com"},255		{"noatsign", ""},256		{"", ""},257	}258	for _, c := range cases {259		if got := emailDomain(c.in); got != c.want {260			t.Errorf("emailDomain(%q) = %q, want %q", c.in, got, c.want)261		}262	}263}264265func equalIDs(a, b []authorID) bool {266	if len(a) != len(b) {267		return false268	}269	for i := range a {270		if a[i] != b[i] {271			return false272		}273	}274	return true275}276277// TestParseMailmapHandlesCaseInsensitiveEmail confirms that mailmap email278// keys are folded to lowercase so commits using mixed-case emails still279// resolve. Real-world commits often have inconsistent casing.280func TestParseMailmapHandlesCaseInsensitiveEmail(t *testing.T) {281	m := parseMailmap([]byte("Alice <alice@example.com> <a@example.com>\n"))282	n, e := m.Resolve("Anyone", strings.ToUpper("a@example.com"))283	if n != "Alice" || e != "alice@example.com" {284		t.Errorf("case-insensitive lookup got (%q,%q), want (Alice, alice@example.com)", n, e)285	}286}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.