// 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 --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) } }