249 lines
9.1 KiB
Go
249 lines
9.1 KiB
Go
// stalled_commands_test.go — stalled/xrefs CLI 接線層測試(#7):
|
||
// 注入假 StalledSource,驗證 Run→dispatch→flagset 完整路徑,
|
||
// 不走網路(與 workflow_commands_test.go 同模式)。
|
||
package cli
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"gitea.alterminal.com/alterminal/teai/internal/gitea"
|
||
"gitea.alterminal.com/alterminal/teai/internal/workflow"
|
||
)
|
||
|
||
// stubStalledSource 以記憶體資料實作 workflow.StalledSource。
|
||
type stubStalledSource struct {
|
||
username string
|
||
orgs []string
|
||
repos map[string][]workflow.Repo
|
||
issues map[string]map[int64]workflow.Issue
|
||
comments map[string][]workflow.Comment
|
||
getErr error
|
||
}
|
||
|
||
func (s *stubStalledSource) CurrentUser() (string, error) { return s.username, nil }
|
||
func (s *stubStalledSource) MyOrganizations() ([]string, error) {
|
||
return s.orgs, nil
|
||
}
|
||
func (s *stubStalledSource) OrganizationRepositories(org string) ([]workflow.Repo, error) {
|
||
return s.repos[org], nil
|
||
}
|
||
func (s *stubStalledSource) OpenIssues(owner, repo string) ([]workflow.Issue, error) {
|
||
var out []workflow.Issue
|
||
for _, i := range s.issues[owner+"/"+repo] {
|
||
out = append(out, i)
|
||
}
|
||
return out, nil
|
||
}
|
||
func (s *stubStalledSource) OpenPulls(owner, repo string) ([]workflow.PullRequest, error) {
|
||
return nil, nil
|
||
}
|
||
func (s *stubStalledSource) IssueComments(owner, repo string, number int64) ([]workflow.Comment, error) {
|
||
return s.comments[owner+"/"+repo+"/"+itoa(number)], nil
|
||
}
|
||
func (s *stubStalledSource) TryGetIssue(owner, repo string, number int64) (workflow.Issue, bool, error) {
|
||
i, ok := s.issues[owner+"/"+repo][number]
|
||
return i, ok, nil
|
||
}
|
||
func (s *stubStalledSource) IssueTimeline(owner, repo string, number int64) ([]workflow.TimelineEvent, error) {
|
||
return nil, nil
|
||
}
|
||
func (s *stubStalledSource) GetIssue(owner, repo string, number int64) (workflow.Issue, error) {
|
||
if s.getErr != nil {
|
||
return workflow.Issue{}, s.getErr
|
||
}
|
||
i, ok := s.issues[owner+"/"+repo][number]
|
||
if !ok {
|
||
return workflow.Issue{}, &gitea.ErrAPI{Op: "GET /repos/" + owner + "/" + repo + "/issues/" + itoa(number), StatusCode: 404}
|
||
}
|
||
return i, nil
|
||
}
|
||
|
||
func itoa(n int64) string {
|
||
if n == 0 {
|
||
return "0"
|
||
}
|
||
var b []byte
|
||
for n > 0 {
|
||
b = append([]byte{byte('0' + n%10)}, b...)
|
||
n /= 10
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
// newStubStalledEnv 注入假資料源並固定時刻。
|
||
func newStubStalledEnv(t *testing.T, src *stubStalledSource, frozen time.Time) {
|
||
t.Helper()
|
||
origSrc, origNow := newStalledSourceFunc, nowFunc
|
||
newStalledSourceFunc = func(env *Env) (workflow.StalledSource, error) { return src, nil }
|
||
nowFunc = func() time.Time { return frozen }
|
||
t.Cleanup(func() { newStalledSourceFunc, nowFunc = origSrc, origNow })
|
||
}
|
||
|
||
// isoAt 把相對分鐘轉成 ISO8601 字串(假資料用;錨點同 workflow 測試)。
|
||
func isoAt(minutes int) string {
|
||
return workflow.FormatMoment(time.Date(2026, 9, 10, 8, 0, 0, 0, time.UTC).Add(time.Duration(minutes) * time.Minute))
|
||
}
|
||
|
||
// newStubStalledData 建構 stalled 情境:agents#1 停滯 10 小時(no-assignee)。
|
||
func newStubStalledData() *stubStalledSource {
|
||
return &stubStalledSource{
|
||
username: "ceo",
|
||
orgs: []string{"alterminal"},
|
||
repos: map[string][]workflow.Repo{"alterminal": {{Name: "agents"}}},
|
||
issues: map[string]map[int64]workflow.Issue{
|
||
"alterminal/agents": {
|
||
1: {Number: 1, Title: "停滯項", State: "open",
|
||
CreatedAt: isoAt(-600), UpdatedAt: isoAt(-600),
|
||
Repository: &workflow.Repository{FullName: "alterminal/agents"}},
|
||
},
|
||
},
|
||
comments: map[string][]workflow.Comment{},
|
||
}
|
||
}
|
||
|
||
// TestWiringStalled 驗證 stalled 基本路徑:JSON 輸出含停滯欄位。
|
||
func TestWiringStalled(t *testing.T) {
|
||
newStubStalledEnv(t, newStubStalledData(), time.Date(2026, 9, 10, 8, 0, 0, 0, time.UTC))
|
||
stdout, _, code := run("stalled")
|
||
if code != 0 {
|
||
t.Fatalf("stalled 應回 0,得到 %d(stdout=%q)", code, stdout)
|
||
}
|
||
for _, want := range []string{`"type": "issue"`, `"reason": "no-assignee"`, `"stalled_hours": 10.0`} {
|
||
if !strings.Contains(stdout, want) {
|
||
t.Fatalf("stalled 輸出應含 %q,得到:\n%s", want, stdout)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestWiringStalledEmpty 驗證 stalled 無任何條目時輸出 []、exit 0(#37)。
|
||
func TestWiringStalledEmpty(t *testing.T) {
|
||
newStubStalledEnv(t, &stubStalledSource{
|
||
username: "ceo",
|
||
orgs: []string{"alterminal"},
|
||
repos: map[string][]workflow.Repo{"alterminal": {}},
|
||
issues: map[string]map[int64]workflow.Issue{},
|
||
comments: map[string][]workflow.Comment{},
|
||
}, time.Date(2026, 9, 10, 8, 0, 0, 0, time.UTC))
|
||
stdout, _, code := run("stalled")
|
||
if code != 0 {
|
||
t.Fatalf("stalled 空情境應回 0,得到 %d(stdout=%q)", code, stdout)
|
||
}
|
||
if strings.TrimSpace(stdout) != "[]" {
|
||
t.Fatalf("stalled 無條目應輸出 [],得到 %q", stdout)
|
||
}
|
||
}
|
||
|
||
// TestWiringStalledHoursFlag 驗證 --hours 覆寫門檻與非正數被攔。
|
||
func TestWiringStalledHoursFlag(t *testing.T) {
|
||
freeze := time.Date(2026, 9, 10, 8, 0, 0, 0, time.UTC)
|
||
newStubStalledEnv(t, newStubStalledData(), freeze)
|
||
// 門檻 20h → 10h 停滯未過門檻 → 空陣列。
|
||
stdout, _, code := run("stalled", "--hours", "20")
|
||
if code != 0 {
|
||
t.Fatalf("stalled --hours 20 應回 0,得到 %d", code)
|
||
}
|
||
if strings.TrimSpace(stdout) != "[]" {
|
||
t.Fatalf("無停滯項應輸出 [],得到 %q", stdout)
|
||
}
|
||
// 非正數 → exit 2。
|
||
_, stderr, code := run("stalled", "--hours", "-1")
|
||
if code != int(ExitUsage) {
|
||
t.Fatalf("stalled --hours -1 應回 ExitUsage(%d),得到 %d(stderr=%q)", int(ExitUsage), code, stderr)
|
||
}
|
||
// 位置參數 → exit 2。
|
||
_, _, code = run("stalled", "extra")
|
||
if code != int(ExitUsage) {
|
||
t.Fatalf("stalled extra 應回 ExitUsage(%d),得到 %d", int(ExitUsage), code)
|
||
}
|
||
}
|
||
|
||
// TestWiringStalledTable 驗證 table 輸出(README 範例)。
|
||
func TestWiringStalledTable(t *testing.T) {
|
||
newStubStalledEnv(t, newStubStalledData(), time.Date(2026, 9, 10, 8, 0, 0, 0, time.UTC))
|
||
stdout, _, code := run("stalled", "--output", "table")
|
||
if code != 0 {
|
||
t.Fatalf("stalled --output table 應回 0,得到 %d", code)
|
||
}
|
||
for _, want := range []string{"reason", "no-assignee", "停滯項"} {
|
||
if !strings.Contains(stdout, want) {
|
||
t.Fatalf("table 輸出應含 %q,得到:\n%s", want, stdout)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestWiringXrefs 驗證 xrefs 解析輸出(含 body/comment 出處)。
|
||
func TestWiringXrefs(t *testing.T) {
|
||
src := newStubStalledData()
|
||
src.issues["alterminal/agents"][1] = workflow.Issue{
|
||
Number: 1, Title: "父項", State: "open",
|
||
Body: "子項:agents#2",
|
||
Repository: &workflow.Repository{FullName: "alterminal/agents"},
|
||
}
|
||
src.issues["alterminal/agents"][2] = workflow.Issue{
|
||
Number: 2, Title: "子項", State: "open",
|
||
Repository: &workflow.Repository{FullName: "alterminal/agents"},
|
||
}
|
||
src.comments["alterminal/agents/1"] = []workflow.Comment{
|
||
{Body: "看 agents#2", User: &workflow.User{UserName: "alex"}},
|
||
}
|
||
newStubStalledEnv(t, src, time.Date(2026, 9, 10, 8, 0, 0, 0, time.UTC))
|
||
stdout, _, code := run("xrefs", "alterminal/agents", "1")
|
||
if code != 0 {
|
||
t.Fatalf("xrefs 應回 0,得到 %d(stdout=%q)", code, stdout)
|
||
}
|
||
// Printer JSON 為單行緊湊格式(無縮排);命令清單類輸出一律如此。
|
||
for _, want := range []string{`"repo":"agents"`, `"number":2`, `"source":"body"`} {
|
||
if !strings.Contains(stdout, want) {
|
||
t.Fatalf("xrefs 輸出應含 %q,得到:\n%s", want, stdout)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestWiringXrefsEmpty 驗證無引用 → []。
|
||
func TestWiringXrefsEmpty(t *testing.T) {
|
||
src := newStubStalledData()
|
||
src.issues["alterminal/agents"][1] = workflow.Issue{
|
||
Number: 1, Title: "無引用", State: "open",
|
||
Body: "沒有任何引用",
|
||
Repository: &workflow.Repository{FullName: "alterminal/agents"},
|
||
}
|
||
newStubStalledEnv(t, src, time.Date(2026, 9, 10, 8, 0, 0, 0, time.UTC))
|
||
stdout, _, code := run("xrefs", "alterminal/agents", "1")
|
||
if code != 0 {
|
||
t.Fatalf("xrefs 應回 0,得到 %d", code)
|
||
}
|
||
if strings.TrimSpace(stdout) != "[]" {
|
||
t.Fatalf("無引用應輸出 [],得到 %q", stdout)
|
||
}
|
||
}
|
||
|
||
// TestWiringXrefsUsage 驗證參數不足/格式錯誤 → exit 2。
|
||
func TestWiringXrefsUsage(t *testing.T) {
|
||
newStubStalledEnv(t, newStubStalledData(), time.Date(2026, 9, 10, 8, 0, 0, 0, time.UTC))
|
||
if _, _, code := run("xrefs"); code != int(ExitUsage) {
|
||
t.Fatalf("xrefs 無參數應回 ExitUsage,得到 %d", code)
|
||
}
|
||
if _, _, code := run("xrefs", "alterminal/agents"); code != int(ExitUsage) {
|
||
t.Fatalf("xrefs 缺編號應回 ExitUsage,得到 %d", code)
|
||
}
|
||
if _, _, code := run("xrefs", "not-a-repo", "1"); code != int(ExitUsage) {
|
||
t.Fatalf("xrefs repo 格式錯誤應回 ExitUsage,得到 %d", code)
|
||
}
|
||
if _, _, code := run("xrefs", "alterminal/agents", "abc"); code != int(ExitUsage) {
|
||
t.Fatalf("xrefs 編號非數字應回 ExitUsage,得到 %d", code)
|
||
}
|
||
}
|
||
|
||
// TestWiringXrefsNotFound 驗證 404 → exit 3(API 錯誤)。
|
||
func TestWiringXrefsNotFound(t *testing.T) {
|
||
src := newStubStalledData()
|
||
src.getErr = &gitea.ErrAPI{Op: "GET /repos/alterminal/agents/issues/99", StatusCode: 404}
|
||
newStubStalledEnv(t, src, time.Date(2026, 9, 10, 8, 0, 0, 0, time.UTC))
|
||
_, stderr, code := run("xrefs", "alterminal/agents", "99")
|
||
if code != int(ExitAPI) {
|
||
t.Fatalf("xrefs 404 應回 ExitAPI(%d),得到 %d(stderr=%q)", int(ExitAPI), code, stderr)
|
||
}
|
||
}
|