diff --git a/shortcuts/markdown/helpers.go b/shortcuts/markdown/helpers.go index 1b83cf146..aa7842003 100644 --- a/shortcuts/markdown/helpers.go +++ b/shortcuts/markdown/helpers.go @@ -23,10 +23,16 @@ import ( const markdownSinglePartSizeLimit = common.MaxDriveMediaUploadSinglePartSize const markdownEmptyContentError = "empty markdown content is not supported; cannot create or overwrite an empty file" +const ( + markdownUploadParentTypeExplorer = "explorer" + markdownUploadParentTypeWiki = "wiki" +) + type markdownUploadSpec struct { FileToken string FileName string FolderToken string + WikiToken string FilePath string Content string ContentSet bool @@ -44,6 +50,24 @@ type markdownMultipartSession struct { BlockNum int } +type markdownUploadTarget struct { + ParentType string + ParentNode string +} + +func (spec markdownUploadSpec) Target() markdownUploadTarget { + if spec.WikiToken != "" { + return markdownUploadTarget{ + ParentType: markdownUploadParentTypeWiki, + ParentNode: spec.WikiToken, + } + } + return markdownUploadTarget{ + ParentType: markdownUploadParentTypeExplorer, + ParentNode: spec.FolderToken, + } +} + func validateMarkdownSpec(runtime *common.RuntimeContext, spec markdownUploadSpec, requireName bool) error { switch { case spec.ContentSet && spec.FileSet: @@ -52,14 +76,32 @@ func validateMarkdownSpec(runtime *common.RuntimeContext, spec markdownUploadSpe return common.FlagErrorf("specify exactly one of --content or --file") } - if runtime.Changed("folder-token") && strings.TrimSpace(spec.FolderToken) == "" { + if markdownFlagExplicitlyEmpty(runtime, "folder-token") { return common.FlagErrorf("--folder-token cannot be empty; omit it to upload into Drive root folder") } + if markdownFlagExplicitlyEmpty(runtime, "wiki-token") { + return common.FlagErrorf("--wiki-token cannot be empty; omit it to upload into Drive root folder or pass a wiki node token") + } + targets := 0 + if spec.FolderToken != "" { + targets++ + } + if spec.WikiToken != "" { + targets++ + } + if targets > 1 { + return common.FlagErrorf("--folder-token and --wiki-token are mutually exclusive") + } if spec.FolderToken != "" { if err := validate.ResourceName(spec.FolderToken, "--folder-token"); err != nil { return output.ErrValidation("%s", err) } } + if spec.WikiToken != "" { + if err := validate.ResourceName(spec.WikiToken, "--wiki-token"); err != nil { + return output.ErrValidation("%s", err) + } + } if requireName && spec.ContentSet { if strings.TrimSpace(spec.FileName) == "" { @@ -91,6 +133,10 @@ func validateMarkdownSpec(runtime *common.RuntimeContext, spec markdownUploadSpe return nil } +func markdownFlagExplicitlyEmpty(runtime *common.RuntimeContext, flagName string) bool { + return runtime.Changed(flagName) && strings.TrimSpace(runtime.Str(flagName)) == "" +} + func validateMarkdownFileName(name, flagName string) error { trimmed := strings.TrimSpace(name) if trimmed == "" { @@ -142,12 +188,13 @@ func markdownDryRunFileField(spec markdownUploadSpec) string { func markdownUploadDryRun(spec markdownUploadSpec, fileSize int64, multipart bool) *common.DryRunAPI { fileName := finalMarkdownFileName(spec) + target := spec.Target() if !multipart { body := map[string]interface{}{ "file_name": fileName, - "parent_type": "explorer", - "parent_node": spec.FolderToken, + "parent_type": target.ParentType, + "parent_node": target.ParentNode, "size": fileSize, "file": markdownDryRunFileField(spec), } @@ -168,8 +215,8 @@ func markdownUploadDryRun(spec markdownUploadSpec, fileSize int64, multipart boo prepareBody := map[string]interface{}{ "file_name": fileName, - "parent_type": "explorer", - "parent_node": spec.FolderToken, + "parent_type": target.ParentType, + "parent_node": target.ParentNode, "size": fileSize, } if spec.FileToken != "" { @@ -204,6 +251,7 @@ func markdownUploadDryRun(spec markdownUploadSpec, fileSize int64, multipart boo func markdownOverwriteDryRun(spec markdownUploadSpec, fileSize int64, multipart bool) *common.DryRunAPI { fileName := strings.TrimSpace(spec.FileName) + target := spec.Target() if fileName == "" && spec.FileSet { fileName = finalMarkdownFileName(spec) } @@ -230,8 +278,8 @@ func markdownOverwriteDryRun(spec markdownUploadSpec, fileSize int64, multipart Desc("[2] Overwrite file contents with multipart/form-data upload"). Body(map[string]interface{}{ "file_name": spec.FileName, - "parent_type": "explorer", - "parent_node": spec.FolderToken, + "parent_type": target.ParentType, + "parent_node": target.ParentNode, "size": fileSize, "file": markdownDryRunFileField(spec), "file_token": spec.FileToken, @@ -243,8 +291,8 @@ func markdownOverwriteDryRun(spec markdownUploadSpec, fileSize int64, multipart Desc("[2] Initialize multipart overwrite upload"). Body(map[string]interface{}{ "file_name": spec.FileName, - "parent_type": "explorer", - "parent_node": spec.FolderToken, + "parent_type": target.ParentType, + "parent_node": target.ParentNode, "size": fileSize, "file_token": spec.FileToken, }). @@ -289,10 +337,11 @@ func uploadMarkdownLocalFile(runtime *common.RuntimeContext, spec markdownUpload } func uploadMarkdownFileAll(runtime *common.RuntimeContext, spec markdownUploadSpec, fileReader io.Reader, fileName string, fileSize int64) (markdownUploadResult, error) { + target := spec.Target() fd := larkcore.NewFormdata() fd.AddField("file_name", fileName) - fd.AddField("parent_type", "explorer") - fd.AddField("parent_node", spec.FolderToken) + fd.AddField("parent_type", target.ParentType) + fd.AddField("parent_node", target.ParentNode) fd.AddField("size", fmt.Sprintf("%d", fileSize)) if spec.FileToken != "" { fd.AddField("file_token", spec.FileToken) @@ -320,10 +369,11 @@ func uploadMarkdownFileAll(runtime *common.RuntimeContext, spec markdownUploadSp } func uploadMarkdownFileMultipart(runtime *common.RuntimeContext, spec markdownUploadSpec, fileReader io.Reader, fileName string, fileSize int64) (markdownUploadResult, error) { + target := spec.Target() prepareBody := map[string]interface{}{ "file_name": fileName, - "parent_type": "explorer", - "parent_node": spec.FolderToken, + "parent_type": target.ParentType, + "parent_node": target.ParentNode, "size": fileSize, } if spec.FileToken != "" { diff --git a/shortcuts/markdown/markdown_create.go b/shortcuts/markdown/markdown_create.go index 8633cc336..50dfaa0bb 100644 --- a/shortcuts/markdown/markdown_create.go +++ b/shortcuts/markdown/markdown_create.go @@ -20,15 +20,21 @@ var MarkdownCreate = common.Shortcut{ AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ - {Name: "folder-token", Desc: "target Drive folder token (default: root folder)"}, + {Name: "folder-token", Desc: "target Drive folder token (default: root folder; mutually exclusive with --wiki-token)"}, + {Name: "wiki-token", Desc: "target wiki node token (uploads under that wiki node; mutually exclusive with --folder-token)"}, {Name: "name", Desc: "file name with .md suffix; required with --content, optional with --file"}, {Name: "content", Desc: "Markdown content", Input: []string{common.File, common.Stdin}}, {Name: "file", Desc: "local .md file path"}, }, + Tips: []string{ + "Omit both --folder-token and --wiki-token to create the Markdown file in the caller's Drive root folder.", + "Use --wiki-token to create the Markdown file under a wiki node; the shortcut maps this to parent_type=wiki automatically.", + }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateMarkdownSpec(runtime, markdownUploadSpec{ FileName: strings.TrimSpace(runtime.Str("name")), FolderToken: strings.TrimSpace(runtime.Str("folder-token")), + WikiToken: strings.TrimSpace(runtime.Str("wiki-token")), FilePath: strings.TrimSpace(runtime.Str("file")), FileSet: runtime.Changed("file"), Content: runtime.Str("content"), @@ -39,6 +45,7 @@ var MarkdownCreate = common.Shortcut{ spec := markdownUploadSpec{ FileName: strings.TrimSpace(runtime.Str("name")), FolderToken: strings.TrimSpace(runtime.Str("folder-token")), + WikiToken: strings.TrimSpace(runtime.Str("wiki-token")), FilePath: strings.TrimSpace(runtime.Str("file")), FileSet: runtime.Changed("file"), Content: runtime.Str("content"), @@ -54,6 +61,7 @@ var MarkdownCreate = common.Shortcut{ spec := markdownUploadSpec{ FileName: strings.TrimSpace(runtime.Str("name")), FolderToken: strings.TrimSpace(runtime.Str("folder-token")), + WikiToken: strings.TrimSpace(runtime.Str("wiki-token")), FilePath: strings.TrimSpace(runtime.Str("file")), FileSet: runtime.Changed("file"), Content: runtime.Str("content"), @@ -79,8 +87,10 @@ var MarkdownCreate = common.Shortcut{ "file_name": finalMarkdownFileName(spec), "size_bytes": fileSize, } - if u := common.BuildResourceURL(runtime.Config.Brand, "file", result.FileToken); u != "" { - out["url"] = u + if target := spec.Target(); target.ParentType == markdownUploadParentTypeExplorer { + if u := common.BuildResourceURL(runtime.Config.Brand, "file", result.FileToken); u != "" { + out["url"] = u + } } if grant := common.AutoGrantCurrentUserDrivePermission(runtime, result.FileToken, "file"); grant != nil { out["permission_grant"] = grant diff --git a/shortcuts/markdown/markdown_test.go b/shortcuts/markdown/markdown_test.go index 193ac9897..30607e260 100644 --- a/shortcuts/markdown/markdown_test.go +++ b/shortcuts/markdown/markdown_test.go @@ -269,6 +269,27 @@ func TestMarkdownCreateValidationBranches(t *testing.T) { }, want: "--folder-token cannot be empty", }, + { + name: "wiki token cannot be empty", + args: []string{ + "+create", + "--name", "README.md", + "--content", "# hello", + "--wiki-token=", + }, + want: "--wiki-token cannot be empty", + }, + { + name: "folder and wiki tokens are mutually exclusive", + args: []string{ + "+create", + "--name", "README.md", + "--content", "# hello", + "--folder-token", "fld_target", + "--wiki-token", "wikcn_target", + }, + want: "--folder-token and --wiki-token are mutually exclusive", + }, { name: "folder token must be valid", args: []string{ @@ -279,6 +300,16 @@ func TestMarkdownCreateValidationBranches(t *testing.T) { }, want: "--folder-token", }, + { + name: "wiki token must be valid", + args: []string{ + "+create", + "--name", "README.md", + "--content", "# hello", + "--wiki-token", "../bad", + }, + want: "--wiki-token", + }, { name: "content mode still validates markdown file name", args: []string{ @@ -377,6 +408,29 @@ func TestMarkdownCreateDryRunWithInlineContent(t *testing.T) { } } +func TestMarkdownCreateDryRunWithWikiToken(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig()) + + err := mountAndRunMarkdown(t, MarkdownCreate, []string{ + "+create", + "--name", "README.md", + "--content", "# hello", + "--wiki-token", "wikcn_markdown_dryrun_target", + "--dry-run", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out := stdout.String() + if !strings.Contains(out, `"parent_type": "wiki"`) { + t.Fatalf("dry-run missing wiki parent_type: %s", out) + } + if !strings.Contains(out, `"parent_node": "wikcn_markdown_dryrun_target"`) { + t.Fatalf("dry-run missing wiki parent_node: %s", out) + } +} + func TestMarkdownCreateDryRunReportsSourceFileError(t *testing.T) { f, stdout, _, _ := cmdutil.TestFactory(t, markdownTestConfig()) @@ -472,6 +526,43 @@ func TestMarkdownCreateSuccessUploadAll(t *testing.T) { } } +func TestMarkdownCreateSuccessUploadAllToWikiOmitsURL(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig()) + uploadStub := &httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/files/upload_all", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "file_token": "box_md_create_wiki", + "version": "1002", + }, + }, + } + reg.Register(uploadStub) + + err := mountAndRunMarkdown(t, MarkdownCreate, []string{ + "+create", + "--name", "README.md", + "--content", "# hello\n", + "--wiki-token", "wikcn_markdown_create_target", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + body := decodeCapturedMultipartBody(t, uploadStub) + if got := body.Fields["parent_type"]; got != markdownUploadParentTypeWiki { + t.Fatalf("parent_type = %q, want %q", got, markdownUploadParentTypeWiki) + } + if got := body.Fields["parent_node"]; got != "wikcn_markdown_create_target" { + t.Fatalf("parent_node = %q, want %q", got, "wikcn_markdown_create_target") + } + if strings.Contains(stdout.String(), `"url":`) { + t.Fatalf("stdout should omit url for wiki-hosted markdown files: %s", stdout.String()) + } +} + func TestMarkdownCreatePrettyOutputIncludesPermissionGrant(t *testing.T) { f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig()) reg.Register(&httpmock.Stub{ @@ -588,6 +679,81 @@ func TestMarkdownCreateMultipartUploadSuccess(t *testing.T) { } } +func TestMarkdownCreateMultipartUploadToWikiUsesWikiParent(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig()) + prepareStub := &httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/files/upload_prepare", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "upload_id": "upload_markdown_wiki_ok", + "block_size": float64(markdownSinglePartSizeLimit), + "block_num": float64(2), + }, + }, + } + reg.Register(prepareStub) + uploadPartStub := &httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/files/upload_part", + Reusable: true, + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{}, + }, + } + reg.Register(uploadPartStub) + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/files/upload_finish", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "file_token": "box_md_multipart_wiki", + "version": "1005", + }, + }, + }) + + tmpDir := t.TempDir() + withMarkdownWorkingDir(t, tmpDir) + fh, err := os.Create("large.md") + if err != nil { + t.Fatalf("Create() error: %v", err) + } + if err := fh.Truncate(markdownSinglePartSizeLimit + 1); err != nil { + fh.Close() + t.Fatalf("Truncate() error: %v", err) + } + if err := fh.Close(); err != nil { + t.Fatalf("Close() error: %v", err) + } + + err = mountAndRunMarkdown(t, MarkdownCreate, []string{ + "+create", + "--file", "large.md", + "--wiki-token", "wikcn_markdown_multipart_target", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var body map[string]interface{} + if err := json.Unmarshal(prepareStub.CapturedBody, &body); err != nil { + t.Fatalf("decode upload_prepare body: %v\nraw=%s", err, string(prepareStub.CapturedBody)) + } + if got := body["parent_type"]; got != markdownUploadParentTypeWiki { + t.Fatalf("parent_type = %#v, want %q", got, markdownUploadParentTypeWiki) + } + if got := body["parent_node"]; got != "wikcn_markdown_multipart_target" { + t.Fatalf("parent_node = %#v, want %q", got, "wikcn_markdown_multipart_target") + } + if strings.Contains(stdout.String(), `"url":`) { + t.Fatalf("stdout should omit url for wiki-hosted multipart markdown files: %s", stdout.String()) + } +} + func TestMarkdownCreateFailsWhenMultipartPlanIsTooSmall(t *testing.T) { f, stdout, _, reg := cmdutil.TestFactory(t, markdownTestConfig()) reg.Register(&httpmock.Stub{ diff --git a/skills/lark-markdown/references/lark-markdown-create.md b/skills/lark-markdown/references/lark-markdown-create.md index 0bb54460e..c03f89b35 100644 --- a/skills/lark-markdown/references/lark-markdown-create.md +++ b/skills/lark-markdown/references/lark-markdown-create.md @@ -2,7 +2,7 @@ > **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 -在 Drive 中创建一个原生 Markdown 文件(`.md`)。 +在 Drive 中创建一个原生 Markdown 文件(`.md`),支持创建到普通 Drive 文件夹或 Wiki 节点下。 ## 命令 @@ -32,6 +32,11 @@ lark-cli markdown +create \ --folder-token fldcn_xxx \ --file ./README.md +# 创建到指定 wiki 节点 +lark-cli markdown +create \ + --wiki-token wikcn_xxx \ + --file ./README.md + # 预览底层请求 lark-cli markdown +create \ --name README.md \ @@ -43,7 +48,8 @@ lark-cli markdown +create \ | 参数 | 必填 | 说明 | |------|------|------| -| `--folder-token` | 否 | 目标 Drive 文件夹 token;省略时创建到根目录 | +| `--folder-token` | 否 | 目标 Drive 文件夹 token;与 `--wiki-token` 互斥;省略时创建到根目录 | +| `--wiki-token` | 否 | 目标 wiki 节点 token;与 `--folder-token` 互斥;传入后自动映射为 `parent_type=wiki` | | `--name` | 条件必填 | 文件名,**必须显式带 `.md` 后缀**;使用 `--content` 时必填;使用 `--file` 时可省略,默认取本地文件名 | | `--content` | 条件必填 | Markdown 内容;与 `--file` 互斥;支持直接传字符串、`@file`、`-`(stdin) | | `--file` | 条件必填 | 本地 `.md` 文件路径;与 `--content` 互斥 | @@ -51,8 +57,10 @@ lark-cli markdown +create \ ## 关键约束 - `--content` 与 `--file` 必须二选一 +- `--folder-token` 与 `--wiki-token` 互斥 - `--name` 必须带 `.md` 后缀 - `--file` 指向的本地文件名也必须带 `.md` 后缀 +- 传 `--wiki-token` 时,返回值中不会附带 `/file/` URL,因为 wiki 承载文件没有稳定的独立 file URL ## 返回值 diff --git a/tests/cli_e2e/markdown/markdown_dryrun_test.go b/tests/cli_e2e/markdown/markdown_dryrun_test.go index b957de34e..a9a076e8f 100644 --- a/tests/cli_e2e/markdown/markdown_dryrun_test.go +++ b/tests/cli_e2e/markdown/markdown_dryrun_test.go @@ -43,6 +43,33 @@ func TestMarkdownCreateDryRun_Content(t *testing.T) { assert.Contains(t, output, `"size": 7`) } +func TestMarkdownCreateDryRun_WikiTarget(t *testing.T) { + setMarkdownDryRunConfigEnv(t) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + t.Cleanup(cancel) + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "markdown", "+create", + "--name", "README.md", + "--content", "# hello", + "--wiki-token", "wikcnMarkdownDryRun", + "--dry-run", + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + + output := strings.TrimSpace(result.Stdout) + assert.Contains(t, output, "/open-apis/drive/v1/files/upload_all") + assert.Contains(t, output, `"file_name": "README.md"`) + assert.Contains(t, output, `"parent_node": "wikcnMarkdownDryRun"`) + assert.Contains(t, output, `"parent_type": "wiki"`) + assert.Contains(t, output, `"size": 7`) +} + func TestMarkdownCreateDryRun_FileShowsConcreteSize(t *testing.T) { setMarkdownDryRunConfigEnv(t) @@ -96,6 +123,27 @@ func TestMarkdownCreateDryRun_RejectsEmptyContent(t *testing.T) { assert.Contains(t, errMsg, "empty markdown content is not supported") } +func TestMarkdownCreateDryRun_RejectsEmptyWikiToken(t *testing.T) { + setMarkdownDryRunConfigEnv(t) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + t.Cleanup(cancel) + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "markdown", "+create", + "--name", "README.md", + "--content", "# hello", + "--wiki-token", "", + "--dry-run", + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 2) + assert.Contains(t, result.Stderr, "--wiki-token cannot be empty") +} + func TestMarkdownFetchDryRun_OutputFile(t *testing.T) { setMarkdownDryRunConfigEnv(t) diff --git a/tests/cli_e2e/markdown/markdown_workflow_test.go b/tests/cli_e2e/markdown/markdown_workflow_test.go index 149a85f0e..ca66ee847 100644 --- a/tests/cli_e2e/markdown/markdown_workflow_test.go +++ b/tests/cli_e2e/markdown/markdown_workflow_test.go @@ -6,6 +6,7 @@ package markdown import ( "context" "os" + "strings" "testing" "time" @@ -98,3 +99,84 @@ func TestMarkdownLifecycleWorkflow(t *testing.T) { fetchUpdatedResult.AssertStdoutStatus(t, true) require.Equal(t, updatedContent, gjson.Get(fetchUpdatedResult.Stdout, "data.content").String(), "stdout:\n%s", fetchUpdatedResult.Stdout) } + +func TestMarkdownCreateWorkflow_WikiParent(t *testing.T) { + if os.Getenv("LARK_MARKDOWN_E2E") == "" { + t.Skip("set LARK_MARKDOWN_E2E=1 to run markdown live workflow after backend version support is deployed") + } + + wikiToken := strings.TrimSpace(os.Getenv("LARK_MARKDOWN_E2E_WIKI_TOKEN")) + if wikiToken == "" { + t.Skip("set LARK_MARKDOWN_E2E_WIKI_TOKEN to run markdown live workflow against a wiki parent node") + } + + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + + parentT := t + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + t.Cleanup(cancel) + + suffix := clie2e.GenerateSuffix() + fileName := "lark-cli-e2e-markdown-wiki-" + suffix + ".md" + initialContent := "# Wiki Parent\n\nhello wiki markdown workflow\n" + + createResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "markdown", "+create", + "--wiki-token", wikiToken, + "--name", fileName, + "--content", initialContent, + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + createResult.AssertExitCode(t, 0) + createResult.AssertStdoutStatus(t, true) + + fileToken := gjson.Get(createResult.Stdout, "data.file_token").String() + require.NotEmpty(t, fileToken, "stdout:\n%s", createResult.Stdout) + require.False(t, gjson.Get(createResult.Stdout, "data.url").Exists(), "stdout:\n%s", createResult.Stdout) + + parentT.Cleanup(func() { + bestEffortDeleteWikiHostedMarkdownFile(parentT, fileToken) + }) + + fetchResult, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "markdown", "+fetch", + "--file-token", fileToken, + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + fetchResult.AssertExitCode(t, 0) + fetchResult.AssertStdoutStatus(t, true) + require.Equal(t, initialContent, gjson.Get(fetchResult.Stdout, "data.content").String(), "stdout:\n%s", fetchResult.Stdout) +} + +func bestEffortDeleteWikiHostedMarkdownFile(parentT *testing.T, fileToken string) { + parentT.Helper() + + request := clie2e.Request{ + Args: []string{ + "drive", "+delete", + "--file-token", fileToken, + "--type", "file", + "--yes", + }, + } + + for _, identity := range []string{"bot", "user"} { + cleanupCtx, cleanupCancel := clie2e.CleanupContext() + result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{ + Args: request.Args, + DefaultAs: identity, + }) + cleanupCancel() + if err == nil && result != nil && result.ExitCode == 0 { + return + } + } + + parentT.Logf("cleanup skipped: could not delete wiki-hosted markdown file %s with either bot or user identity", fileToken) +}