Files
larksuite-cli/tests/cli_e2e/docs/docs_script_test.go
sunpeiyang.996 20cf972725 feat(docs): add local document authoring tools
Add local XML/Markdown parsing, profiling, conversion, and unique draft creation for docs workflows. Refresh authoring guidance and tests, and ignore generated lark-cli E2E reports.

Change-Id: I19cf93d576b6b2cb9ffa4cc4e1640126c95002f8
2026-07-24 16:32:20 +08:00

317 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package docs
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestDocsScriptParseXMLFromFile(t *testing.T) {
workDir := t.TempDir()
input := `<title>标题</title><p>一个苹果是 an apple。</p>`
if err := os.WriteFile(filepath.Join(workDir, "draft.xml"), []byte(input), 0o600); err != nil {
t.Fatalf("write draft: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"docs", "+script",
"--command", "parse",
"--content", "@draft.xml",
},
DefaultAs: "bot",
WorkDir: workDir,
Env: docsScriptE2EEnv(),
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
require.Equal(t, int64(10), gjson.Get(result.Stdout, "data.profile.word_count").Int())
require.Equal(t, int64(15), gjson.Get(result.Stdout, "data.profile.char_count").Int())
require.Equal(t, int64(2), gjson.Get(result.Stdout, "data.profile.block_count").Int())
require.False(t, gjson.Get(result.Stdout, "data.xml").Exists())
require.False(t, gjson.Get(result.Stdout, "data.input_format").Exists())
require.False(t, gjson.Get(result.Stdout, "data.command").Exists())
require.False(t, gjson.Get(result.Stdout, "data.profile.breakdown").Exists())
require.False(t, gjson.Get(result.Stdout, "data.profile.compatibility").Exists())
}
func TestDocsScriptParseAcceptsServerSDKAttributeAmpersand(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
input := `<block_insert><parameter><block_id>-1</block_id><content><img href="https://picsum.photos/320/200?seed=lark-cli&raw=1"/></content></parameter></block_insert>`
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"docs", "+script", "--command", "parse", "--content", input},
DefaultAs: "bot",
Env: docsScriptE2EEnv(),
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
require.Equal(t, int64(1), gjson.Get(result.Stdout, "data.profile.block_count").Int())
}
func TestDocsScriptParseMarkdownFromFile(t *testing.T) {
workDir := t.TempDir()
if err := os.WriteFile(filepath.Join(workDir, "draft.md"), []byte("# 标题\n\n- item"), 0o600); err != nil {
t.Fatalf("write draft: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"docs", "+script",
"--command", "parse",
"--content", "@draft.md",
},
DefaultAs: "bot",
WorkDir: workDir,
Env: docsScriptE2EEnv(),
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
require.Equal(t, int64(3), gjson.Get(result.Stdout, "data.profile.block_count").Int())
require.False(t, gjson.Get(result.Stdout, "data.xml").Exists())
require.False(t, gjson.Get(result.Stdout, "data.profile.breakdown").Exists())
}
func TestDocsScriptCreatesUniqueTempXMLFiles(t *testing.T) {
workDir := t.TempDir()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
const count = 8
type outcome struct {
result *clie2e.Result
err error
}
outcomes := make(chan outcome, count)
for i := 0; i < count; i++ {
go func() {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"docs", "+script", "--command", "create-temp-xml", "--file-name", "川西"},
DefaultAs: "bot",
WorkDir: workDir,
Env: docsScriptE2EEnv(),
})
outcomes <- outcome{result: result, err: err}
}()
}
seen := make(map[string]struct{}, count)
for i := 0; i < count; i++ {
outcome := <-outcomes
require.NoError(t, outcome.err)
require.NotNil(t, outcome.result)
outcome.result.AssertExitCode(t, 0)
outcome.result.AssertStdoutStatus(t, true)
path := gjson.Get(outcome.result.Stdout, "data.path").String()
require.False(t, gjson.Get(outcome.result.Stdout, "data.saved_path").Exists())
require.False(t, gjson.Get(outcome.result.Stdout, "data.size_bytes").Exists())
directory := filepath.Dir(path)
require.Equal(t, "川西.xml", filepath.Base(path))
require.Equal(t, filepath.Base(directory), directory)
require.True(t, strings.HasPrefix(directory, "川西_"), "path: %q", path)
require.True(t, strings.HasSuffix(directory, "_folder"), "path: %q", path)
_, duplicate := seen[path]
require.False(t, duplicate, "duplicate temporary path: %q", path)
seen[path] = struct{}{}
info, err := os.Stat(filepath.Join(workDir, path))
require.NoError(t, err)
require.Zero(t, info.Size())
}
require.Len(t, seen, count)
}
func TestDocsScriptConvertsMarkdown(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"docs", "+script",
"--command", "markdown-to-xml",
"--content", "# 标题\n\n- item",
},
DefaultAs: "bot",
Env: docsScriptE2EEnv(),
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
require.Equal(t, `<h1>标题</h1><ul><li>item</li></ul>`, gjson.Get(result.Stdout, "data.xml").String())
require.False(t, gjson.Get(result.Stdout, "data.profile").Exists())
require.False(t, gjson.Get(result.Stdout, "data.input_format").Exists())
require.False(t, gjson.Get(result.Stdout, "data.command").Exists())
}
func TestDocsScriptConvertsMarkdownToFile(t *testing.T) {
workDir := t.TempDir()
if err := os.WriteFile(filepath.Join(workDir, "draft.md"), []byte("# 标题\n\n- item"), 0o600); err != nil {
t.Fatalf("write draft: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"docs", "+script",
"--command", "markdown-to-xml",
"--content", "@draft.md",
"--output", "draft.xml",
},
DefaultAs: "bot",
WorkDir: workDir,
Env: docsScriptE2EEnv(),
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
wantXML := `<h1>标题</h1><ul><li>item</li></ul>`
gotXML, err := os.ReadFile(filepath.Join(workDir, "draft.xml"))
require.NoError(t, err)
require.Equal(t, wantXML, string(gotXML))
require.Equal(t, filepath.Join(workDir, "draft.xml"), gjson.Get(result.Stdout, "data.saved_path").String())
require.Equal(t, int64(len(wantXML)), gjson.Get(result.Stdout, "data.size_bytes").Int())
require.False(t, gjson.Get(result.Stdout, "data.xml").Exists())
}
func TestDocsScriptConvertsSoftLineBreaksToSpaces(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"docs", "+script",
"--command", "markdown-to-xml",
"--content", "**文号桂汛旱指202617号**\n**签发人XXX**\n**发布日期2026年7月13日**",
},
DefaultAs: "bot",
Env: docsScriptE2EEnv(),
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
require.Equal(t,
`<p><b>文号桂汛旱指202617号</b> <b>签发人XXX</b> <b>发布日期2026年7月13日</b></p>`,
gjson.Get(result.Stdout, "data.xml").String(),
)
}
func TestDocsScriptDryRunIsLocal(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"docs", "+script",
"--command", "parse",
"--content", `<p>text</p>`,
"--dry-run",
},
DefaultAs: "bot",
Env: docsScriptE2EEnv(),
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
require.Equal(t, int64(0), gjson.Get(result.Stdout, "api.#").Int())
require.False(t, gjson.Get(result.Stdout, "network").Bool())
require.Equal(t, "parse", gjson.Get(result.Stdout, "command").String())
}
func TestDocsScriptCreateTempXMLDryRunDoesNotWrite(t *testing.T) {
workDir := t.TempDir()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"docs", "+script", "--command", "create-temp-xml", "--file-name", "川西", "--dry-run"},
DefaultAs: "bot",
WorkDir: workDir,
Env: docsScriptE2EEnv(),
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
require.Equal(t, int64(0), gjson.Get(result.Stdout, "api.#").Int())
require.False(t, gjson.Get(result.Stdout, "network").Bool())
require.False(t, gjson.Get(result.Stdout, "creates_file").Bool())
require.Equal(t, "create-temp-xml", gjson.Get(result.Stdout, "command").String())
require.Equal(t, "川西_*_folder", gjson.Get(result.Stdout, "directory_pattern").String())
require.Equal(t, "川西", gjson.Get(result.Stdout, "file_name").String())
require.Equal(t, "川西.xml", gjson.Get(result.Stdout, "xml_file_name").String())
entries, err := os.ReadDir(workDir)
require.NoError(t, err)
require.Empty(t, entries)
}
func TestDocsScriptOnlineDryRunFetchesXML(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"docs", "+script",
"--command", "parse",
"--doc", "https://example.larksuite.com/docx/doxcnScriptDryRun",
"--dry-run",
},
DefaultAs: "bot",
Env: docsScriptE2EEnv(),
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
require.Equal(t, int64(1), gjson.Get(result.Stdout, "api.#").Int())
require.Equal(t, "POST", gjson.Get(result.Stdout, "api.0.method").String())
require.Equal(t, "/open-apis/docs_ai/v1/documents/doxcnScriptDryRun/fetch", gjson.Get(result.Stdout, "api.0.url").String())
require.Equal(t, "xml", gjson.Get(result.Stdout, "api.0.body.format").String())
require.True(t, gjson.Get(result.Stdout, "network").Bool())
require.Equal(t, "parse", gjson.Get(result.Stdout, "command").String())
}
func TestDocsScriptOutputDryRunIsLocal(t *testing.T) {
workDir := t.TempDir()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"docs", "+script",
"--command", "markdown-to-xml",
"--content", "# title",
"--output", "draft.xml",
"--overwrite",
"--dry-run",
},
DefaultAs: "bot",
WorkDir: workDir,
Env: docsScriptE2EEnv(),
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
require.Equal(t, int64(0), gjson.Get(result.Stdout, "api.#").Int())
require.False(t, gjson.Get(result.Stdout, "network").Bool())
require.Equal(t, "markdown-to-xml", gjson.Get(result.Stdout, "command").String())
require.Equal(t, "draft.xml", gjson.Get(result.Stdout, "output").String())
require.True(t, gjson.Get(result.Stdout, "overwrite").Bool())
_, statErr := os.Stat(filepath.Join(workDir, "draft.xml"))
require.ErrorIs(t, statErr, os.ErrNotExist)
}
func docsScriptE2EEnv() map[string]string {
return map[string]string{
"LARKSUITE_CLI_APP_ID": "docs-script-e2e",
"LARKSUITE_CLI_APP_SECRET": "secret",
"LARKSUITE_CLI_BRAND": "feishu",
}
}