Files
teai/internal/workflow/judge_test.go
T
chenyunda218 4a5d02fd70 fix: stalled PR 作者已交棒審核改判 reviewer-idle(agents #51)
considerPull 的 author-idle 分支缺少 #47 的交棒檢查,且既有
IsDeliveryReport 訊號為 issue 交付報告設計,PR 端「已就緒,可審閱+
@審核者」形態無對應訊號——fox#60 作者留言交棒後仍被誤判 author-idle。

新增 IsReviewHandoff(@目前審核者帳號 ∧ 就緒語,mention 邊界感知
避免 @chenyunda218 誤命中 chenyunda),considerPull 在判 author-idle
前檢查,命中改判 reviewer-idle,讓既有 48 小時改派規則自然適用。
gitea.py 側 _consider pull 分支以 _is_review_handoff 同步(另 PR)。
2026-09-16 13:17:55 +08:00

216 lines
8.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}
}
// TestIsDeliveryReport(agents #47):交付報告=結構訊號 ∧ 交棒訊號。
// 案例與 gitea.py _is_delivery_report 的離線驗證同集(bear#52 實例)。
func TestIsDeliveryReport(t *testing.T) {
body52 := "## 完成定義\n- 逐項回報查核結果至此 issue;\n- 全數通過 → 回報 #39 並由 #39 assignee 關閉"
claim := "認領。我來執行四項驗收查核,完成後逐項回報結果至此。"
report := "## 全階段驗收查核結果(#52)\n**結論:四項全數通過。** 逐項如下…\n四項全過,符合「完成定義」。回報 #39:全數驗收通過,可由 @ceo 關閉 #39 與里程碑。"
reportFail := "## 查核結果\n結論:第 2 項不通過。已開立修復 issue #60,請 @ceo 確認分配。"
cases := []struct {
name string
issueBody string
comment string
want bool
}{
{"交付報告+回報#39+@ceo 關閉", body52, report, true},
{"失敗型交付(開修復 issue+請確認)", body52, reportFail, true},
{"認領留言(逐項+回報 但無交棒)", body52, claim, false},
{"純進度留言", body52, "進行中,已看完 P1,預計今晚交付。", false},
{"已完成但無交棒", body52, "已完成,程式碼都在分支上。", false},
{"無完成定義內文+逐項交棒", "一般 issue 內文", "逐項測試結果全數通過,請 @ceo 確認後關閉。", true},
{"無完成定義+僅交棒(無結構)", "一般 issue 內文", "請 @ceo 確認後關閉。", false},
{"空留言", body52, "", false},
}
for _, tc := range cases {
var last *Comment
if tc.comment != "" {
last = &Comment{Body: tc.comment}
}
if got := IsDeliveryReport(tc.issueBody, last); got != tc.want {
t.Errorf("%s:IsDeliveryReport = %v,want %v", tc.name, got, tc.want)
}
}
if IsDeliveryReport(body52, nil) {
t.Error("nil 最後留言應為 false")
}
}
// TestIsReviewHandoff(agents #51):審核交棒=@審核者帳號 ∧ 就緒語。
// 案例與 gitea.py _is_review_handoff 的離線驗證同集(fox#60 實例)。
func TestIsReviewHandoff(t *testing.T) {
// fox#60 作者(alex)最後留言結尾(2026-09-16 08:05)。
fox60 := "狀態回報(作者端自查,分支無需再變更):\n\n- 已重新核對分支與 main 的差異…\n- PR 分支 CI 已通過(run 209)。\n\n@chenyunda218 PR 已就緒,可審閱。@ceo 已知悉管理者巡邏的備註;審核合併後,我會在下一個 main 部署 run 跑通時回 #58 驗收。"
cases := []struct {
name string
comment string
reviewers []string
want bool
}{
{"fox#60 實例(@審核者+已就緒,可審閱)", fox60, []string{"chenyunda218"}, true},
{"多位審核者命中其一", fox60, []string{"ceo", "chenyunda218"}, true},
{"前綴碰撞(@chenyunda218 ≠ chenyunda)", fox60, []string{"chenyunda"}, false},
{"就緒語但無 @(無點名對象)", fox60, []string{"dan"}, false},
{"進度回報(無就緒語)", "進行中:已改完兩步,CI 跑著,@chenyunda218 稍後再看。", []string{"chenyunda218"}, false},
{"預告式(即將就緒不算)", "@chenyunda218 快好了,即將整理完成。", []string{"chenyunda218"}, false},
{"無審核者", fox60, nil, false},
{"空留言", "", []string{"chenyunda218"}, false},
}
for _, tc := range cases {
var last *Comment
if tc.comment != "" {
last = &Comment{Body: tc.comment}
}
if got := IsReviewHandoff(last, tc.reviewers); got != tc.want {
t.Errorf("%s:IsReviewHandoff = %v,want %v", tc.name, got, tc.want)
}
}
if IsReviewHandoff(nil, []string{"chenyunda218"}) {
t.Error("nil 最後留言應為 false")
}
}