Files
teai/internal/cli/interleaved_flags_test.go
T
alex 3ed20b6aba 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 的交錯組合。
2026-09-10 15:35:16 +08:00

161 lines
6.0 KiB
Go
Raw 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.
// 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)
}
}