290 lines
12 KiB
Go
290 lines
12 KiB
Go
// 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 於需要時)
|