// judge_test.go — 驗證工作流判定語義(對照 gitea.py): // 最後留言判定、作者/審核者跟進條件、assignee 交集與排序。 package workflow import ( "reflect" "testing" ) // mkUser 構造 User。 func mkUser(name string) *User { return &User{UserName: name} } func TestUserNameFallback(t *testing.T) { if got := (&User{UserName: "a", Login: "b"}).Name(); got != "a" { t.Errorf("username 優先,得到 %q", got) } if got := (&User{Login: "b"}).Name(); got != "b" { t.Errorf("login 回退,得到 %q", got) } if got := mkUser("").Name(); got != "" { t.Errorf("空帳號,得到 %q", got) } } func TestLastComment(t *testing.T) { comments := []Comment{ {CreatedAt: "2026-09-09T01:00:00Z", User: mkUser("alex")}, {CreatedAt: "2026-09-09T03:00:00Z", User: mkUser("max")}, {CreatedAt: "2026-09-09T02:00:00Z", User: mkUser("ceo")}, } last := LastComment(comments) if last == nil || last.Author() != "max" { t.Errorf("應依 created_at 排序取最後(03:00 max),得到 %+v", last) } if LastComment(nil) != nil { t.Errorf("無留言應回 nil") } } func TestNeedsAuthorFollowup(t *testing.T) { pr := &PullRequest{User: mkUser("alex")} cases := []struct { name string comments []Comment want bool }{ {"無留言", nil, false}, {"最後留言是作者", []Comment{{CreatedAt: "a", User: mkUser("alex")}}, false}, {"最後留言是他人", []Comment{ {CreatedAt: "a", User: mkUser("alex")}, {CreatedAt: "b", User: mkUser("ceo")}, }, true}, {"留言者未知", []Comment{{CreatedAt: "a"}}, false}, } for _, c := range cases { if got := NeedsAuthorFollowup(pr, c.comments); got != c.want { t.Errorf("%s:得到 %v,want %v", c.name, got, c.want) } } } func TestNeedsReviewerFollowup(t *testing.T) { pr := &PullRequest{ User: mkUser("max"), RequestedReviewers: []User{*mkUser("alex"), *mkUser("queena")}, } comments := []Comment{{CreatedAt: "a", User: mkUser("max")}} if !NeedsReviewerFollowup(pr, comments, "alex") { t.Errorf("審核者且最後留言非自己 → 應需跟進") } if NeedsReviewerFollowup(pr, comments, "max") { t.Errorf("作者本人 → 不應列入審核清單") } if NeedsReviewerFollowup(pr, []Comment{{CreatedAt: "a", User: mkUser("alex")}}, "alex") { t.Errorf("最後留言是自己 → 不應需跟進") } if NeedsReviewerFollowup(pr, comments, "ceo") { t.Errorf("非審核者 → 不應需跟進") } // 尚無留言的 PR:審核者仍應被列入(含尚無留言)。 if !NeedsReviewerFollowup(pr, nil, "queena") { t.Errorf("尚無留言 → 審核者應需跟進") } } func TestAssigneeNames(t *testing.T) { issue := &Issue{ Assignee: mkUser("alex"), Assignees: []User{*mkUser("alex"), *mkUser("max")}, } if got := AssigneeNames(issue); !reflect.DeepEqual(got, []string{"alex", "max"}) { t.Errorf("assignee 應去重排序,得到 %v", got) } } func TestAssigneeOverlapsEmptyMembers(t *testing.T) { issue := &Issue{Assignee: mkUser("alex")} if AssigneeOverlaps(issue, nil) { t.Errorf("成員集為空 → 不應列入(同 gitea.py filter_issues_by_assignee)") } } func TestLastCommentNotByAssignee(t *testing.T) { issue := &Issue{Assignee: mkUser("alex")} if !LastCommentNotByAssignee(issue, nil) { t.Errorf("無留言 → 應視為需要處理") } if LastCommentNotByAssignee(issue, []Comment{{CreatedAt: "a", User: mkUser("alex")}}) { t.Errorf("最後留言來自 assignee → 不應列入") } if !LastCommentNotByAssignee(issue, []Comment{{CreatedAt: "a", User: mkUser("ceo")}}) { t.Errorf("最後留言來自他人 → 應列入") } } func TestUpdatedKeyFallbackChain(t *testing.T) { i := &Issue{UpdatedAt: "", Updated: "u", CreatedAt: "c"} if got := i.updatedKey(); got != "u" { t.Errorf("updated 回退鏈,得到 %q", got) } if got := (&Issue{}).updatedKey(); got != "9999" { t.Errorf("全缺 → 9999,得到 %q", got) } } func TestFullNameFallbacks(t *testing.T) { pr := &PullRequest{ HTMLURL: "https://gitea.alterminal.com/alterminal/teai/pulls/2", Base: &PRRef{Repo: &Repository{FullName: "alterminal/teai"}}, } if got := pr.FullName(); got != "alterminal/teai" { t.Errorf("base.repo 優先,得到 %q", got) } pr2 := &PullRequest{HTMLURL: "https://gitea.alterminal.com/alterminal/teai/pulls/2"} if got := pr2.FullName(); got != "alterminal/teai" { t.Errorf("html_url 回退,得到 %q", got) } i := &Issue{Repository: &Repository{FullName: "alterminal/agents"}} if got := i.FullName(); got != "alterminal/agents" { t.Errorf("repository 欄位,得到 %q", got) } if got := (&Issue{}).FullName(); got != "" { t.Errorf("全缺 → 空,得到 %q", got) } }