// workflow_commands_test.go — CLI 接線層測試:驗證 Run→dispatch→命令 flagset // 的完整路徑。ceo 於 PR #13 審核指出帶旗標命令(members --has-work、 // pulls --mine/--reviewer)在 CLI 層全部 exit 2,workflow 單元測試覆蓋不到 // 這條路徑——本檔以注入假 client 的方式補上,不走網路。 package cli import ( "strings" "testing" "gitea.alterminal.com/alterminal/teai/internal/workflow" ) // stubClient 以記憶體資料實作 workflow.Client(最小集,僅供接線測試)。 type stubClient struct { currentUser string orgs []workflow.Org teams map[string][]workflow.Team members map[int64][]string repos map[string][]workflow.Repo issues map[string][]workflow.Issue pulls map[string][]workflow.PullRequest comments map[string][]workflow.Comment } func (c *stubClient) CurrentUser() (string, error) { return c.currentUser, nil } func (c *stubClient) ListOrgs() ([]string, error) { out := make([]string, 0, len(c.orgs)) for _, o := range c.orgs { out = append(out, o.Account()) } return out, nil } func (c *stubClient) ListTeams(org string) ([]workflow.Team, error) { return c.teams[org], nil } func (c *stubClient) ListTeamMembers(teamID int64) ([]string, error) { return c.members[teamID], nil } func (c *stubClient) ListOrgRepos(org string) ([]workflow.Repo, error) { return c.repos[org], nil } func (c *stubClient) ListIssues(owner, repo string) ([]workflow.Issue, error) { return c.issues[owner+"/"+repo], nil } func (c *stubClient) ListPulls(owner, repo string) ([]workflow.PullRequest, error) { return c.pulls[owner+"/"+repo], nil } func (c *stubClient) ListComments(owner, repo string, number int64) ([]workflow.Comment, error) { return c.comments[owner+"/"+repo], nil } // newStubEnv 注入假 client,回傳還原函式(測試結束時呼叫)。 func newStubEnv(t *testing.T, c *stubClient) { t.Helper() orig := newScannerFunc newScannerFunc = func(env *Env) (*workflow.Scanner, error) { return &workflow.Scanner{Client: c}, nil } t.Cleanup(func() { newScannerFunc = orig }) } // newStubData 建構標準情境:alterminal 組織、Agents 團隊、teai 倉庫。 func newStubData() *stubClient { return &stubClient{ currentUser: "alex", orgs: []workflow.Org{{UserName: "alterminal"}}, teams: map[string][]workflow.Team{"alterminal": {{ID: 1, Name: "Agents"}}}, members: map[int64][]string{1: {"alex", "max", "queena"}}, repos: map[string][]workflow.Repo{"alterminal": {{Name: "teai"}}}, issues: map[string][]workflow.Issue{}, pulls: map[string][]workflow.PullRequest{}, comments: map[string][]workflow.Comment{}, } } // TestWiringMembersHasWork 驗證 members --has-work 不再被攔成 exit 2。 func TestWiringMembersHasWork(t *testing.T) { newStubEnv(t, newStubData()) stdout, _, code := run("members", "--has-work") if code != 0 { t.Fatalf("members --has-work 應回 0,得到 %d(stdout=%q)", code, stdout) } // 無未完成工作 → 空輸出(每行一個帳號,無帳號即無輸出)。 if strings.TrimSpace(stdout) != "" { t.Fatalf("無工作時 members --has-work 應無輸出,得到 %q", stdout) } } // TestWiringPullsMine 驗證 pulls --mine 不再被攔成 exit 2。 func TestWiringPullsMine(t *testing.T) { newStubEnv(t, newStubData()) stdout, _, code := run("pulls", "--mine") if code != 0 { t.Fatalf("pulls --mine 應回 0,得到 %d(stdout=%q)", code, stdout) } if strings.TrimSpace(stdout) != "[]" { t.Fatalf("pulls --mine 空清單應輸出 [],得到 %q", stdout) } } // TestWiringPullsReviewer 驗證 pulls --reviewer 不再被攔成 exit 2。 func TestWiringPullsReviewer(t *testing.T) { newStubEnv(t, newStubData()) stdout, _, code := run("pulls", "--reviewer") if code != 0 { t.Fatalf("pulls --reviewer 應回 0,得到 %d(stdout=%q)", code, stdout) } if strings.TrimSpace(stdout) != "[]" { t.Fatalf("pulls --reviewer 空清單應輸出 [],得到 %q", stdout) } } // TestWiringPullsRepoFilter 驗證 --repo 旗標可解析且值可帶斜線。 func TestWiringPullsRepoFilter(t *testing.T) { newStubEnv(t, newStubData()) _, _, code := run("pulls", "--repo", "alterminal/teai") if code != 0 { t.Fatalf("pulls --repo alterminal/teai 應回 0,得到 %d", code) } // --repo 值格式錯誤 → exit 2。 _, _, code = run("pulls", "--repo", "not-a-repo") if code != int(ExitUsage) { t.Fatalf("pulls --repo not-a-repo 應回 ExitUsage(%d),得到 %d", int(ExitUsage), code) } } // TestWiringGlobalAfterCommandFlag 驗證命令旗標與全域選項交錯出現。 func TestWiringGlobalAfterCommandFlag(t *testing.T) { newStubEnv(t, newStubData()) // members --has-work --output table:命令旗標在前、全域選項在後。 stdout, _, code := run("members", "--has-work", "--output", "table") if code != 0 { t.Fatalf("members --has-work --output table 應回 0,得到 %d", code) } if strings.Contains(stdout, "--has-work") { t.Fatalf("全域剖析不應把 --has-work 當成全域選項處理:%q", stdout) } } // TestWiringMembersAll 驗證 members 不帶旗標仍正常(回歸保護)。 func TestWiringMembersAll(t *testing.T) { newStubEnv(t, newStubData()) stdout, _, code := run("members") if code != 0 { t.Fatalf("members 應回 0,得到 %d", code) } for _, want := range []string{"alex", "max", "queena"} { if !strings.Contains(stdout, want) { t.Fatalf("members 輸出應含 %q,得到 %q", want, stdout) } } } // TestWiringMine 驗證 mine 空清單輸出 [](與 gitea.py 空輸出的差異見 README)。 func TestWiringMine(t *testing.T) { newStubEnv(t, newStubData()) stdout, _, code := run("mine") if code != 0 { t.Fatalf("mine 應回 0,得到 %d", code) } if strings.TrimSpace(stdout) != "[]" { t.Fatalf("mine 空清單應輸出 [],得到 %q", stdout) } } // TestWiringNextNull 驗證 next 無工作輸出 null、exit 0。 func TestWiringNextNull(t *testing.T) { newStubEnv(t, newStubData()) stdout, _, code := run("next") if code != 0 { t.Fatalf("next 應回 0,得到 %d", code) } if strings.TrimSpace(stdout) != "null" { t.Fatalf("next 無工作應輸出 null,得到 %q", stdout) } } // TestWiringNextTable 驗證 teai next --output table(README 範例)。 func TestWiringNextTable(t *testing.T) { newStubEnv(t, newStubData()) stdout, _, code := run("next", "--output", "table") if code != 0 { t.Fatalf("next --output table 應回 0,得到 %d", code) } if !strings.Contains(stdout, "(none)") { t.Fatalf("next --output table 無工作應輸出 (none),得到 %q", stdout) } } // TestWiringNextWithPull 驗證有工作時 next 輸出 JSON 物件(role=author)。 func TestWiringNextWithPull(t *testing.T) { c := newStubData() c.pulls["alterminal/teai"] = []workflow.PullRequest{{ Number: 13, Title: "gitea.py 工作流命令 Go 化", User: &workflow.User{UserName: "alex"}, }} c.comments["alterminal/teai"] = []workflow.Comment{ {CreatedAt: "2026-09-10T00:44:00Z", User: &workflow.User{UserName: "ceo"}}, } newStubEnv(t, c) stdout, _, code := run("next") if code != 0 { t.Fatalf("next 應回 0,得到 %d", code) } for _, want := range []string{`"type":"pull"`, `"role":"author"`, `"number":13`} { if !strings.Contains(stdout, want) { t.Fatalf("next 輸出應含 %q,得到 %q", want, stdout) } } } // TestWiringWhoamiOrgs 驗證 whoami/orgs 的接線輸出。 func TestWiringWhoamiOrgs(t *testing.T) { newStubEnv(t, newStubData()) stdout, _, code := run("whoami") if code != 0 || !strings.Contains(stdout, "alex") { t.Fatalf("whoami code=%d stdout=%q", code, stdout) } stdout, _, code = run("orgs") if code != 0 || !strings.Contains(stdout, "alterminal") { t.Fatalf("orgs code=%d stdout=%q", code, stdout) } } // TestWiringVersionRejectsArgs 驗證 version 不接受參數(新剖析語義下仍 exit 2)。 func TestWiringVersionRejectsArgs(t *testing.T) { _, _, code := run("version", "--wat") if code != int(ExitUsage) { t.Fatalf("version --wat 應回 ExitUsage(%d),得到 %d", int(ExitUsage), code) } }