diff --git a/shortcuts/doc/docs_create_test.go b/shortcuts/doc/docs_create_test.go index c0970e5d7..b25315d63 100644 --- a/shortcuts/doc/docs_create_test.go +++ b/shortcuts/doc/docs_create_test.go @@ -249,6 +249,84 @@ func TestDocsCreateV2PreservesBackendURL(t *testing.T) { } } +func TestDocsCreateV2RejectsV1OnlyFlags(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + args []string + wantErr string + }{ + { + name: "markdown", + args: []string{ + "+create", + "--api-version", "v2", + "--markdown", "## legacy", + }, + wantErr: "use --content with --doc-format markdown", + }, + { + name: "wiki node", + args: []string{ + "+create", + "--api-version", "v2", + "--content", "内容

正文

", + "--wiki-node", "wikcn_legacy_node", + }, + wantErr: "use --parent-token", + }, + { + name: "title", + args: []string{ + "+create", + "--api-version", "v2", + "--content", "

正文

", + "--title", "Legacy title", + }, + wantErr: "include the document title in --content", + }, + { + name: "folder token", + args: []string{ + "+create", + "--api-version", "v2", + "--content", "内容

正文

", + "--folder-token", "fldcn_legacy_folder", + }, + wantErr: "use --parent-token", + }, + { + name: "wiki space", + args: []string{ + "+create", + "--api-version", "v2", + "--content", "内容

正文

", + "--wiki-space", "my_library", + }, + wantErr: "use --parent-position or --parent-token", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + f, stdout, _, _ := cmdutil.TestFactory(t, docsCreateTestConfig(t, "")) + err := runDocsCreateShortcut(t, f, stdout, tt.args) + if err == nil { + t.Fatal("expected validation error, got nil") + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("error = %v, want to contain %q", err, tt.wantErr) + } + if !strings.Contains(err.Error(), "--api-version v2") { + t.Fatalf("error = %v, want v2 guidance", err) + } + }) + } +} + // ── V1 (MCP) tests ── func TestDocsCreateV1BotAutoGrantSuccess(t *testing.T) { diff --git a/shortcuts/doc/docs_create_v2.go b/shortcuts/doc/docs_create_v2.go index 68ae824c9..be5f88bc2 100644 --- a/shortcuts/doc/docs_create_v2.go +++ b/shortcuts/doc/docs_create_v2.go @@ -21,6 +21,9 @@ func v2CreateFlags() []common.Flag { } func validateCreateV2(_ context.Context, runtime *common.RuntimeContext) error { + if err := rejectV1CreateFlagsForV2(runtime); err != nil { + return err + } if runtime.Str("content") == "" { return common.FlagErrorf("--content is required") } @@ -30,6 +33,25 @@ func validateCreateV2(_ context.Context, runtime *common.RuntimeContext) error { return nil } +func rejectV1CreateFlagsForV2(runtime *common.RuntimeContext) error { + v1Flags := []struct { + name string + hint string + }{ + {name: "markdown", hint: "use --content with --doc-format markdown"}, + {name: "title", hint: "include the document title in --content"}, + {name: "folder-token", hint: "use --parent-token"}, + {name: "wiki-node", hint: "use --parent-token"}, + {name: "wiki-space", hint: "use --parent-position or --parent-token"}, + } + for _, flag := range v1Flags { + if runtime.Str(flag.name) != "" { + return common.FlagErrorf("--%s is only supported by docs +create v1; for --api-version v2, %s", flag.name, flag.hint) + } + } + return nil +} + func dryRunCreateV2(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { body := buildCreateBody(runtime) desc := "OpenAPI: create document" diff --git a/tests/cli_e2e/docs/coverage.md b/tests/cli_e2e/docs/coverage.md index 4feef51ba..5835645d4 100644 --- a/tests/cli_e2e/docs/coverage.md +++ b/tests/cli_e2e/docs/coverage.md @@ -8,6 +8,7 @@ ## Summary - TestDocs_CreateAndFetchWorkflow: proves `docs +create` and `docs +fetch`; key `t.Run(...)` proof points are `create as bot` and `fetch as bot`. - TestDocs_CreateAndFetchWorkflowAsUser: proves the same shortcut pair with UAT injection via `create as user` and `fetch as user`; creates its own Drive folder fixture first, then reads back the created doc by token. +- TestDocs_CreateV2RejectsLegacyFlagsDryRun: proves `docs +create --api-version v2 --dry-run` rejects legacy v1-only flags before emitting any API steps. - TestDocs_UpdateWorkflow: proves `docs +update` via `update-title-and-content as bot`, then re-fetches the same doc in `verify as bot` to assert persisted title/content changes. - Setup note: docs workflows create a Drive folder through `drive files create_folder` in `helpers_test.go`; that helper is external to the docs domain and is not counted here. - Blocked area: media and search shortcuts still need deterministic fixtures and local file orchestration. @@ -16,7 +17,7 @@ | Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason | | --- | --- | --- | --- | --- | --- | -| ✓ | docs +create | shortcut | docs/helpers_test.go::createDocWithRetry; docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflowAsUser/create as user | `--folder-token`; `--title`; `--markdown` | helper asserts returned doc id | +| ✓ | docs +create | shortcut | docs/helpers_test.go::createDocWithRetry; docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflowAsUser/create as user; docs_create_dryrun_test.go::TestDocs_CreateV2RejectsLegacyFlagsDryRun | `--folder-token`; `--title`; `--markdown`; v2 legacy flag rejection under `--dry-run` | helper asserts returned doc id; dry-run case asserts no API calls on validation failure | | ✓ | docs +fetch | shortcut | docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflow/fetch as bot; docs_update_test.go::TestDocs_UpdateWorkflow/verify as bot; docs_create_fetch_test.go::TestDocs_CreateAndFetchWorkflowAsUser/fetch as user | `--doc ` | | | ✕ | docs +media-download | shortcut | | none | no media fixture workflow yet | | ✕ | docs +media-insert | shortcut | | none | requires deterministic upload fixture and rollback assertions | diff --git a/tests/cli_e2e/docs/docs_create_dryrun_test.go b/tests/cli_e2e/docs/docs_create_dryrun_test.go new file mode 100644 index 000000000..f498dd497 --- /dev/null +++ b/tests/cli_e2e/docs/docs_create_dryrun_test.go @@ -0,0 +1,112 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package docs + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestDocs_CreateV2RejectsLegacyFlagsDryRun(t *testing.T) { + setDocsDryRunEnv(t) + + tests := []struct { + name string + args []string + wantErr string + }{ + { + name: "markdown", + args: []string{ + "docs", "+create", + "--api-version", "v2", + "--markdown", "## legacy", + "--dry-run", + }, + wantErr: "use --content with --doc-format markdown", + }, + { + name: "wiki node", + args: []string{ + "docs", "+create", + "--api-version", "v2", + "--content", "内容

正文

", + "--wiki-node", "wikcn_legacy_node", + "--dry-run", + }, + wantErr: "use --parent-token", + }, + { + name: "title", + args: []string{ + "docs", "+create", + "--api-version", "v2", + "--content", "

正文

", + "--title", "Legacy title", + "--dry-run", + }, + wantErr: "include the document title in --content", + }, + { + name: "folder token", + args: []string{ + "docs", "+create", + "--api-version", "v2", + "--content", "内容

正文

", + "--folder-token", "fldcn_legacy_folder", + "--dry-run", + }, + wantErr: "use --parent-token", + }, + { + name: "wiki space", + args: []string{ + "docs", "+create", + "--api-version", "v2", + "--content", "内容

正文

", + "--wiki-space", "my_library", + "--dry-run", + }, + wantErr: "use --parent-position or --parent-token", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + t.Cleanup(cancel) + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: tt.args, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 2) + assert.Contains(t, docsValidationErrorMessage(result), tt.wantErr) + assert.Equal(t, int64(0), gjson.Get(result.Stdout, "api.#").Int(), + "validation failure must not produce dry-run API calls, stdout:\n%s", result.Stdout) + }) + } +} + +func setDocsDryRunEnv(t *testing.T) { + t.Helper() + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + t.Setenv("LARKSUITE_CLI_APP_ID", "docs_dryrun_e2e_app") + t.Setenv("LARKSUITE_CLI_APP_SECRET", "docs_dryrun_e2e_secret") + t.Setenv("LARKSUITE_CLI_BRAND", "feishu") +} + +func docsValidationErrorMessage(r *clie2e.Result) string { + if msg := gjson.Get(r.Stdout, "error.message").String(); msg != "" { + return msg + } + return gjson.Get(r.Stderr, "error.message").String() +}