mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
2a1613484a
* feat: add framework flag aliases and unified IM pagination Introduce declarative exact-name flag aliases at the shortcut framework boundary while keeping semantic compatibility domain-owned. Add a shared, format-aware IM pagination pipeline with consistent flags, metadata, safety bounds, resumable cursors, and request throttling. * fix: align alias attribution and pagination contracts * fix: align alias contracts and documentation * test: remove environment-dependent contact bot e2e * test: restore contact bot e2e * docs: reduce IM pagination guidance noise --------- Co-authored-by: liangshuo-1 <266696938+liangshuo-1@users.noreply.github.com>
162 lines
5.5 KiB
Go
162 lines
5.5 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/flagalias"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func TestUnknownFlagName(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
name string
|
|
ok bool
|
|
}{
|
|
{"unknown flag: --query", "query", true},
|
|
{"unknown flag: --with-styles", "with-styles", true},
|
|
{"unknown shorthand flag: 'z' in -z", "", false},
|
|
{"flag needs an argument: --find", "", false},
|
|
{`invalid argument "x" for "--count"`, "", false},
|
|
}
|
|
for _, c := range cases {
|
|
name, ok := unknownFlagName(errors.New(c.in))
|
|
if name != c.name || ok != c.ok {
|
|
t.Errorf("unknownFlagName(%q) = (%q,%v), want (%q,%v)", c.in, name, ok, c.name, c.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFlagDidYouMean_UnknownFlagSuggestsAndListsValid(t *testing.T) {
|
|
c := &cobra.Command{Use: "demo"}
|
|
c.Flags().String("range", "", "")
|
|
c.Flags().String("find", "", "")
|
|
c.Flags().Bool("dry-run", false, "")
|
|
|
|
err := flagDidYouMean(c, errors.New("unknown flag: --rang")) // typo of --range
|
|
var verr *errs.ValidationError
|
|
if !errors.As(err, &verr) {
|
|
t.Fatalf("expected *errs.ValidationError, got %T", err)
|
|
}
|
|
if verr.Subtype != errs.SubtypeInvalidArgument {
|
|
t.Errorf("subtype = %q, want invalid_argument", verr.Subtype)
|
|
}
|
|
if code := output.ExitCodeOf(err); code != output.ExitValidation {
|
|
t.Errorf("exit code = %d, want %d (ExitValidation)", code, output.ExitValidation)
|
|
}
|
|
// The offending flag is carried structurally on Params (replaces the
|
|
// legacy detail map) and named in the message.
|
|
if len(verr.Params) != 1 || verr.Params[0].Name != "--rang" {
|
|
t.Errorf("Params = %v, want one entry named --rang", verr.Params)
|
|
}
|
|
if len(verr.Params) == 1 && verr.Params[0].Reason == "" {
|
|
t.Error("Params[0].Reason must explain the rejection")
|
|
}
|
|
if !strings.Contains(verr.Message, "--rang") {
|
|
t.Errorf("message should name the offending flag, got %q", verr.Message)
|
|
}
|
|
// The ranked candidate rides on the param as a machine-readable suggestion
|
|
// so an agent can retry without parsing prose.
|
|
if len(verr.Params) == 1 {
|
|
found := false
|
|
for _, s := range verr.Params[0].Suggestions {
|
|
if s == "--range" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("Params[0].Suggestions should include --range, got %v", verr.Params[0].Suggestions)
|
|
}
|
|
}
|
|
// The same candidate is also carried in the human-facing hint.
|
|
if !strings.Contains(verr.Hint, "--range") {
|
|
t.Errorf("hint should suggest --range, got %q", verr.Hint)
|
|
}
|
|
}
|
|
|
|
func TestFlagDidYouMean_OtherErrorStaysGeneric(t *testing.T) {
|
|
c := &cobra.Command{Use: "demo"}
|
|
err := flagDidYouMean(c, errors.New("flag needs an argument: --find"))
|
|
var verr *errs.ValidationError
|
|
if !errors.As(err, &verr) {
|
|
t.Fatalf("expected *errs.ValidationError, got %T", err)
|
|
}
|
|
// Non-unknown-flag errors stay generic: invalid_argument subtype, no
|
|
// structured param, generic --help hint (no "did you mean" suggestion).
|
|
if verr.Subtype != errs.SubtypeInvalidArgument {
|
|
t.Errorf("subtype = %q, want invalid_argument (non-unknown-flag errors stay generic)", verr.Subtype)
|
|
}
|
|
if code := output.ExitCodeOf(err); code != output.ExitValidation {
|
|
t.Errorf("exit code = %d, want %d (ExitValidation)", code, output.ExitValidation)
|
|
}
|
|
if verr.Param != "" || len(verr.Params) != 0 {
|
|
t.Errorf("Param=%q Params=%v, want both empty for generic flag error", verr.Param, verr.Params)
|
|
}
|
|
if strings.Contains(verr.Hint, "did you mean") {
|
|
t.Errorf("generic flag error must not produce a did-you-mean hint, got %q", verr.Hint)
|
|
}
|
|
}
|
|
|
|
func TestFlagDidYouMean_InvalidAliasValueUsesCallerSpelling(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
wantParam string
|
|
wantMap bool
|
|
}{
|
|
{name: "alias equals value", args: []string{"--page-size=bad"}, wantParam: "--page-size", wantMap: true},
|
|
{name: "canonical equals value", args: []string{"--limit=bad"}, wantParam: "--limit"},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
c := &cobra.Command{Use: "demo"}
|
|
c.Flags().Int("limit", 10, "")
|
|
if err := flagalias.Bind(c, []flagalias.Spec{{Canonical: "limit", Aliases: []string{"page-size"}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parseErr := c.ParseFlags(test.args)
|
|
if parseErr == nil {
|
|
t.Fatal("ParseFlags() succeeded, want invalid integer error")
|
|
}
|
|
|
|
err := flagDidYouMean(c, parseErr)
|
|
var validationErr *errs.ValidationError
|
|
if !errors.As(err, &validationErr) {
|
|
t.Fatalf("error = %T, want *errs.ValidationError", err)
|
|
}
|
|
if validationErr.Param != test.wantParam {
|
|
t.Fatalf("param = %q, want %q", validationErr.Param, test.wantParam)
|
|
}
|
|
hasMapping := strings.Contains(validationErr.Hint, "maps to canonical flag --limit")
|
|
if hasMapping != test.wantMap {
|
|
t.Fatalf("hint = %q, mapping guidance = %v, want %v", validationErr.Hint, hasMapping, test.wantMap)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFlagDidYouMean_InvalidNonAliasValueStaysGeneric(t *testing.T) {
|
|
c := &cobra.Command{Use: "demo"}
|
|
c.Flags().Int("limit", 10, "")
|
|
parseErr := c.ParseFlags([]string{"--limit=bad"})
|
|
if parseErr == nil {
|
|
t.Fatal("ParseFlags() succeeded, want invalid integer error")
|
|
}
|
|
|
|
err := flagDidYouMean(c, parseErr)
|
|
var validationErr *errs.ValidationError
|
|
if !errors.As(err, &validationErr) {
|
|
t.Fatalf("error = %T, want *errs.ValidationError", err)
|
|
}
|
|
if validationErr.Param != "" || len(validationErr.Params) != 0 {
|
|
t.Fatalf("Param=%q Params=%v, want ordinary pflag behavior unchanged", validationErr.Param, validationErr.Params)
|
|
}
|
|
}
|