From e21b7144d18f8e9698e5fe08a4960bfd13baa62c Mon Sep 17 00:00:00 2001 From: "sunpeiyang.996" Date: Tue, 23 Jun 2026 17:09:08 +0800 Subject: [PATCH] feat(docs): add reference map flags --- shortcuts/doc/docs_fetch_v2.go | 5 +- shortcuts/doc/docs_fetch_v2_test.go | 39 ++++++ shortcuts/doc/docs_update_test.go | 122 ++++++++++++++++++ shortcuts/doc/docs_update_v2.go | 70 +++++++++- shortcuts/drive/drive_export_test.go | 12 ++ skills/lark-doc/references/lark-doc-update.md | 1 + tests/cli_e2e/docs/coverage.md | 6 +- tests/cli_e2e/docs/docs_update_dryrun_test.go | 32 ++++- tests/cli_e2e/drive/coverage.md | 4 +- .../cli_e2e/drive/drive_export_dryrun_test.go | 3 + 10 files changed, 282 insertions(+), 12 deletions(-) diff --git a/shortcuts/doc/docs_fetch_v2.go b/shortcuts/doc/docs_fetch_v2.go index 16ca133f7..936b193d4 100644 --- a/shortcuts/doc/docs_fetch_v2.go +++ b/shortcuts/doc/docs_fetch_v2.go @@ -14,6 +14,8 @@ import ( "github.com/larksuite/cli/shortcuts/common" ) +const docsFetchExtraParam = `{"enable_user_cite_reference_map":true}` + // v2FetchFlags returns the flag definitions for the v2 (OpenAPI) fetch path. func v2FetchFlags() []common.Flag { return []common.Flag{ @@ -88,7 +90,8 @@ func executeFetchV2(_ context.Context, runtime *common.RuntimeContext) error { func buildFetchBody(runtime *common.RuntimeContext) map[string]interface{} { body := map[string]interface{}{ - "format": effectiveFetchFormat(runtime), + "format": effectiveFetchFormat(runtime), + "extra_param": docsFetchExtraParam, } if v := runtime.Int("revision-id"); v > 0 { body["revision_id"] = v diff --git a/shortcuts/doc/docs_fetch_v2_test.go b/shortcuts/doc/docs_fetch_v2_test.go index 6bb959250..2ba7c0b0c 100644 --- a/shortcuts/doc/docs_fetch_v2_test.go +++ b/shortcuts/doc/docs_fetch_v2_test.go @@ -488,6 +488,44 @@ func TestAddFetchDetailDowngradeWarningNoops(t *testing.T) { } } +func TestBuildFetchBodyIncludesFetchExtraParamByDefault(t *testing.T) { + t.Parallel() + + runtime := newFetchBodyTestRuntime(context.Background()) + + body := buildFetchBody(runtime) + extraParam, ok := body["extra_param"].(string) + if !ok || extraParam == "" { + t.Fatalf("extra_param = %#v, want JSON string", body["extra_param"]) + } + var got map[string]bool + if err := json.Unmarshal([]byte(extraParam), &got); err != nil { + t.Fatalf("decode extra_param %q: %v", extraParam, err) + } + if got["enable_user_cite_reference_map"] != true { + t.Fatalf("enable_user_cite_reference_map = %#v, want true in %#v", got["enable_user_cite_reference_map"], got) + } + if _, ok := got["return_html5_block_data"]; ok { + t.Fatalf("extra_param should not request html5 block data: %#v", got) + } + if _, ok := got["reference_map_mode"]; ok { + t.Fatalf("extra_param should not use legacy reference_map_mode: %#v", got) + } + if len(got) != 1 { + t.Fatalf("extra_param should only contain fetch reference_map toggle: %#v", got) + } +} + +func TestDocsFetchV2ReferenceMapFlagIsNotAvailable(t *testing.T) { + t.Parallel() + + for _, flag := range v2FetchFlags() { + if flag.Name == "reference-map" { + t.Fatal("fetch should not expose reference-map flag") + } + } +} + func TestDocsFetchDryRunDefaultsToV2Endpoint(t *testing.T) { t.Parallel() @@ -904,6 +942,7 @@ func newUpdateBodyTestRuntime(ctx context.Context) *common.RuntimeContext { cmd.Flags().String("command", "append", "") cmd.Flags().Int("revision-id", 0, "") cmd.Flags().String("content", "

hello

", "") + cmd.Flags().String("reference-map", "", "") cmd.Flags().String("pattern", "", "") cmd.Flags().String("block-id", "", "") cmd.Flags().String("src-block-ids", "", "") diff --git a/shortcuts/doc/docs_update_test.go b/shortcuts/doc/docs_update_test.go index b6b757cb0..6d8dcfd47 100644 --- a/shortcuts/doc/docs_update_test.go +++ b/shortcuts/doc/docs_update_test.go @@ -4,9 +4,11 @@ package doc import ( "context" + "errors" "strings" "testing" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/shortcuts/common" "github.com/spf13/cobra" ) @@ -61,6 +63,116 @@ func TestDocsUpdateDryRunIgnoresAPIVersionCompatFlag(t *testing.T) { } } +func TestDocsUpdateV2ReferenceMapFlagIsPublicFileInput(t *testing.T) { + t.Parallel() + + var flag common.Flag + for _, candidate := range v2UpdateFlags() { + if candidate.Name == "reference-map" { + flag = candidate + break + } + } + if flag.Name == "" { + t.Fatal("reference-map flag not found") + } + if flag.Hidden { + t.Fatal("reference-map flag should be public") + } + if flag.Type != "" { + t.Fatalf("reference-map flag Type = %q, want default string", flag.Type) + } + if !hasUpdateTestInput(flag, common.File) || !hasUpdateTestInput(flag, common.Stdin) { + t.Fatalf("reference-map Input = %#v, want file and stdin", flag.Input) + } + if flag.Desc != docsUpdateReferenceMapFlagDesc { + t.Fatalf("reference-map help = %q, want %q", flag.Desc, docsUpdateReferenceMapFlagDesc) + } +} + +func TestBuildUpdateBodyIncludesReferenceMap(t *testing.T) { + t.Parallel() + + runtime := newUpdateShortcutTestRuntime(t, "", map[string]string{ + "command": "append", + "content": `

`, + "reference-map": `{"widget":{"r1":{"label":"widget-ref-value"}}}`, + }) + body := buildUpdateBody(runtime) + + refMap, ok := body["reference_map"].(map[string]interface{}) + if !ok { + t.Fatalf("reference_map = %#v, want object", body["reference_map"]) + } + widget, _ := refMap["widget"].(map[string]interface{}) + r1, _ := widget["r1"].(map[string]interface{}) + if got := r1["label"]; got != "widget-ref-value" { + t.Fatalf("reference_map.widget.r1.label = %#v, want widget-ref-value; body=%#v", got, body) + } + if got, want := body["command"], "block_insert_after"; got != want { + t.Fatalf("command = %#v, want %q", got, want) + } + if got, want := body["block_id"], "-1"; got != want { + t.Fatalf("block_id = %#v, want %q", got, want) + } +} + +func TestValidateUpdateV2RejectsInvalidReferenceMap(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setFlags map[string]string + wantCause bool + }{ + { + name: "invalid json", + setFlags: map[string]string{ + "reference-map": "{", + }, + wantCause: true, + }, + { + name: "empty", + setFlags: map[string]string{ + "reference-map": "", + }, + }, + { + name: "without content", + setFlags: map[string]string{ + "content": "", + "reference-map": `{"widget":{"r1":{"label":"widget-ref-value"}}}`, + }, + }, + { + name: "unsupported command", + setFlags: map[string]string{ + "command": "block_move_after", + "block-id": "blk_anchor", + "src-block-ids": "blk_src", + "reference-map": `{"widget":{"r1":{"label":"widget-ref-value"}}}`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + runtime := newUpdateShortcutTestRuntime(t, "", tt.setFlags) + err := validateUpdateV2(context.Background(), runtime) + if err == nil { + t.Fatal("validateUpdateV2() succeeded, want error") + } + assertValidationContract(t, err, errs.SubtypeInvalidArgument, "--reference-map") + if tt.wantCause && errors.Unwrap(err) == nil { + t.Fatal("validateUpdateV2() error lost underlying JSON cause") + } + }) + } +} + func TestDocsUpdateRejectsLegacyFlags(t *testing.T) { tests := []struct { name string @@ -103,6 +215,15 @@ func TestDocsUpdateRejectsLegacyFlags(t *testing.T) { } } +func hasUpdateTestInput(flag common.Flag, input string) bool { + for _, candidate := range flag.Input { + if candidate == input { + return true + } + } + return false +} + func newUpdateShortcutTestRuntime(t *testing.T, apiVersion string, setFlags map[string]string) *common.RuntimeContext { t.Helper() @@ -113,6 +234,7 @@ func newUpdateShortcutTestRuntime(t *testing.T, apiVersion string, setFlags map[ cmd.Flags().String("command", "append", "") cmd.Flags().Int("revision-id", -1, "") cmd.Flags().String("content", "

hello

", "") + cmd.Flags().String("reference-map", "", "") cmd.Flags().String("pattern", "", "") cmd.Flags().String("block-id", "", "") cmd.Flags().String("src-block-ids", "", "") diff --git a/shortcuts/doc/docs_update_v2.go b/shortcuts/doc/docs_update_v2.go index cc75d44ed..d9c12b47b 100644 --- a/shortcuts/doc/docs_update_v2.go +++ b/shortcuts/doc/docs_update_v2.go @@ -5,7 +5,9 @@ package doc import ( "context" + "encoding/json" "fmt" + "strings" "github.com/larksuite/cli/errs" "github.com/larksuite/cli/shortcuts/common" @@ -22,12 +24,15 @@ var validCommandsV2 = map[string]bool{ "append": true, } +const docsUpdateReferenceMapFlagDesc = "结构化 `reference_map` JSON object;当 `--content` 使用正文外部载荷 / 引用映射时与内容一起传给服务,支持直接 JSON、`@reference-map.json`(相对路径)或 `-` 从 stdin 读取。通常用于回写已有 `document.reference_map`。" + // v2UpdateFlags returns the flag definitions for the v2 (OpenAPI) update path. func v2UpdateFlags() []common.Flag { return []common.Flag{ {Name: "command", Desc: "operation; requirements: str_replace(--pattern), block_delete(--block-id, comma-separated for batch), block_insert_after/block_replace(--block-id,--content), block_copy_insert_after/block_move_after(--block-id,--src-block-ids), overwrite/append(--content)", Enum: validCommandsV2Keys()}, {Name: "doc-format", Desc: "content format for --content; xml is default for precise rich edits, markdown for user-provided Markdown or plain append/overwrite", Default: "xml", Enum: []string{"xml", "markdown"}}, {Name: "content", Desc: "replacement or inserted content; XML by default or Markdown when --doc-format markdown; empty with str_replace deletes match. " + docsContentSkillHelp + "; use --help for the latest command flags", Input: []string{common.File, common.Stdin}}, + {Name: "reference-map", Desc: docsUpdateReferenceMapFlagDesc, Input: []string{common.File, common.Stdin}}, {Name: "pattern", Desc: "str_replace match pattern; XML mode is inline text, Markdown mode can match multiline text"}, {Name: "block-id", Desc: "target block ID(s) for block operations (comma-separated for batch delete); -1 means document end where supported"}, {Name: "src-block-ids", Desc: "comma-separated source block ids for block_copy_insert_after and block_move_after"}, @@ -54,6 +59,9 @@ func validateUpdateV2(_ context.Context, runtime *common.RuntimeContext) error { return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --command %q, valid: str_replace | block_delete | block_insert_after | block_copy_insert_after | block_replace | block_move_after | overwrite | append", cmd).WithParam("--command") } content := runtime.Str("content") + if err := validateUpdateReferenceMap(runtime, cmd, content); err != nil { + return err + } pattern := runtime.Str("pattern") blockID := runtime.Str("block-id") srcBlockIDs := runtime.Str("src-block-ids") @@ -113,7 +121,7 @@ func validateUpdateV2(_ context.Context, runtime *common.RuntimeContext) error { func dryRunUpdateV2(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { // Validate has already accepted --doc; parseDocumentRef cannot fail here. ref, _ := parseDocumentRef(runtime.Str("doc")) - body := buildUpdateBody(runtime) + body, _ := buildUpdateBodyWithReferenceMap(runtime) apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s", ref.Token) return common.NewDryRunAPI(). PUT(apiPath). @@ -126,7 +134,10 @@ func executeUpdateV2(_ context.Context, runtime *common.RuntimeContext) error { ref, _ := parseDocumentRef(runtime.Str("doc")) apiPath := fmt.Sprintf("/open-apis/docs_ai/v1/documents/%s", ref.Token) - body := buildUpdateBody(runtime) + body, err := buildUpdateBodyWithReferenceMap(runtime) + if err != nil { + return err + } data, err := doDocAPI(runtime, "PUT", apiPath, body) if err != nil { @@ -138,6 +149,24 @@ func executeUpdateV2(_ context.Context, runtime *common.RuntimeContext) error { } func buildUpdateBody(runtime *common.RuntimeContext) map[string]interface{} { + body, _ := buildUpdateBodyWithReferenceMap(runtime) + return body +} + +func buildUpdateBodyWithReferenceMap(runtime *common.RuntimeContext) (map[string]interface{}, error) { + body := buildUpdateBodyBase(runtime) + if !runtime.Changed("reference-map") { + return body, nil + } + refMap, err := parseUpdateReferenceMap(runtime.Str("reference-map")) + if err != nil { + return body, err + } + body["reference_map"] = refMap + return body, nil +} + +func buildUpdateBodyBase(runtime *common.RuntimeContext) map[string]interface{} { cmd := runtime.Str("command") // append is a shorthand for block_insert_after with block_id "-1" (end of document) @@ -169,3 +198,40 @@ func buildUpdateBody(runtime *common.RuntimeContext) map[string]interface{} { injectDocsScene(runtime, body) return body } + +func validateUpdateReferenceMap(runtime *common.RuntimeContext, command string, content string) error { + if !runtime.Changed("reference-map") { + return nil + } + if !updateCommandAcceptsReferenceMap(command) { + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--reference-map is only supported with update commands that send --content").WithParam("--reference-map") + } + if content == "" { + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--reference-map requires --content that uses matching sidecar refs").WithParam("--reference-map") + } + _, err := parseUpdateReferenceMap(runtime.Str("reference-map")) + return err +} + +func updateCommandAcceptsReferenceMap(command string) bool { + switch command { + case "str_replace", "block_insert_after", "block_replace", "overwrite", "append": + return true + default: + return false + } +} + +func parseUpdateReferenceMap(raw string) (map[string]interface{}, error) { + if strings.TrimSpace(raw) == "" { + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--reference-map must be a non-empty JSON object").WithParam("--reference-map") + } + var refMap map[string]interface{} + if err := json.Unmarshal([]byte(raw), &refMap); err != nil { + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--reference-map must be a valid JSON object: %v", err).WithParam("--reference-map").WithCause(err) + } + if refMap == nil { + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--reference-map must be a JSON object, got null").WithParam("--reference-map") + } + return refMap, nil +} diff --git a/shortcuts/drive/drive_export_test.go b/shortcuts/drive/drive_export_test.go index 8c6c189d2..ec026e419 100644 --- a/shortcuts/drive/drive_export_test.go +++ b/shortcuts/drive/drive_export_test.go @@ -162,6 +162,9 @@ func TestDriveExportMarkdownWritesFile(t *testing.T) { if reqBody["format"] != "markdown" { t.Fatalf("docs_ai fetch body format = %v, want %q", reqBody["format"], "markdown") } + if _, ok := reqBody["extra_param"]; ok { + t.Fatalf("drive markdown export must not enable docs fetch extra_param: %#v", reqBody) + } data, err := os.ReadFile(filepath.Join(tmpDir, "Weekly Notes.md")) if err != nil { @@ -213,6 +216,9 @@ func TestDriveExportMarkdownUsesProvidedFileName(t *testing.T) { if reqBody["format"] != "markdown" { t.Fatalf("docs_ai fetch body format = %v, want %q", reqBody["format"], "markdown") } + if _, ok := reqBody["extra_param"]; ok { + t.Fatalf("drive markdown export must not enable docs fetch extra_param: %#v", reqBody) + } data, err := os.ReadFile(filepath.Join(tmpDir, "custom-notes.md")) if err != nil { @@ -283,6 +289,9 @@ func TestDriveExportDryRunIncludesLocalFileNameMetadata(t *testing.T) { if !strings.Contains(out, `"output_dir": "./exports"`) { t.Fatalf("stdout missing output_dir metadata: %s", out) } + if tt.name == "markdown" && strings.Contains(out, `"extra_param"`) { + t.Fatalf("markdown dry-run must not enable docs fetch extra_param: %s", out) + } }) } } @@ -333,6 +342,9 @@ func TestDriveExportMarkdownFallsBackToTokenWhenTitleLookupFails(t *testing.T) { if reqBody["format"] != "markdown" { t.Fatalf("docs_ai fetch body format = %v, want %q", reqBody["format"], "markdown") } + if _, ok := reqBody["extra_param"]; ok { + t.Fatalf("drive markdown export must not enable docs fetch extra_param: %#v", reqBody) + } data, err := os.ReadFile(filepath.Join(tmpDir, "docx123.md")) if err != nil { diff --git a/skills/lark-doc/references/lark-doc-update.md b/skills/lark-doc/references/lark-doc-update.md index 62fc467ad..918408fa6 100644 --- a/skills/lark-doc/references/lark-doc-update.md +++ b/skills/lark-doc/references/lark-doc-update.md @@ -24,6 +24,7 @@ | `--command` | 是 | 操作指令(见下方指令速查表) | | `--doc-format` | 否 | 内容格式:`xml`(默认,始终优先使用)\| `markdown`(仅用户明确要求时) | | `--content` | 视指令 | 写入内容(`str_replace` 传空字符串可实现删除) | +| `--reference-map` | 否 | 结构化 `reference_map` JSON object;当 `--content` 使用正文外部载荷 / 引用映射时与内容一起传给服务,支持直接 JSON、`@reference-map.json`(相对路径)或 `-` 从 stdin 读取。通常用于回写已有 `document.reference_map`。 | | `--pattern` | 视指令 | 匹配文本(str_replace) | | `--block-id` | 视指令 | 目标 block ID(block_* 操作),逗号分隔可批量删除,-1 表示末尾 | | `--src-block-ids` | 视指令 | 源 block ID(逗号分隔),用于 block_copy_insert_after / block_move_after | diff --git a/tests/cli_e2e/docs/coverage.md b/tests/cli_e2e/docs/coverage.md index a35946f97..b25f35fc0 100644 --- a/tests/cli_e2e/docs/coverage.md +++ b/tests/cli_e2e/docs/coverage.md @@ -9,7 +9,7 @@ - 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_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. -- TestDocs_DryRunDefaultsToV2OpenAPI: proves `docs +create`, `docs +fetch`, and `docs +update` dry-run all emit `/open-apis/docs_ai/v1/...` requests without MCP or `--api-version` guidance. +- TestDocs_DryRunDefaultsToV2OpenAPI: proves `docs +create`, `docs +fetch`, and `docs +update` dry-run all emit `/open-apis/docs_ai/v1/...` requests without MCP or `--api-version` guidance; its fetch case asserts fetch sends the default `extra_param`, and its update case asserts `--reference-map` is sent as request body `reference_map`. - TestDocs_CreateTitleDryRunPrependsContent: proves `docs +create --title` dry-run prepends an escaped `...` tag to request body `content`. - 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. @@ -19,10 +19,10 @@ | 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; docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/create; docs_update_dryrun_test.go::TestDocs_CreateTitleDryRunPrependsContent | `--parent-token`; `--doc-format markdown`; `--content`; `--title` | helper asserts returned doc id from `data.document.document_id`; dry-run asserts title is prepended into request body content | -| ✓ | 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; docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/fetch | `--doc `; `--doc-format markdown` | | +| ✓ | 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; docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/fetch | `--doc `; `--doc-format markdown`; default `extra_param.enable_user_cite_reference_map=true` | | | ✕ | docs +media-download | shortcut | | none | no media fixture workflow yet | | ✕ | docs +media-insert | shortcut | | none | requires deterministic upload fixture and rollback assertions | | ✕ | docs +media-preview | shortcut | | none | requires deterministic media fixture | | ✕ | docs +search | shortcut | | none | search results are ambient and not yet stabilized for E2E | -| ✓ | docs +update | shortcut | docs_update_test.go::TestDocs_UpdateWorkflow/update-title-and-content as bot; docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/update | `--doc`; `--command overwrite`; `--doc-format markdown`; `--content` | | +| ✓ | docs +update | shortcut | docs_update_test.go::TestDocs_UpdateWorkflow/update-title-and-content as bot; docs_update_dryrun_test.go::TestDocs_DryRunDefaultsToV2OpenAPI/update | `--doc`; `--command overwrite`; `--doc-format markdown`; `--content`; optional `--reference-map` -> body `reference_map` | | | ✕ | docs +whiteboard-update | shortcut | | none | requires whiteboard fixture and DSL-specific assertions | diff --git a/tests/cli_e2e/docs/docs_update_dryrun_test.go b/tests/cli_e2e/docs/docs_update_dryrun_test.go index c0bd05864..7f47490e0 100644 --- a/tests/cli_e2e/docs/docs_update_dryrun_test.go +++ b/tests/cli_e2e/docs/docs_update_dryrun_test.go @@ -24,9 +24,11 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) { t.Cleanup(cancel) tests := []struct { - name string - args []string - wantURL string + name string + args []string + wantURL string + wantExtraParam string + wantRefLabel string }{ { name: "create", @@ -54,7 +56,8 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) { "--doc", "doxcnDryRunE2E", "--dry-run", }, - wantURL: "/open-apis/docs_ai/v1/documents/doxcnDryRunE2E/fetch", + wantURL: "/open-apis/docs_ai/v1/documents/doxcnDryRunE2E/fetch", + wantExtraParam: `{"enable_user_cite_reference_map":true}`, }, { name: "update", @@ -67,6 +70,19 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) { }, wantURL: "/open-apis/docs_ai/v1/documents/doxcnDryRunE2E", }, + { + name: "update reference-map", + args: []string{ + "docs", "+update", + "--doc", "doxcnDryRunE2E", + "--command", "append", + "--content", `

`, + "--reference-map", `{"widget":{"r1":{"label":"widget-ref-value"}}}`, + "--dry-run", + }, + wantURL: "/open-apis/docs_ai/v1/documents/doxcnDryRunE2E", + wantRefLabel: "widget-ref-value", + }, { name: "block_delete batch", args: []string{ @@ -104,6 +120,14 @@ func TestDocs_DryRunDefaultsToV2OpenAPI(t *testing.T) { if strings.Contains(combined, "--api-version") { t.Fatalf("dry-run output should not ask for --api-version\nstdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr) } + if tt.wantExtraParam != "" { + extraParam := gjson.Get(result.Stdout, "api.0.body.extra_param").String() + require.JSONEq(t, tt.wantExtraParam, extraParam, "stdout:\n%s", result.Stdout) + } + if tt.wantRefLabel != "" { + got := gjson.Get(result.Stdout, "api.0.body.reference_map.widget.r1.label").String() + require.Equal(t, tt.wantRefLabel, got, "stdout:\n%s", result.Stdout) + } }) } } diff --git a/tests/cli_e2e/drive/coverage.md b/tests/cli_e2e/drive/coverage.md index bde35dcb0..115c46bc7 100644 --- a/tests/cli_e2e/drive/coverage.md +++ b/tests/cli_e2e/drive/coverage.md @@ -14,7 +14,7 @@ - TestDriveAddCommentDryRun_File / TestDriveAddCommentDryRun_Base: dry-run coverage for `drive +add-comment` on supported Drive file and Base targets; pins the `metas.batch_query -> files/:token/new_comments` file chain, Base `file_type=bitable`, and Base anchor fields. - TestDriveAddCommentMarkdownFileWorkflow: opt-in live workflow skeleton for the same path, gated by `LARK_DRIVE_MD_COMMENT_E2E=1`. - TestDrive_SecureLabelDryRun: dry-run coverage for `drive +secure-label-list` and `drive +secure-label-update`; asserts label-list query params and update URL→type inference, request method/URL/type query, and `label-id` body shape. Runs without hitting live APIs because update can trigger document-level security approval flows. -- TestDriveExportDryRun_FileNameMetadata / TestDriveExportDryRun_BitableBaseOnlySchema: dry-run coverage for `drive +export`; asserts export task request shape, local `--file-name` / `--output-dir` metadata, and `bitable` `.base` `only_schema` request body without calling live APIs. +- TestDriveExportDryRun_FileNameMetadata / TestDriveExportDryRun_MarkdownFetchAPI / TestDriveExportDryRun_BitableBaseOnlySchema: dry-run coverage for `drive +export`; asserts export task request shape, markdown fetch request shape without docs fetch `extra_param`, local `--file-name` / `--output-dir` metadata, and `bitable` `.base` `only_schema` request body without calling live APIs. - TestDrive_PullDryRun / TestDrive_PullDryRunAcceptsDuplicateRemoteStrategies: dry-run coverage for `drive +pull`; asserts the list-files request shape, Validate-stage safety guards, and acceptance of `--on-duplicate-remote=rename|newest|oldest` by the real CLI binary. - TestDrive_PushDryRun / TestDrive_PushDryRunAcceptsDuplicateRemoteStrategies: dry-run coverage for `drive +push`; asserts the list-files request shape, Validate-stage safety guards, conditional delete preflight, and acceptance of `--on-duplicate-remote=newest|oldest` by the real CLI binary. - Cleanup note: `drive files delete` is only exercised in cleanup and is intentionally left uncovered. @@ -29,7 +29,7 @@ | ✓ | drive +apply-permission | shortcut | drive_apply_permission_dryrun_test.go::TestDrive_ApplyPermissionDryRun | `--token` URL vs bare; `--type` (enum) with URL inference; `--perm view\|edit`; `--remark` optional | dry-run only; no live-apply E2E because a real request pushes a card to the owner | | ✕ | drive +delete | shortcut | | none | no primary delete workflow yet | | ✕ | drive +download | shortcut | | none | no file fixture workflow yet | -| ✓ | drive +export | shortcut | drive_export_dryrun_test.go::TestDriveExportDryRun_FileNameMetadata + TestDriveExportDryRun_BitableBaseOnlySchema | `--token`; `--doc-type`; `--file-extension`; `--file-name`; `--output-dir`; `--only-schema` | dry-run only; no live export workflow yet | +| ✓ | drive +export | shortcut | drive_export_dryrun_test.go::TestDriveExportDryRun_FileNameMetadata + TestDriveExportDryRun_MarkdownFetchAPI + TestDriveExportDryRun_BitableBaseOnlySchema | `--token`; `--doc-type`; `--file-extension`; `--file-name`; `--output-dir`; `--only-schema`; markdown fetch omits docs fetch `extra_param` | dry-run only; no live export workflow yet | | ✕ | drive +export-download | shortcut | | none | no export-download workflow yet | | ✕ | drive +import | shortcut | | none | no import workflow yet | | ✕ | drive +move | shortcut | | none | no move workflow yet | diff --git a/tests/cli_e2e/drive/drive_export_dryrun_test.go b/tests/cli_e2e/drive/drive_export_dryrun_test.go index b4cb7d2b3..928197ba9 100644 --- a/tests/cli_e2e/drive/drive_export_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_export_dryrun_test.go @@ -92,6 +92,9 @@ func TestDriveExportDryRun_MarkdownFetchAPI(t *testing.T) { if got := gjson.Get(out, "api.0.body.format").String(); got != "markdown" { t.Fatalf("body.format=%q, want markdown\nstdout:\n%s", got, out) } + if gjson.Get(out, "api.0.body.extra_param").Exists() { + t.Fatalf("markdown drive export must not enable docs fetch extra_param\nstdout:\n%s", out) + } if got := gjson.Get(out, "file_name").String(); got != "my-notes.md" { t.Fatalf("file_name=%q, want my-notes.md\nstdout:\n%s", got, out) }