Files
teai/internal/cli/workflow_commands_test.go
2026-09-14 16:13:02 +08:00

299 lines
10 KiB
Go
Raw Permalink 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.
// 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)
}
}
// TestWiringRoleWorker 驗證 Agents 成員的 role 輸出 worker。
func TestWiringRoleWorker(t *testing.T) {
newStubEnv(t, newStubData())
stdout, _, code := run("role")
if code != 0 {
t.Fatalf("role 應回 0,得到 %d(stdout=%q)", code, stdout)
}
if strings.TrimSpace(stdout) != `{"role":"worker"}` {
t.Fatalf("Agents 成員 role 應輸出 worker,得到 %q", stdout)
}
}
// TestWiringRoleSupervisor 驗證 Supervisor 成員的 role 輸出 supervisor。
func TestWiringRoleSupervisor(t *testing.T) {
c := newStubData()
c.teams["alterminal"] = []workflow.Team{
{ID: 1, Name: "Agents"},
{ID: 2, Name: "Supervisor"},
}
c.members[2] = []string{"alex"}
newStubEnv(t, c)
stdout, _, code := run("role")
if code != 0 {
t.Fatalf("role 應回 0,得到 %d(stdout=%q)", code, stdout)
}
if strings.TrimSpace(stdout) != `{"role":"supervisor"}` {
t.Fatalf("Supervisor 成員 role 應輸出 supervisor,得到 %q", stdout)
}
}
// TestWiringRoleNone 驗證不在任一角色團隊時輸出 null、exit 0。
func TestWiringRoleNone(t *testing.T) {
c := newStubData()
c.currentUser = "outsider"
newStubEnv(t, c)
stdout, _, code := run("role")
if code != 0 {
t.Fatalf("role 無角色應回 0,得到 %d", code)
}
if strings.TrimSpace(stdout) != "null" {
t.Fatalf("role 無角色應輸出 null,得到 %q", stdout)
}
}
// TestWiringRoleRejectsArgs 驗證 role 不接受參數。
func TestWiringRoleRejectsArgs(t *testing.T) {
newStubEnv(t, newStubData())
_, _, code := run("role", "extra")
if code != int(ExitUsage) {
t.Fatalf("role extra 應回 ExitUsage(%d),得到 %d", int(ExitUsage), code)
}
}
// TestWiringRoleTable 驗證 teai role --output table。
func TestWiringRoleTable(t *testing.T) {
newStubEnv(t, newStubData())
stdout, _, code := run("role", "--output", "table")
if code != 0 {
t.Fatalf("role --output table 應回 0,得到 %d", code)
}
if !strings.Contains(stdout, "worker") {
t.Fatalf("role --output table 應含 worker,得到 %q", 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)
}
}