dailyops:補齊 tea 對等指令(#40)— pulls approve/close/checkout、通用 comment、issues list --labels
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
// dailyops_parity_commands_test.go — #40 的驗收測試:補齊 tea 對等指令
|
||||
// pulls approve/close/checkout、通用 comment、issues list --labels 過濾。
|
||||
// 寫入命令未帶 --yes 不得發出任何請求;帶 --yes 時打對端點與 payload。
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestParityWriteCommandsRequireYes 驗證新增寫入命令未帶 --yes:
|
||||
// 回 2 且不發請求(#40 驗收:寫入閘門慣例)。
|
||||
func TestParityWriteCommandsRequireYes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"pulls approve", []string{"pulls", "approve", "--repo", "a/b", "1"}},
|
||||
{"pulls close", []string{"pulls", "close", "--repo", "a/b", "1"}},
|
||||
{"comment direct", []string{"comment", "--repo", "a/b", "1", "內容"}},
|
||||
{"comment direct body flag", []string{"comment", "--repo", "a/b", "--body", "內容", "1"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Errorf("%s:未帶 --yes 不得發出請求(%s %s)", tc.name, r.Method, r.URL.Path)
|
||||
})
|
||||
_, stderr, code := run(tc.args...)
|
||||
if code != 2 {
|
||||
t.Errorf("%s 未帶 --yes 應回 2,得到 %d", tc.name, code)
|
||||
}
|
||||
if !strings.Contains(stderr, "--yes") {
|
||||
t.Errorf("%s 錯誤訊息應提及 --yes,得到:%s", tc.name, stderr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPullsApprovePostsReview 驗證 pulls approve --yes:
|
||||
// POST /repos/{owner}/{repo}/pulls/{n}/reviews,payload 帶 event=APPROVED 與意見;
|
||||
// 意見可用位置參數(tea 相容:teai pulls approve --repo a/b <n> "驗證通過。")。
|
||||
func TestPullsApprovePostsReview(t *testing.T) {
|
||||
var gotMethod, gotPath, gotBody string
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod, gotPath = r.Method, r.URL.Path
|
||||
buf := make([]byte, 512)
|
||||
n, _ := r.Body.Read(buf)
|
||||
gotBody = string(buf[:n])
|
||||
fmt.Fprint(w, `{"ID":42,"number":13,"state":"APPROVED","body":"驗證通過。","submitted_at":"t","user":{"login":"max"}}`)
|
||||
})
|
||||
stdout, _, code := run("pulls", "approve", "--repo", "alterminal/teai", "--yes", "13", "驗證通過。")
|
||||
if code != 0 {
|
||||
t.Fatalf("approve --yes 應回 0,得到 %d", code)
|
||||
}
|
||||
if gotMethod != "POST" || !strings.HasSuffix(gotPath, "/repos/alterminal/teai/pulls/13/reviews") {
|
||||
t.Errorf("應 POST /repos/alterminal/teai/pulls/13/reviews,得到 %s %s", gotMethod, gotPath)
|
||||
}
|
||||
if !strings.Contains(gotBody, `"event":"APPROVED"`) || !strings.Contains(gotBody, "驗證通過。") {
|
||||
t.Errorf("body 應含 event=APPROVED 與意見,得到:%s", gotBody)
|
||||
}
|
||||
if !strings.Contains(stdout, `"state":"APPROVED"`) {
|
||||
t.Errorf("輸出應含 APPROVED,得到:%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPullsClosePatchesState 驗證 pulls close --yes:
|
||||
// PATCH /repos/{owner}/{repo}/pulls/{n},payload 帶 state=closed。
|
||||
func TestPullsClosePatchesState(t *testing.T) {
|
||||
var gotMethod, gotPath, gotBody string
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod, gotPath = r.Method, r.URL.Path
|
||||
buf := make([]byte, 512)
|
||||
n, _ := r.Body.Read(buf)
|
||||
gotBody = string(buf[:n])
|
||||
fmt.Fprint(w, `{"number":13,"title":"PR","state":"closed","html_url":"https://x/13"}`)
|
||||
})
|
||||
_, _, code := run("pulls", "close", "--repo", "alterminal/teai", "--yes", "13")
|
||||
if code != 0 {
|
||||
t.Fatalf("close --yes 應回 0,得到 %d", code)
|
||||
}
|
||||
if gotMethod != "PATCH" || !strings.HasSuffix(gotPath, "/repos/alterminal/teai/pulls/13") {
|
||||
t.Errorf("應 PATCH /repos/alterminal/teai/pulls/13,得到 %s %s", gotMethod, gotPath)
|
||||
}
|
||||
if !strings.Contains(gotBody, `"state":"closed"`) {
|
||||
t.Errorf("body 應含 state=closed,得到:%s", gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommentDirectPostsComment 驗證通用 comment(tea 相容形式):
|
||||
// --repo a/b <number> <body> --yes → POST issues/<n>/comments,
|
||||
// issue 與 PR 共用(不需要 type 旗標)。
|
||||
func TestCommentDirectPostsComment(t *testing.T) {
|
||||
var gotMethod, gotPath, gotBody string
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod, gotPath = r.Method, r.URL.Path
|
||||
buf := make([]byte, 512)
|
||||
n, _ := r.Body.Read(buf)
|
||||
gotBody = string(buf[:n])
|
||||
fmt.Fprint(w, `{"body":"已依評論更新:commit abc1234。","created_at":"t","user":{"login":"max"}}`)
|
||||
})
|
||||
stdout, _, code := run("comment", "--repo", "alterminal/teai", "--yes", "13", "已依評論更新:commit abc1234。")
|
||||
if code != 0 {
|
||||
t.Fatalf("comment --yes 應回 0,得到 %d", code)
|
||||
}
|
||||
if gotMethod != "POST" || !strings.HasSuffix(gotPath, "/repos/alterminal/teai/issues/13/comments") {
|
||||
t.Errorf("應 POST /repos/alterminal/teai/issues/13/comments,得到 %s %s", gotMethod, gotPath)
|
||||
}
|
||||
if !strings.Contains(gotBody, "已依評論更新:commit abc1234。") {
|
||||
t.Errorf("body 應含留言內容,得到:%s", gotBody)
|
||||
}
|
||||
if !strings.Contains(stdout, "已依評論更新") {
|
||||
t.Errorf("輽出應含留言,得到:%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommentDirectBodyFlag 驗證 comment 也接受 --body(teai 慣用),
|
||||
// 且位置參數與 --body 同時存在時位置參數優先。
|
||||
func TestCommentDirectBodyFlag(t *testing.T) {
|
||||
var gotBody string
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
buf := make([]byte, 512)
|
||||
n, _ := r.Body.Read(buf)
|
||||
gotBody = string(buf[:n])
|
||||
fmt.Fprint(w, `{"body":"x","created_at":"t","user":{"login":"max"}}`)
|
||||
})
|
||||
if _, _, code := run("comment", "--repo", "a/b", "--body", "旗標版", "--yes", "5"); code != 0 {
|
||||
t.Fatalf("--body 版應回 0,得到 %d", code)
|
||||
}
|
||||
if !strings.Contains(gotBody, "旗標版") {
|
||||
t.Errorf("body 應為 --body 值,得到:%s", gotBody)
|
||||
}
|
||||
if _, _, code := run("comment", "--repo", "a/b", "--body", "旗標版", "--yes", "5", "位置版"); code != 0 {
|
||||
t.Fatalf("位置參數版應回 0,得到 %d", code)
|
||||
}
|
||||
if !strings.Contains(gotBody, "位置版") || strings.Contains(gotBody, "旗標版") {
|
||||
t.Errorf("位置參數應優先於 --body,得到:%s", gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuesListLabelsFilter 驗證 issues list --labels:
|
||||
// 查詢參數帶 labels=a,b(多標籤 AND 語義),輸出仍為 JSON 陣列。
|
||||
func TestIssuesListLabelsFilter(t *testing.T) {
|
||||
var gotQuery string
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
gotQuery = r.URL.RawQuery
|
||||
fmt.Fprint(w, `[{"number":9,"title":"标签過濾","state":"open","user":{"login":"ceo"},"updated_at":"t"}]`)
|
||||
})
|
||||
stdout, _, code := run("issues", "list", "--repo", "alterminal/teai", "--labels", "bug, help wanted")
|
||||
if code != 0 {
|
||||
t.Fatalf("--labels 應回 0,得到 %d", code)
|
||||
}
|
||||
if !strings.Contains(gotQuery, "labels=bug%2Chelp+wanted") && !strings.Contains(gotQuery, "labels=bug%2C+help+wanted") {
|
||||
t.Errorf("查詢應帶 labels=bug,help+wanted(URL 編碼後),得到 %q", gotQuery)
|
||||
}
|
||||
if !strings.Contains(stdout, `"number":9`) {
|
||||
t.Errorf("輽出應含過濾後的 issue,得到:%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPullsCheckoutSameRepo 驗證 checkout 同倉庫分支:
|
||||
// 在 --dir 的 git 工作樹 fetch origin <ref> 並 checkout -B <ref>。
|
||||
func TestPullsCheckoutSameRepo(t *testing.T) {
|
||||
// 建立裸「origin」倉庫與分支 feat/x。
|
||||
origin := t.TempDir()
|
||||
gitRun := func(args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = origin
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %v 失敗:%v\n%s", args, err, out)
|
||||
}
|
||||
}
|
||||
gitRun("init", "-q", "-b", "main", ".")
|
||||
gitRun("config", "user.email", "t@t")
|
||||
gitRun("config", "user.name", "t")
|
||||
gitRun("commit", "--allow-empty", "-m", "init")
|
||||
gitRun("checkout", "-q", "-b", "feat/x")
|
||||
gitRun("commit", "--allow-empty", "-m", "x1")
|
||||
gitRun("checkout", "-q", "main")
|
||||
// 工作樹 clone 自 origin(file:// 協定讓 fetch 生效)。
|
||||
work := t.TempDir()
|
||||
if out, err := exec.Command("git", "clone", "-q", "file://"+origin, work).CombinedOutput(); err != nil {
|
||||
t.Fatalf("clone 失敗:%v\n%s", err, out)
|
||||
}
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `{"number":7,"title":"PR","state":"open","head":{"ref":"feat/x","repo":{"full_name":"alterminal/teai","clone_url":"https://gitea.alterminal.com/alterminal/teai.git"}},"base":{"ref":"main"}}`)
|
||||
})
|
||||
stdout, _, code := run("pulls", "checkout", "--repo", "alterminal/teai", "--dir", work, "7")
|
||||
if code != 0 {
|
||||
t.Fatalf("checkout 應回 0,得到 %d", code)
|
||||
}
|
||||
brOut, err := exec.Command("git", "-C", work, "branch", "--show-current").Output()
|
||||
if err != nil || strings.TrimSpace(string(brOut)) != "feat/x" {
|
||||
t.Errorf("工作樹應在 feat/x,得到 %q(err=%v)", string(brOut), err)
|
||||
}
|
||||
if !strings.Contains(stdout, `"branch":"feat/x"`) {
|
||||
t.Errorf("輽出應含 branch 欄位,得到:%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPullsCheckoutCrossRepo 驗證 checkout 跨倉庫 head:
|
||||
// 需 --dir(新目錄),git clone --branch <ref> <clone_url> <dir>。
|
||||
func TestPullsCheckoutCrossRepo(t *testing.T) {
|
||||
// 假伺服器同時當 git remote:把 PR head 指向本地裸倉庫。
|
||||
origin := t.TempDir()
|
||||
gitRun := func(args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = origin
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %v 失敗:%v\n%s", args, err, out)
|
||||
}
|
||||
}
|
||||
gitRun("init", "-q", "-b", "main", "--bare", ".")
|
||||
seed := t.TempDir()
|
||||
cmd := exec.Command("git", "clone", "-q", "file://"+origin, seed)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("clone seed 失敗:%v\n%s", err, out)
|
||||
}
|
||||
for _, args := range [][]string{
|
||||
{"config", "user.email", "t@t"},
|
||||
{"config", "user.name", "t"},
|
||||
{"commit", "--allow-empty", "-m", "init"},
|
||||
{"checkout", "-q", "-b", "feat/y"},
|
||||
{"commit", "--allow-empty", "-m", "y1"},
|
||||
{"push", "-q", "origin", "feat/y"},
|
||||
} {
|
||||
c := exec.Command("git", args...)
|
||||
c.Dir = seed
|
||||
if out, err := c.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %v 失敗:%v\n%s", args, err, out)
|
||||
}
|
||||
}
|
||||
target := filepath.Join(t.TempDir(), "wk")
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, `{"number":8,"title":"PR","state":"open","head":{"ref":"feat/y","repo":{"full_name":"other/fork","clone_url":"file://%s"}},"base":{"ref":"main"}}`, origin)
|
||||
})
|
||||
stdout, _, code := run("pulls", "checkout", "--repo", "alterminal/teai", "--dir", target, "8")
|
||||
if code != 0 {
|
||||
t.Fatalf("checkout 跨倉庫應回 0,得到 %d", code)
|
||||
}
|
||||
brOut, err := exec.Command("git", "-C", target, "branch", "--show-current").Output()
|
||||
if err != nil || strings.TrimSpace(string(brOut)) != "feat/y" {
|
||||
t.Errorf("工作樹應在 feat/y,得到 %q(err=%v)", string(brOut), err)
|
||||
}
|
||||
if !strings.Contains(stdout, `"repo":"other/fork"`) {
|
||||
t.Errorf("輽出應含 head 倉庫,得到:%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPullsCheckoutCrossRepoRequiresDir 驗證跨倉庫 head 未帶 --dir:
|
||||
// 用法錯誤(exit 2)。
|
||||
func TestPullsCheckoutCrossRepoRequiresDir(t *testing.T) {
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `{"number":8,"title":"PR","state":"open","head":{"ref":"feat/y","repo":{"full_name":"other/fork","clone_url":"https://x/y.git"}},"base":{"ref":"main"}}`)
|
||||
})
|
||||
_, stderr, code := run("pulls", "checkout", "--repo", "alterminal/teai", "8")
|
||||
if code != 2 {
|
||||
t.Errorf("跨倉庫 head 未帶 --dir 應回 2,得到 %d", code)
|
||||
}
|
||||
if !strings.Contains(stderr, "--dir") {
|
||||
t.Errorf("錯誤訊息應提及 --dir,得到:%s", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuesCommentPositionalBody 驗證 issues comment 也吃位置參數留言
|
||||
// (tea comment 相容语法在子命令下的行為)。
|
||||
func TestIssuesCommentPositionalBody(t *testing.T) {
|
||||
var gotBody string
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
buf := make([]byte, 512)
|
||||
n, _ := r.Body.Read(buf)
|
||||
gotBody = string(buf[:n])
|
||||
fmt.Fprint(w, `{"body":"x","created_at":"t","user":{"login":"max"}}`)
|
||||
})
|
||||
if _, _, code := run("issues", "comment", "--repo", "a/b", "--yes", "3", "多餘", "引數", "串留言"); code != 0 {
|
||||
t.Fatalf("issues comment 位置參數應回 0,得到 %d", code)
|
||||
}
|
||||
if !strings.Contains(gotBody, "多餘 引數 串留言") {
|
||||
t.Errorf("尾端引數應串成留言,得到:%s", gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
var _ = os.Getenv // 保持 import 一致(checkout 測試用 exec,此檔亦引用 os 於需要時)
|
||||
Reference in New Issue
Block a user