chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
func newGroupTree() (root, drive, files *cobra.Command) {
|
||||
root = &cobra.Command{Use: "lark-cli"}
|
||||
drive = &cobra.Command{Use: "drive", Short: "drive ops"}
|
||||
root.AddCommand(drive)
|
||||
|
||||
search := &cobra.Command{Use: "+search", RunE: func(*cobra.Command, []string) error { return nil }}
|
||||
upload := &cobra.Command{Use: "+upload", RunE: func(*cobra.Command, []string) error { return nil }}
|
||||
hidden := &cobra.Command{Use: "+secret", Hidden: true, RunE: func(*cobra.Command, []string) error { return nil }}
|
||||
drive.AddCommand(search, upload, hidden)
|
||||
|
||||
files = &cobra.Command{Use: "files", Short: "files ops"}
|
||||
drive.AddCommand(files)
|
||||
files.AddCommand(&cobra.Command{Use: "list", RunE: func(*cobra.Command, []string) error { return nil }})
|
||||
|
||||
return root, drive, files
|
||||
}
|
||||
|
||||
func TestInstallUnknownSubcommandGuard_InstallsOnGroupsOnly(t *testing.T) {
|
||||
root, drive, files := newGroupTree()
|
||||
leaf := drive.Commands()[0] // +search
|
||||
|
||||
installUnknownSubcommandGuard(root)
|
||||
|
||||
if drive.RunE == nil {
|
||||
t.Error("drive should have RunE installed")
|
||||
}
|
||||
if files.RunE == nil {
|
||||
t.Error("files should have RunE installed")
|
||||
}
|
||||
if err := leaf.RunE(leaf, []string{"unexpected-arg"}); err != nil {
|
||||
t.Errorf("leaf +search RunE should be untouched, got error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallUnknownSubcommandGuard_PreservesExistingRunE(t *testing.T) {
|
||||
root := &cobra.Command{Use: "lark-cli"}
|
||||
called := false
|
||||
custom := &cobra.Command{
|
||||
Use: "custom",
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
called = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
// Child makes custom a "group" command, exercising the Run/RunE override guard.
|
||||
custom.AddCommand(&cobra.Command{Use: "leaf", RunE: func(*cobra.Command, []string) error { return nil }})
|
||||
root.AddCommand(custom)
|
||||
|
||||
installUnknownSubcommandGuard(root)
|
||||
|
||||
if err := custom.RunE(custom, nil); err != nil {
|
||||
t.Fatalf("preserved RunE returned error: %v", err)
|
||||
}
|
||||
if !called {
|
||||
t.Error("guard must not overwrite a command that already defines Run/RunE")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownFlagTokens(t *testing.T) {
|
||||
_, drive, _ := newGroupTree()
|
||||
// Give a subcommand a flag so a misplaced-but-known flag (the user omitted
|
||||
// the subcommand) is distinguished from a genuinely unknown one.
|
||||
for _, c := range drive.Commands() {
|
||||
if c.Name() == "+search" {
|
||||
c.Flags().String("query", "", "")
|
||||
}
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
rawArgs []string
|
||||
want []string
|
||||
}{
|
||||
{"genuinely unknown long flag", []string{"drive", "--badflag"}, []string{"--badflag"}},
|
||||
{"flag known on a subcommand (misplaced)", []string{"drive", "--query", "x"}, nil},
|
||||
{"no flags at all", []string{"drive"}, nil},
|
||||
{"tokens after -- are positional", []string{"drive", "--", "--badflag"}, nil},
|
||||
{"unknown shorthand", []string{"drive", "-Z"}, []string{"-Z"}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := unknownFlagTokens(drive, tc.rawArgs)
|
||||
if len(got) != len(tc.want) {
|
||||
t.Fatalf("unknownFlagTokens(%v) = %v, want %v", tc.rawArgs, got, tc.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tc.want[i] {
|
||||
t.Errorf("token[%d] = %q, want %q", i, got[i], tc.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownSubcommandRunE_FlagBeforeSubcommandIsStructured(t *testing.T) {
|
||||
_, drive, _ := newGroupTree()
|
||||
installUnknownSubcommandGuard(drive.Root())
|
||||
|
||||
// Simulate `lark-cli drive --badflag`: the UnknownFlags whitelist swallows
|
||||
// --badflag, so RunE sees no args; the guard must recover it from
|
||||
// rawInvocationArgs and fail structured rather than print help + exit 0.
|
||||
rawInvocationArgs = []string{"drive", "--badflag"}
|
||||
t.Cleanup(func() { rawInvocationArgs = nil })
|
||||
|
||||
err := drive.RunE(drive, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected a structured unknown_flag error, got nil (help fallthrough)")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unknown flag") {
|
||||
t.Errorf("error = %q, want it to mention an unknown flag", err.Error())
|
||||
}
|
||||
|
||||
// Typed surface: a validation error (exit 2) whose Params carries the
|
||||
// offending flag so an agent can recover the token without parsing prose.
|
||||
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 output.ExitCodeOf(err) != output.ExitValidation {
|
||||
t.Errorf("exit code = %d, want %d", output.ExitCodeOf(err), output.ExitValidation)
|
||||
}
|
||||
if len(verr.Params) != 1 || verr.Params[0].Name != "--badflag" {
|
||||
t.Errorf("params = %v, want one entry named --badflag", verr.Params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownSubcommandRunE_ValidFlagWithoutSubcommandIsStructured(t *testing.T) {
|
||||
_, drive, _ := newGroupTree()
|
||||
// --query is defined on the +search subcommand, so it is a *valid* flag that
|
||||
// was placed before the (omitted) subcommand. Unlike an unknown flag, this
|
||||
// must still fail structured (missing_subcommand) rather than fall through to
|
||||
// help + exit 0 — `drive --query x` is a malformed call, not a help request.
|
||||
for _, c := range drive.Commands() {
|
||||
if c.Name() == "+search" {
|
||||
c.Flags().String("query", "", "")
|
||||
}
|
||||
}
|
||||
installUnknownSubcommandGuard(drive.Root())
|
||||
|
||||
rawInvocationArgs = []string{"drive", "--query", "x"}
|
||||
t.Cleanup(func() { rawInvocationArgs = nil })
|
||||
|
||||
err := drive.RunE(drive, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected a structured missing_subcommand error, got nil (help fallthrough)")
|
||||
}
|
||||
var verr *errs.ValidationError
|
||||
if !errors.As(err, &verr) {
|
||||
t.Fatalf("expected *errs.ValidationError, got %T", err)
|
||||
}
|
||||
if output.ExitCodeOf(err) != output.ExitValidation {
|
||||
t.Errorf("exit code = %d, want %d", output.ExitCodeOf(err), output.ExitValidation)
|
||||
}
|
||||
if !strings.Contains(verr.Message, "missing subcommand") {
|
||||
t.Errorf("message = %q, want it to mention a missing subcommand", verr.Message)
|
||||
}
|
||||
if len(verr.Params) != 1 || verr.Params[0].Name != "--query" {
|
||||
t.Errorf("params = %v, want one entry named --query", verr.Params)
|
||||
}
|
||||
if !strings.Contains(verr.Message, "lark-cli drive") {
|
||||
t.Errorf("message = %q, want it to name the group path", verr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// A bare group carrying only a group-valid global flag (e.g. the inherited
|
||||
// --profile) is not missing a subcommand — those flags do not belong to a
|
||||
// subcommand — so it must print help, not fail with missing_subcommand.
|
||||
func TestUnknownSubcommandRunE_GroupValidGlobalFlagShowsHelp(t *testing.T) {
|
||||
_, drive, _ := newGroupTree()
|
||||
drive.Root().PersistentFlags().String("profile", "", "") // global, inherited by drive
|
||||
installUnknownSubcommandGuard(drive.Root())
|
||||
|
||||
rawInvocationArgs = []string{"--profile", "p", "drive"}
|
||||
t.Cleanup(func() { rawInvocationArgs = nil })
|
||||
|
||||
var buf bytes.Buffer
|
||||
drive.SetOut(&buf)
|
||||
drive.SetErr(&buf)
|
||||
if err := drive.RunE(drive, nil); err != nil {
|
||||
t.Fatalf("bare group with only a global flag should print help, got error: %v", err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "drive ops") {
|
||||
t.Errorf("expected help output, got:\n%s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownSubcommandRunE_NoArgsShowsHelp(t *testing.T) {
|
||||
_, drive, _ := newGroupTree()
|
||||
installUnknownSubcommandGuard(drive.Root())
|
||||
|
||||
var buf bytes.Buffer
|
||||
drive.SetOut(&buf)
|
||||
drive.SetErr(&buf)
|
||||
|
||||
if err := drive.RunE(drive, nil); err != nil {
|
||||
t.Fatalf("expected no-args invocation to succeed, got: %v", err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "drive ops") {
|
||||
t.Errorf("expected help output to include the command's Short, got:\n%s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownSubcommandRunE_UnknownReturnsStructuredError(t *testing.T) {
|
||||
_, drive, _ := newGroupTree()
|
||||
installUnknownSubcommandGuard(drive.Root())
|
||||
|
||||
err := drive.RunE(drive, []string{"+bogus"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown subcommand")
|
||||
}
|
||||
|
||||
var verr *errs.ValidationError
|
||||
if !errors.As(err, &verr) {
|
||||
t.Fatalf("expected *errs.ValidationError, got %T", err)
|
||||
}
|
||||
if output.ExitCodeOf(err) != output.ExitValidation {
|
||||
t.Errorf("expected exit code %d, got %d", output.ExitValidation, output.ExitCodeOf(err))
|
||||
}
|
||||
if !strings.Contains(verr.Message, `"+bogus"`) {
|
||||
t.Errorf("message should echo the unknown token, got %q", verr.Message)
|
||||
}
|
||||
if !strings.Contains(verr.Message, "lark-cli drive") {
|
||||
t.Errorf("message should name the group path, got %q", verr.Message)
|
||||
}
|
||||
// "+bogus" has no close neighbor among drive's subcommands, so the hint falls
|
||||
// back to pointing at --help (suggestions, when present, are folded into hint).
|
||||
if !strings.Contains(verr.Hint, "--help") {
|
||||
t.Errorf("hint should guide to --help when there is no suggestion, got %q", verr.Hint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownSubcommandRunE_NestedResourceGroup(t *testing.T) {
|
||||
root, _, files := newGroupTree()
|
||||
installUnknownSubcommandGuard(root)
|
||||
|
||||
err := files.RunE(files, []string{"bogus"})
|
||||
var verr *errs.ValidationError
|
||||
if !errors.As(err, &verr) {
|
||||
t.Fatalf("expected *errs.ValidationError on nested group, got %T", err)
|
||||
}
|
||||
if !strings.Contains(verr.Message, "lark-cli drive files") {
|
||||
t.Errorf("message should reflect the nested resource path, got %q", verr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailableSubcommandNames_FiltersHelpAndCompletion(t *testing.T) {
|
||||
root := &cobra.Command{Use: "lark-cli"}
|
||||
root.AddCommand(
|
||||
&cobra.Command{Use: "alpha", RunE: func(*cobra.Command, []string) error { return nil }},
|
||||
&cobra.Command{Use: "help", RunE: func(*cobra.Command, []string) error { return nil }},
|
||||
&cobra.Command{Use: "completion", RunE: func(*cobra.Command, []string) error { return nil }},
|
||||
&cobra.Command{Use: "beta", Hidden: true, RunE: func(*cobra.Command, []string) error { return nil }},
|
||||
&cobra.Command{Use: "gamma", RunE: func(*cobra.Command, []string) error { return nil }},
|
||||
)
|
||||
|
||||
got, _ := availableSubcommandNames(root)
|
||||
want := []string{"alpha", "gamma"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("expected %v, got %v", want, got)
|
||||
}
|
||||
for i, name := range want {
|
||||
if got[i] != name {
|
||||
t.Errorf("availableSubcommandNames[%d] = %q, want %q", i, got[i], name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailableSubcommandNames_SplitsDeprecatedGroup(t *testing.T) {
|
||||
root := &cobra.Command{Use: "lark-cli"}
|
||||
root.AddGroup(&cobra.Group{ID: cmdutil.DeprecatedGroupID, Title: "Deprecated"})
|
||||
root.AddCommand(
|
||||
&cobra.Command{Use: "+new-cmd", RunE: func(*cobra.Command, []string) error { return nil }},
|
||||
&cobra.Command{Use: "+old-cmd", GroupID: cmdutil.DeprecatedGroupID, RunE: func(*cobra.Command, []string) error { return nil }},
|
||||
)
|
||||
|
||||
available, deprecated := availableSubcommandNames(root)
|
||||
if len(available) != 1 || available[0] != "+new-cmd" {
|
||||
t.Errorf("available = %v, want [+new-cmd]", available)
|
||||
}
|
||||
if len(deprecated) != 1 || deprecated[0] != "+old-cmd" {
|
||||
t.Errorf("deprecated = %v, want [+old-cmd]", deprecated)
|
||||
}
|
||||
}
|
||||
|
||||
// unknownSubcommandRunE ranks suggestions across both current and deprecated
|
||||
// subcommands so a mistyped legacy alias resolves; the closest match is folded
|
||||
// into the hint.
|
||||
func TestUnknownSubcommandRunE_SuggestsAcrossDeprecatedBucket(t *testing.T) {
|
||||
svc := &cobra.Command{Use: "sheets"}
|
||||
svc.AddGroup(&cobra.Group{ID: cmdutil.DeprecatedGroupID, Title: "Deprecated"})
|
||||
svc.AddCommand(
|
||||
&cobra.Command{Use: "+cells-get", RunE: func(*cobra.Command, []string) error { return nil }},
|
||||
&cobra.Command{Use: "+read", GroupID: cmdutil.DeprecatedGroupID, RunE: func(*cobra.Command, []string) error { return nil }},
|
||||
)
|
||||
|
||||
err := unknownSubcommandRunE(svc, []string{"+reat"})
|
||||
var verr *errs.ValidationError
|
||||
if !errors.As(err, &verr) {
|
||||
t.Fatalf("expected *errs.ValidationError, got %T", err)
|
||||
}
|
||||
// "+reat" is closest to the deprecated +read: the candidate must surface
|
||||
// both as a machine-readable param suggestion (for agent retry) and in the
|
||||
// hint, proving ranking spans the deprecated bucket.
|
||||
if len(verr.Params) != 1 || verr.Params[0].Name != "+reat" {
|
||||
t.Fatalf("params = %v, want one entry named +reat (the offending subcommand)", verr.Params)
|
||||
}
|
||||
foundSuggestion := false
|
||||
for _, s := range verr.Params[0].Suggestions {
|
||||
if s == "+read" {
|
||||
foundSuggestion = true
|
||||
}
|
||||
}
|
||||
if !foundSuggestion {
|
||||
t.Errorf("Params[0].Suggestions should include +read, got %v", verr.Params[0].Suggestions)
|
||||
}
|
||||
if !strings.Contains(verr.Hint, "+read") {
|
||||
t.Errorf("hint %q should suggest +read (typo target across deprecated bucket)", verr.Hint)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user