// exitcode_test.go 驗證結束碼映射:用法錯誤 → 2、API 錯誤 → 3、其他 → 1, // 以及全域選項剖析(README「輸出與結束碼」「全域介面」)。 package cli import ( "strings" "testing" "gitea.alterminal.com/alterminal/teai/internal/gitea" ) func TestExitCodeMapping(t *testing.T) { cases := []struct { name string err error want ExitCode }{ {"usage error", &ErrUsage{Msg: "bad flag"}, ExitUsage}, {"api error 401", &gitea.ErrAPI{StatusCode: 401, Op: "GET /api/v1/user"}, ExitAPI}, {"api error transport", &gitea.ErrAPI{Op: "GET /api/v1/user", Err: errString("dial tcp")}, ExitAPI}, {"wrapped api error", wrapped(&gitea.ErrAPI{StatusCode: 500}), ExitAPI}, {"other error", errString("boom"), ExitInternal}, {"nil maps to OK", nil, ExitOK}, } for _, tc := range cases { if got := exitCodeFor(tc.err); got != tc.want { t.Errorf("%s: exitCodeFor = %d, want %d", tc.name, got, tc.want) } } } func TestRunExitCodes(t *testing.T) { t.Run("unknown command exits 2", func(t *testing.T) { _, _, code := run("no-such") if code != 2 { t.Fatalf("code = %d, want 2", code) } }) t.Run("unknown global option exits 2", func(t *testing.T) { _, _, code := run("--wat", "version") if code != 2 { t.Fatalf("code = %d, want 2", code) } }) t.Run("missing option value exits 2", func(t *testing.T) { _, _, code := run("--output") if code != 2 { t.Fatalf("code = %d, want 2", code) } }) t.Run("invalid output value exits 2", func(t *testing.T) { _, _, code := run("--output=yaml", "version") if code != 2 { t.Fatalf("code = %d, want 2", code) } }) t.Run("invalid timeout exits 2", func(t *testing.T) { _, _, code := run("--timeout=soon", "version") if code != 2 { t.Fatalf("code = %d, want 2", code) } }) t.Run("version still works with globals", func(t *testing.T) { stdout, _, code := run("--output=table", "--timeout=10s", "version") if code != 0 || !strings.Contains(stdout, "teai version ") { t.Fatalf("code = %d stdout = %q", code, stdout) } }) } func TestExtractGlobals(t *testing.T) { g := defaultGlobals() rest, err := extractGlobals(&g, []string{ "--url", "https://example.com", "--token=tok", "--config", "/tmp/c.yml", "--output", "table", "--timeout", "45s", "next", }) if err != nil { t.Fatal(err) } if len(rest) != 1 || rest[0] != "next" { t.Fatalf("rest = %v, want [next]", rest) } if g.URL != "https://example.com" || g.Token != "tok" || g.ConfigPath != "/tmp/c.yml" { t.Fatalf("globals = %+v", g) } if g.Output != gitea.FormatTable { t.Fatalf("output = %v, want table", g.Output) } if g.Timeout.String() != "45s" { t.Fatalf("timeout = %s", g.Timeout) } } func TestExtractGlobalsAfterCommand(t *testing.T) { // 全域選項出現在命令之後也應抽出(README:teai next --output table)。 g := defaultGlobals() rest, err := extractGlobals(&g, []string{"version", "--output", "table"}) if err != nil { t.Fatal(err) } if len(rest) != 1 || rest[0] != "version" { t.Fatalf("rest = %v, want [version]", rest) } if g.Output != gitea.FormatTable { t.Fatalf("output = %v, want table", g.Output) } } func TestExtractGlobalsKeepsCommandFlags(t *testing.T) { // 非全域選項屬於子命令:原樣依序交回,不能攔成 ErrUsage。 cases := [][]string{ {"members", "--has-work"}, {"pulls", "--mine"}, {"pulls", "--reviewer"}, {"pulls", "--repo", "alterminal/teai"}, {"members", "--has-work", "--output", "table"}, {"pulls", "--mine", "--repo", "alterminal/teai", "--output", "table"}, } for _, args := range cases { g := defaultGlobals() rest, err := extractGlobals(&g, args) if err != nil { t.Fatalf("extractGlobals(%v) 不應報錯:%v", args, err) } want := args if strings.HasSuffix(strings.Join(args, " "), "--output table") { want = args[:len(args)-2] } if len(rest) != len(want) { t.Fatalf("extractGlobals(%v) rest = %v, want %v", args, rest, want) } for i := range want { if rest[i] != want[i] { t.Fatalf("extractGlobals(%v) rest = %v, want %v", args, rest, want) } } } } func TestExtractGlobalsDoubleDashStops(t *testing.T) { g := defaultGlobals() rest, err := extractGlobals(&g, []string{"--output", "table", "--", "--url", "x"}) if err != nil { t.Fatal(err) } // -- 之後全部交回,--url 不再視為全域選項。 if len(rest) != 2 || rest[0] != "--url" || rest[1] != "x" { t.Fatalf("rest = %v", rest) } if g.Output != gitea.FormatTable { t.Fatalf("output = %v, want table", g.Output) } } // errString 把字串轉成 error(測試輔助)。 type errStr string func (e errStr) Error() string { return string(e) } func errString(s string) error { return errStr(s) } // wrapped 包一層 error(測試 errors.As 穿透)。 type wrappedErr struct{ inner error } func (w wrappedErr) Error() string { return "wrapped: " + w.inner.Error() } func (w wrappedErr) Unwrap() error { return w.inner } func wrapped(inner error) error { return wrappedErr{inner} }