mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 16:18:05 +08:00
783 lines
22 KiB
Go
783 lines
22 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package rules
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/internal/qualitygate/facts"
|
|
"github.com/larksuite/cli/internal/qualitygate/manifest"
|
|
"github.com/larksuite/cli/internal/qualitygate/report"
|
|
"github.com/larksuite/cli/internal/vfs"
|
|
)
|
|
|
|
func TestCollectErrorFactsMarksHelperBareErrorAsNonBoundaryWarning(t *testing.T) {
|
|
src := `package demo
|
|
import "fmt"
|
|
func parseTimeValue(s string) error {
|
|
return fmt.Errorf("invalid timestamp %q", s)
|
|
}`
|
|
facts, diags := CollectErrorFacts("cmd/demo.go", src, BoundaryIndex{})
|
|
if len(facts) != 1 {
|
|
t.Fatalf("got %d facts", len(facts))
|
|
}
|
|
if facts[0].Boundary {
|
|
t.Fatalf("helper bare error must not be marked boundary")
|
|
}
|
|
if len(diags) != 1 || diags[0].Rule != "no_bare_helper_error" || diags[0].Action != report.ActionWarning {
|
|
t.Fatalf("helper bare error should warn only, got %#v", diags)
|
|
}
|
|
}
|
|
|
|
func TestCollectErrorFactsCountsHintActions(t *testing.T) {
|
|
hint := "run `lark-cli docs +fetch --doc abc` with --api-version v2"
|
|
if got := HintActionCount(hint); got < 2 {
|
|
t.Fatalf("HintActionCount() = %d, want at least 2", got)
|
|
}
|
|
}
|
|
|
|
func TestHintActionCountDoesNotCountIdentifierSuffixes(t *testing.T) {
|
|
for _, hint := range []string{
|
|
"provide file_token in the input",
|
|
"missing open_id",
|
|
"not_found",
|
|
} {
|
|
if got := HintActionCount(hint); got != 0 {
|
|
t.Fatalf("HintActionCount(%q) = %d, want 0", hint, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHintActionCountCountsLocaleToken(t *testing.T) {
|
|
if got := HintActionCount("set locale to zh_CN"); got != 1 {
|
|
t.Fatalf("HintActionCount() = %d, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestCollectRepoErrorFactsAnnotatesShortcutBoundaryScope(t *testing.T) {
|
|
repo := t.TempDir()
|
|
if err := vfs.MkdirAll(filepath.Join(repo, "cmd"), 0o755); err != nil {
|
|
t.Fatalf("mkdir cmd: %v", err)
|
|
}
|
|
path := filepath.Join(repo, "shortcuts", "wiki", "move.go")
|
|
if err := vfs.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
src := `package wiki
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
var WikiMove = common.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+move",
|
|
Execute: executeWikiMove,
|
|
}
|
|
|
|
func executeWikiMove(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
return output.ErrWithHint("invalid_input", "validation", "missing token", "run lark-cli wiki +move --help")
|
|
}
|
|
`
|
|
if err := vfs.WriteFile(path, []byte(src), 0o644); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
errorFacts, _, err := CollectRepoErrorFacts(repo, nil, false)
|
|
if err != nil {
|
|
t.Fatalf("CollectRepoErrorFacts() error = %v", err)
|
|
}
|
|
if len(errorFacts) != 1 {
|
|
t.Fatalf("got %d error facts", len(errorFacts))
|
|
}
|
|
if !errorFacts[0].Boundary || errorFacts[0].Command != "wiki +move" {
|
|
t.Fatalf("boundary command not annotated: %#v", errorFacts[0])
|
|
}
|
|
|
|
got := facts.Build(
|
|
manifest.Manifest{Commands: []manifest.Command{{Path: "wiki +move", Domain: "wiki", Source: manifest.SourceShortcut}}},
|
|
nil,
|
|
nil,
|
|
errorFacts,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
map[string]bool{"shortcuts/wiki/move.go": true},
|
|
)
|
|
if got.Errors[0].CommandPath != "wiki +move" || got.Errors[0].Domain != "wiki" || got.Errors[0].Source != "shortcut" || !got.Errors[0].Changed {
|
|
t.Fatalf("error fact scope not enriched: %#v", got.Errors[0])
|
|
}
|
|
}
|
|
|
|
func TestCollectErrorFactsTreatsCommonValidationErrorfAsStructuredBoundary(t *testing.T) {
|
|
src := `package contact
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
var ContactGetUser = common.Shortcut{
|
|
Service: "contact",
|
|
Command: "+get-user",
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
return common.ValidationErrorf("invalid --user-id-type").
|
|
WithHint("the identifier type is unsupported").
|
|
WithParam("--user-id-type")
|
|
},
|
|
}
|
|
`
|
|
path := "shortcuts/contact/contact_get_user.go"
|
|
errorFacts, diags := CollectErrorFacts(path, src, BuildErrorBoundaryIndex(path, src))
|
|
if len(diags) != 0 {
|
|
t.Fatalf("unexpected diagnostics: %#v", diags)
|
|
}
|
|
if len(errorFacts) != 1 {
|
|
t.Fatalf("got %d error facts, want 1", len(errorFacts))
|
|
}
|
|
got := errorFacts[0]
|
|
if !got.Boundary || got.Command != "contact +get-user" {
|
|
t.Fatalf("common.ValidationErrorf boundary not annotated: %#v", got)
|
|
}
|
|
if !got.UsesStructuredError || !got.HasHint || !got.RequiredHint {
|
|
t.Fatalf("common.ValidationErrorf metadata not structured with required hint: %#v", got)
|
|
}
|
|
if got.HintActionCount != 0 {
|
|
t.Fatalf("HintActionCount = %d, want 0 for non-actionable hint", got.HintActionCount)
|
|
}
|
|
}
|
|
|
|
func TestCollectErrorFactsTracksCommonTypedValidatorMultiReturnBoundary(t *testing.T) {
|
|
src := `package minutes
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
var MinutesSearch = common.Shortcut{
|
|
Service: "minutes",
|
|
Command: "+search",
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
if _, err := common.ValidatePageSizeTyped(runtime, "page-size", 50, 1, 100); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
`
|
|
path := "shortcuts/minutes/minutes_search.go"
|
|
errorFacts, diags := CollectErrorFacts(path, src, BuildErrorBoundaryIndex(path, src))
|
|
if len(diags) != 0 {
|
|
t.Fatalf("unexpected diagnostics: %#v", diags)
|
|
}
|
|
got, ok := findErrorFact(errorFacts, path, "common.ValidatePageSizeTyped")
|
|
if !ok {
|
|
t.Fatalf("common typed validator boundary fact not found: %#v", errorFacts)
|
|
}
|
|
if !got.Boundary || got.Command != "minutes +search" || !got.UsesStructuredError {
|
|
t.Fatalf("common typed validator boundary not annotated: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestCollectErrorFactsTreatsDomainValidationHelperAsStructuredBoundary(t *testing.T) {
|
|
src := `package base
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
func baseFlagErrorf(format string, args ...any) error {
|
|
return baseValidationErrorf(format, args...)
|
|
}
|
|
|
|
func baseValidationErrorf(format string, args ...any) error {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, format, args...)
|
|
}
|
|
|
|
var BaseRoleCreate = common.Shortcut{
|
|
Service: "base",
|
|
Command: "+role-create",
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
return baseFlagErrorf("--base-token must not be blank")
|
|
},
|
|
}
|
|
`
|
|
path := "shortcuts/base/base_role_create.go"
|
|
errorFacts, diags := CollectErrorFacts(path, src, BuildErrorBoundaryIndex(path, src))
|
|
if len(diags) != 0 {
|
|
t.Fatalf("unexpected diagnostics: %#v", diags)
|
|
}
|
|
if len(errorFacts) != 3 {
|
|
t.Fatalf("got %d error facts, want helper definitions plus boundary call: %#v", len(errorFacts), errorFacts)
|
|
}
|
|
got := errorFacts[2]
|
|
if !got.Boundary || got.Command != "base +role-create" {
|
|
t.Fatalf("domain validation helper boundary not annotated: %#v", got)
|
|
}
|
|
if !got.UsesStructuredError || got.Code != "baseFlagErrorf" {
|
|
t.Fatalf("domain validation helper not treated as structured: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestCollectErrorFactsTracksDomainValidateHelperMultiReturnBoundary(t *testing.T) {
|
|
src := `package base
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
func baseValidationErrorf(format string, args ...any) error {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, format, args...)
|
|
}
|
|
|
|
func validateRoleName(name string) (string, error) {
|
|
if name == "" {
|
|
return "", baseValidationErrorf("--role-name must not be blank")
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
var BaseRoleCreate = common.Shortcut{
|
|
Service: "base",
|
|
Command: "+role-create",
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
if _, err := validateRoleName(""); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
`
|
|
path := "shortcuts/base/base_role_create.go"
|
|
errorFacts, diags := CollectErrorFacts(path, src, BuildErrorBoundaryIndex(path, src))
|
|
if len(diags) != 0 {
|
|
t.Fatalf("unexpected diagnostics: %#v", diags)
|
|
}
|
|
got, ok := findErrorFact(errorFacts, path, "validateRoleName")
|
|
if !ok {
|
|
t.Fatalf("domain validate helper boundary fact not found: %#v", errorFacts)
|
|
}
|
|
if !got.Boundary || got.Command != "base +role-create" || !got.UsesStructuredError {
|
|
t.Fatalf("domain validate helper boundary not annotated: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestCollectErrorFactsDoesNotTreatOrdinaryMultiReturnAsStructuredBoundary(t *testing.T) {
|
|
src := `package base
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
func parseRoleName(name string) (string, error) {
|
|
if name == "" {
|
|
return "", errors.New("role name is required")
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
var BaseRoleCreate = common.Shortcut{
|
|
Service: "base",
|
|
Command: "+role-create",
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
if _, err := parseRoleName(""); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
`
|
|
path := "shortcuts/base/base_role_create.go"
|
|
errorFacts, _ := CollectErrorFacts(path, src, BuildErrorBoundaryIndex(path, src))
|
|
if got, ok := findErrorFact(errorFacts, path, "parseRoleName"); ok {
|
|
t.Fatalf("ordinary multi-return helper should not be treated as structured boundary: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestCollectRepoErrorFactsUsesPackageStructuredHelpersAcrossFiles(t *testing.T) {
|
|
repo := t.TempDir()
|
|
if err := vfs.MkdirAll(filepath.Join(repo, "cmd"), 0o755); err != nil {
|
|
t.Fatalf("mkdir cmd: %v", err)
|
|
}
|
|
baseDir := filepath.Join(repo, "shortcuts", "base")
|
|
if err := vfs.MkdirAll(baseDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir base: %v", err)
|
|
}
|
|
helperSrc := `package base
|
|
|
|
import "github.com/larksuite/cli/errs"
|
|
|
|
func baseFlagErrorf(format string, args ...any) error {
|
|
return baseValidationErrorf(format, args...)
|
|
}
|
|
|
|
func baseValidationErrorf(format string, args ...any) error {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, format, args...)
|
|
}
|
|
`
|
|
if err := vfs.WriteFile(filepath.Join(baseDir, "base_errors.go"), []byte(helperSrc), 0o644); err != nil {
|
|
t.Fatalf("write helper: %v", err)
|
|
}
|
|
shortcutSrc := `package base
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
var BaseRoleCreate = common.Shortcut{
|
|
Service: "base",
|
|
Command: "+role-create",
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
return baseFlagErrorf("--base-token must not be blank")
|
|
},
|
|
}
|
|
`
|
|
if err := vfs.WriteFile(filepath.Join(baseDir, "base_role_create.go"), []byte(shortcutSrc), 0o644); err != nil {
|
|
t.Fatalf("write shortcut: %v", err)
|
|
}
|
|
|
|
errorFacts, _, err := CollectRepoErrorFacts(repo, nil, false)
|
|
if err != nil {
|
|
t.Fatalf("CollectRepoErrorFacts() error = %v", err)
|
|
}
|
|
var found bool
|
|
for _, fact := range errorFacts {
|
|
if fact.File == "shortcuts/base/base_role_create.go" && fact.Line == 13 {
|
|
found = true
|
|
if !fact.Boundary || fact.Command != "base +role-create" || !fact.UsesStructuredError {
|
|
t.Fatalf("cross-file helper fact not annotated: %#v", fact)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("cross-file helper boundary fact not found: %#v", errorFacts)
|
|
}
|
|
}
|
|
|
|
func findErrorFact(errorFacts []facts.ErrorFact, path, code string) (facts.ErrorFact, bool) {
|
|
for _, fact := range errorFacts {
|
|
if fact.File == path && fact.Code == code {
|
|
return fact, true
|
|
}
|
|
}
|
|
return facts.ErrorFact{}, false
|
|
}
|
|
|
|
func TestCollectRepoErrorFactsAnnotatesCobraRunEBoundaryScope(t *testing.T) {
|
|
repo := t.TempDir()
|
|
if err := vfs.MkdirAll(filepath.Join(repo, "shortcuts"), 0o755); err != nil {
|
|
t.Fatalf("mkdir shortcuts: %v", err)
|
|
}
|
|
path := filepath.Join(repo, "cmd", "demo.go")
|
|
if err := vfs.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
src := `package cmd
|
|
|
|
import (
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var demoCmd = &cobra.Command{
|
|
Use: "demo [id]",
|
|
RunE: runDemo,
|
|
}
|
|
|
|
func runDemo(cmd *cobra.Command, args []string) error {
|
|
return errs.NewValidationError("missing demo id").WithHint("run lark-cli demo --help")
|
|
}
|
|
`
|
|
if err := vfs.WriteFile(path, []byte(src), 0o644); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
errorFacts, _, err := CollectRepoErrorFacts(repo, nil, false)
|
|
if err != nil {
|
|
t.Fatalf("CollectRepoErrorFacts() error = %v", err)
|
|
}
|
|
if len(errorFacts) != 1 {
|
|
t.Fatalf("got %d error facts", len(errorFacts))
|
|
}
|
|
if !errorFacts[0].Boundary || errorFacts[0].Command != "demo" {
|
|
t.Fatalf("cobra RunE boundary command not annotated: %#v", errorFacts[0])
|
|
}
|
|
}
|
|
|
|
func TestCollectRepoErrorFactsAnnotatesReturnedLocalBareErrorBoundary(t *testing.T) {
|
|
repo := t.TempDir()
|
|
if err := vfs.MkdirAll(filepath.Join(repo, "shortcuts"), 0o755); err != nil {
|
|
t.Fatalf("mkdir shortcuts: %v", err)
|
|
}
|
|
path := filepath.Join(repo, "cmd", "demo.go")
|
|
if err := vfs.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
src := `package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var demoCmd = &cobra.Command{
|
|
Use: "demo [id]",
|
|
RunE: runDemo,
|
|
}
|
|
|
|
func runDemo(cmd *cobra.Command, args []string) error {
|
|
err := fmt.Errorf("missing demo id")
|
|
return err
|
|
}
|
|
`
|
|
if err := vfs.WriteFile(path, []byte(src), 0o644); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
errorFacts, diags, err := CollectRepoErrorFacts(repo, nil, false)
|
|
if err != nil {
|
|
t.Fatalf("CollectRepoErrorFacts() error = %v", err)
|
|
}
|
|
if len(errorFacts) != 1 {
|
|
t.Fatalf("got %d error facts", len(errorFacts))
|
|
}
|
|
if !errorFacts[0].Boundary || errorFacts[0].Command != "demo" {
|
|
t.Fatalf("returned local bare error boundary not annotated: facts=%#v diags=%#v", errorFacts, diags)
|
|
}
|
|
if len(diags) != 0 {
|
|
t.Fatalf("boundary bare error should not also be reported as helper warning: %#v", diags)
|
|
}
|
|
}
|
|
|
|
func TestCollectRepoErrorFactsAnnotatesReturnedLocalStructuredErrorBoundary(t *testing.T) {
|
|
repo := t.TempDir()
|
|
if err := vfs.MkdirAll(filepath.Join(repo, "shortcuts"), 0o755); err != nil {
|
|
t.Fatalf("mkdir shortcuts: %v", err)
|
|
}
|
|
path := filepath.Join(repo, "cmd", "demo.go")
|
|
if err := vfs.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
src := `package cmd
|
|
|
|
import (
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var demoCmd = &cobra.Command{
|
|
Use: "demo [id]",
|
|
RunE: runDemo,
|
|
}
|
|
|
|
func runDemo(cmd *cobra.Command, args []string) error {
|
|
err := errs.NewValidationError("missing demo id").WithHint("run lark-cli demo --help")
|
|
return err
|
|
}
|
|
`
|
|
if err := vfs.WriteFile(path, []byte(src), 0o644); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
errorFacts, diags, err := CollectRepoErrorFacts(repo, nil, false)
|
|
if err != nil {
|
|
t.Fatalf("CollectRepoErrorFacts() error = %v", err)
|
|
}
|
|
if len(errorFacts) != 1 {
|
|
t.Fatalf("got %d error facts", len(errorFacts))
|
|
}
|
|
if !errorFacts[0].Boundary || errorFacts[0].Command != "demo" || !errorFacts[0].UsesStructuredError || !errorFacts[0].HasHint {
|
|
t.Fatalf("returned local structured error boundary not annotated: facts=%#v diags=%#v", errorFacts, diags)
|
|
}
|
|
if len(diags) != 0 {
|
|
t.Fatalf("structured boundary error should not produce helper diagnostics: %#v", diags)
|
|
}
|
|
}
|
|
|
|
func TestCollectRepoErrorFactsAnnotatesFluentStructuredErrorBoundary(t *testing.T) {
|
|
repo := t.TempDir()
|
|
if err := vfs.MkdirAll(filepath.Join(repo, "cmd"), 0o755); err != nil {
|
|
t.Fatalf("mkdir cmd: %v", err)
|
|
}
|
|
path := filepath.Join(repo, "shortcuts", "wiki", "move.go")
|
|
if err := vfs.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
src := `package wiki
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
var WikiMove = common.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+move",
|
|
Execute: executeWikiMove,
|
|
}
|
|
|
|
func executeWikiMove(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
return errs.NewValidationError("missing token").WithParam("node_token").WithHint("run lark-cli wiki +move --help")
|
|
}
|
|
`
|
|
if err := vfs.WriteFile(path, []byte(src), 0o644); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
errorFacts, _, err := CollectRepoErrorFacts(repo, nil, false)
|
|
if err != nil {
|
|
t.Fatalf("CollectRepoErrorFacts() error = %v", err)
|
|
}
|
|
if len(errorFacts) != 1 {
|
|
t.Fatalf("got %d error facts", len(errorFacts))
|
|
}
|
|
if !errorFacts[0].Boundary || errorFacts[0].Command != "wiki +move" {
|
|
t.Fatalf("fluent structured error boundary not annotated: %#v", errorFacts[0])
|
|
}
|
|
if !errorFacts[0].HasHint || errorFacts[0].HintActionCount == 0 || !errorFacts[0].RequiredHint {
|
|
t.Fatalf("fluent structured error hint metadata not annotated: %#v", errorFacts[0])
|
|
}
|
|
}
|
|
|
|
func TestCollectRepoErrorFactsDoesNotMarkSameNameMethodBoundary(t *testing.T) {
|
|
repo := t.TempDir()
|
|
if err := vfs.MkdirAll(filepath.Join(repo, "cmd"), 0o755); err != nil {
|
|
t.Fatalf("mkdir cmd: %v", err)
|
|
}
|
|
path := filepath.Join(repo, "shortcuts", "wiki", "move.go")
|
|
if err := vfs.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
src := `package wiki
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
type executor struct{}
|
|
|
|
var WikiMove = common.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+move",
|
|
Execute: executeWikiMove,
|
|
}
|
|
|
|
func executeWikiMove(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
return nil
|
|
}
|
|
|
|
func (executor) executeWikiMove(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
return errs.NewValidationError("missing token").WithHint("run lark-cli wiki +move --help")
|
|
}
|
|
`
|
|
if err := vfs.WriteFile(path, []byte(src), 0o644); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
errorFacts, _, err := CollectRepoErrorFacts(repo, nil, false)
|
|
if err != nil {
|
|
t.Fatalf("CollectRepoErrorFacts() error = %v", err)
|
|
}
|
|
for _, fact := range errorFacts {
|
|
if fact.Boundary {
|
|
t.Fatalf("same-name method must not be marked as command boundary: %#v", errorFacts)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCollectRepoErrorFactsAnnotatesVariableFluentHintBoundary(t *testing.T) {
|
|
repo := t.TempDir()
|
|
if err := vfs.MkdirAll(filepath.Join(repo, "cmd"), 0o755); err != nil {
|
|
t.Fatalf("mkdir cmd: %v", err)
|
|
}
|
|
path := filepath.Join(repo, "shortcuts", "wiki", "move.go")
|
|
if err := vfs.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
src := `package wiki
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
var WikiMove = common.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+move",
|
|
Execute: executeWikiMove,
|
|
}
|
|
|
|
func executeWikiMove(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
base := errs.NewValidationError("missing token").WithParam("node_token")
|
|
return base.WithHint("run lark-cli wiki +move --help")
|
|
}
|
|
`
|
|
if err := vfs.WriteFile(path, []byte(src), 0o644); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
errorFacts, _, err := CollectRepoErrorFacts(repo, nil, false)
|
|
if err != nil {
|
|
t.Fatalf("CollectRepoErrorFacts() error = %v", err)
|
|
}
|
|
var boundary facts.ErrorFact
|
|
for _, fact := range errorFacts {
|
|
if fact.Boundary {
|
|
boundary = fact
|
|
}
|
|
}
|
|
if boundary.Command != "wiki +move" || !boundary.HasHint || boundary.HintActionCount == 0 || !boundary.RequiredHint {
|
|
t.Fatalf("variable fluent hint boundary not annotated: %#v", errorFacts)
|
|
}
|
|
}
|
|
|
|
func TestCollectRepoErrorFactsSkipsDeletedChangedFiles(t *testing.T) {
|
|
repo := t.TempDir()
|
|
errorFacts, diags, err := CollectRepoErrorFacts(repo, []string{"shortcuts/wiki/deleted.go"}, true)
|
|
if err != nil {
|
|
t.Fatalf("CollectRepoErrorFacts() should skip deleted changed files, got %v", err)
|
|
}
|
|
if len(errorFacts) != 0 || len(diags) != 0 {
|
|
t.Fatalf("deleted changed files should produce no facts or diagnostics, got facts=%#v diags=%#v", errorFacts, diags)
|
|
}
|
|
}
|
|
|
|
func TestCollectErrorFactsDoesNotTreatUnknownWithHintAsStructured(t *testing.T) {
|
|
src := `package demo
|
|
|
|
func helper(base customError) error {
|
|
return base.WithHint("run lark-cli docs +fetch --doc abc")
|
|
}
|
|
`
|
|
errorFacts, _ := CollectErrorFacts("cmd/demo.go", src, BoundaryIndex{})
|
|
if len(errorFacts) != 0 {
|
|
t.Fatalf("unknown WithHint receiver should not be collected as structured error: %#v", errorFacts)
|
|
}
|
|
}
|
|
|
|
func TestCollectErrorFactsDoesNotLeakStructuredVarsAcrossFunctions(t *testing.T) {
|
|
src := `package demo
|
|
|
|
import "github.com/larksuite/cli/errs"
|
|
|
|
func other() error {
|
|
base := errs.NewValidationError("missing token")
|
|
return base
|
|
}
|
|
|
|
func helper(base customError) error {
|
|
return base.WithHint("run lark-cli docs +fetch --doc abc")
|
|
}
|
|
`
|
|
errorFacts, _ := CollectErrorFacts("cmd/demo.go", src, BoundaryIndex{})
|
|
if len(errorFacts) != 1 || errorFacts[0].Code != "NewValidationError" {
|
|
t.Fatalf("only local structured constructor should be collected: %#v", errorFacts)
|
|
}
|
|
}
|
|
|
|
func TestCollectErrorFactsDoesNotLeakStructuredVarsAcrossBlocks(t *testing.T) {
|
|
src := `package demo
|
|
|
|
import "github.com/larksuite/cli/errs"
|
|
|
|
func helper(base customError) error {
|
|
if true {
|
|
base := errs.NewValidationError("missing token")
|
|
_ = base
|
|
}
|
|
return base.WithHint("run lark-cli docs +fetch --doc abc")
|
|
}
|
|
`
|
|
errorFacts, _ := CollectErrorFacts("cmd/demo.go", src, BoundaryIndex{})
|
|
if len(errorFacts) != 1 || errorFacts[0].Code != "NewValidationError" {
|
|
t.Fatalf("inner block structured var should not leak to outer receiver: %#v", errorFacts)
|
|
}
|
|
}
|
|
|
|
func TestCollectRepoErrorFactsAnnotatesVariableFluentHintThroughWrapper(t *testing.T) {
|
|
repo := t.TempDir()
|
|
if err := vfs.MkdirAll(filepath.Join(repo, "cmd"), 0o755); err != nil {
|
|
t.Fatalf("mkdir cmd: %v", err)
|
|
}
|
|
path := filepath.Join(repo, "shortcuts", "wiki", "move.go")
|
|
if err := vfs.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
src := `package wiki
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
var WikiMove = common.Shortcut{
|
|
Service: "wiki",
|
|
Command: "+move",
|
|
Execute: executeWikiMove,
|
|
}
|
|
|
|
func executeWikiMove(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
base := errs.NewValidationError("missing token")
|
|
wrapped := base.WithParam("node_token")
|
|
return wrapped.WithHint("run lark-cli wiki +move --help")
|
|
}
|
|
`
|
|
if err := vfs.WriteFile(path, []byte(src), 0o644); err != nil {
|
|
t.Fatalf("write source: %v", err)
|
|
}
|
|
|
|
errorFacts, _, err := CollectRepoErrorFacts(repo, nil, false)
|
|
if err != nil {
|
|
t.Fatalf("CollectRepoErrorFacts() error = %v", err)
|
|
}
|
|
var boundary facts.ErrorFact
|
|
for _, fact := range errorFacts {
|
|
if fact.Boundary {
|
|
boundary = fact
|
|
}
|
|
}
|
|
if boundary.Command != "wiki +move" || !boundary.HasHint || boundary.HintActionCount == 0 || !boundary.RequiredHint {
|
|
t.Fatalf("wrapped fluent hint boundary not annotated: %#v", errorFacts)
|
|
}
|
|
}
|
|
|
|
func TestMarkBoundaryLineInitializesEmptyBoundaryIndex(t *testing.T) {
|
|
var idx BoundaryIndex
|
|
|
|
markBoundaryLine(&idx, "cmd/demo.go", 12, "demo")
|
|
|
|
command, ok := idx.commandAt("cmd/demo.go", 12)
|
|
if !ok || command != "demo" {
|
|
t.Fatalf("expected initialized boundary command, got %q ok=%v", command, ok)
|
|
}
|
|
}
|