Files
teai/internal/cli/stalled_commands_test.go
T

249 lines
9.1 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.
// 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)
}
}