bf9395e022
CI / results (push) Blocked by required conditions
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / e2e-live (push) Waiting to run
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 / deadcode (push) Waiting to run
340 lines
12 KiB
Go
340 lines
12 KiB
Go
// 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)
|
|
}
|
|
}
|