internal/workflow + cli:gitea.py 工作流命令 Go 化(#6)

- internal/workflow:Client 介面+判定語義(最後留言、作者/審核者跟進、
  assignee 交集、最久未更新優先),含假 client 單元測試
- internal/workflow/giteaapi.go:以 internal/gitea(#5)實作 Client
- internal/cli:註冊 whoami/orgs/members/mine/pulls/next;全域選項允許
  出現在命令之後;結束碼 0/2/3
- 與 gitea.py 交叉驗證:next/mine/pulls/members(--has-work) 拾取一致
- 依 #5 分支 feature/issue5-gitea-client 為基底
This commit is contained in:
2026-09-10 08:39:35 +08:00
parent 6951817313
commit 5583a00bb5
10 changed files with 1604 additions and 6 deletions
+145
View File
@@ -0,0 +1,145 @@
// 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)
}
}