mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 17:45:15 +08:00
Nudge users whose lark-sheets skill predates the refactor to migrate off the pre-refactor aliases (+read, +write, ...), without requiring anyone to read --help. - internal/deprecation: process-level pending Notice slot (mirrors internal/skillscheck), surfaced in the JSON "_notice" envelope under a "deprecated_command" key. - internal/cmdutil: shared DeprecatedGroupID cobra group + helper so both --help rendering and the unknown-subcommand path classify aliases the same way. - shortcuts/register.go: applySheetsCompatGroups splits the aliases into a dedicated "update your skill" help group with "(-> +new)" pointers; wrapSheetsBackwardDeprecation records the notice from Validate/Execute so direct callers that never read --help still get flagged. - cmd/root.go: extract composePendingNotice (now unit-testable) and split availableSubcommandNames into current vs deprecated buckets while still ranking unknown-subcommand suggestions across both.
237 lines
8.2 KiB
Go
237 lines
8.2 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/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 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 exitErr *output.ExitError
|
|
if !errors.As(err, &exitErr) {
|
|
t.Fatalf("expected *output.ExitError, got %T", err)
|
|
}
|
|
if exitErr.Code != output.ExitValidation {
|
|
t.Errorf("expected exit code %d, got %d", output.ExitValidation, exitErr.Code)
|
|
}
|
|
if exitErr.Detail == nil {
|
|
t.Fatal("expected ExitError to carry Detail")
|
|
}
|
|
if exitErr.Detail.Type != "unknown_subcommand" {
|
|
t.Errorf("expected Detail.Type=unknown_subcommand, got %q", exitErr.Detail.Type)
|
|
}
|
|
if !strings.Contains(exitErr.Detail.Message, `"+bogus"`) {
|
|
t.Errorf("message should echo the unknown token, got %q", exitErr.Detail.Message)
|
|
}
|
|
// "+bogus" has no close neighbor among drive's subcommands, so the hint falls
|
|
// back to pointing at --help; the full machine-readable list lives in
|
|
// detail.available below (which also excludes hidden commands).
|
|
if !strings.Contains(exitErr.Detail.Hint, "--help") {
|
|
t.Errorf("hint should guide to --help when there is no suggestion, got %q", exitErr.Detail.Hint)
|
|
}
|
|
|
|
detail, ok := exitErr.Detail.Detail.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected Detail.Detail to be map[string]any, got %T", exitErr.Detail.Detail)
|
|
}
|
|
if detail["unknown"] != "+bogus" {
|
|
t.Errorf("detail.unknown should be +bogus, got %v", detail["unknown"])
|
|
}
|
|
if detail["command_path"] != "lark-cli drive" {
|
|
t.Errorf("detail.command_path should be %q, got %v", "lark-cli drive", detail["command_path"])
|
|
}
|
|
available, ok := detail["available"].([]string)
|
|
if !ok {
|
|
t.Fatalf("detail.available should be []string, got %T", detail["available"])
|
|
}
|
|
if len(available) != 3 {
|
|
t.Errorf("expected 3 available entries (hidden excluded), got %d: %v", len(available), available)
|
|
}
|
|
}
|
|
|
|
func TestUnknownSubcommandRunE_NestedResourceGroup(t *testing.T) {
|
|
root, _, files := newGroupTree()
|
|
installUnknownSubcommandGuard(root)
|
|
|
|
err := files.RunE(files, []string{"bogus"})
|
|
var exitErr *output.ExitError
|
|
if !errors.As(err, &exitErr) {
|
|
t.Fatalf("expected *output.ExitError on nested group, got %T", err)
|
|
}
|
|
if exitErr.Detail.Detail.(map[string]any)["command_path"] != "lark-cli drive files" {
|
|
t.Errorf("command_path should reflect the nested resource, got %v",
|
|
exitErr.Detail.Detail.(map[string]any)["command_path"])
|
|
}
|
|
}
|
|
|
|
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 must split current vs deprecated subcommands into
|
|
// separate detail buckets, while suggestions still rank across both so a
|
|
// mistyped legacy alias resolves.
|
|
func TestUnknownSubcommandRunE_SplitsDeprecatedBucket(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 exitErr *output.ExitError
|
|
if !errors.As(err, &exitErr) {
|
|
t.Fatalf("expected *output.ExitError, got %T", err)
|
|
}
|
|
detail, ok := exitErr.Detail.Detail.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("detail is not a map: %#v", exitErr.Detail.Detail)
|
|
}
|
|
|
|
if available, _ := detail["available"].([]string); len(available) != 1 || available[0] != "+cells-get" {
|
|
t.Errorf("available = %v, want [+cells-get]", available)
|
|
}
|
|
deprecated, ok := detail["deprecated"].([]string)
|
|
if !ok || len(deprecated) != 1 || deprecated[0] != "+read" {
|
|
t.Errorf("deprecated = %v, want [+read]", deprecated)
|
|
}
|
|
// suggestions rank across both buckets: "+reat" is closest to +read.
|
|
suggestions, _ := detail["suggestions"].([]string)
|
|
found := false
|
|
for _, s := range suggestions {
|
|
if s == "+read" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("suggestions %v should include +read (typo target)", suggestions)
|
|
}
|
|
}
|