| 1 | package changelog |
| 2 | |
| 3 | import ( |
| 4 | "reflect" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | var testPeople = People{ |
| 10 | "paul@tito.io": "Paul Campbell", |
| 11 | "paul@keymail.dev": "Paul Campbell", |
| 12 | "bill@keymail.dev": "Bill Horsman", |
| 13 | "james@lazyatom.com": "James Adam", |
| 14 | "tonywood@keymail.dev": "Tony Wood", |
| 15 | "tony@tctl.co.uk": "Tony Wood", |
| 16 | } |
| 17 | |
| 18 | func TestParseIdentity(t *testing.T) { |
| 19 | cases := []struct { |
| 20 | name, email string |
| 21 | person, agent string |
| 22 | skip bool |
| 23 | }{ |
| 24 | {"claude", "claude~paul@keymail.dev", "Paul Campbell", "claude", false}, |
| 25 | {"claude~paul@keymail.dev", "claude~paul@keymail.dev", "Paul Campbell", "claude", false}, |
| 26 | {"claude-code~bill@keymail.dev", "claude-code~bill@keymail.dev", "Bill Horsman", "claude", false}, |
| 27 | {"codex", "codex~paul@keymail.dev", "Paul Campbell", "codex", false}, |
| 28 | {"Claude Opus 5.5", "noreply@anthropic.com", "", "claude", false}, |
| 29 | {"Claude", "someone@example.com", "", "claude", false}, |
| 30 | {"Codex", "codex@example.com", "", "codex", false}, |
| 31 | {"Paul Campbell", "Paul@Tito.io", "Paul Campbell", "", false}, |
| 32 | {"GitHub", "noreply@github.com", "", "", true}, |
| 33 | {"amadan-ledger", "ledger@amadan", "", "", true}, |
| 34 | } |
| 35 | for _, c := range cases { |
| 36 | person, agent, skip, err := ParseIdentity(c.name, c.email, testPeople) |
| 37 | if err != nil { |
| 38 | t.Fatalf("%s <%s>: %v", c.name, c.email, err) |
| 39 | } |
| 40 | if person != c.person || agent != c.agent || skip != c.skip { |
| 41 | t.Errorf("%s <%s> = (%q, %q, %v), want (%q, %q, %v)", c.name, c.email, person, agent, skip, c.person, c.agent, c.skip) |
| 42 | } |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | func TestParseIdentityUnknownFails(t *testing.T) { |
| 47 | for _, email := range []string{"stranger@example.com", "claude~stranger@keymail.dev"} { |
| 48 | _, _, _, err := ParseIdentity("Someone", email, testPeople) |
| 49 | if err == nil || !strings.Contains(err.Error(), email[strings.Index(email, "~")+1:]) { |
| 50 | t.Errorf("%s: err = %v, want one naming the address", email, err) |
| 51 | } |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | func TestCoAuthors(t *testing.T) { |
| 56 | msg := "subject\n\nbody\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nco-authored-by: Bill Horsman <bill@keymail.dev>\n" |
| 57 | got := CoAuthors(msg) |
| 58 | want := [][2]string{{"Claude Opus 4.6", "noreply@anthropic.com"}, {"Bill Horsman", "bill@keymail.dev"}} |
| 59 | if !reflect.DeepEqual(got, want) { |
| 60 | t.Fatalf("CoAuthors = %v, want %v", got, want) |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | func TestTally(t *testing.T) { |
| 65 | commits := []CommitIdent{ |
| 66 | {"claude", "claude~paul@keymail.dev", "a"}, |
| 67 | {"claude", "claude~paul@keymail.dev", "b"}, |
| 68 | {"codex", "codex~paul@keymail.dev", "c"}, |
| 69 | {"James Adam", "james@lazyatom.com", "d\n\nCo-Authored-By: Claude <noreply@anthropic.com>"}, |
| 70 | {"GitHub", "noreply@github.com", "merge"}, |
| 71 | } |
| 72 | got, err := Tally(commits, testPeople) |
| 73 | if err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | want := []Credit{ |
| 77 | {Person: "Paul Campbell", Agents: []string{"claude", "codex"}}, |
| 78 | {Person: "James Adam", Agents: []string{"claude"}}, |
| 79 | } |
| 80 | if !reflect.DeepEqual(got, want) { |
| 81 | t.Fatalf("Tally = %+v, want %+v", got, want) |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | func TestTallyAgentWithNoPersonStandsAlone(t *testing.T) { |
| 86 | got, err := Tally([]CommitIdent{{"Claude", "noreply@anthropic.com", "x"}}, testPeople) |
| 87 | if err != nil { |
| 88 | t.Fatal(err) |
| 89 | } |
| 90 | want := []Credit{{Agents: []string{"claude"}}} |
| 91 | if !reflect.DeepEqual(got, want) { |
| 92 | t.Fatalf("Tally = %+v, want %+v", got, want) |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | func TestTallyPersonOnly(t *testing.T) { |
| 97 | got, err := Tally([]CommitIdent{{"Paul Campbell", "paul@tito.io", "x"}}, testPeople) |
| 98 | if err != nil { |
| 99 | t.Fatal(err) |
| 100 | } |
| 101 | want := []Credit{{Person: "Paul Campbell"}} |
| 102 | if !reflect.DeepEqual(got, want) { |
| 103 | t.Fatalf("Tally = %+v, want %+v", got, want) |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | // da866a92 (v0.23.0): an agent commit with a human co-author credits the |
| 108 | // agent to its own operator, not to the co-author. |
| 109 | func TestTallyKeepsEachIdentitysPairing(t *testing.T) { |
| 110 | commits := []CommitIdent{ |
| 111 | {"claude", "claude~paul@keymail.dev", "x\n\nCo-Authored-By: Tony Wood <tony@tctl.co.uk>"}, |
| 112 | {"claude", "claude~paul@keymail.dev", "y"}, |
| 113 | } |
| 114 | got, err := Tally(commits, testPeople) |
| 115 | if err != nil { |
| 116 | t.Fatal(err) |
| 117 | } |
| 118 | want := []Credit{{Person: "Paul Campbell", Agents: []string{"claude"}}, {Person: "Tony Wood"}} |
| 119 | if !reflect.DeepEqual(got, want) { |
| 120 | t.Fatalf("Tally = %+v, want %+v", got, want) |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | // A person's commit with a Claude trailer and a human co-author: Claude |
| 125 | // worked with the author. |
| 126 | func TestTallyLoneAgentGoesToTheAuthor(t *testing.T) { |
| 127 | msg := "x\n\nCo-Authored-By: James Adam <james@lazyatom.com>\nCo-Authored-By: Claude <noreply@anthropic.com>" |
| 128 | got, err := Tally([]CommitIdent{{"Paul Campbell", "paul@tito.io", msg}}, testPeople) |
| 129 | if err != nil { |
| 130 | t.Fatal(err) |
| 131 | } |
| 132 | want := []Credit{{Person: "James Adam"}, {Person: "Paul Campbell", Agents: []string{"claude"}}} |
| 133 | if !reflect.DeepEqual(got, want) { |
| 134 | t.Fatalf("Tally = %+v, want %+v", got, want) |
| 135 | } |
| 136 | } |
| 137 | |