mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 00:55:53 +08:00
883 lines
24 KiB
Go
883 lines
24 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package doc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/httpmock"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func TestBuildFetchBodyIncludesSceneFromContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.WithValue(context.Background(), docsSceneContextKey, " DoubaoCLI ")
|
|
runtime := newFetchBodyTestRuntime(ctx)
|
|
|
|
body := buildFetchBody(runtime)
|
|
if got := body["scene"]; got != "DoubaoCLI" {
|
|
t.Fatalf("scene = %#v, want %q", got, "DoubaoCLI")
|
|
}
|
|
}
|
|
|
|
func TestBuildCreateBodyIncludesSceneFromContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.WithValue(context.Background(), docsSceneContextKey, "DoubaoCLI")
|
|
runtime := newCreateBodyTestRuntime(ctx)
|
|
|
|
body := buildCreateBody(runtime)
|
|
if got := body["scene"]; got != "DoubaoCLI" {
|
|
t.Fatalf("scene = %#v, want %q", got, "DoubaoCLI")
|
|
}
|
|
}
|
|
|
|
func TestBuildCreateBodyPrependsTitleToContent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newCreateBodyTestRuntime(context.Background())
|
|
if err := runtime.Cmd.Flags().Set("title", "A & B <C>"); err != nil {
|
|
t.Fatalf("set title: %v", err)
|
|
}
|
|
if err := runtime.Cmd.Flags().Set("content", "## Body"); err != nil {
|
|
t.Fatalf("set content: %v", err)
|
|
}
|
|
|
|
body := buildCreateBody(runtime)
|
|
if got, want := body["content"], "<title>A & B <C></title>\n## Body"; got != want {
|
|
t.Fatalf("content = %#v, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildUpdateBodyIncludesSceneFromContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.WithValue(context.Background(), docsSceneContextKey, "DoubaoCLI")
|
|
runtime := newUpdateBodyTestRuntime(ctx)
|
|
|
|
body := buildUpdateBody(runtime)
|
|
if got := body["scene"]; got != "DoubaoCLI" {
|
|
t.Fatalf("scene = %#v, want %q", got, "DoubaoCLI")
|
|
}
|
|
}
|
|
|
|
func TestBuildFetchBodyOmitsEmptyScene(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchBodyTestRuntime(context.Background())
|
|
|
|
body := buildFetchBody(runtime)
|
|
if _, ok := body["scene"]; ok {
|
|
t.Fatalf("did not expect empty scene in fetch body: %#v", body)
|
|
}
|
|
}
|
|
|
|
func TestBuildFetchBodyIncludesExplicitLang(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchBodyTestRuntime(context.Background())
|
|
if err := runtime.Cmd.Flags().Set("lang", "en-US"); err != nil {
|
|
t.Fatalf("set lang: %v", err)
|
|
}
|
|
|
|
body := buildFetchBody(runtime)
|
|
if got := body["lang"]; got != "en-US" {
|
|
t.Fatalf("lang = %#v, want %q", got, "en-US")
|
|
}
|
|
}
|
|
|
|
func TestBuildFetchBodyUsesRuntimeConfigLang(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchBodyTestRuntime(context.Background())
|
|
runtime.Config = &core.CliConfig{Lang: "zh_cn"}
|
|
|
|
body := buildFetchBody(runtime)
|
|
if got := body["lang"]; got != "zh_cn" {
|
|
t.Fatalf("lang = %#v, want %q", got, "zh_cn")
|
|
}
|
|
}
|
|
|
|
func TestBuildFetchBodyExplicitBlankLangOmitsLang(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchBodyTestRuntime(context.Background())
|
|
runtime.Config = &core.CliConfig{Lang: "zh_cn"}
|
|
if err := runtime.Cmd.Flags().Set("lang", ""); err != nil {
|
|
t.Fatalf("set lang: %v", err)
|
|
}
|
|
|
|
body := buildFetchBody(runtime)
|
|
if _, ok := body["lang"]; ok {
|
|
t.Fatalf("did not expect blank explicit lang in fetch body: %#v", body)
|
|
}
|
|
}
|
|
|
|
func TestBuildFetchBodyIncludesRevisionAndFullDetail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchBodyTestRuntime(context.Background())
|
|
mustSetFetchFlag(t, runtime, "revision-id", "42")
|
|
mustSetFetchFlag(t, runtime, "detail", "full")
|
|
|
|
body := buildFetchBody(runtime)
|
|
if got := body["revision_id"]; got != 42 {
|
|
t.Fatalf("revision_id = %#v, want 42", got)
|
|
}
|
|
exportOption, _ := body["export_option"].(map[string]interface{})
|
|
want := map[string]interface{}{
|
|
"export_block_id": true,
|
|
"export_style_attrs": true,
|
|
"export_cite_extra_data": true,
|
|
}
|
|
if !reflect.DeepEqual(exportOption, want) {
|
|
t.Fatalf("export_option = %#v, want %#v", exportOption, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildFetchBodyIncludesWithIDsDetail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchBodyTestRuntime(context.Background())
|
|
mustSetFetchFlag(t, runtime, "detail", "with-ids")
|
|
|
|
body := buildFetchBody(runtime)
|
|
exportOption, _ := body["export_option"].(map[string]interface{})
|
|
want := map[string]interface{}{
|
|
"export_block_id": true,
|
|
}
|
|
if !reflect.DeepEqual(exportOption, want) {
|
|
t.Fatalf("export_option = %#v, want %#v", exportOption, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildFetchBodyIncludesReadOption(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchBodyTestRuntime(context.Background())
|
|
mustSetFetchFlag(t, runtime, "scope", "section")
|
|
mustSetFetchFlag(t, runtime, "start-block-id", "blk_heading")
|
|
|
|
body := buildFetchBody(runtime)
|
|
want := map[string]interface{}{
|
|
"read_mode": "section",
|
|
"start_block_id": "blk_heading",
|
|
}
|
|
if got := body["read_option"]; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("read_option = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildReadOptionModes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
setFlags map[string]string
|
|
want map[string]interface{}
|
|
}{
|
|
{
|
|
name: "full omits read option",
|
|
setFlags: map[string]string{
|
|
"scope": "full",
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "outline with max depth",
|
|
setFlags: map[string]string{
|
|
"scope": "outline",
|
|
"max-depth": "3",
|
|
},
|
|
want: map[string]interface{}{
|
|
"read_mode": "outline",
|
|
"max_depth": "3",
|
|
},
|
|
},
|
|
{
|
|
name: "range with block ids and context",
|
|
setFlags: map[string]string{
|
|
"scope": "range",
|
|
"start-block-id": "blk_start",
|
|
"end-block-id": "blk_end",
|
|
"context-before": "2",
|
|
"context-after": "1",
|
|
"max-depth": "0",
|
|
},
|
|
want: map[string]interface{}{
|
|
"read_mode": "range",
|
|
"start_block_id": "blk_start",
|
|
"end_block_id": "blk_end",
|
|
"context_before": "2",
|
|
"context_after": "1",
|
|
"max_depth": "0",
|
|
},
|
|
},
|
|
{
|
|
name: "keyword with query",
|
|
setFlags: map[string]string{
|
|
"scope": "keyword",
|
|
"keyword": "foo|bar",
|
|
"context-before": "1",
|
|
},
|
|
want: map[string]interface{}{
|
|
"read_mode": "keyword",
|
|
"keyword": "foo|bar",
|
|
"context_before": "1",
|
|
},
|
|
},
|
|
{
|
|
name: "section keeps unlimited depth omitted",
|
|
setFlags: map[string]string{
|
|
"scope": "section",
|
|
"start-block-id": "blk_heading",
|
|
"max-depth": "-1",
|
|
},
|
|
want: map[string]interface{}{
|
|
"read_mode": "section",
|
|
"start_block_id": "blk_heading",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchBodyTestRuntime(context.Background())
|
|
for name, value := range tt.setFlags {
|
|
mustSetFetchFlag(t, runtime, name, value)
|
|
}
|
|
|
|
if got := buildReadOption(runtime); !reflect.DeepEqual(got, tt.want) {
|
|
t.Fatalf("buildReadOption() = %#v, want %#v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateReadModeFlagsRejectsInvalidScopeOptions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
setFlags map[string]string
|
|
wantParam string
|
|
wantParams []string
|
|
}{
|
|
{
|
|
name: "negative context before",
|
|
setFlags: map[string]string{
|
|
"scope": "range",
|
|
"start-block-id": "blk_start",
|
|
"context-before": "-1",
|
|
},
|
|
wantParam: "--context-before",
|
|
},
|
|
{
|
|
name: "negative context after",
|
|
setFlags: map[string]string{
|
|
"scope": "range",
|
|
"start-block-id": "blk_start",
|
|
"context-after": "-1",
|
|
},
|
|
wantParam: "--context-after",
|
|
},
|
|
{
|
|
name: "max depth below unlimited sentinel",
|
|
setFlags: map[string]string{
|
|
"scope": "range",
|
|
"start-block-id": "blk_start",
|
|
"max-depth": "-2",
|
|
},
|
|
wantParam: "--max-depth",
|
|
},
|
|
{
|
|
name: "range needs boundary",
|
|
setFlags: map[string]string{
|
|
"scope": "range",
|
|
},
|
|
wantParams: []string{
|
|
"--start-block-id",
|
|
"--end-block-id",
|
|
},
|
|
},
|
|
{
|
|
name: "keyword needs keyword",
|
|
setFlags: map[string]string{
|
|
"scope": "keyword",
|
|
},
|
|
wantParam: "--keyword",
|
|
},
|
|
{
|
|
name: "section needs start block",
|
|
setFlags: map[string]string{
|
|
"scope": "section",
|
|
},
|
|
wantParam: "--start-block-id",
|
|
},
|
|
{
|
|
name: "unknown scope",
|
|
setFlags: map[string]string{
|
|
"scope": "bad",
|
|
},
|
|
wantParam: "--scope",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchBodyTestRuntime(context.Background())
|
|
for name, value := range tt.setFlags {
|
|
mustSetFetchFlag(t, runtime, name, value)
|
|
}
|
|
|
|
err := validateReadModeFlags(runtime)
|
|
if err == nil {
|
|
t.Fatal("validateReadModeFlags() succeeded, want error")
|
|
}
|
|
assertValidationContract(t, err, errs.SubtypeInvalidArgument, tt.wantParam, tt.wantParams...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateReadModeFlagsAcceptsValidScopeOptions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
setFlags map[string]string
|
|
}{
|
|
{
|
|
name: "outline",
|
|
setFlags: map[string]string{
|
|
"scope": "outline",
|
|
},
|
|
},
|
|
{
|
|
name: "range with end block",
|
|
setFlags: map[string]string{
|
|
"scope": "range",
|
|
"end-block-id": "blk_end",
|
|
},
|
|
},
|
|
{
|
|
name: "keyword with keyword",
|
|
setFlags: map[string]string{
|
|
"scope": "keyword",
|
|
"keyword": "bug|缺陷",
|
|
},
|
|
},
|
|
{
|
|
name: "section with start block",
|
|
setFlags: map[string]string{
|
|
"scope": "section",
|
|
"start-block-id": "blk_heading",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchBodyTestRuntime(context.Background())
|
|
for name, value := range tt.setFlags {
|
|
mustSetFetchFlag(t, runtime, name, value)
|
|
}
|
|
|
|
if err := validateReadModeFlags(runtime); err != nil {
|
|
t.Fatalf("validateReadModeFlags() error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateFetchV2RejectsInvalidDocAndScope(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
setFlags map[string]string
|
|
wantParam string
|
|
}{
|
|
{
|
|
name: "invalid doc",
|
|
setFlags: map[string]string{
|
|
"doc": "https://example.com/sheets/sht_token",
|
|
},
|
|
wantParam: "--doc",
|
|
},
|
|
{
|
|
name: "invalid scope",
|
|
setFlags: map[string]string{
|
|
"scope": "bad",
|
|
},
|
|
wantParam: "--scope",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchShortcutTestRuntime(t, "", tt.setFlags)
|
|
err := validateFetchV2(context.Background(), runtime)
|
|
if err == nil {
|
|
t.Fatal("validateFetchV2() succeeded, want error")
|
|
}
|
|
assertValidationContract(t, err, errs.SubtypeInvalidArgument, tt.wantParam)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddFetchDetailDowngradeWarningNoops(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
setFlags map[string]string
|
|
}{
|
|
{
|
|
name: "xml format",
|
|
setFlags: map[string]string{
|
|
"doc-format": "xml",
|
|
"detail": "full",
|
|
},
|
|
},
|
|
{
|
|
name: "markdown simple detail",
|
|
setFlags: map[string]string{
|
|
"doc-format": "markdown",
|
|
"detail": "simple",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchBodyTestRuntime(context.Background())
|
|
for name, value := range tt.setFlags {
|
|
mustSetFetchFlag(t, runtime, name, value)
|
|
}
|
|
|
|
data := map[string]interface{}{}
|
|
if got := addFetchDetailDowngradeWarning(runtime, data); got != "" {
|
|
t.Fatalf("warning = %q, want empty", got)
|
|
}
|
|
if _, ok := data["warnings"]; ok {
|
|
t.Fatalf("unexpected warnings: %#v", data["warnings"])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDocsFetchDryRunDefaultsToV2Endpoint(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchShortcutTestRuntime(t, "", nil)
|
|
if err := validateFetchV2(context.Background(), runtime); err != nil {
|
|
t.Fatalf("validateFetchV2() error = %v", err)
|
|
}
|
|
|
|
dry := decodeDocDryRun(t, DocsFetch.DryRun(context.Background(), runtime))
|
|
if len(dry.API) != 1 {
|
|
t.Fatalf("expected 1 dry-run API call, got %d", len(dry.API))
|
|
}
|
|
if got, want := dry.API[0].URL, "/open-apis/docs_ai/v1/documents/doxcnFetchDryRun/fetch"; got != want {
|
|
t.Fatalf("dry-run URL = %q, want %q", got, want)
|
|
}
|
|
if got, want := dry.API[0].Body["format"], "xml"; got != want {
|
|
t.Fatalf("dry-run format = %#v, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDocsFetchAPIVersionCompatFlagIsIgnored(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchShortcutTestRuntime(t, "legacy", nil)
|
|
if err := validateFetchV2(context.Background(), runtime); err != nil {
|
|
t.Fatalf("validateFetchV2() error = %v", err)
|
|
}
|
|
|
|
dry := decodeDocDryRun(t, DocsFetch.DryRun(context.Background(), runtime))
|
|
if len(dry.API) != 1 {
|
|
t.Fatalf("expected 1 dry-run API call, got %d", len(dry.API))
|
|
}
|
|
if got, want := dry.API[0].URL, "/open-apis/docs_ai/v1/documents/doxcnFetchDryRun/fetch"; got != want {
|
|
t.Fatalf("dry-run URL = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDocsFetchIMMarkdownRequestsMarkdownFromAPI(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchShortcutTestRuntime(t, "", map[string]string{
|
|
"doc-format": "im-markdown",
|
|
})
|
|
if err := validateFetchV2(context.Background(), runtime); err != nil {
|
|
t.Fatalf("validateFetchV2() error = %v", err)
|
|
}
|
|
|
|
dry := decodeDocDryRun(t, DocsFetch.DryRun(context.Background(), runtime))
|
|
if got, want := dry.API[0].Body["format"], "markdown"; got != want {
|
|
t.Fatalf("dry-run format = %#v, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDocsFetchMarkdownDetailDowngradesToSimple(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, format := range []string{"markdown", "im-markdown"} {
|
|
for _, detail := range []string{"with-ids", "full"} {
|
|
t.Run(format+"/"+detail, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchShortcutTestRuntime(t, "", map[string]string{
|
|
"doc-format": format,
|
|
"detail": detail,
|
|
})
|
|
if err := validateFetchV2(context.Background(), runtime); err != nil {
|
|
t.Fatalf("validateFetchV2() error = %v", err)
|
|
}
|
|
|
|
dry := decodeDocDryRun(t, DocsFetch.DryRun(context.Background(), runtime))
|
|
exportOption, _ := dry.API[0].Body["export_option"].(map[string]interface{})
|
|
if exportOption == nil {
|
|
t.Fatalf("missing export_option: %#v", dry.API[0].Body)
|
|
}
|
|
if got := exportOption["export_block_id"]; got != false {
|
|
t.Fatalf("export_block_id = %#v, want false after markdown detail downgrade", got)
|
|
}
|
|
if got := exportOption["export_style_attrs"]; got != false {
|
|
t.Fatalf("export_style_attrs = %#v, want false after markdown detail downgrade", got)
|
|
}
|
|
if got := exportOption["export_cite_extra_data"]; got != false {
|
|
t.Fatalf("export_cite_extra_data = %#v, want false after markdown detail downgrade", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDocsFetchMarkdownDetailDowngradeWarnsInOutput(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-fetch-detail-warning"))
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/docs_ai/v1/documents/doxcnFetchWarning/fetch",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"document": map[string]interface{}{
|
|
"document_id": "doxcnFetchWarning",
|
|
"revision_id": float64(1),
|
|
"content": "# hello",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunDocs(t, DocsFetch, []string{
|
|
"+fetch",
|
|
"--doc", "doxcnFetchWarning",
|
|
"--doc-format", "markdown",
|
|
"--detail", "with-ids",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
var envelope map[string]interface{}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("decode output: %v\nraw=%s", err, stdout.String())
|
|
}
|
|
data, _ := envelope["data"].(map[string]interface{})
|
|
warnings, _ := data["warnings"].([]interface{})
|
|
if len(warnings) != 1 {
|
|
t.Fatalf("warnings = %#v, want one downgrade warning", data["warnings"])
|
|
}
|
|
if got, _ := warnings[0].(string); !strings.Contains(got, "returning markdown output") || !strings.Contains(got, "ignoring the unsupported detail option") {
|
|
t.Fatalf("unexpected warning: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDocsFetchMarkdownDetailDowngradeWarnsInPrettyOutput(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
f, stdout, stderr, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-fetch-detail-pretty-warning"))
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/docs_ai/v1/documents/doxcnFetchPrettyWarning/fetch",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"document": map[string]interface{}{
|
|
"document_id": "doxcnFetchPrettyWarning",
|
|
"revision_id": float64(1),
|
|
"content": "# hello",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunDocs(t, DocsFetch, []string{
|
|
"+fetch",
|
|
"--doc", "doxcnFetchPrettyWarning",
|
|
"--doc-format", "markdown",
|
|
"--detail", "full",
|
|
"--format", "pretty",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if got := stdout.String(); got != "# hello\n" {
|
|
t.Fatalf("stdout = %q, want markdown content only", got)
|
|
}
|
|
if got := stderr.String(); !strings.Contains(got, "warning: --detail full is only supported with --doc-format xml") ||
|
|
!strings.Contains(got, "returning markdown output") ||
|
|
!strings.Contains(got, "ignoring the unsupported detail option") {
|
|
t.Fatalf("stderr missing downgrade warning: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDocsFetchV2ReturnsAPIError(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-fetch-api-error"))
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/docs_ai/v1/documents/doxcnFetchAPIError/fetch",
|
|
Body: map[string]interface{}{
|
|
"code": 999999,
|
|
"msg": "fetch failed",
|
|
},
|
|
})
|
|
|
|
err := mountAndRunDocs(t, DocsFetch, []string{
|
|
"+fetch",
|
|
"--doc", "doxcnFetchAPIError",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err == nil {
|
|
t.Fatal("mountAndRunDocs() succeeded, want API error")
|
|
}
|
|
var apiErr *errs.APIError
|
|
if !errors.As(err, &apiErr) {
|
|
t.Fatalf("error type = %T, want *errs.APIError (%v)", err, err)
|
|
}
|
|
p, ok := errs.ProblemOf(err)
|
|
if !ok {
|
|
t.Fatalf("ProblemOf() ok = false for %T (%v)", err, err)
|
|
}
|
|
if p.Category != errs.CategoryAPI {
|
|
t.Errorf("category = %q, want %q", p.Category, errs.CategoryAPI)
|
|
}
|
|
if p.Subtype != errs.SubtypeUnknown {
|
|
t.Errorf("subtype = %q, want %q", p.Subtype, errs.SubtypeUnknown)
|
|
}
|
|
if p.Code != 999999 {
|
|
t.Errorf("code = %d, want 999999", p.Code)
|
|
}
|
|
if p.Message != "fetch failed" {
|
|
t.Errorf("message = %q, want %q", p.Message, "fetch failed")
|
|
}
|
|
if cause := errors.Unwrap(err); cause != nil {
|
|
t.Fatalf("unexpected wrapped cause for API response error: %T %v", cause, cause)
|
|
}
|
|
}
|
|
|
|
func TestDocsFetchIMMarkdownConvertsContentInJSONOutput(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, docsTestConfigWithAppID("docs-fetch-im-markdown"))
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/docs_ai/v1/documents/doxcnFetchIMMarkdown/fetch",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"document": map[string]interface{}{
|
|
"document_id": "doxcnFetchIMMarkdown",
|
|
"revision_id": float64(1),
|
|
"content": strings.Join([]string{
|
|
`<title>Doc Title</title>`,
|
|
`<callout emoji="💡">Read **this**.</callout>`,
|
|
`<bookmark name="Example" href="https://example.com"></bookmark>`,
|
|
}, "\n\n"),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunDocs(t, DocsFetch, []string{
|
|
"+fetch",
|
|
"--doc", "doxcnFetchIMMarkdown",
|
|
"--doc-format", "im-markdown",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
var envelope map[string]interface{}
|
|
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
|
t.Fatalf("decode output: %v\nraw=%s", err, stdout.String())
|
|
}
|
|
data, _ := envelope["data"].(map[string]interface{})
|
|
doc, _ := data["document"].(map[string]interface{})
|
|
content, _ := doc["content"].(string)
|
|
for _, want := range []string{
|
|
"# Doc Title",
|
|
"---\n💡 Read **this**.\n---",
|
|
"[Example](https://example.com)",
|
|
} {
|
|
if !strings.Contains(content, want) {
|
|
t.Fatalf("converted content missing %q:\n%s", want, content)
|
|
}
|
|
}
|
|
if strings.Contains(content, "<title>") || strings.Contains(content, "<callout") || strings.Contains(content, "<bookmark") {
|
|
t.Fatalf("converted content still contains downgraded XML tags:\n%s", content)
|
|
}
|
|
}
|
|
|
|
func TestDocsFetchRejectsLegacyFlags(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setFlags map[string]string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "legacy offset",
|
|
setFlags: map[string]string{"offset": "10"},
|
|
want: []string{
|
|
"docs +fetch is v2-only",
|
|
"the old v1 interface has been shut down",
|
|
"legacy v1 flag(s) --offset are no longer supported",
|
|
"--offset -> use --scope outline/range/keyword/section",
|
|
"lark-cli skills read lark-doc references/lark-doc-fetch.md",
|
|
"MUST NOT grep/open local SKILL.md files",
|
|
"lark-cli docs +fetch --help",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runtime := newFetchShortcutTestRuntime(t, "", tt.setFlags)
|
|
err := validateFetchV2(context.Background(), runtime)
|
|
if err == nil {
|
|
t.Fatal("expected v2-only validation error")
|
|
}
|
|
assertValidationContract(t, err, errs.SubtypeInvalidArgument, "--offset")
|
|
for _, want := range tt.want {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("error missing %q: %v", want, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func newFetchBodyTestRuntime(ctx context.Context) *common.RuntimeContext {
|
|
cmd := &cobra.Command{Use: "+fetch"}
|
|
cmd.Flags().String("doc-format", "xml", "")
|
|
cmd.Flags().String("detail", "simple", "")
|
|
cmd.Flags().String("lang", "", "")
|
|
cmd.Flags().Int("revision-id", -1, "")
|
|
cmd.Flags().String("scope", "full", "")
|
|
cmd.Flags().String("start-block-id", "", "")
|
|
cmd.Flags().String("end-block-id", "", "")
|
|
cmd.Flags().String("keyword", "", "")
|
|
cmd.Flags().Int("context-before", 0, "")
|
|
cmd.Flags().Int("context-after", 0, "")
|
|
cmd.Flags().Int("max-depth", -1, "")
|
|
return common.TestNewRuntimeContextWithCtx(ctx, cmd, nil)
|
|
}
|
|
|
|
func mustSetFetchFlag(t *testing.T, runtime *common.RuntimeContext, name, value string) {
|
|
t.Helper()
|
|
|
|
if err := runtime.Cmd.Flags().Set(name, value); err != nil {
|
|
t.Fatalf("set %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
func newFetchShortcutTestRuntime(t *testing.T, apiVersion string, setFlags map[string]string) *common.RuntimeContext {
|
|
t.Helper()
|
|
|
|
cmd := &cobra.Command{Use: "+fetch"}
|
|
cmd.Flags().String("api-version", "", "")
|
|
cmd.Flags().String("doc", "doxcnFetchDryRun", "")
|
|
cmd.Flags().String("doc-format", "xml", "")
|
|
cmd.Flags().String("detail", "simple", "")
|
|
cmd.Flags().String("lang", "", "")
|
|
cmd.Flags().Int("revision-id", -1, "")
|
|
cmd.Flags().String("scope", "full", "")
|
|
cmd.Flags().String("start-block-id", "", "")
|
|
cmd.Flags().String("end-block-id", "", "")
|
|
cmd.Flags().String("keyword", "", "")
|
|
cmd.Flags().Int("context-before", 0, "")
|
|
cmd.Flags().Int("context-after", 0, "")
|
|
cmd.Flags().Int("max-depth", -1, "")
|
|
cmd.Flags().String("offset", "", "")
|
|
cmd.Flags().String("limit", "", "")
|
|
if apiVersion != "" {
|
|
if err := cmd.Flags().Set("api-version", apiVersion); err != nil {
|
|
t.Fatalf("set api-version: %v", err)
|
|
}
|
|
}
|
|
for name, value := range setFlags {
|
|
if err := cmd.Flags().Set(name, value); err != nil {
|
|
t.Fatalf("set %s: %v", name, err)
|
|
}
|
|
}
|
|
return common.TestNewRuntimeContext(cmd, nil)
|
|
}
|
|
|
|
func newCreateBodyTestRuntime(ctx context.Context) *common.RuntimeContext {
|
|
cmd := &cobra.Command{Use: "+create"}
|
|
cmd.Flags().String("doc-format", "xml", "")
|
|
cmd.Flags().String("title", "", "")
|
|
cmd.Flags().String("content", "<title>hello</title>", "")
|
|
cmd.Flags().String("parent-token", "", "")
|
|
cmd.Flags().String("parent-position", "", "")
|
|
return common.TestNewRuntimeContextWithCtx(ctx, cmd, nil)
|
|
}
|
|
|
|
func newUpdateBodyTestRuntime(ctx context.Context) *common.RuntimeContext {
|
|
cmd := &cobra.Command{Use: "+update"}
|
|
cmd.Flags().String("doc-format", "xml", "")
|
|
cmd.Flags().String("command", "append", "")
|
|
cmd.Flags().Int("revision-id", 0, "")
|
|
cmd.Flags().String("content", "<p>hello</p>", "")
|
|
cmd.Flags().String("pattern", "", "")
|
|
cmd.Flags().String("block-id", "", "")
|
|
cmd.Flags().String("src-block-ids", "", "")
|
|
return common.TestNewRuntimeContextWithCtx(ctx, cmd, nil)
|
|
}
|