Files
larksuite-cli/lint/errscontract/rules_test.go
evandance c5b5aece33 refactor: retire legacy error envelopes and enforce typed contract (#1449)
* refactor: retire legacy error envelopes and enforce typed contract

Consolidate all command error reporting onto the typed errs.* contract, remove
the legacy error surface that predated it, and tighten the lint guards so the
contract holds across the whole repository going forward.

Every failure now reaches stderr as one envelope shape: a category, an
optional subtype, a human- and agent-readable message, and a recovery hint,
with invalid parameters listed under `params`. The legacy ExitError envelope,
its constructors, and the boundary bridge that promoted untyped config and
authorization errors are deleted, leaving a single path from error to wire.
Predicate commands keep their silent-exit behavior through a dedicated signal
that carries only an exit code.

Infrastructure paths that still emitted ad-hoc envelopes — flag parsing,
unknown commands and subcommands, plugin and policy guards, confirmation
prompts, and auth/config failures — now classify into the same taxonomy.
Business, API, auth, and config exit codes are preserved; the one behavioral
change is that Cobra usage failures (missing required flag, unknown command,
bad arguments) now emit the typed validation envelope and exit 2, matching the
explicit flag and subcommand guards, instead of Cobra's plain-text exit 1.

Enforcement is repo-wide rather than per-path:
- The errscontract guards run by default everywhere instead of through a
  migration allowlist, so legacy envelopes cannot be reintroduced anywhere.
- errorlint runs across the whole repository: every error wrap must use %w and
  every comparison must use errors.Is/errors.As, so interior wraps stay legal
  but can no longer break the chain the typed boundary relies on.
- The errs-no-bare-wrap guard is keyed by structural prefix instead of an
  explicit per-domain allowlist, so new shortcut domains are covered without
  editing a list. It runs where forbidigo is enabled (the shortcut domains and
  the auth/config/service command groups); repo-wide chain integrity for the
  remaining command paths is carried by errorlint above.

* test: align cli_e2e success assertions to the ok envelope

The api and service success path now emits the {"ok":true} envelope, so the
cli_e2e workflow assertions that still expected the old {"code":0} shape via
AssertStdoutStatus(t, 0) fail once they run with live credentials. Switch those
workflow assertions to AssertStdoutStatus(t, true); the fake-payload helper test
in core_test.go keeps its code-shape assertion.
2026-06-17 19:42:38 +08:00

1286 lines
37 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package errscontract
import (
"strings"
"testing"
)
// 4 source-level rules:
// (B) typed Error must embed Problem → REJECT
// (C) no service-side mergeCodeMeta / registrar → REJECT
// (D) Subtype: "ad_hoc_*" literal → LABEL (governance signal)
// (E) Subtype value not in declared allowlist → REJECT / LABEL / WARNING
func TestCheckProblemEmbed_RejectsMissingProblemEmbed(t *testing.T) {
src := `package errs
type FrobnicateError struct {
Code int
Msg string
}
`
v := CheckProblemEmbed("errs/types.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Message, "FrobnicateError") {
t.Errorf("message should name the violating type: %s", v[0].Message)
}
}
func TestCheckProblemEmbed_AcceptsPackageLocalEmbed(t *testing.T) {
src := `package errs
type Problem struct{}
type GoodError struct {
Problem
Extra string
}
`
v := CheckProblemEmbed("errs/types.go", src)
if len(v) != 0 {
t.Errorf("compliant struct should pass, got: %+v", v)
}
}
func TestCheckProblemEmbed_AcceptsImportedEmbed(t *testing.T) {
// `errs.Problem` selector form: used by re-export packages.
src := `package alias
import "github.com/larksuite/cli/errs"
type GoodError struct {
errs.Problem
Extra string
}
`
v := CheckProblemEmbed("internal/alias/x.go", src)
if len(v) != 0 {
t.Errorf("imported-embed should pass, got: %+v", v)
}
}
func TestCheckProblemEmbed_RejectsSecurityPolicyErrorWithoutProblem(t *testing.T) {
// Production SecurityPolicyError embeds Problem (see errs/types.go); the
// previous CheckProblemEmbed whitelist for this type was dead code that would also
// mask a future regression where the embed gets dropped.
src := `package errs
type SecurityPolicyError struct {
ChallengeURL string
}
`
v := CheckProblemEmbed("errs/types.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Message, "SecurityPolicyError") {
t.Errorf("message should name the violating type: %s", v[0].Message)
}
}
func TestCheckProblemEmbed_AcceptsSecurityPolicyErrorWithProblem(t *testing.T) {
// Mirrors the real errs/types.go declaration — must pass with no violation.
src := `package errs
type Problem struct{}
type SecurityPolicyError struct {
Problem
ChallengeURL string
}
`
v := CheckProblemEmbed("errs/types.go", src)
if len(v) != 0 {
t.Errorf("compliant SecurityPolicyError must pass, got: %+v", v)
}
}
func TestCheckNoRegistrar_RejectsMergeCodeMetaInShortcuts(t *testing.T) {
src := `package task
func init() {
mergeCodeMeta(taskMap, "task")
}
var taskMap = map[int]any{}
`
v := CheckNoRegistrar("shortcuts/task/init.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Message, "mergeCodeMeta") {
t.Errorf("message must name the offending call: %s", v[0].Message)
}
if !strings.Contains(v[0].Suggestion, "internal/errclass/codemeta_") {
t.Errorf("suggestion must point to the right location: %s", v[0].Suggestion)
}
}
func TestCheckNoRegistrar_RejectsRegisterServiceMapInInternal(t *testing.T) {
src := `package auth
import "github.com/larksuite/cli/internal/output"
func init() {
output.RegisterServiceMap("auth", nil)
}
`
v := CheckNoRegistrar("internal/auth/init.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if !strings.Contains(v[0].Message, "RegisterServiceMap") {
t.Errorf("message must name the offending call: %s", v[0].Message)
}
}
func TestCheckNoRegistrar_AllowsInternalErrclass(t *testing.T) {
// internal/errclass legitimately owns mergeCodeMeta; rule must not fire here.
src := `package errclass
func init() {
mergeCodeMeta(taskCodeMeta, "task")
}
var taskCodeMeta = map[int]any{}
`
v := CheckNoRegistrar("internal/errclass/codemeta_task.go", src)
if len(v) != 0 {
t.Errorf("internal/errclass must be exempt, got: %+v", v)
}
}
func TestCheckNoRegistrar_IgnoresTestFiles(t *testing.T) {
src := `package task_test
func TestFoo(t *testing.T) {
mergeCodeMeta(nil, "fixture")
}
`
v := CheckNoRegistrar("shortcuts/task/init_test.go", src)
if len(v) != 0 {
t.Errorf("test fixtures must be exempt, got: %+v", v)
}
}
func TestCheckNoRegistrar_IgnoresCmdAndRoot(t *testing.T) {
src := `package main
func init() {
mergeCodeMeta(nil, "x")
}
`
v := CheckNoRegistrar("cmd/foo/main.go", src)
if len(v) != 0 {
t.Errorf("cmd/ paths are out of CheckNoRegistrar scope, got: %+v", v)
}
}
func TestCheckAdHocSubtype_EmitsLabel(t *testing.T) {
src := `package task
func makeErr() any {
return struct{ Subtype string }{Subtype: "ad_hoc_task_quota_breach"}
}
`
v := CheckAdHocSubtype("shortcuts/task/quota.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionLabel {
t.Errorf("action = %q, want LABEL (ad_hoc_* is soft governance signal)", v[0].Action)
}
if !strings.Contains(v[0].Message, "needs-taxonomy-decision") {
t.Errorf("message should carry the label prefix so CI can grep it: %s", v[0].Message)
}
if !strings.Contains(v[0].Suggestion, "1 week") {
t.Errorf("suggestion should state the ad_hoc_* promotion window: %s", v[0].Suggestion)
}
}
func TestCheckAdHocSubtype_DetectsCastForm(t *testing.T) {
// Subtype field assigned via errs.Subtype("ad_hoc_xxx") cast.
src := `package task
type problem struct{ Subtype any }
var _ = problem{Subtype: Subtype("ad_hoc_new_feature")}
func Subtype(s string) string { return s }
`
v := CheckAdHocSubtype("shortcuts/task/x.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionLabel {
t.Errorf("cast form must also LABEL, got %q", v[0].Action)
}
}
func TestCheckDeclaredSubtype(t *testing.T) {
allowlist := map[string]struct{}{
"missing_scope": {},
"rate_limit": {},
"invalid_parameters": {},
}
cases := []struct {
name string
src string
wantAction Action
wantInMsg string
}{
{
name: "named_const_selector_accepted",
src: `package x
import "github.com/larksuite/cli/errs"
var _ = struct{ Subtype errs.Subtype }{Subtype: errs.SubtypeMissingScope}
`,
wantAction: "",
},
{
name: "literal_in_allowlist_accepted",
src: `package x
var _ = struct{ Subtype string }{Subtype: "missing_scope"}
`,
wantAction: "",
},
{
name: "undeclared_literal_rejected",
src: `package x
var _ = struct{ Subtype string }{Subtype: "my_custom_thing"}
`,
wantAction: ActionReject,
wantInMsg: "my_custom_thing",
},
{
name: "undeclared_via_cast_rejected",
src: `package x
import "github.com/larksuite/cli/errs"
var _ = struct{ Subtype errs.Subtype }{Subtype: errs.Subtype("custom_value")}
`,
wantAction: ActionReject,
wantInMsg: "custom_value",
},
{
name: "ad_hoc_does_not_fire_in_rule_e",
src: `package x
var _ = struct{ Subtype string }{Subtype: "ad_hoc_thing"}
`,
// CheckDeclaredSubtype hands ad_hoc_* off to CheckAdHocSubtype — returns no E-class violation.
wantAction: "",
},
{
name: "dynamic_local_var_warns",
src: `package x
var loc = "x"
var _ = struct{ Subtype string }{Subtype: loc}
`,
wantAction: ActionWarning,
wantInMsg: "manual review",
},
{
name: "dynamic_cast_warns",
src: `package x
import "github.com/larksuite/cli/errs"
func f(raw string) { _ = struct{ Subtype errs.Subtype }{Subtype: errs.Subtype(raw)} }
`,
wantAction: ActionWarning,
wantInMsg: "non-literal",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v := CheckDeclaredSubtype("x.go", tc.src, allowlist)
if tc.wantAction == "" {
if len(v) != 0 {
t.Fatalf("expected pass, got %d violations: %+v", len(v), v)
}
return
}
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != tc.wantAction {
t.Errorf("action = %q, want %q", v[0].Action, tc.wantAction)
}
if tc.wantInMsg != "" && !strings.Contains(v[0].Message, tc.wantInMsg) {
t.Errorf("message %q lacks expected substring %q", v[0].Message, tc.wantInMsg)
}
})
}
}
// TestCheckDeclaredSubtype_DetectsPositionalCodeMetaLiteral pins that codemeta_task.go and
// codemeta.go use positional `{cat, subtype, retryable}` literals inside a
// `map[int]CodeMeta{...}` — element [1] is the Subtype slot. The AST walker
// must recognise the positional form; otherwise an undeclared subtype cast
// here would bypass CheckDeclaredSubtype.
func TestCheckDeclaredSubtype_DetectsPositionalCodeMetaLiteral(t *testing.T) {
allowlist := map[string]struct{}{
"missing_scope": {},
}
src := `package output
import "github.com/larksuite/cli/errs"
type CodeMeta struct {
Category errs.Category
Subtype errs.Subtype
Retryable bool
}
var m = map[int]CodeMeta{
1: {errs.CategoryAPI, errs.Subtype("totally_bogus_undeclared"), false},
}
`
v := CheckDeclaredSubtype("internal/output/codemeta_test_fixture.go", src, allowlist)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Message, "totally_bogus_undeclared") {
t.Errorf("message should name the violating subtype: %s", v[0].Message)
}
}
// TestCheckDeclaredSubtype_AcceptsPositionalCodeMetaLiteral: same positional form but the
// Subtype literal is in the allowlist — no violation should fire.
func TestCheckDeclaredSubtype_AcceptsPositionalCodeMetaLiteral(t *testing.T) {
allowlist := map[string]struct{}{
"missing_scope": {},
}
src := `package output
import "github.com/larksuite/cli/errs"
type CodeMeta struct {
Category errs.Category
Subtype errs.Subtype
Retryable bool
}
var m = map[int]CodeMeta{
1: {errs.CategoryAuthorization, errs.SubtypeMissingScope, false},
2: {errs.CategoryAuthorization, errs.Subtype("missing_scope"), false},
}
`
v := CheckDeclaredSubtype("internal/output/codemeta_test_fixture.go", src, allowlist)
if len(v) != 0 {
t.Errorf("allowlisted subtypes in positional form must pass, got: %+v", v)
}
}
// TestCheckDeclaredSubtype_DetectsPositionalCodeMetaLiteralInSlice: covers the slice form
// `[]CodeMeta{{cat, subtype, retryable}}` so other call-site shapes are also
// guarded.
func TestCheckDeclaredSubtype_DetectsPositionalCodeMetaLiteralInSlice(t *testing.T) {
allowlist := map[string]struct{}{
"missing_scope": {},
}
src := `package output
import "github.com/larksuite/cli/errs"
type CodeMeta struct {
Category errs.Category
Subtype errs.Subtype
Retryable bool
}
var s = []CodeMeta{
{errs.CategoryAPI, errs.Subtype("undeclared_via_slice"), false},
}
`
v := CheckDeclaredSubtype("internal/output/codemeta_test_fixture.go", src, allowlist)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if !strings.Contains(v[0].Message, "undeclared_via_slice") {
t.Errorf("message should name the violating subtype: %s", v[0].Message)
}
}
// TestCheckDeclaredSubtype_WithNames_RejectsTypoedSelector pins the strengthened CheckDeclaredSubtype:
// when a nameset is supplied, selectors like `errs.SubtypeBogus` that satisfy
// the "Subtype*" prefix but reference no declared constant must REJECT. The
// nil-nameset path preserves the legacy prefix-only acceptance.
func TestCheckDeclaredSubtype_WithNames_RejectsTypoedSelector(t *testing.T) {
allowlist := map[string]struct{}{"missing_scope": {}}
nameset := map[string]struct{}{"SubtypeMissingScope": {}}
// Typo'd selector — REJECT under strengthened rule.
src := `package x
import "github.com/larksuite/cli/errs"
var _ = struct{ Subtype errs.Subtype }{Subtype: errs.SubtypeBogus}
`
v := CheckDeclaredSubtypeWithNames("x.go", src, allowlist, nameset)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Message, "SubtypeBogus") {
t.Errorf("message should name the offending selector: %s", v[0].Message)
}
// Same source, nil nameset → legacy prefix-only path, no violation.
v2 := CheckDeclaredSubtypeWithNames("x.go", src, allowlist, nil)
if len(v2) != 0 {
t.Errorf("nil nameset must preserve legacy prefix acceptance, got: %+v", v2)
}
}
// TestCheckDeclaredSubtype_WithNames_AcceptsDeclaredSelector: declared selector with nameset
// supplied must still pass.
func TestCheckDeclaredSubtype_WithNames_AcceptsDeclaredSelector(t *testing.T) {
allowlist := map[string]struct{}{"missing_scope": {}}
nameset := map[string]struct{}{"SubtypeMissingScope": {}}
src := `package x
import "github.com/larksuite/cli/errs"
var _ = struct{ Subtype errs.Subtype }{Subtype: errs.SubtypeMissingScope}
`
v := CheckDeclaredSubtypeWithNames("x.go", src, allowlist, nameset)
if len(v) != 0 {
t.Errorf("declared selector must pass, got: %+v", v)
}
}
// TestCheckDeclaredSubtype_WithNames_RejectsTypoedIdent: in-package identifier form (no errs.
// prefix) must also be checked against the nameset.
func TestCheckDeclaredSubtype_WithNames_RejectsTypoedIdent(t *testing.T) {
allowlist := map[string]struct{}{"missing_scope": {}}
nameset := map[string]struct{}{"SubtypeMissingScope": {}}
src := `package errs
type Subtype string
type myErr struct{ Subtype Subtype }
var _ = myErr{Subtype: SubtypeNotDeclared}
`
v := CheckDeclaredSubtypeWithNames("internal/x.go", src, allowlist, nameset)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if !strings.Contains(v[0].Message, "SubtypeNotDeclared") {
t.Errorf("message should name the offending identifier: %s", v[0].Message)
}
}
func TestCheckDeclaredSubtype_NilAllowlist_IsNoOp(t *testing.T) {
// Caller can disable CheckDeclaredSubtype by passing nil; that should not panic and must
// not emit any E-class violation, even on undeclared subtypes.
src := `package x
var _ = struct{ Subtype string }{Subtype: "anything"}
`
v := CheckDeclaredSubtype("x.go", src, nil)
if len(v) != 0 {
t.Errorf("nil allowlist must disable CheckDeclaredSubtype, got: %+v", v)
}
}
// TestRunAll_OneFileFourViolations exercises the combined entry point: a
// synthetic file under shortcuts/ that violates B, C, D, and E together.
func TestRunAll_OneFileFourViolations(t *testing.T) {
// Path is shortcuts/* so CheckNoRegistrar fires; file declared in errs-like package
// header is irrelevant for B (we test B in errs/ files only via path).
src := `package task
type LooseError struct{}
func init() {
mergeCodeMeta(nil, "task")
}
var _ = struct{ Subtype string }{Subtype: "ad_hoc_thing"}
var _ = struct{ Subtype string }{Subtype: "bogus"}
`
allowlist := map[string]struct{}{
"missing_scope": {},
}
v := RunAll("shortcuts/task/all_bad.go", src, allowlist)
byRule := map[string]int{}
byAction := map[Action]int{}
for _, vv := range v {
byRule[vv.Rule]++
byAction[vv.Action]++
}
// CheckProblemEmbed is path-scoped to errs/, so it does NOT fire on shortcuts/.
if byRule["problem_embed"] != 0 {
t.Errorf("CheckProblemEmbed should not fire outside errs/, got %d", byRule["problem_embed"])
}
if byRule["no_registrar"] != 1 {
t.Errorf("CheckNoRegistrar count = %d, want 1", byRule["no_registrar"])
}
if byRule["adhoc_subtype"] != 1 {
t.Errorf("CheckAdHocSubtype count = %d, want 1", byRule["adhoc_subtype"])
}
if byRule["declared_subtype"] != 1 {
t.Errorf("CheckDeclaredSubtype count = %d, want 1", byRule["declared_subtype"])
}
if byAction[ActionReject] != 2 {
t.Errorf("REJECT count = %d, want 2 (Rules C+E)", byAction[ActionReject])
}
if byAction[ActionLabel] != 1 {
t.Errorf("LABEL count = %d, want 1 (CheckAdHocSubtype)", byAction[ActionLabel])
}
}
func TestRunAll_ErrsPathRunsRuleB(t *testing.T) {
src := `package errs
type NoEmbedError struct {
Code int
}
`
v := RunAll("errs/types.go", src, nil)
if len(v) != 1 || v[0].Rule != "problem_embed" {
t.Fatalf("expected one CheckProblemEmbed violation, got %+v", v)
}
}
// TestCheckProblemEmbed_SkipsUnexportedErrorType pins that CheckProblemEmbed only
// enforces the Problem embed on EXPORTED *Error types — unexported helper
// types that happen to end in "Error" are not part of the public taxonomy
// and would create false-positive REJECT violations.
func TestCheckProblemEmbed_SkipsUnexportedErrorType(t *testing.T) {
src := `package internal
type myInternalError struct {
Code int
Msg string
}
`
v := CheckProblemEmbed("internal/foo/internal.go", src)
if len(v) != 0 {
t.Errorf("expected 0 violations for unexported helper, got %d: %+v", len(v), v)
}
}
// TestCheckNoRegistrar_CatchesMiddleAffix pins that the registrar matcher
// catches RegisterServiceMap even when it has affixes on both sides — the
// older prefix-or-suffix-only check would have missed FooRegisterServiceMapBar.
func TestCheckNoRegistrar_CatchesMiddleAffix(t *testing.T) {
src := `package auth
func init() {
FooRegisterServiceMapBar("auth", nil)
}
func FooRegisterServiceMapBar(name string, _ interface{}) {}
`
v := CheckNoRegistrar("internal/auth/init.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation for middle-affix registrar, got %d: %+v", len(v), v)
}
if !strings.Contains(v[0].Message, "FooRegisterServiceMapBar") {
t.Errorf("message must name the offending call: %s", v[0].Message)
}
}
// (F) direct legacy output.ExitError / output.ErrDetail literals on migrated
// paths → REJECT; output.ErrBare(...) calls and non-migrated paths pass.
func TestCheckNoLegacyEnvelopeLiteral_RejectsExitErrorLiteralOnDrivePath(t *testing.T) {
src := `package drive
import "github.com/larksuite/cli/internal/output"
func boom() error {
return &output.ExitError{Code: 1}
}
`
v := CheckNoLegacyEnvelopeLiteral("shortcuts/drive/drive_export.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Message, "ExitError") {
t.Errorf("message should name the legacy type: %s", v[0].Message)
}
}
func TestCheckNoLegacyEnvelopeLiteral_RejectsExitErrorLiteralOnMigratedShortcutPaths(t *testing.T) {
for _, path := range []string{
"shortcuts/markdown/markdown_fetch.go",
"shortcuts/okr/okr_image_upload.go",
"shortcuts/task/task_update.go",
"shortcuts/whiteboard/whiteboard_update.go",
} {
t.Run(path, func(t *testing.T) {
src := `package migrated
import "github.com/larksuite/cli/internal/output"
func boom() error {
return &output.ExitError{Code: 1}
}
`
v := CheckNoLegacyEnvelopeLiteral(path, src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Message, "ExitError") {
t.Errorf("message should name the legacy type: %s", v[0].Message)
}
})
}
}
func TestCheckNoLegacyEnvelopeLiteral_RejectsErrDetailLiteralOnDrivePath(t *testing.T) {
src := `package drive
import "github.com/larksuite/cli/internal/output"
func boom() *output.ErrDetail {
return &output.ErrDetail{Code: 7}
}
`
v := CheckNoLegacyEnvelopeLiteral("shortcuts/drive/drive_export_common.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if !strings.Contains(v[0].Message, "ErrDetail") {
t.Errorf("message should name the legacy type: %s", v[0].Message)
}
}
func TestCheckNoLegacyEnvelopeLiteral_AllowsErrBareCallOnDrivePath(t *testing.T) {
// output.ErrBare(...) is a CallExpr, not a CompositeLit — must NOT fire.
src := `package drive
import "github.com/larksuite/cli/internal/output"
func boom() error {
return output.ErrBare(output.ExitAPI)
}
`
v := CheckNoLegacyEnvelopeLiteral("shortcuts/drive/drive_export.go", src)
if len(v) != 0 {
t.Errorf("ErrBare call should pass, got: %+v", v)
}
}
func TestCheckNoLegacyEnvelopeLiteral_FiresOnAnyPath(t *testing.T) {
// The guard is now repo-wide: any .go path that re-introduces the legacy
// literal is flagged, regardless of domain.
for _, path := range []string{
"shortcuts/im/im_send.go",
"shortcuts/some_new_domain/foo.go",
"internal/auth/login.go",
"cmd/config/bind.go",
} {
t.Run(path, func(t *testing.T) {
src := `package other
import "github.com/larksuite/cli/internal/output"
func boom() error {
return &output.ExitError{Code: 1}
}
`
v := CheckNoLegacyEnvelopeLiteral(path, src)
if len(v) != 1 {
t.Fatalf("expected 1 violation on %s, got %d: %+v", path, len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
})
}
}
func TestCheckNoLegacyEnvelopeLiteral_SkipsTestFiles(t *testing.T) {
src := `package drive
import "github.com/larksuite/cli/internal/output"
func boom() error {
return &output.ExitError{Code: 1}
}
`
v := CheckNoLegacyEnvelopeLiteral("shortcuts/drive/drive_export_test.go", src)
if len(v) != 0 {
t.Errorf("_test.go file should be skipped, got: %+v", v)
}
}
// TestCheckNoLegacyEnvelopeLiteral_RejectsAliasedImport pins that an aliased
// import of internal/output cannot bypass the rule: the qualifier is resolved
// from the import declaration, not matched against the literal string "output".
func TestCheckNoLegacyEnvelopeLiteral_RejectsAliasedImport(t *testing.T) {
src := `package drive
import legacy "github.com/larksuite/cli/internal/output"
func boom() error {
return &legacy.ExitError{Code: 1}
}
`
v := CheckNoLegacyEnvelopeLiteral("shortcuts/drive/drive_export.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation for aliased import, got %d: %+v", len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Message, "ExitError") {
t.Errorf("message should name the legacy type: %s", v[0].Message)
}
}
// TestCheckNoLegacyEnvelopeLiteral_NormalImportStillRejected guards against a
// regression where resolving by import path accidentally drops the default
// (non-aliased) `output` case.
func TestCheckNoLegacyEnvelopeLiteral_NormalImportStillRejected(t *testing.T) {
src := `package drive
import "github.com/larksuite/cli/internal/output"
func boom() error {
return &output.ExitError{Code: 1}
}
`
v := CheckNoLegacyEnvelopeLiteral("shortcuts/drive/drive_export.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation for default import, got %d: %+v", len(v), v)
}
}
// TestCheckNoLegacyEnvelopeLiteral_ErrBareAliasedStillAllowed: output.ErrBare is
// a CallExpr, not a composite literal — even under an alias it must not fire.
func TestCheckNoLegacyEnvelopeLiteral_ErrBareAliasedStillAllowed(t *testing.T) {
src := `package drive
import legacy "github.com/larksuite/cli/internal/output"
func boom() error {
return legacy.ErrBare(legacy.ExitAPI)
}
`
v := CheckNoLegacyEnvelopeLiteral("shortcuts/drive/drive_export.go", src)
if len(v) != 0 {
t.Errorf("ErrBare call should pass, got: %+v", v)
}
}
// TestCheckNoLegacyEnvelopeLiteral_RejectsDotImport: a dot-import surfaces
// ExitError / ErrDetail as bare unqualified idents; the rule must still catch
// the composite literal.
func TestCheckNoLegacyEnvelopeLiteral_RejectsDotImport(t *testing.T) {
src := `package drive
import . "github.com/larksuite/cli/internal/output"
func boom() error {
return &ExitError{Code: 1}
}
`
v := CheckNoLegacyEnvelopeLiteral("shortcuts/drive/drive_export.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation for dot-import, got %d: %+v", len(v), v)
}
if !strings.Contains(v[0].Message, "ExitError") {
t.Errorf("message should name the legacy type: %s", v[0].Message)
}
}
// TestCheckNoLegacyEnvelopeLiteral_UnrelatedSelectorPasses: a same-named
// selector on an unrelated package (not the legacy output import path) must not
// trigger a false positive.
func TestCheckNoLegacyEnvelopeLiteral_UnrelatedSelectorPasses(t *testing.T) {
src := `package drive
import "example.com/other/output"
func boom() error {
return &output.ExitError{Code: 1}
}
`
v := CheckNoLegacyEnvelopeLiteral("shortcuts/drive/drive_export.go", src)
if len(v) != 0 {
t.Errorf("unrelated package selector must not fire, got: %+v", v)
}
}
func TestCheckNoLegacyRuntimeAPICall_RejectsCallAPIOnDrivePath(t *testing.T) {
src := `package drive
import "github.com/larksuite/cli/shortcuts/common"
func boom(runtime *common.RuntimeContext) error {
_, err := runtime.CallAPI("POST", "/x", nil, nil)
return err
}
`
v := CheckNoLegacyRuntimeAPICall("shortcuts/drive/drive_create_folder.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Message, "CallAPI") {
t.Errorf("message should name the legacy method: %s", v[0].Message)
}
}
func TestCheckNoLegacyRuntimeAPICall_RejectsCallAPIOnTaskPath(t *testing.T) {
src := `package task
import "github.com/larksuite/cli/shortcuts/common"
func boom(runtime *common.RuntimeContext) error {
_, err := runtime.CallAPI("POST", "/x", nil, nil)
return err
}
`
v := CheckNoLegacyRuntimeAPICall("shortcuts/task/task_update.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Message, "CallAPI") {
t.Errorf("message should name the legacy method: %s", v[0].Message)
}
}
func TestCheckNoLegacyRuntimeAPICall_RejectsDoAPIJSONWithLogIDOnDrivePath(t *testing.T) {
src := `package drive
import "github.com/larksuite/cli/shortcuts/common"
func boom(runtime *common.RuntimeContext) error {
_, err := runtime.DoAPIJSONWithLogID("POST", "/x", nil, nil)
return err
}
`
v := CheckNoLegacyRuntimeAPICall("shortcuts/drive/drive_export.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if !strings.Contains(v[0].Message, "DoAPIJSONWithLogID") {
t.Errorf("message should name the legacy method: %s", v[0].Message)
}
}
func TestCheckNoLegacyRuntimeAPICall_AllowsTypedWrapperCall(t *testing.T) {
// driveCallAPI is an unqualified call (*ast.Ident), not a selector — must NOT fire.
src := `package drive
func boom(runtime *common.RuntimeContext) error {
_, err := driveCallAPI(runtime, "POST", "/x", nil, nil)
return err
}
`
v := CheckNoLegacyRuntimeAPICall("shortcuts/drive/drive_create_folder.go", src)
if len(v) != 0 {
t.Errorf("typed wrapper call must not fire, got: %+v", v)
}
}
func TestCheckNoLegacyRuntimeAPICall_AllowsRawAPIAndDoAPI(t *testing.T) {
// RawAPI / DoAPI return the raw response for the caller to classify and do
// not emit a legacy envelope — they are not banned.
src := `package drive
func boom(runtime *common.RuntimeContext) error {
_, _ = runtime.RawAPI("POST", "/x", nil, nil)
_, err := runtime.DoAPI(nil)
return err
}
`
v := CheckNoLegacyRuntimeAPICall("shortcuts/drive/drive_api.go", src)
if len(v) != 0 {
t.Errorf("RawAPI / DoAPI must not fire, got: %+v", v)
}
}
func TestCheckNoLegacyRuntimeAPICall_FiresOnAnyCommonImportingPath(t *testing.T) {
// The guard is now repo-wide: any path importing shortcuts/common that
// re-introduces a legacy runtime call is flagged, regardless of domain.
for _, path := range []string{
"shortcuts/im/im_send.go",
"shortcuts/some_new_domain/sample.go",
"internal/cmdutil/helper.go",
} {
t.Run(path, func(t *testing.T) {
src := `package contact
import "github.com/larksuite/cli/shortcuts/common"
func boom(runtime *common.RuntimeContext) error {
_, err := runtime.CallAPI("POST", "/x", nil, nil)
return err
}
`
v := CheckNoLegacyRuntimeAPICall(path, src)
if len(v) != 1 {
t.Fatalf("expected 1 violation on %s, got %d: %+v", path, len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
})
}
}
func TestCheckNoLegacyRuntimeAPICall_SkipsFilesWithoutCommonImport(t *testing.T) {
// The import gate stays: without a shortcuts/common import, a same-named
// CallAPI method on another receiver is not the legacy RuntimeContext helper.
src := `package contact
func boom(runtime *common.RuntimeContext) error {
_, err := runtime.CallAPI("POST", "/x", nil, nil)
return err
}
`
v := CheckNoLegacyRuntimeAPICall("shortcuts/some_new_domain/sample.go", src)
if len(v) != 0 {
t.Errorf("file without shortcuts/common import must not fire, got: %+v", v)
}
}
func TestCheckNoLegacyRuntimeAPICall_SkipsTestFiles(t *testing.T) {
src := `package drive
func boom(runtime *common.RuntimeContext) error {
_, err := runtime.CallAPI("POST", "/x", nil, nil)
return err
}
`
v := CheckNoLegacyRuntimeAPICall("shortcuts/drive/drive_create_folder_test.go", src)
if len(v) != 0 {
t.Errorf("test files must be skipped, got: %+v", v)
}
}
func TestCheckNoLegacyCommonHelperCall_RejectsLegacyHelpersOnMigratedPath(t *testing.T) {
helpers := []string{
"FlagErrorf",
"MutuallyExclusive",
"AtLeastOne",
"ExactlyOne",
"ValidatePageSize",
"ValidateChatID",
"ValidateUserID",
"ValidateSafePath",
"RejectDangerousChars",
"WrapInputStatError",
"WrapSaveErrorByCategory",
"ResolveOpenIDs",
"HandleApiResult",
}
paths := []string{
"shortcuts/doc/docs_fetch_v2.go",
"shortcuts/drive/drive_search.go",
"shortcuts/im/im_messages_send.go",
"shortcuts/mail/mail_send.go",
"shortcuts/markdown/markdown_fetch.go",
"shortcuts/okr/okr_progress_create.go",
"shortcuts/sheets/helpers.go",
"shortcuts/slides/slides_create.go",
"shortcuts/task/task_update.go",
"shortcuts/whiteboard/whiteboard_query.go",
"shortcuts/wiki/wiki_node_get.go",
}
for _, path := range paths {
for _, helper := range helpers {
t.Run(path+"_"+helper, func(t *testing.T) {
src := `package migrated
import "github.com/larksuite/cli/shortcuts/common"
func boom() {
common.` + helper + `()
}
`
v := CheckNoLegacyCommonHelperCall(path, src)
if len(v) != 1 {
t.Fatalf("expected 1 violation for %s on %s, got %d: %+v", helper, path, len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Message, "common."+helper) {
t.Errorf("message should name helper %s: %s", helper, v[0].Message)
}
})
}
}
}
func TestCheckNoLegacyCommonHelperCall_RejectsDangerousCharsOnCalendarPath(t *testing.T) {
src := `package calendar
import "github.com/larksuite/cli/shortcuts/common"
func boom() {
common.RejectDangerousChars("--summary", "x")
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/calendar/calendar_create.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation, got %d: %+v", len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
if !strings.Contains(v[0].Suggestion, "common.RejectDangerousCharsTyped") {
t.Errorf("suggestion should name typed replacement, got: %s", v[0].Suggestion)
}
}
func TestCheckNoLegacyCommonHelperCall_CoversDocPathWithAliasAndFunctionValue(t *testing.T) {
src := `package migrated
import c "github.com/larksuite/cli/shortcuts/common"
func boom() {
f := c.FlagErrorf
_ = f
c.WrapInputStatError(nil)
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/doc/docs_fetch_v2.go", src)
if len(v) != 2 {
t.Fatalf("expected 2 violations for aliased/function-value legacy helpers on doc path, got %d: %+v", len(v), v)
}
}
func TestCheckNoLegacyCommonHelperCall_CoversSheetsPathWithAliasAndFunctionValue(t *testing.T) {
src := `package migrated
import c "github.com/larksuite/cli/shortcuts/common"
func boom() {
f := c.FlagErrorf
_ = f
c.WrapInputStatError(nil)
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/sheets/helpers.go", src)
if len(v) != 2 {
t.Fatalf("expected 2 violations for aliased/function-value legacy helpers on sheets path, got %d: %+v", len(v), v)
}
}
func TestCheckNoLegacyCommonHelperCall_CoversSlidesPathWithAliasAndFunctionValue(t *testing.T) {
src := `package migrated
import c "github.com/larksuite/cli/shortcuts/common"
func boom() {
f := c.FlagErrorf
_ = f
c.WrapInputStatError(nil)
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/slides/slides_create.go", src)
if len(v) != 2 {
t.Fatalf("expected 2 violations for aliased/function-value legacy helpers on slides path, got %d: %+v", len(v), v)
}
}
func TestCheckNoLegacyCommonHelperCall_CoversMarkdownPathWithAliasAndFunctionValue(t *testing.T) {
src := `package migrated
import c "github.com/larksuite/cli/shortcuts/common"
func boom() {
f := c.FlagErrorf
_ = f
c.WrapInputStatError(nil)
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/markdown/markdown_fetch.go", src)
if len(v) != 2 {
t.Fatalf("expected 2 violations for aliased/function-value legacy helpers on markdown path, got %d: %+v", len(v), v)
}
}
func TestCheckNoLegacyCommonHelperCall_CoversWikiPathWithAliasAndFunctionValue(t *testing.T) {
src := `package migrated
import c "github.com/larksuite/cli/shortcuts/common"
func boom() {
f := c.FlagErrorf
_ = f
c.WrapInputStatError(nil)
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/wiki/wiki_node_get.go", src)
if len(v) != 2 {
t.Fatalf("expected 2 violations for aliased/function-value legacy helpers on wiki path, got %d: %+v", len(v), v)
}
}
func TestCheckNoLegacyCommonHelperCall_FiresOnAnyPath(t *testing.T) {
// The guard is now repo-wide: re-introducing a legacy common helper is
// flagged regardless of domain.
for _, path := range []string{
"shortcuts/im/im_send.go",
"shortcuts/some_new_domain/sample.go",
"internal/cmdutil/helper.go",
} {
t.Run(path, func(t *testing.T) {
src := `package contact
import "github.com/larksuite/cli/shortcuts/common"
func boom() {
common.FlagErrorf("relapse")
}
`
v := CheckNoLegacyCommonHelperCall(path, src)
if len(v) != 1 {
t.Fatalf("expected 1 violation on %s, got %d: %+v", path, len(v), v)
}
if v[0].Action != ActionReject {
t.Errorf("action = %q, want REJECT", v[0].Action)
}
})
}
}
func TestCheckNoLegacyCommonHelperCall_RejectsReintroducedUploadAndCallAPIHelpers(t *testing.T) {
// The three relapse-guard entries added when the legacy bodies were deleted:
// re-introducing a same-named helper must be rejected with a typed pointer.
cases := []struct {
helper string
wantInSugg string
}{
{"UploadDriveMediaAll", "common.UploadDriveMediaAllTyped"},
{"UploadDriveMediaMultipart", "common.UploadDriveMediaMultipartTyped"},
{"CallAPI", "runtime.CallAPITyped"},
}
for _, tc := range cases {
t.Run(tc.helper, func(t *testing.T) {
src := `package drive
import "github.com/larksuite/cli/shortcuts/common"
func boom() {
common.` + tc.helper + `()
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/drive/drive_upload.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation for %s, got %d: %+v", tc.helper, len(v), v)
}
if !strings.Contains(v[0].Suggestion, tc.wantInSugg) {
t.Errorf("suggestion should name typed replacement %q, got: %s", tc.wantInSugg, v[0].Suggestion)
}
})
}
}
func TestCheckNoLegacyCommonHelperCall_AllowsTypedHelpersOnMigratedPath(t *testing.T) {
src := `package drive
import "github.com/larksuite/cli/shortcuts/common"
func boom() {
common.ValidationErrorf("typed")
common.MutuallyExclusiveTyped(nil, "a", "b")
common.ValidateChatIDTyped("--chat-ids", "oc_abc")
common.ResolveOpenIDsTyped("--user-ids", nil, nil)
common.WrapSaveErrorTyped(nil)
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/drive/drive_search.go", src)
if len(v) != 0 {
t.Errorf("typed helpers must pass, got: %+v", v)
}
}
func TestCheckNoLegacyCommonHelperCall_RejectsAliasedImport(t *testing.T) {
src := `package drive
import c "github.com/larksuite/cli/shortcuts/common"
func boom() {
c.FlagErrorf("legacy")
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/drive/drive_search.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation for aliased common import, got %d: %+v", len(v), v)
}
}
func TestCheckNoLegacyCommonHelperCall_RejectsDotImport(t *testing.T) {
src := `package drive
import . "github.com/larksuite/cli/shortcuts/common"
func boom() {
FlagErrorf("legacy")
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/drive/drive_search.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation for dot-imported common, got %d: %+v", len(v), v)
}
}
func TestCheckNoLegacyCommonHelperCall_RejectsFunctionValueReference(t *testing.T) {
src := `package drive
import "github.com/larksuite/cli/shortcuts/common"
func boom() error {
f := common.FlagErrorf
return f("legacy")
}
`
v := CheckNoLegacyCommonHelperCall("shortcuts/drive/drive_search.go", src)
if len(v) != 1 {
t.Fatalf("expected 1 violation for function-value reference, got %d: %+v", len(v), v)
}
}
func TestCheckNoLegacyRuntimeAPICall_SkipsNonCommonReceiver(t *testing.T) {
// The event domain's APIClient interface has a same-named CallAPI method
// whose implementation classifies into typed errs.* errors; without the
// shortcuts/common import the call cannot be the legacy RuntimeContext
// helper and must not fire.
src := `package vc
import "github.com/larksuite/cli/internal/event"
func boom(rt event.APIClient) error {
_, err := rt.CallAPI(nil, "POST", "/x", nil)
return err
}
`
v := CheckNoLegacyRuntimeAPICall("events/vc/preconsume.go", src)
if len(v) != 0 {
t.Errorf("non-common CallAPI receiver must not fire, got: %+v", v)
}
}