feat(base): add dashboard and form share shortcuts (#2282)
- preserve explicit false values and validate partial share updates - add dry-run and deployment-gated live E2E coverage - document share routing in the bundled Base skill
This commit is contained in:
@@ -172,8 +172,8 @@ func TestShortcutsCatalog(t *testing.T) {
|
||||
"+data-query",
|
||||
"+form-create", "+form-delete", "+form-list", "+form-update", "+form-get", "+form-detail",
|
||||
"+form-questions-create", "+form-questions-delete", "+form-questions-update", "+form-questions-list",
|
||||
"+form-submit",
|
||||
"+dashboard-list", "+dashboard-get", "+dashboard-create", "+dashboard-update", "+dashboard-delete", "+dashboard-arrange",
|
||||
"+form-submit", "+form-share-get", "+form-share-update",
|
||||
"+dashboard-list", "+dashboard-get", "+dashboard-share-get", "+dashboard-share-update", "+dashboard-create", "+dashboard-update", "+dashboard-delete", "+dashboard-arrange",
|
||||
"+dashboard-block-list", "+dashboard-block-get", "+dashboard-block-get-data", "+dashboard-block-create", "+dashboard-block-update", "+dashboard-block-delete",
|
||||
"+workspace-create", "+workspace-entity-list", "+workspace-move-in",
|
||||
"+app-create", "+app-get",
|
||||
@@ -190,6 +190,43 @@ func TestShortcutsCatalog(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShareManagementShortcutScopes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scopes []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "dashboard get requires update scope",
|
||||
scopes: BaseDashboardShareGet.Scopes,
|
||||
want: []string{"base:dashboard:update"},
|
||||
},
|
||||
{
|
||||
name: "dashboard update requires update scope",
|
||||
scopes: BaseDashboardShareUpdate.Scopes,
|
||||
want: []string{"base:dashboard:update"},
|
||||
},
|
||||
{
|
||||
name: "form get requires update scope",
|
||||
scopes: BaseFormShareGet.Scopes,
|
||||
want: []string{"base:form:update"},
|
||||
},
|
||||
{
|
||||
name: "form update requires update scope",
|
||||
scopes: BaseFormShareUpdate.Scopes,
|
||||
want: []string{"base:form:update"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !reflect.DeepEqual(tt.scopes, tt.want) {
|
||||
t.Fatalf("Scopes=%v want=%v", tt.scopes, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortcutsDryRunCoverage(t *testing.T) {
|
||||
for _, shortcut := range Shortcuts() {
|
||||
if shortcut.DryRun == nil {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
var dashboardShareUpdateFlagNames = []string{
|
||||
"enabled",
|
||||
"access-scope",
|
||||
"show-source",
|
||||
"enable-auto-analysis",
|
||||
}
|
||||
|
||||
var BaseDashboardShareGet = common.Shortcut{
|
||||
Service: "base",
|
||||
Command: "+dashboard-share-get",
|
||||
Description: "Get dashboard share status and settings",
|
||||
Risk: "read",
|
||||
Scopes: []string{"base:dashboard:update"},
|
||||
AuthTypes: authTypes(),
|
||||
Flags: []common.Flag{
|
||||
baseTokenFlag(true),
|
||||
dashboardIDFlag(true),
|
||||
},
|
||||
DryRun: func(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
return common.NewDryRunAPI().
|
||||
GET("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/share").
|
||||
Set("base_token", runtime.Str("base-token")).
|
||||
Set("dashboard_id", runtime.Str("dashboard-id"))
|
||||
},
|
||||
Execute: func(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
data, err := baseV3Call(runtime, "GET", baseV3Path(
|
||||
"bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "share",
|
||||
), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(data, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var BaseDashboardShareUpdate = common.Shortcut{
|
||||
Service: "base",
|
||||
Command: "+dashboard-share-update",
|
||||
Description: "Update dashboard share status and settings",
|
||||
Risk: "write",
|
||||
Scopes: []string{"base:dashboard:update"},
|
||||
AuthTypes: authTypes(),
|
||||
Flags: []common.Flag{
|
||||
baseTokenFlag(true),
|
||||
dashboardIDFlag(true),
|
||||
{Name: "enabled", Type: "bool", Desc: "enable or disable dashboard sharing"},
|
||||
{Name: "access-scope", Desc: "share access scope", Enum: shareAccessScopeEnums},
|
||||
{Name: "show-source", Type: "bool", Desc: "show the entry back to the source Base"},
|
||||
{Name: "enable-auto-analysis", Type: "bool", Desc: "enable intelligent analysis on the shared dashboard"},
|
||||
},
|
||||
Tips: []string{
|
||||
"Boolean settings use PATCH semantics: pass --show-source=false or --enable-auto-analysis=false to explicitly turn them off.",
|
||||
"Update exactly one field per invocation; run separate commands to change multiple share fields.",
|
||||
},
|
||||
Validate: func(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateSingleShareUpdate(runtime, dashboardShareUpdateFlagNames...)
|
||||
},
|
||||
DryRun: func(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
return common.NewDryRunAPI().
|
||||
PATCH("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/share").
|
||||
Body(buildDashboardShareUpdateBody(runtime)).
|
||||
Set("base_token", runtime.Str("base-token")).
|
||||
Set("dashboard_id", runtime.Str("dashboard-id"))
|
||||
},
|
||||
Execute: func(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
data, err := baseV3Call(runtime, "PATCH", baseV3Path(
|
||||
"bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "share",
|
||||
), nil, buildDashboardShareUpdateBody(runtime))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(data, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func buildDashboardShareUpdateBody(runtime *common.RuntimeContext) map[string]interface{} {
|
||||
body := map[string]interface{}{}
|
||||
addCommonShareUpdateFields(runtime, body)
|
||||
|
||||
settings := map[string]interface{}{}
|
||||
if runtime.Changed("show-source") {
|
||||
settings["show_source"] = runtime.Bool("show-source")
|
||||
}
|
||||
if runtime.Changed("enable-auto-analysis") {
|
||||
settings["enable_auto_analysis"] = runtime.Bool("enable-auto-analysis")
|
||||
}
|
||||
if len(settings) > 0 {
|
||||
body["settings"] = settings
|
||||
}
|
||||
return body
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
var formShareUpdateFlagNames = []string{
|
||||
"enabled",
|
||||
"access-scope",
|
||||
"allow-anonymous",
|
||||
"require-login",
|
||||
}
|
||||
|
||||
var BaseFormShareGet = common.Shortcut{
|
||||
Service: "base",
|
||||
Command: "+form-share-get",
|
||||
Description: "Get form share status and settings",
|
||||
Risk: "read",
|
||||
Scopes: []string{"base:form:update"},
|
||||
AuthTypes: authTypes(),
|
||||
Flags: []common.Flag{
|
||||
baseTokenFlag(true),
|
||||
{Name: "table-id", Desc: "table ID", Required: true},
|
||||
{Name: "form-id", Desc: "form ID", Required: true},
|
||||
},
|
||||
DryRun: func(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
return common.NewDryRunAPI().
|
||||
GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id/share").
|
||||
Set("base_token", runtime.Str("base-token")).
|
||||
Set("table_id", runtime.Str("table-id")).
|
||||
Set("form_id", runtime.Str("form-id"))
|
||||
},
|
||||
Execute: func(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
data, err := baseV3Call(runtime, "GET", baseV3Path(
|
||||
"bases", runtime.Str("base-token"), "tables", runtime.Str("table-id"), "forms", runtime.Str("form-id"), "share",
|
||||
), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(data, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var BaseFormShareUpdate = common.Shortcut{
|
||||
Service: "base",
|
||||
Command: "+form-share-update",
|
||||
Description: "Update form share status and settings",
|
||||
Risk: "write",
|
||||
Scopes: []string{"base:form:update"},
|
||||
AuthTypes: authTypes(),
|
||||
Flags: []common.Flag{
|
||||
baseTokenFlag(true),
|
||||
{Name: "table-id", Desc: "table ID", Required: true},
|
||||
{Name: "form-id", Desc: "form ID", Required: true},
|
||||
{Name: "enabled", Type: "bool", Desc: "enable or disable form sharing"},
|
||||
{Name: "access-scope", Desc: "share access scope", Enum: shareAccessScopeEnums},
|
||||
{Name: "allow-anonymous", Type: "bool", Desc: "anonymize the submitter identity"},
|
||||
{Name: "require-login", Type: "bool", Desc: "require submitters to sign in before submitting"},
|
||||
},
|
||||
Tips: []string{
|
||||
"Boolean settings use PATCH semantics: pass --allow-anonymous=false or another boolean flag with =false to explicitly turn it off.",
|
||||
"--allow-anonymous controls submitter identity and --require-login controls sign-in; run separate update commands to change both settings.",
|
||||
"Update exactly one field per invocation; run separate commands to change multiple share fields.",
|
||||
},
|
||||
Validate: func(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateFormShareUpdate(runtime)
|
||||
},
|
||||
DryRun: func(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
return common.NewDryRunAPI().
|
||||
PATCH("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id/share").
|
||||
Body(buildFormShareUpdateBody(runtime)).
|
||||
Set("base_token", runtime.Str("base-token")).
|
||||
Set("table_id", runtime.Str("table-id")).
|
||||
Set("form_id", runtime.Str("form-id"))
|
||||
},
|
||||
Execute: func(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
data, err := baseV3Call(runtime, "PATCH", baseV3Path(
|
||||
"bases", runtime.Str("base-token"), "tables", runtime.Str("table-id"), "forms", runtime.Str("form-id"), "share",
|
||||
), nil, buildFormShareUpdateBody(runtime))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runtime.Out(data, nil)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func validateFormShareUpdate(runtime *common.RuntimeContext) error {
|
||||
return validateSingleShareUpdate(runtime, formShareUpdateFlagNames...)
|
||||
}
|
||||
|
||||
func buildFormShareUpdateBody(runtime *common.RuntimeContext) map[string]interface{} {
|
||||
body := map[string]interface{}{}
|
||||
addCommonShareUpdateFields(runtime, body)
|
||||
|
||||
settings := map[string]interface{}{}
|
||||
if runtime.Changed("allow-anonymous") {
|
||||
settings["allow_anonymous"] = runtime.Bool("allow-anonymous")
|
||||
}
|
||||
if runtime.Changed("require-login") {
|
||||
settings["require_login"] = runtime.Bool("require-login")
|
||||
}
|
||||
|
||||
if len(settings) > 0 {
|
||||
body["settings"] = settings
|
||||
}
|
||||
return body
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
var shareAccessScopeEnums = []string{"invite", "tenant", "anyone"}
|
||||
|
||||
func validateSingleShareUpdate(runtime *common.RuntimeContext, flagNames ...string) error {
|
||||
changedNames := make([]string, 0, len(flagNames))
|
||||
for _, name := range flagNames {
|
||||
if runtime.Changed(name) {
|
||||
changedNames = append(changedNames, name)
|
||||
}
|
||||
}
|
||||
switch len(changedNames) {
|
||||
case 1:
|
||||
return nil
|
||||
case 0:
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "exactly one share field must be provided").
|
||||
WithHint("use one of: %s", shareFlagNames(flagNames))
|
||||
default:
|
||||
return baseFlagErrorf("share update accepts exactly one field; do not combine %s", shareFlagNames(changedNames))
|
||||
}
|
||||
}
|
||||
|
||||
func shareFlagNames(flagNames []string) string {
|
||||
names := make([]string, 0, len(flagNames))
|
||||
for _, name := range flagNames {
|
||||
names = append(names, "--"+name)
|
||||
}
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
func addCommonShareUpdateFields(runtime *common.RuntimeContext, body map[string]interface{}) {
|
||||
if runtime.Changed("enabled") {
|
||||
body["enabled"] = runtime.Bool("enabled")
|
||||
}
|
||||
if runtime.Changed("access-scope") {
|
||||
body["access_scope"] = runtime.Str("access-scope")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
)
|
||||
|
||||
func TestDashboardShareGetCallsResourceEndpoint(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_1/share",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"enabled": true,
|
||||
"access_scope": "tenant",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := runShortcut(t, BaseDashboardShareGet, []string{
|
||||
"+dashboard-share-get",
|
||||
"--base-token", "app_x",
|
||||
"--dashboard-id", "dsh_1",
|
||||
}, factory, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("run shortcut: %v", err)
|
||||
}
|
||||
if got := stdout.String(); !strings.Contains(got, `"access_scope": "tenant"`) {
|
||||
t.Fatalf("stdout=%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardShareUpdatePreservesExplicitFalse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flag string
|
||||
want map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "show source",
|
||||
flag: "--show-source=false",
|
||||
want: map[string]interface{}{"settings": map[string]interface{}{"show_source": false}},
|
||||
},
|
||||
{
|
||||
name: "auto analysis",
|
||||
flag: "--enable-auto-analysis=false",
|
||||
want: map[string]interface{}{"settings": map[string]interface{}{"enable_auto_analysis": false}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
stub := &httpmock.Stub{
|
||||
Method: "PATCH",
|
||||
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_1/share",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{"enabled": true},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
err := runShortcut(t, BaseDashboardShareUpdate, []string{
|
||||
"+dashboard-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--dashboard-id", "dsh_1",
|
||||
tt.flag,
|
||||
}, factory, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("run shortcut: %v", err)
|
||||
}
|
||||
if got := decodeCapturedJSONBody(t, stub); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("request body=%#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardShareUpdateBuildsCommonFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string
|
||||
want map[string]interface{}
|
||||
}{
|
||||
{name: "enabled", flags: []string{"--enabled=true"}, want: map[string]interface{}{"enabled": true}},
|
||||
{name: "access scope", flags: []string{"--access-scope", "invite"}, want: map[string]interface{}{"access_scope": "invite"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
stub := &httpmock.Stub{
|
||||
Method: "PATCH",
|
||||
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_1/share",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{"enabled": true},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
args := []string{
|
||||
"+dashboard-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--dashboard-id", "dsh_1",
|
||||
}
|
||||
args = append(args, tt.flags...)
|
||||
if err := runShortcut(t, BaseDashboardShareUpdate, args, factory, stdout); err != nil {
|
||||
t.Fatalf("run shortcut: %v", err)
|
||||
}
|
||||
if got := decodeCapturedJSONBody(t, stub); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("request body=%#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardShareUpdatePreservesExplicitFalseForEnabled(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
stub := &httpmock.Stub{
|
||||
Method: "PATCH",
|
||||
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_1/share",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{"enabled": false},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
err := runShortcut(t, BaseDashboardShareUpdate, []string{
|
||||
"+dashboard-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--dashboard-id", "dsh_1",
|
||||
"--enabled=false",
|
||||
}, factory, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("run shortcut: %v", err)
|
||||
}
|
||||
|
||||
want := map[string]interface{}{"enabled": false}
|
||||
if got := decodeCapturedJSONBody(t, stub); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("request body=%#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormShareGetCallsResourceEndpoint(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_1/forms/vew_1/share",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"enabled": false,
|
||||
"access_scope": "tenant",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := runShortcut(t, BaseFormShareGet, []string{
|
||||
"+form-share-get",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_1",
|
||||
"--form-id", "vew_1",
|
||||
}, factory, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("run shortcut: %v", err)
|
||||
}
|
||||
if got := stdout.String(); !strings.Contains(got, `"enabled": false`) {
|
||||
t.Fatalf("stdout=%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormShareUpdateBuildsAccessScope(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
stub := &httpmock.Stub{
|
||||
Method: "PATCH",
|
||||
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_1/forms/vew_1/share",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{"enabled": true},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
err := runShortcut(t, BaseFormShareUpdate, []string{
|
||||
"+form-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_1",
|
||||
"--form-id", "vew_1",
|
||||
"--access-scope", "anyone",
|
||||
}, factory, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("run shortcut: %v", err)
|
||||
}
|
||||
|
||||
want := map[string]interface{}{"access_scope": "anyone"}
|
||||
if got := decodeCapturedJSONBody(t, stub); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("request body=%#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormShareUpdateBuildsSingleSettingsField(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flag string
|
||||
want map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "allow anonymous false",
|
||||
flag: "--allow-anonymous=false",
|
||||
want: map[string]interface{}{"settings": map[string]interface{}{"allow_anonymous": false}},
|
||||
},
|
||||
{
|
||||
name: "require login true",
|
||||
flag: "--require-login=true",
|
||||
want: map[string]interface{}{"settings": map[string]interface{}{"require_login": true}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
stub := &httpmock.Stub{
|
||||
Method: "PATCH",
|
||||
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_1/forms/vew_1/share",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{"enabled": true},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
err := runShortcut(t, BaseFormShareUpdate, []string{
|
||||
"+form-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_1",
|
||||
"--form-id", "vew_1",
|
||||
tt.flag,
|
||||
}, factory, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("run shortcut: %v", err)
|
||||
}
|
||||
if got := decodeCapturedJSONBody(t, stub); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("request body=%#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormShareUpdatePreservesExplicitFalseForEnabled(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
stub := &httpmock.Stub{
|
||||
Method: "PATCH",
|
||||
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_1/forms/vew_1/share",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{"enabled": false},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
err := runShortcut(t, BaseFormShareUpdate, []string{
|
||||
"+form-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_1",
|
||||
"--form-id", "vew_1",
|
||||
"--enabled=false",
|
||||
}, factory, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("run shortcut: %v", err)
|
||||
}
|
||||
|
||||
want := map[string]interface{}{"enabled": false}
|
||||
if got := decodeCapturedJSONBody(t, stub); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("request body=%#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardShareUpdateRejectsMultipleFields(t *testing.T) {
|
||||
factory, stdout, _ := newExecuteFactory(t)
|
||||
err := runShortcut(t, BaseDashboardShareUpdate, []string{
|
||||
"+dashboard-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--dashboard-id", "dsh_1",
|
||||
"--enabled=false",
|
||||
"--access-scope", "tenant",
|
||||
"--show-source=true",
|
||||
}, factory, stdout)
|
||||
|
||||
assertInvalidArgumentValidation(t, err, "--enabled", []string{"--enabled", "--access-scope", "--show-source"}, "exactly one")
|
||||
}
|
||||
|
||||
func TestFormShareUpdateRejectsMultipleSettings(t *testing.T) {
|
||||
factory, stdout, _ := newExecuteFactory(t)
|
||||
err := runShortcut(t, BaseFormShareUpdate, []string{
|
||||
"+form-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_1",
|
||||
"--form-id", "vew_1",
|
||||
"--allow-anonymous=true",
|
||||
"--require-login=true",
|
||||
}, factory, stdout)
|
||||
|
||||
assertInvalidArgumentValidation(t, err, "--allow-anonymous", []string{"--allow-anonymous", "--require-login"}, "exactly one")
|
||||
}
|
||||
|
||||
func TestShareUpdateRejectsUnsupportedAccessScope(t *testing.T) {
|
||||
factory, stdout, _ := newExecuteFactory(t)
|
||||
err := runShortcut(t, BaseDashboardShareUpdate, []string{
|
||||
"+dashboard-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--dashboard-id", "dsh_1",
|
||||
"--access-scope", "off",
|
||||
}, factory, stdout)
|
||||
|
||||
assertInvalidArgumentValidation(t, err, "--access-scope", nil, "allowed")
|
||||
}
|
||||
|
||||
func TestShareUpdateRequiresExactlyOneChange(t *testing.T) {
|
||||
factory, stdout, _ := newExecuteFactory(t)
|
||||
err := runShortcut(t, BaseFormShareUpdate, []string{
|
||||
"+form-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_1",
|
||||
"--form-id", "vew_1",
|
||||
}, factory, stdout)
|
||||
|
||||
assertInvalidArgumentValidation(t, err, "", []string{}, "exactly one")
|
||||
problem, ok := errs.ProblemOf(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected typed error, got %T %v", err, err)
|
||||
}
|
||||
for _, flag := range []string{
|
||||
"--enabled",
|
||||
"--access-scope",
|
||||
"--allow-anonymous",
|
||||
"--require-login",
|
||||
} {
|
||||
if !strings.Contains(problem.Hint, flag) {
|
||||
t.Fatalf("hint=%q, want flag %q", problem.Hint, flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,8 +85,12 @@ func Shortcuts() []common.Shortcut {
|
||||
BaseFormQuestionsUpdate,
|
||||
BaseFormQuestionsList,
|
||||
BaseFormSubmit,
|
||||
BaseFormShareGet,
|
||||
BaseFormShareUpdate,
|
||||
BaseDashboardList,
|
||||
BaseDashboardGet,
|
||||
BaseDashboardShareGet,
|
||||
BaseDashboardShareUpdate,
|
||||
BaseDashboardCreate,
|
||||
BaseDashboardUpdate,
|
||||
BaseDashboardDelete,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: lark-base
|
||||
version: 1.2.19
|
||||
version: 1.2.20
|
||||
description: "飞书多维表格(Base)操作:建表、字段、记录、视图、统计、公式/lookup、表单、仪表盘、应用模式(BaseApp/AppMode 页面与组件)、Workspace 目录、workflow、角色权限;遇到 Base/多维表格/bitable、BaseApp/AppMode、/base/ 或 /app/ 链接时使用。BaseApp 不走 lark-apps;文件导入/导出转 lark-drive,认证/授权转 lark-shared。"
|
||||
metadata:
|
||||
requires:
|
||||
@@ -98,12 +98,15 @@ Form 依附于 Table,以 Field 作为题目,每次有效提交会创建一
|
||||
|
||||
1. **读取 Table 中的表单配置:** 使用 `+form-list` / `+form-get` 读取表单,使用 `+form-questions-list` 读取题目配置;这些命令使用表单所属的 `base_token + table_id`。
|
||||
2. **创建或修改 Table 中的表单配置:** 使用 `+form-create` / `+form-update` / `+form-delete` 管理表单;题目由 Table Field 承载,question ID 对应 `field_id`,创建和更新分别读取 [questions create](references/lark-base-form-questions-create.md) / [questions update](references/lark-base-form-questions-update.md),删除使用 `+form-questions-delete`。
|
||||
3. **填写分享表单并提交:** 对表单分享链接使用 `+url-resolve` 取得 `share_token`,按 [Form detail](references/lark-base-form-detail.md) 执行 `+form-detail` 读取真实题目、必填项和显示条件,再按 [Form submit](references/lark-base-form-submit.md) 构造字段与附件并执行 `+form-submit`。
|
||||
3. **管理表单分享:** 使用 `+form-share-get` / `+form-share-update` 管理启停、访问范围和匿名/登录要求;更新前先读取现状,每次只修改一个字段,布尔值显式传 `true` 或 `false`。
|
||||
4. **填写分享表单并提交:** 对表单分享链接使用 `+url-resolve` 取得 `share_token`,按 [Form detail](references/lark-base-form-detail.md) 执行 `+form-detail` 读取真实题目、必填项和显示条件,再按 [Form submit](references/lark-base-form-submit.md) 构造字段与附件并执行 `+form-submit`。
|
||||
|
||||
## Dashboard Block
|
||||
|
||||
Dashboard Block 是 Base Block 树中的仪表盘容器,负责承载页面主题、布局和内部组件集合,本身不表示某一项图表数据。使用 `+dashboard-list` 定位容器,`+dashboard-get` 读取容器信息,`+dashboard-update` 修改主题,`+dashboard-arrange` 统一编排内部组件布局。
|
||||
|
||||
**管理 Dashboard 分享:** 使用 `+dashboard-share-get` / `+dashboard-share-update` 管理启停、访问范围、返回源 Base 入口和智能分析;更新前先读取现状,每次只修改一个字段,显式 `false` 会被保留。
|
||||
|
||||
容器内部的图表、指标卡和文本等组件在 Dashboard API 中也称为 Block,但不属于 Base Block 树。内部 Block 分为三条操作路径:
|
||||
|
||||
1. **读取配置:** `+dashboard-block-list` / `+dashboard-block-get` 读取组件类型、布局和 `data_config`;文本组件的正文也属于配置。
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBaseShareDryRun(t *testing.T) {
|
||||
t.Run("dashboard get", func(t *testing.T) {
|
||||
result := runBaseDryRun(t, 0,
|
||||
"base", "+dashboard-share-get",
|
||||
"--base-token", "app_x",
|
||||
"--dashboard-id", "dsh_1",
|
||||
)
|
||||
assert.Contains(t, result.Stdout, `"method": "GET"`)
|
||||
assert.Contains(t, result.Stdout, "/open-apis/base/v3/bases/app_x/dashboards/dsh_1/share")
|
||||
})
|
||||
|
||||
t.Run("dashboard partial update", func(t *testing.T) {
|
||||
result := runBaseDryRun(t, 0,
|
||||
"base", "+dashboard-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--dashboard-id", "dsh_1",
|
||||
"--show-source=false",
|
||||
)
|
||||
assert.Contains(t, result.Stdout, `"method": "PATCH"`)
|
||||
assert.Contains(t, result.Stdout, "/open-apis/base/v3/bases/app_x/dashboards/dsh_1/share")
|
||||
assert.Contains(t, result.Stdout, `"show_source": false`)
|
||||
assert.NotContains(t, result.Stdout, `"enabled":`)
|
||||
assert.NotContains(t, result.Stdout, `"enable_auto_analysis":`)
|
||||
})
|
||||
|
||||
t.Run("form get", func(t *testing.T) {
|
||||
result := runBaseDryRun(t, 0,
|
||||
"base", "+form-share-get",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_1",
|
||||
"--form-id", "vew_1",
|
||||
)
|
||||
assert.Contains(t, result.Stdout, `"method": "GET"`)
|
||||
assert.Contains(t, result.Stdout, "/open-apis/base/v3/bases/app_x/tables/tbl_1/forms/vew_1/share")
|
||||
})
|
||||
|
||||
t.Run("form settings update", func(t *testing.T) {
|
||||
result := runBaseDryRun(t, 0,
|
||||
"base", "+form-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_1",
|
||||
"--form-id", "vew_1",
|
||||
"--allow-anonymous=true",
|
||||
)
|
||||
assert.Contains(t, result.Stdout, `"method": "PATCH"`)
|
||||
assert.Contains(t, result.Stdout, "/open-apis/base/v3/bases/app_x/tables/tbl_1/forms/vew_1/share")
|
||||
assert.Contains(t, result.Stdout, `"allow_anonymous": true`)
|
||||
assert.NotContains(t, result.Stdout, `"enabled":`)
|
||||
assert.NotContains(t, result.Stdout, `"require_login":`)
|
||||
})
|
||||
|
||||
t.Run("form submission policy is not exposed", func(t *testing.T) {
|
||||
result := runBaseDryRun(t, 2,
|
||||
"base", "+form-share-update",
|
||||
"--base-token", "app_x",
|
||||
"--table-id", "tbl_1",
|
||||
"--form-id", "vew_1",
|
||||
"--valid-period-enabled=true",
|
||||
)
|
||||
assert.Contains(t, result.Stderr, "unknown flag")
|
||||
assert.Contains(t, result.Stderr, "--valid-period-enabled")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestBaseShareWorkflow(t *testing.T) {
|
||||
if os.Getenv("LARK_CLI_E2E_BASE_SHARE_READY") != "1" {
|
||||
t.Skip("set LARK_CLI_E2E_BASE_SHARE_READY=1 after the dashboard/form share OpenAPI is deployed")
|
||||
}
|
||||
clie2e.SkipWithoutTenantAccessToken(t)
|
||||
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
baseToken := createBaseWithRetry(t, ctx, "lark-cli-e2e-base-share-"+clie2e.GenerateSuffix())
|
||||
tableID, _, _ := createTableWithRetry(
|
||||
t,
|
||||
parentT,
|
||||
ctx,
|
||||
baseToken,
|
||||
"Share workflow "+clie2e.GenerateSuffix(),
|
||||
`[{"name":"Name","type":"text"}]`,
|
||||
`{"name":"Main","type":"grid"}`,
|
||||
)
|
||||
|
||||
formCreate, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+form-create",
|
||||
"--base-token", baseToken,
|
||||
"--table-id", tableID,
|
||||
"--name", "Share form " + clie2e.GenerateSuffix(),
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
formCreate.AssertExitCode(t, 0)
|
||||
formCreate.AssertStdoutStatus(t, true)
|
||||
formID := gjson.Get(formCreate.Stdout, "data.id").String()
|
||||
require.NotEmpty(t, formID, formCreate.Stdout)
|
||||
|
||||
dashboardCreate, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+dashboard-create",
|
||||
"--base-token", baseToken,
|
||||
"--name", "Share dashboard " + clie2e.GenerateSuffix(),
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
dashboardCreate.AssertExitCode(t, 0)
|
||||
dashboardCreate.AssertStdoutStatus(t, true)
|
||||
dashboardID := gjson.Get(dashboardCreate.Stdout, "data.dashboard.dashboard_id").String()
|
||||
require.NotEmpty(t, dashboardID, dashboardCreate.Stdout)
|
||||
|
||||
t.Cleanup(func() {
|
||||
cleanupCtx, cleanupCancel := cleanupContext()
|
||||
defer cleanupCancel()
|
||||
for _, args := range [][]string{
|
||||
{"base", "+dashboard-share-update", "--base-token", baseToken, "--dashboard-id", dashboardID, "--enabled=false"},
|
||||
{"base", "+form-share-update", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID, "--enabled=false"},
|
||||
} {
|
||||
result, cleanupErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{Args: args, DefaultAs: "bot"})
|
||||
if cleanupErr != nil || result == nil || result.ExitCode != 0 {
|
||||
reportCleanupFailure(parentT, "disable share", result, cleanupErr)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("dashboard share update and get", func(t *testing.T) {
|
||||
runUpdate := func(fieldArgs ...string) {
|
||||
args := append([]string{
|
||||
"base", "+dashboard-share-update",
|
||||
"--base-token", baseToken,
|
||||
"--dashboard-id", dashboardID,
|
||||
}, fieldArgs...)
|
||||
update, runErr := clie2e.RunCmd(ctx, clie2e.Request{Args: args, DefaultAs: "bot"})
|
||||
require.NoError(t, runErr)
|
||||
update.AssertExitCode(t, 0)
|
||||
update.AssertStdoutStatus(t, true)
|
||||
}
|
||||
runUpdate("--enabled=true")
|
||||
runUpdate("--access-scope", "invite")
|
||||
runUpdate("--show-source=true")
|
||||
runUpdate("--enable-auto-analysis=true")
|
||||
runUpdate("--show-source=false")
|
||||
runUpdate("--enable-auto-analysis=false")
|
||||
|
||||
get, runErr := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"base", "+dashboard-share-get", "--base-token", baseToken, "--dashboard-id", dashboardID},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, runErr)
|
||||
get.AssertExitCode(t, 0)
|
||||
get.AssertStdoutStatus(t, true)
|
||||
require.True(t, gjson.Get(get.Stdout, "data.enabled").Bool(), get.Stdout)
|
||||
require.Equal(t, "invite", gjson.Get(get.Stdout, "data.access_scope").String(), get.Stdout)
|
||||
showSource := gjson.Get(get.Stdout, "data.settings.show_source")
|
||||
require.True(t, showSource.Exists(), get.Stdout)
|
||||
require.False(t, showSource.Bool(), get.Stdout)
|
||||
autoAnalysis := gjson.Get(get.Stdout, "data.settings.enable_auto_analysis")
|
||||
require.True(t, autoAnalysis.Exists(), get.Stdout)
|
||||
require.False(t, autoAnalysis.Bool(), get.Stdout)
|
||||
})
|
||||
|
||||
t.Run("form share update and get", func(t *testing.T) {
|
||||
runUpdate := func(fieldArgs ...string) {
|
||||
args := append([]string{
|
||||
"base", "+form-share-update",
|
||||
"--base-token", baseToken,
|
||||
"--table-id", tableID,
|
||||
"--form-id", formID,
|
||||
}, fieldArgs...)
|
||||
update, runErr := clie2e.RunCmd(ctx, clie2e.Request{Args: args, DefaultAs: "bot"})
|
||||
require.NoError(t, runErr)
|
||||
update.AssertExitCode(t, 0)
|
||||
update.AssertStdoutStatus(t, true)
|
||||
}
|
||||
runUpdate("--enabled=true")
|
||||
runUpdate("--access-scope", "invite")
|
||||
runUpdate("--allow-anonymous=true")
|
||||
runUpdate("--require-login=true")
|
||||
runUpdate("--allow-anonymous=false")
|
||||
|
||||
get, runErr := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"base", "+form-share-get", "--base-token", baseToken, "--table-id", tableID, "--form-id", formID},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, runErr)
|
||||
get.AssertExitCode(t, 0)
|
||||
get.AssertStdoutStatus(t, true)
|
||||
require.True(t, gjson.Get(get.Stdout, "data.enabled").Bool(), get.Stdout)
|
||||
require.Equal(t, "invite", gjson.Get(get.Stdout, "data.access_scope").String(), get.Stdout)
|
||||
allowAnonymous := gjson.Get(get.Stdout, "data.settings.allow_anonymous")
|
||||
require.True(t, allowAnonymous.Exists(), get.Stdout)
|
||||
require.False(t, allowAnonymous.Bool(), get.Stdout)
|
||||
requireLogin := gjson.Get(get.Stdout, "data.settings.require_login")
|
||||
require.True(t, requireLogin.Exists(), get.Stdout)
|
||||
require.True(t, requireLogin.Bool(), get.Stdout)
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
# Base CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 89 leaf commands
|
||||
- Covered: 37
|
||||
- Coverage: 41.6%
|
||||
- Denominator: 93 leaf commands
|
||||
- Covered: 41
|
||||
- Coverage: 44.1%
|
||||
|
||||
## Summary
|
||||
- TestBase_BasicWorkflow: proves `+base-create`, `+base-get`, `+table-create`, `+table-get`, and `+table-list`; key `t.Run(...)` proof points are `get base as bot`, `get table as bot`, and `list tables and find created table as bot`.
|
||||
@@ -15,6 +15,8 @@
|
||||
- TestBaseFormDetailDryRun / TestBaseFormSubmitDryRun: prove shared-form detail and submission request shapes.
|
||||
- TestBaseDashboardBlockGetDataDryRun: proves dashboard block data request shapes and identifier handling.
|
||||
- TestBaseDashboardBlockLayoutPrecisionWorkflow: creates a temporary Base/table/dashboard, creates a statistics block with `position` and omitted `number_format`, asserts the server default, updates to a custom format, then verifies a precision-only update preserves `formatName`, and cleans up the block/dashboard/base. `+dashboard-create`, `+dashboard-delete`, `+dashboard-block-get` and `+dashboard-block-delete` have no dry-run coverage and rest on this test alone. This workflow was executed successfully against a live tenant on 2026-08-20 while validating PR #2118.
|
||||
- TestBaseShareDryRun: proves dashboard/form share GET and PATCH routes, one-field update requests, explicit false preservation, and nested form settings without touching live data.
|
||||
- TestBaseShareWorkflow: deployment-gated by `LARK_CLI_E2E_BASE_SHARE_READY=1`; creates a Base, table, form, and dashboard, updates each share field in a separate request, verifies get round trips for both resources, disables sharing, and cleans up the Base.
|
||||
- TestBaseRecordBatchUpdatePerRecordDryRun: proves `+record-batch-update` preserves the per-record `update_records` request shape.
|
||||
- TestBaseRecordBatchUpdatePerRecordWorkflow: creates two records, updates different field types in one request, asserts the minimal response contract, reads both records back, verifies a missing record ID is not prevalidated, and cleans up the temporary Base.
|
||||
- TestBaseRecordHistoryListDryRunUsesExplicitRecordID / TestBaseRecordHistoryListDryRunRejectsNonPositiveMaxVersion: prove the history request keeps the explicit record ID and rejects explicitly non-positive cursors with a typed validation error.
|
||||
@@ -51,6 +53,8 @@
|
||||
| ✓ | base +dashboard-delete | shortcut | base_dashboard_block_layout_precision_workflow_test.go::TestBaseDashboardBlockLayoutPrecisionWorkflow (cleanup) | `--base-token`; `--dashboard-id`; `--yes`; live cleanup | deletes the temporary dashboard |
|
||||
| ✕ | base +dashboard-get | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-list | shortcut | | none | dashboard workflows not covered |
|
||||
| ✓ | base +dashboard-share-get | shortcut | base_share_dryrun_test.go::TestBaseShareDryRun/dashboard get; base_share_workflow_test.go::TestBaseShareWorkflow/dashboard share update and get | `--base-token`; `--dashboard-id`; dry-run + deployment-gated live | live requires `LARK_CLI_E2E_BASE_SHARE_READY=1` |
|
||||
| ✓ | base +dashboard-share-update | shortcut | base_share_dryrun_test.go::TestBaseShareDryRun/dashboard partial update; base_share_workflow_test.go::TestBaseShareWorkflow/dashboard share update and get | one of `--enabled`; `--access-scope=invite`; `--show-source`; `--enable-auto-analysis` per request | single-field updates, explicit false, invite-only scope, and live read-back covered |
|
||||
| ✕ | base +dashboard-update | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +data-query | shortcut | | none | no data-query assertions yet |
|
||||
| ✓ | base +field-create | shortcut | base_field_dryrun_test.go::TestBaseFieldCreateDryRunArrayCompat | `--base-token`; `--table-id`; `--json`; dry-run only | request shape only |
|
||||
@@ -64,6 +68,8 @@
|
||||
| ✓ | base +form-detail | shortcut | base_form_detail_dryrun_test.go::TestBaseFormDetailDryRun | `--share-token`; dry-run only | shared-form request shape |
|
||||
| ✕ | base +form-get | shortcut | | none | form workflows not covered |
|
||||
| ✓ | base +form-list | shortcut | base_form_detail_dryrun_test.go::TestBaseFormListDryRun_UsesBaseAndTableIdentifiers | `--base-token`; `--table-id`; dry-run only | request shape only |
|
||||
| ✓ | base +form-share-get | shortcut | base_share_dryrun_test.go::TestBaseShareDryRun/form get; base_share_workflow_test.go::TestBaseShareWorkflow/form share update and get | `--base-token`; `--table-id`; `--form-id`; dry-run + deployment-gated live | live requires `LARK_CLI_E2E_BASE_SHARE_READY=1` |
|
||||
| ✓ | base +form-share-update | shortcut | base_share_dryrun_test.go::TestBaseShareDryRun/form settings update; base_share_workflow_test.go::TestBaseShareWorkflow/form share update and get | one of share enablement; `access-scope=invite`; anonymous/login settings per request | single-field updates, login-plus-anonymous across separate requests, explicit false, and live read-back covered |
|
||||
| ✓ | base +form-questions-create | shortcut | TestBaseFormQuestionsCreateVisibleRuleDryRun; base_form_questions_create_dryrun_test.go | questions[].visible_rule; dry-run | request body, visible_rule passthrough, and help guard covered |
|
||||
| ✕ | base +form-questions-delete | shortcut | | none | form workflows not covered |
|
||||
| ✕ | base +form-questions-list | shortcut | | none | form workflows not covered |
|
||||
|
||||
Reference in New Issue
Block a user