Switches the discovery version code bamboo -> cedar and aligns the open edition CLI with dws-wukong across communication (calendar book/acl/attendee, minutes tag, mail folder/template/contact, chat file upload, todo add-attachment, attendance transforms) and structured-office (aitable advperm/view/section/workflow/record, sheet/drive/wiki/doc) domains. Includes output-envelope parity, parse_bool/attendance_class_check_time transforms, --calendar-id support, CHANGELOG 1.0.43 and README command-index refresh. cedar config validated on pre and prod endpoints.
750 lines
22 KiB
Go
750 lines
22 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package helpers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type docCommandRunner struct {
|
|
calls int
|
|
last executor.Invocation
|
|
all []executor.Invocation
|
|
responses []map[string]any
|
|
}
|
|
|
|
func (r *docCommandRunner) Run(_ context.Context, invocation executor.Invocation) (executor.Result, error) {
|
|
r.calls++
|
|
r.last = invocation
|
|
r.all = append(r.all, invocation)
|
|
result := executor.Result{Invocation: invocation}
|
|
if idx := r.calls - 1; idx >= 0 && idx < len(r.responses) {
|
|
result.Response = r.responses[idx]
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func executeDocCommand(t *testing.T, cmd *cobra.Command, args ...string) (string, string, error) {
|
|
t.Helper()
|
|
|
|
var out, errOut bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
cmd.SetErr(&errOut)
|
|
cmd.SetArgs(args)
|
|
err := cmd.Execute()
|
|
return out.String(), errOut.String(), err
|
|
}
|
|
|
|
func newDocTestRoot(runner executor.Runner) *cobra.Command {
|
|
cmd := docHandler{}.Command(runner)
|
|
cmd.PersistentFlags().Bool("dry-run", false, "dry run")
|
|
cmd.PersistentFlags().Bool("yes", false, "skip confirmation")
|
|
return cmd
|
|
}
|
|
|
|
func TestDocPermissionListLimitAliases(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
want int
|
|
}{
|
|
{name: "limit", args: []string{"--node", "NODE_001", "--limit", "50"}, want: 50},
|
|
{name: "max results", args: []string{"--node", "NODE_001", "--max-results", "40"}, want: 40},
|
|
{name: "page size", args: []string{"--node", "NODE_001", "--page-size", "10"}, want: 10},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocPermissionListCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd, tc.args...)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "list_permission" {
|
|
t.Fatalf("tool = %q, want list_permission", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["maxResults"]; got != tc.want {
|
|
t.Fatalf("maxResults = %#v, want %d", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDocPermissionRemoveRoutesToRemovePermission(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocTestRoot(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd,
|
|
"permission", "rm",
|
|
"--node", "NODE_001",
|
|
"--users", "uid1,uid2",
|
|
"--workspace", "WS_001",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "remove_permission" {
|
|
t.Fatalf("tool = %q, want remove_permission", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["nodeId"]; got != "NODE_001" {
|
|
t.Fatalf("nodeId = %#v, want NODE_001", got)
|
|
}
|
|
users, ok := runner.last.Params["userIds"].([]string)
|
|
if !ok || strings.Join(users, ",") != "uid1,uid2" {
|
|
t.Fatalf("userIds = %#v, want uid1,uid2", runner.last.Params["userIds"])
|
|
}
|
|
if got := runner.last.Params["workspaceId"]; got != "WS_001" {
|
|
t.Fatalf("workspaceId = %#v, want WS_001", got)
|
|
}
|
|
}
|
|
|
|
func TestDocPermissionListMaxresultsRejected(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocPermissionListCommand(runner)
|
|
_, _, err := executeDocCommand(t, cmd, "--node", "NODE_001", "--maxresults", "10")
|
|
if err == nil {
|
|
t.Fatal("Execute() error = nil, want unknown flag")
|
|
}
|
|
if !strings.Contains(err.Error(), "unknown flag: --maxresults") {
|
|
t.Fatalf("error = %q, want unknown flag for --maxresults", err.Error())
|
|
}
|
|
if runner.calls != 0 {
|
|
t.Fatalf("runner calls = %d, want 0", runner.calls)
|
|
}
|
|
}
|
|
|
|
func TestDocUpdateOverwriteRequiresYes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocTestRoot(runner)
|
|
_, _, err := executeDocCommand(t, cmd,
|
|
"update",
|
|
"--node", "NODE_001",
|
|
"--content", "# overwrite probe",
|
|
"--mode", "overwrite",
|
|
)
|
|
if err == nil {
|
|
t.Fatal("Execute() error = nil, want --yes validation failure")
|
|
}
|
|
if !strings.Contains(err.Error(), "--yes") {
|
|
t.Fatalf("error = %q, want --yes hint", err.Error())
|
|
}
|
|
if runner.calls != 0 {
|
|
t.Fatalf("runner calls = %d, want 0", runner.calls)
|
|
}
|
|
}
|
|
|
|
func TestDocUpdateOverwriteAllowsYesAndDryRun(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
extraArgs []string
|
|
wantDryRun bool
|
|
}{
|
|
{name: "yes", extraArgs: []string{"--yes"}},
|
|
{name: "dry run", extraArgs: []string{"--dry-run"}, wantDryRun: true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocTestRoot(runner)
|
|
args := []string{
|
|
"update",
|
|
"--node", "NODE_001",
|
|
"--content", "# overwrite probe",
|
|
"--mode", "overwrite",
|
|
}
|
|
args = append(args, tc.extraArgs...)
|
|
_, errOut, err := executeDocCommand(t, cmd, args...)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.calls != 1 {
|
|
t.Fatalf("runner calls = %d, want 1", runner.calls)
|
|
}
|
|
if runner.last.DryRun != tc.wantDryRun {
|
|
t.Fatalf("DryRun = %v, want %v", runner.last.DryRun, tc.wantDryRun)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDocListAcceptsFolderCompatibilityAliases(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
}{
|
|
{name: "node", args: []string{"--node", "FOLDER_001"}},
|
|
{name: "file id", args: []string{"--file-id", "FOLDER_001"}},
|
|
{name: "nodee typo", args: []string{"--nodee", "FOLDER_001"}},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocListCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd, tc.args...)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "list_nodes" {
|
|
t.Fatalf("tool = %q, want list_nodes", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["folderId"]; got != "FOLDER_001" {
|
|
t.Fatalf("folderId = %#v, want FOLDER_001", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDocListAcceptsWukongPaginationAliases(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocListCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd, "--workspace", "WS_001", "--limit", "20", "--cursor", "TOKEN_001")
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "list_nodes" {
|
|
t.Fatalf("tool = %q, want list_nodes", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["workspaceId"]; got != "WS_001" {
|
|
t.Fatalf("workspaceId = %#v, want WS_001", got)
|
|
}
|
|
if got := runner.last.Params["pageSize"]; got != 20 {
|
|
t.Fatalf("pageSize = %#v, want 20", got)
|
|
}
|
|
if got := runner.last.Params["pageToken"]; got != "TOKEN_001" {
|
|
t.Fatalf("pageToken = %#v, want TOKEN_001", got)
|
|
}
|
|
}
|
|
|
|
func TestDocSearchAcceptsWukongPaginationAliases(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocSearchCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd, "--query", "方案", "--limit", "20", "--cursor", "TOKEN_001")
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "search_documents" {
|
|
t.Fatalf("tool = %q, want search_documents", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["pageSize"]; got != 20 {
|
|
t.Fatalf("pageSize = %#v, want 20", got)
|
|
}
|
|
if got := runner.last.Params["pageToken"]; got != "TOKEN_001" {
|
|
t.Fatalf("pageToken = %#v, want TOKEN_001", got)
|
|
}
|
|
}
|
|
|
|
func TestDocExportDryRunPassesWukongExportFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocTestRoot(runner)
|
|
out, errOut, err := executeDocCommand(t, cmd,
|
|
"--dry-run",
|
|
"export",
|
|
"--node", "NODE_001",
|
|
"--output", "out.docx",
|
|
"--export-format", "docx",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.calls != 0 {
|
|
t.Fatalf("runner calls = %d, want 0 for dry-run", runner.calls)
|
|
}
|
|
var inv executor.Invocation
|
|
if err := json.Unmarshal([]byte(out), &inv); err != nil {
|
|
t.Fatalf("Unmarshal(%q) error = %v", out, err)
|
|
}
|
|
if inv.Tool != "submit_export_job" {
|
|
t.Fatalf("tool = %q, want submit_export_job", inv.Tool)
|
|
}
|
|
if got := inv.Params["exportFormat"]; got != "docx" {
|
|
t.Fatalf("exportFormat = %#v, want docx", got)
|
|
}
|
|
}
|
|
|
|
func TestDocUploadDryRunUsesWukongFileUploadWorkflow(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
contentPath := filepath.Join(t.TempDir(), "report.pdf")
|
|
if err := os.WriteFile(contentPath, []byte("pdf"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocTestRoot(runner)
|
|
out, errOut, err := executeDocCommand(t, cmd,
|
|
"--dry-run",
|
|
"upload",
|
|
"--file", contentPath,
|
|
"--name", "Q1汇报",
|
|
"--folder", "FOLDER_001",
|
|
"--convert",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.calls != 0 {
|
|
t.Fatalf("runner calls = %d, want 0 for dry-run", runner.calls)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal([]byte(out), &payload); err != nil {
|
|
t.Fatalf("Unmarshal(%q) error = %v", out, err)
|
|
}
|
|
step1, ok := payload["step_1_get_file_upload_info"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("missing step_1_get_file_upload_info in %#v", payload)
|
|
}
|
|
if got := step1["tool"]; got != "get_file_upload_info" {
|
|
t.Fatalf("step1 tool = %#v, want get_file_upload_info", got)
|
|
}
|
|
step3, ok := payload["step_3_commit_uploaded_file"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("missing step_3_commit_uploaded_file in %#v", payload)
|
|
}
|
|
params, ok := step3["params"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("step3 params type = %T", step3["params"])
|
|
}
|
|
if got := params["name"]; got != "Q1汇报.pdf" {
|
|
t.Fatalf("name = %#v, want Q1汇报.pdf", got)
|
|
}
|
|
if got := params["folderId"]; got != "FOLDER_001" {
|
|
t.Fatalf("folderId = %#v, want FOLDER_001", got)
|
|
}
|
|
if got := params["convertToOnlineDoc"]; got != true {
|
|
t.Fatalf("convertToOnlineDoc = %#v, want true", got)
|
|
}
|
|
}
|
|
|
|
func TestDocCommentListAcceptsWukongPaginationAliases(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocCommentListCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd, "--node", "NODE_001", "--limit", "20", "--cursor", "TOKEN_001")
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "list_comments" {
|
|
t.Fatalf("tool = %q, want list_comments", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["pageSize"]; got != 20 {
|
|
t.Fatalf("pageSize = %#v, want 20", got)
|
|
}
|
|
if got := runner.last.Params["nextToken"]; got != "TOKEN_001" {
|
|
t.Fatalf("nextToken = %#v, want TOKEN_001", got)
|
|
}
|
|
}
|
|
|
|
func TestDocCommentReplyEmojiIsBoolFlag(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocCommentReplyCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd,
|
|
"--node", "NODE_001",
|
|
"--comment-key", "COMMENT_KEY",
|
|
"--content", "比心",
|
|
"--emoji",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "reply_comment" {
|
|
t.Fatalf("tool = %q, want reply_comment", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["emoji"]; got != true {
|
|
t.Fatalf("emoji = %#v, want true", got)
|
|
}
|
|
}
|
|
|
|
func TestDocCreateAcceptsParentFolderAliases(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
}{
|
|
{
|
|
name: "parent folder id",
|
|
args: []string{"--name", "doc", "--parent-folder-id", "FOLDER_001", "--content", "hello"},
|
|
},
|
|
{
|
|
name: "parent folder",
|
|
args: []string{"--name", "doc", "--parent-folder", "FOLDER_001", "--content", "hello"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocCreateCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd, tc.args...)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "create_document" {
|
|
t.Fatalf("tool = %q, want create_document", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["folderId"]; got != "FOLDER_001" {
|
|
t.Fatalf("folderId = %#v, want FOLDER_001", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDocCreateAcceptsContentPathAlias(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
contentPath := filepath.Join(t.TempDir(), "doc.md")
|
|
if err := os.WriteFile(contentPath, []byte("# from file"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocCreateCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd, "--name", "doc", "--content-path", contentPath)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "create_document" {
|
|
t.Fatalf("tool = %q, want create_document", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["markdown"]; got != "# from file" {
|
|
t.Fatalf("markdown = %#v, want # from file", got)
|
|
}
|
|
}
|
|
|
|
func TestDocReadPassesJsonMLFormatAndOutput(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocReadCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd,
|
|
"--node", "NODE_001",
|
|
"--content-format", "jsonml",
|
|
"--output", "body.json",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "get_document_content" {
|
|
t.Fatalf("tool = %q, want get_document_content", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["format"]; got != "jsonml" {
|
|
t.Fatalf("format = %#v, want jsonml", got)
|
|
}
|
|
if got := runner.last.Params["__output__"]; got != "body.json" {
|
|
t.Fatalf("__output__ = %#v, want body.json", got)
|
|
}
|
|
}
|
|
|
|
func TestDocCreatePassesJsonMLContentAndFixFlag(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{responses: []map[string]any{{"nodeId": "NODE_NEW"}}}
|
|
cmd := newDocCreateCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd,
|
|
"--name", "doc",
|
|
"--content", `{"jsonml":[["p",{},"hello"]]}`,
|
|
"--content-format", "jsonml",
|
|
"--fix-jsonml",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.calls != 2 {
|
|
t.Fatalf("calls = %d, want 2", runner.calls)
|
|
}
|
|
if runner.all[0].Tool != "create_document" {
|
|
t.Fatalf("first tool = %q, want create_document", runner.all[0].Tool)
|
|
}
|
|
if runner.last.Tool != "update_document" {
|
|
t.Fatalf("last tool = %q, want update_document", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["format"]; got != "jsonml" {
|
|
t.Fatalf("format = %#v, want jsonml", got)
|
|
}
|
|
jsonml, ok := runner.last.Params["jsonml"].(string)
|
|
if !ok || !strings.Contains(jsonml, `"root"`) || !strings.Contains(jsonml, `"hello"`) {
|
|
t.Fatalf("jsonml = %#v, want normalized root JSONML", runner.last.Params["jsonml"])
|
|
}
|
|
if _, ok := runner.last.Params["markdown"]; ok {
|
|
t.Fatalf("markdown = %#v, want omitted", runner.last.Params["markdown"])
|
|
}
|
|
if _, ok := runner.last.Params["fixJsonml"]; ok {
|
|
t.Fatalf("fixJsonml = %#v, want omitted", runner.last.Params["fixJsonml"])
|
|
}
|
|
}
|
|
|
|
func TestDocUpdatePassesJsonMLRevisionAndNoFixFlag(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocTestRoot(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd,
|
|
"update",
|
|
"--node", "NODE_001",
|
|
"--content", `["root",{},["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"updated"]]]]`,
|
|
"--content-format", "jsonml",
|
|
"--revision", "42",
|
|
"--no-fix-jsonml",
|
|
"--mode", "overwrite",
|
|
"--yes",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "update_document" {
|
|
t.Fatalf("tool = %q, want update_document", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["format"]; got != "jsonml" {
|
|
t.Fatalf("format = %#v, want jsonml", got)
|
|
}
|
|
if got := runner.last.Params["revision"]; got != 42 {
|
|
t.Fatalf("revision = %#v, want 42", got)
|
|
}
|
|
if _, ok := runner.last.Params["noFixJsonml"]; ok {
|
|
t.Fatalf("noFixJsonml = %#v, want omitted", runner.last.Params["noFixJsonml"])
|
|
}
|
|
if _, ok := runner.last.Params["index"]; ok {
|
|
t.Fatalf("index = %#v, want omitted for JSONML update", runner.last.Params["index"])
|
|
}
|
|
}
|
|
|
|
func TestDocBlockListPassesJsonMLFormatAndBlockID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocBlockListCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd,
|
|
"--node", "DOC_001",
|
|
"--content-format", "jsonml",
|
|
"--block-id", "BLOCK_001",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "list_document_blocks" {
|
|
t.Fatalf("tool = %q, want list_document_blocks", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["format"]; got != "jsonml" {
|
|
t.Fatalf("format = %#v, want jsonml", got)
|
|
}
|
|
if got := runner.last.Params["blockId"]; got != "BLOCK_001" {
|
|
t.Fatalf("blockId = %#v, want BLOCK_001", got)
|
|
}
|
|
}
|
|
|
|
func TestDocBlockInsertPassesJsonMLAndParentBlock(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocBlockInsertCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd,
|
|
"--node", "DOC_001",
|
|
"--content-format", "jsonml",
|
|
"--element", `["p",{},"hello"]`,
|
|
"--parent-block", "PARENT_001",
|
|
"--index", "1",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "insert_document_block" {
|
|
t.Fatalf("tool = %q, want insert_document_block", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["format"]; got != "jsonml" {
|
|
t.Fatalf("format = %#v, want jsonml", got)
|
|
}
|
|
jsonml, ok := runner.last.Params["jsonml"].(string)
|
|
if !ok || !strings.Contains(jsonml, `"span"`) || !strings.Contains(jsonml, `"hello"`) {
|
|
t.Fatalf("jsonml = %#v, want normalized JSONML node", runner.last.Params["jsonml"])
|
|
}
|
|
if got := runner.last.Params["referenceBlockId"]; got != "PARENT_001" {
|
|
t.Fatalf("referenceBlockId = %#v, want PARENT_001", got)
|
|
}
|
|
if got := runner.last.Params["index"]; got != 1 {
|
|
t.Fatalf("index = %#v, want 1", got)
|
|
}
|
|
if _, ok := runner.last.Params["element"]; ok {
|
|
t.Fatalf("element = %#v, want omitted", runner.last.Params["element"])
|
|
}
|
|
}
|
|
|
|
func TestDocBlockUpdatePassesJsonMLAndFixFlags(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocBlockUpdateCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd,
|
|
"--node", "DOC_001",
|
|
"--block-id", "BLOCK_001",
|
|
"--content-format", "jsonml",
|
|
"--element", `["p",{},"new"]`,
|
|
"--fix-jsonml",
|
|
"--no-fix-jsonml",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "update_document_block" {
|
|
t.Fatalf("tool = %q, want update_document_block", runner.last.Tool)
|
|
}
|
|
if got := runner.last.Params["format"]; got != "jsonml" {
|
|
t.Fatalf("format = %#v, want jsonml", got)
|
|
}
|
|
if got := runner.last.Params["jsonml"]; got != `["p",{},"new"]` {
|
|
t.Fatalf("jsonml = %#v, want original node when --no-fix-jsonml wins", got)
|
|
}
|
|
if _, ok := runner.last.Params["fixJsonml"]; ok {
|
|
t.Fatalf("fixJsonml = %#v, want omitted", runner.last.Params["fixJsonml"])
|
|
}
|
|
if _, ok := runner.last.Params["noFixJsonml"]; ok {
|
|
t.Fatalf("noFixJsonml = %#v, want omitted", runner.last.Params["noFixJsonml"])
|
|
}
|
|
}
|
|
|
|
func TestDocBlockInsertTypeCallout(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocBlockInsertCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd,
|
|
"--node", "DOC_001",
|
|
"--type", "callout",
|
|
"--text", "接口变更通知;DBA 审核;告警规则;安全评审",
|
|
"--where", "end",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
if runner.last.Tool != "insert_document_block" {
|
|
t.Fatalf("tool = %q, want insert_document_block", runner.last.Tool)
|
|
}
|
|
if _, ok := runner.last.Params["where"]; ok {
|
|
t.Fatalf("where = %#v, want omitted for --where end", runner.last.Params["where"])
|
|
}
|
|
element, ok := runner.last.Params["element"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("element = %#v, want map", runner.last.Params["element"])
|
|
}
|
|
if got := element["blockType"]; got != "callout" {
|
|
t.Fatalf("blockType = %#v, want callout", got)
|
|
}
|
|
callout, ok := element["callout"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("callout = %#v, want map", element["callout"])
|
|
}
|
|
if got := callout["text"]; got != "接口变更通知;DBA 审核;告警规则;安全评审" {
|
|
t.Fatalf("callout.text = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestDocBlockInsertTypeListAndColumns(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("ordered list", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocBlockInsertCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd,
|
|
"--node", "DOC_001",
|
|
"--type", "ordered-list",
|
|
"--list-id", "schedule",
|
|
"--text", "需求评审",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
element := runner.last.Params["element"].(map[string]any)
|
|
if got := element["blockType"]; got != "orderedList" {
|
|
t.Fatalf("blockType = %#v, want orderedList", got)
|
|
}
|
|
list := element["orderedList"].(map[string]any)["list"].(map[string]any)
|
|
if got := list["listId"]; got != "schedule" {
|
|
t.Fatalf("listId = %#v, want schedule", got)
|
|
}
|
|
})
|
|
|
|
t.Run("columns", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runner := &docCommandRunner{}
|
|
cmd := newDocBlockInsertCommand(runner)
|
|
_, errOut, err := executeDocCommand(t, cmd,
|
|
"--node", "DOC_001",
|
|
"--type", "columns",
|
|
"--columns", "2",
|
|
"--text", "方案A||方案B",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut)
|
|
}
|
|
element := runner.last.Params["element"].(map[string]any)
|
|
if got := element["blockType"]; got != "columns" {
|
|
t.Fatalf("blockType = %#v, want columns", got)
|
|
}
|
|
columns := element["columns"].(map[string]any)
|
|
if got := columns["size"]; got != 2 {
|
|
t.Fatalf("columns.size = %#v, want 2", got)
|
|
}
|
|
children := element["children"].([]any)
|
|
if len(children) != 2 {
|
|
t.Fatalf("children len = %d, want 2", len(children))
|
|
}
|
|
})
|
|
}
|