Skip to content

Commit 747b6dc

Browse files
author
zh
committed
fix(docs): reject legacy create flags in v2
1 parent e98471c commit 747b6dc

4 files changed

Lines changed: 181 additions & 1 deletion

File tree

shortcuts/doc/docs_create_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,84 @@ func TestDocsCreateV2PreservesBackendURL(t *testing.T) {
249249
}
250250
}
251251

252+
func TestDocsCreateV2RejectsV1OnlyFlags(t *testing.T) {
253+
t.Parallel()
254+
255+
tests := []struct {
256+
name string
257+
args []string
258+
wantErr string
259+
}{
260+
{
261+
name: "markdown",
262+
args: []string{
263+
"+create",
264+
"--api-version", "v2",
265+
"--markdown", "## legacy",
266+
},
267+
wantErr: "use --content with --doc-format markdown",
268+
},
269+
{
270+
name: "wiki node",
271+
args: []string{
272+
"+create",
273+
"--api-version", "v2",
274+
"--content", "<title>内容</title><p>正文</p>",
275+
"--wiki-node", "wikcn_legacy_node",
276+
},
277+
wantErr: "use --parent-token",
278+
},
279+
{
280+
name: "title",
281+
args: []string{
282+
"+create",
283+
"--api-version", "v2",
284+
"--content", "<p>正文</p>",
285+
"--title", "Legacy title",
286+
},
287+
wantErr: "include the document title in --content",
288+
},
289+
{
290+
name: "folder token",
291+
args: []string{
292+
"+create",
293+
"--api-version", "v2",
294+
"--content", "<title>内容</title><p>正文</p>",
295+
"--folder-token", "fldcn_legacy_folder",
296+
},
297+
wantErr: "use --parent-token",
298+
},
299+
{
300+
name: "wiki space",
301+
args: []string{
302+
"+create",
303+
"--api-version", "v2",
304+
"--content", "<title>内容</title><p>正文</p>",
305+
"--wiki-space", "my_library",
306+
},
307+
wantErr: "use --parent-position or --parent-token",
308+
},
309+
}
310+
311+
for _, tt := range tests {
312+
t.Run(tt.name, func(t *testing.T) {
313+
t.Parallel()
314+
315+
f, stdout, _, _ := cmdutil.TestFactory(t, docsCreateTestConfig(t, ""))
316+
err := runDocsCreateShortcut(t, f, stdout, tt.args)
317+
if err == nil {
318+
t.Fatal("expected validation error, got nil")
319+
}
320+
if !strings.Contains(err.Error(), tt.wantErr) {
321+
t.Fatalf("error = %v, want to contain %q", err, tt.wantErr)
322+
}
323+
if !strings.Contains(err.Error(), "--api-version v2") {
324+
t.Fatalf("error = %v, want v2 guidance", err)
325+
}
326+
})
327+
}
328+
}
329+
252330
// ── V1 (MCP) tests ──
253331

254332
func TestDocsCreateV1BotAutoGrantSuccess(t *testing.T) {

shortcuts/doc/docs_create_v2.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ func v2CreateFlags() []common.Flag {
2121
}
2222

2323
func validateCreateV2(_ context.Context, runtime *common.RuntimeContext) error {
24+
if err := rejectV1CreateFlagsForV2(runtime); err != nil {
25+
return err
26+
}
2427
if runtime.Str("content") == "" {
2528
return common.FlagErrorf("--content is required")
2629
}
@@ -30,6 +33,25 @@ func validateCreateV2(_ context.Context, runtime *common.RuntimeContext) error {
3033
return nil
3134
}
3235

36+
func rejectV1CreateFlagsForV2(runtime *common.RuntimeContext) error {
37+
v1Flags := []struct {
38+
name string
39+
hint string
40+
}{
41+
{name: "markdown", hint: "use --content with --doc-format markdown"},
42+
{name: "title", hint: "include the document title in --content"},
43+
{name: "folder-token", hint: "use --parent-token"},
44+
{name: "wiki-node", hint: "use --parent-token"},
45+
{name: "wiki-space", hint: "use --parent-position or --parent-token"},
46+
}
47+
for _, flag := range v1Flags {
48+
if runtime.Str(flag.name) != "" {
49+
return common.FlagErrorf("--%s is only supported by docs +create v1; for --api-version v2, %s", flag.name, flag.hint)
50+
}
51+
}
52+
return nil
53+
}
54+
3355
func dryRunCreateV2(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
3456
body := buildCreateBody(runtime)
3557
desc := "OpenAPI: create document"

tests/cli_e2e/docs/coverage.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
## Summary
99
- TestDocs_CreateAndFetchWorkflow: proves `docs +create` and `docs +fetch`; key `t.Run(...)` proof points are `create as bot` and `fetch as bot`.
1010
- 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.
11+
- TestDocs_CreateV2RejectsLegacyFlagsDryRun: proves `docs +create --api-version v2 --dry-run` rejects legacy v1-only flags before emitting any API steps.
1112
- 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.
1213
- 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.
1314
- Blocked area: media and search shortcuts still need deterministic fixtures and local file orchestration.
@@ -16,7 +17,7 @@
1617

1718
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
1819
| --- | --- | --- | --- | --- | --- |
19-
|| 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 |
20+
|| 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 |
2021
|| 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>` | |
2122
|| docs +media-download | shortcut | | none | no media fixture workflow yet |
2223
|| docs +media-insert | shortcut | | none | requires deterministic upload fixture and rollback assertions |
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package docs
5+
6+
import (
7+
"context"
8+
"testing"
9+
"time"
10+
11+
clie2e "github.com/larksuite/cli/tests/cli_e2e"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
"github.com/tidwall/gjson"
15+
)
16+
17+
func TestDocs_CreateV2RejectsLegacyFlagsDryRun(t *testing.T) {
18+
setDocsDryRunEnv(t)
19+
20+
tests := []struct {
21+
name string
22+
args []string
23+
wantErr string
24+
}{
25+
{
26+
name: "markdown",
27+
args: []string{
28+
"docs", "+create",
29+
"--api-version", "v2",
30+
"--markdown", "## legacy",
31+
"--dry-run",
32+
},
33+
wantErr: "use --content with --doc-format markdown",
34+
},
35+
{
36+
name: "wiki node",
37+
args: []string{
38+
"docs", "+create",
39+
"--api-version", "v2",
40+
"--content", "<title>内容</title><p>正文</p>",
41+
"--wiki-node", "wikcn_legacy_node",
42+
"--dry-run",
43+
},
44+
wantErr: "use --parent-token",
45+
},
46+
}
47+
48+
for _, tt := range tests {
49+
t.Run(tt.name, func(t *testing.T) {
50+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
51+
t.Cleanup(cancel)
52+
53+
result, err := clie2e.RunCmd(ctx, clie2e.Request{
54+
Args: tt.args,
55+
DefaultAs: "bot",
56+
})
57+
require.NoError(t, err)
58+
result.AssertExitCode(t, 2)
59+
assert.Contains(t, docsValidationErrorMessage(result), tt.wantErr)
60+
assert.Equal(t, int64(0), gjson.Get(result.Stdout, "api.#").Int(),
61+
"validation failure must not produce dry-run API calls, stdout:\n%s", result.Stdout)
62+
})
63+
}
64+
}
65+
66+
func setDocsDryRunEnv(t *testing.T) {
67+
t.Helper()
68+
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
69+
t.Setenv("LARKSUITE_CLI_APP_ID", "docs_dryrun_e2e_app")
70+
t.Setenv("LARKSUITE_CLI_APP_SECRET", "docs_dryrun_e2e_secret")
71+
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
72+
}
73+
74+
func docsValidationErrorMessage(r *clie2e.Result) string {
75+
if msg := gjson.Get(r.Stdout, "error.message").String(); msg != "" {
76+
return msg
77+
}
78+
return gjson.Get(r.Stderr, "error.message").String()
79+
}

0 commit comments

Comments
 (0)