internal/cli:允許命令旗標與位置參數交錯(#25)
parseFlags 改為先分離旗標與位置參數、再剖析旗標串、最後以第二次 Parse 把位置參數設回 fs.Args;Go 標準 flag 遇位置參數即停止的問題 不再影響「位置參數在前、旗標在後」的用法(teai api --method、 issues/pulls view --comments 等照 README 範例可用了)。 - login.go/workflow_commands.go 直接呼叫 fs.Parse 的六處改走 parseFlags,行為一致(login default/remove 的名稱也可與旗標交錯)。 - errHelp(命令 -h/--help)在 exitCodeFor 視為成功,與其既有註解 宣稱一致;命令層 -h 結束碼由 1/2 修正為 0。 - 新增 interleaved_flags_test.go 回歸測試:api/issues view/ pulls view/stalled/login default/login remove 的交錯組合。
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
// interleaved_flags_test.go — #25 的回歸測試:命令旗標放在位置參數
|
||||
// (path/編號/名稱)之後也必須被剖析,不得被當成位置參數。
|
||||
// 涵蓋 parseFlags 走的命令(issues/pulls view、api、stalled)與
|
||||
// 原本直接 fs.Parse 的命令(login default/remove、members)。
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestAPIFlagsAfterPath 驗證 #25 重現步驟一:teai api <path> --method POST
|
||||
// --data '{}' --yes 的旗標在 path 之後仍被剖析(POST 送達伺服器而非 exit 2)。
|
||||
func TestAPIFlagsAfterPath(t *testing.T) {
|
||||
var gotMethod string
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod = r.Method
|
||||
fmt.Fprint(w, `{}`)
|
||||
})
|
||||
_, _, code := run("api", "/version", "--method", "POST", "--data", `{}`, "--yes")
|
||||
if code != 0 {
|
||||
t.Fatalf("api 旗標在 path 之後應回 0(#25),得到 %d", code)
|
||||
}
|
||||
if gotMethod != "POST" {
|
||||
t.Fatalf("應送出 POST,得到 %s", gotMethod)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuesViewCommentsFlagAfterNumber 驗證 #25 重現步驟二:
|
||||
// teai issues view --repo owner/repo <編號> --comments 的 --comments
|
||||
// 在編號之後仍生效(有打 /comments 端點)。
|
||||
func TestIssuesViewCommentsFlagAfterNumber(t *testing.T) {
|
||||
var commentsHit bool
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/comments"):
|
||||
commentsHit = true
|
||||
fmt.Fprint(w, `[]`)
|
||||
case strings.HasSuffix(r.URL.Path, "/issues/8"):
|
||||
fmt.Fprint(w, `{"number":8,"title":"t","state":"open","user":{"login":"ceo"},"body":"","html_url":"u","labels":[],"updated_at":"t"}`)
|
||||
default:
|
||||
fmt.Fprint(w, `[]`)
|
||||
}
|
||||
})
|
||||
stdout, _, code := run("issues", "view", "--repo", "alterminal/teai", "8", "--comments")
|
||||
if code != 0 {
|
||||
t.Fatalf("issues view 編號在前旗標在後應回 0(#25),得到 %d", code)
|
||||
}
|
||||
if !commentsHit {
|
||||
t.Fatalf("--comments 在編號之後應觸發 /comments 請求;輸出:%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPullsViewCommentsFlagAfterNumber 同上,驗證 pulls view。
|
||||
func TestPullsViewCommentsFlagAfterNumber(t *testing.T) {
|
||||
var commentsHit bool
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/comments"):
|
||||
commentsHit = true
|
||||
fmt.Fprint(w, `[]`)
|
||||
case strings.HasSuffix(r.URL.Path, "/pulls/13"):
|
||||
fmt.Fprint(w, `{"number":13,"title":"t","state":"open","user":{"login":"ceo"},"body":"","html_url":"u","labels":[],"updated_at":"t"}`)
|
||||
default:
|
||||
fmt.Fprint(w, `[]`)
|
||||
}
|
||||
})
|
||||
_, _, code := run("pulls", "view", "--repo", "alterminal/teai", "13", "--comments")
|
||||
if code != 0 {
|
||||
t.Fatalf("pulls view 編號在前旗標在後應回 0(#25),得到 %d", code)
|
||||
}
|
||||
if !commentsHit {
|
||||
t.Fatal("--comments 在編號之後應觸發 /comments 請求")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStalledHoursFlagAfterPositional 驗證 stalled 即使混入位置參數
|
||||
// 也不誤剖析:--hours 在位置參數後仍被剖析,而 stalled 本身不接受
|
||||
// 位置參數 → 應回 2(用法錯誤)而非誤把 --hours 當位置參數。
|
||||
func TestStalledHoursFlagAfterPositional(t *testing.T) {
|
||||
run := opsTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `[]`)
|
||||
})
|
||||
_, stderr, code := run("stalled", "9", "--hours", "3")
|
||||
if code != 2 {
|
||||
t.Fatalf("stalled 帶位置參數應回 2,得到 %d", code)
|
||||
}
|
||||
if !strings.Contains(stderr, "位置參數") {
|
||||
t.Fatalf("錯誤訊息應提及位置參數,得到:%s", stderr)
|
||||
}
|
||||
// 對照組:--hours 值非法(剖析層錯誤)應回 2,而非把旗標當位置參數。
|
||||
_, stderr, code = run("stalled", "--hours", "abc")
|
||||
if code != 2 || !strings.Contains(stderr, "invalid value") {
|
||||
t.Fatalf("stalled --hours abc 應回 2 且提及 invalid value,得到 %d:%s", code, stderr)
|
||||
}
|
||||
// 交錯但無多餘位置參數:--hours 值不會被誤收。
|
||||
_, stderr, code = run("stalled", "--hours")
|
||||
if code != 2 || !strings.Contains(stderr, "flag needs an argument") {
|
||||
t.Fatalf("stalled --hours 缺值應回 2 且提及 flag needs an argument,得到 %d:%s", code, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoginDefaultNameOnly 驗證 login default <名稱>:位置參數在旗標
|
||||
// 之後(--config 在前)仍正確收取一個名稱。
|
||||
func TestLoginDefaultNameOnly(t *testing.T) {
|
||||
cfg := `logins:
|
||||
- name: a
|
||||
url: https://a.example.com
|
||||
token: ta
|
||||
default: true
|
||||
- name: b
|
||||
url: https://b.example.com
|
||||
token: tb
|
||||
default: false
|
||||
`
|
||||
prefix, _ := loginTestEnv(t, cfg)
|
||||
stdout, _, code := run(append(prefix, "login", "default", "b")...)
|
||||
if code != 0 || !strings.Contains(stdout, `"name":"b"`) {
|
||||
t.Fatalf("login default b 應回 0 並輸出 b:code=%d out=%s", code, stdout)
|
||||
}
|
||||
// 多給一個位置參數仍應報用法錯誤(不受交錯支援影響)。
|
||||
_, stderr, code := run(append(prefix, "login", "default", "b", "c")...)
|
||||
if code != 2 || !strings.Contains(stderr, "exactly one") {
|
||||
t.Fatalf("兩個名稱應回 2,得到 %d:%s", code, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMemberHelpExitZero 驗證命令的 -h 結束碼為 0(#25 順帶修正:
|
||||
// errHelp 視為成功)。
|
||||
func TestMemberHelpExitZero(t *testing.T) {
|
||||
_, _, code := run("members", "-h")
|
||||
if code != 0 {
|
||||
t.Fatalf("members -h 應回 0,得到 %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoginRemoveFlagInterleaved 驗證 login remove <名稱> --yes:
|
||||
// --yes 在名稱之後仍視為旗標(未帶時走確認流程會讀 stdin)。
|
||||
func TestLoginRemoveFlagInterleaved(t *testing.T) {
|
||||
prefix, configPath := loginTestEnv(t, `logins:
|
||||
- name: a
|
||||
url: https://a.example.com
|
||||
token: ta
|
||||
default: true
|
||||
`)
|
||||
old := loginStdin
|
||||
loginStdin = strings.NewReader("y\n") // 若 --yes 失效會讀到這行
|
||||
defer func() { loginStdin = old }()
|
||||
_, _, code := run(append(prefix, "login", "remove", "a", "--yes")...)
|
||||
if code != 0 {
|
||||
t.Fatalf("login remove a --yes 應回 0,得到 %d", code)
|
||||
}
|
||||
data, _ := os.ReadFile(configPath)
|
||||
if strings.Contains(string(data), "token: ta") {
|
||||
t.Fatalf("--yes 在名稱之後應生效(直接刪除):\n%s", data)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user