Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions shortcuts/doc/docs_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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", "<title>内容</title><p>正文</p>",
"--wiki-node", "wikcn_legacy_node",
},
wantErr: "use --parent-token",
},
{
name: "title",
args: []string{
"+create",
"--api-version", "v2",
"--content", "<p>正文</p>",
"--title", "Legacy title",
},
wantErr: "include the document title in --content",
},
{
name: "folder token",
args: []string{
"+create",
"--api-version", "v2",
"--content", "<title>内容</title><p>正文</p>",
"--folder-token", "fldcn_legacy_folder",
},
wantErr: "use --parent-token",
},
{
name: "wiki space",
args: []string{
"+create",
"--api-version", "v2",
"--content", "<title>内容</title><p>正文</p>",
"--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) {
Expand Down
22 changes: 22 additions & 0 deletions shortcuts/doc/docs_create_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion tests/cli_e2e/docs/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 <docToken>` | |
| ✕ | docs +media-download | shortcut | | none | no media fixture workflow yet |
| ✕ | docs +media-insert | shortcut | | none | requires deterministic upload fixture and rollback assertions |
Expand Down
112 changes: 112 additions & 0 deletions tests/cli_e2e/docs/docs_create_dryrun_test.go
Original file line number Diff line number Diff line change
@@ -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", "<title>内容</title><p>正文</p>",
"--wiki-node", "wikcn_legacy_node",
"--dry-run",
},
wantErr: "use --parent-token",
},
{
name: "title",
args: []string{
"docs", "+create",
"--api-version", "v2",
"--content", "<p>正文</p>",
"--title", "Legacy title",
"--dry-run",
},
wantErr: "include the document title in --content",
},
{
name: "folder token",
args: []string{
"docs", "+create",
"--api-version", "v2",
"--content", "<title>内容</title><p>正文</p>",
"--folder-token", "fldcn_legacy_folder",
"--dry-run",
},
wantErr: "use --parent-token",
},
{
name: "wiki space",
args: []string{
"docs", "+create",
"--api-version", "v2",
"--content", "<title>内容</title><p>正文</p>",
"--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()
}
Loading