// 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. package helpers import ( "context" "encoding/json" "fmt" "io" "os" "path/filepath" "strings" "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" "github.com/spf13/cobra" ) type nativePrimaryParamCall struct { server string tool string args map[string]any } type nativePrimaryParamCaller struct { calls []nativePrimaryParamCall } func (c *nativePrimaryParamCaller) CallTool(_ context.Context, server, tool string, args map[string]any) (*edition.ToolResult, error) { copied := make(map[string]any, len(args)) for key, value := range args { copied[key] = value } c.calls = append(c.calls, nativePrimaryParamCall{server: server, tool: tool, args: copied}) switch tool { case "init_conversation_file_upload", "init_todo_file_upload": return textToolResult(`{"resourceUrl":"https://upload.invalid/file","uploadKey":"upload-key"}`), nil case "commit_conversation_file_upload": fileName, _ := args["fileName"].(string) payload, _ := json.Marshal(map[string]any{ "dentryId": 123, "spaceId": 456, "downloadUrl": "https://download.invalid/" + fileName, }) return textToolResult(string(payload)), nil case "commit_todo_file_upload": return textToolResult(`{"dentryId":123,"spaceId":456}`), nil default: return textToolResult(`{}`), nil } } func (*nativePrimaryParamCaller) Format() string { return "json" } func (*nativePrimaryParamCaller) DryRun() bool { return false } func (*nativePrimaryParamCaller) Fields() string { return "" } func (*nativePrimaryParamCaller) JQ() string { return "" } func (c *nativePrimaryParamCaller) lastCall(t *testing.T, tool string) nativePrimaryParamCall { t.Helper() for i := len(c.calls) - 1; i >= 0; i-- { if c.calls[i].tool == tool { return c.calls[i] } } t.Fatalf("tool %q not called; calls = %#v", tool, c.calls) return nativePrimaryParamCall{} } func executeNativePrimaryTodo(t *testing.T, caller edition.ToolCaller, args ...string) error { t.Helper() previousDeps, previousArgs := deps, os.Args os.Args = []string{"dws", "todo"} InitDeps(caller) deps.Out.w = io.Discard deps.Out.errW = io.Discard t.Cleanup(func() { deps = previousDeps os.Args = previousArgs }) cmd := newTodoCommand() if cmd.PersistentFlags().Lookup("yes") == nil { cmd.PersistentFlags().Bool("yes", false, "confirm execution") } if cmd.PersistentFlags().Lookup("dry-run") == nil { cmd.PersistentFlags().Bool("dry-run", false, "preview without executing") } cmd.SilenceErrors = true cmd.SilenceUsage = true cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(args) return cmd.ExecuteContext(context.Background()) } func findNativePrimaryLeaf(t *testing.T, root *cobra.Command, path ...string) *cobra.Command { t.Helper() leaf, remaining, err := root.Find(path) if err != nil { t.Fatalf("find %v: %v", path, err) } if len(remaining) != 0 { t.Fatalf("find %v left arguments %v", path, remaining) } return leaf } func assertNativePrimaryFlagPair(t *testing.T, cmd *cobra.Command, primary, legacy string) { t.Helper() primaryFlag := cmd.Flags().Lookup(primary) if primaryFlag == nil || primaryFlag.Hidden { t.Fatalf("%s --%s = %#v, want visible Primary", cmd.CommandPath(), primary, primaryFlag) } legacyFlag := cmd.Flags().Lookup(legacy) if legacyFlag == nil || !legacyFlag.Hidden { t.Fatalf("%s --%s = %#v, want hidden compatibility alias", cmd.CommandPath(), legacy, legacyFlag) } if got := legacyFlag.Annotations[runtimeannotate.AnnotationFlagAliasOf]; len(got) != 1 || got[0] != primary { t.Fatalf("%s --%s alias_of = %#v, want %q", cmd.CommandPath(), legacy, got, primary) } if got := legacyFlag.Annotations[runtimeannotate.AnnotationFlagAliasOrigin]; len(got) != 1 || got[0] != runtimeannotate.FlagAliasOriginCorecmdV1 { t.Fatalf("%s --%s alias_origin = %#v, want %q", cmd.CommandPath(), legacy, got, runtimeannotate.FlagAliasOriginCorecmdV1) } } func TestNativePrimaryParamFlagSurfaces(t *testing.T) { aisearch := newAisearchCommand() assertNativePrimaryFlagPair(t, aisearch, "query", "keyword") person := findNativePrimaryLeaf(t, aisearch, "person") assertNativePrimaryFlagPair(t, person, "query", "keyword") for _, cmd := range []*cobra.Command{aisearch, person} { if got := cmd.Flags().Lookup("keyword").Shorthand; got != "w" { t.Fatalf("%s --keyword shorthand = %q, want preserved -w", cmd.CommandPath(), got) } if got := cmd.Flags().Lookup("query").Shorthand; got != "" { t.Fatalf("%s --query shorthand = %q, want none", cmd.CommandPath(), got) } } chat := newChatCommand() send := findNativePrimaryLeaf(t, chat, "message", "send") assertNativePrimaryFlagPair(t, send, "content", "text") assertNativePrimaryFlagPair(t, send, "file", "file-path") sendByWebhook := findNativePrimaryLeaf(t, chat, "message", "send-by-webhook") assertNativePrimaryFlagPair(t, sendByWebhook, "content", "text") sendByBot := findNativePrimaryLeaf(t, chat, "message", "send-by-bot") for _, name := range []string{"text", "file-path"} { flag := sendByBot.Flags().Lookup(name) if flag == nil || flag.Hidden { t.Fatalf("%s --%s = %#v, want unchanged visible Primary", sendByBot.CommandPath(), name, flag) } } for _, name := range []string{"content", "file"} { if flag := sendByBot.Flags().Lookup(name); flag != nil && !flag.Hidden { t.Fatalf("%s --%s = %#v, pending migration must not become visible", sendByBot.CommandPath(), name, flag) } } reply := findNativePrimaryLeaf(t, chat, "message", "reply") assertNativePrimaryFlagPair(t, reply, "group", "conversation-id") assertNativePrimaryFlagPair(t, reply, "content", "text") doc := newDocCommand() assertNativePrimaryFlagPair(t, findNativePrimaryLeaf(t, doc, "block", "insert"), "content", "text") assertNativePrimaryFlagPair(t, findNativePrimaryLeaf(t, doc, "block", "update"), "content", "text") todo := newTodoCommand() assertNativePrimaryFlagPair(t, findNativePrimaryLeaf(t, todo, "task", "add-attachment"), "file", "file-path") } type nativePrimaryParamSpelling struct { name string args []string want string } func nativePrimaryParamSpellings(primary, legacy string) []nativePrimaryParamSpelling { return []nativePrimaryParamSpelling{ {name: "legacy only", args: []string{"--" + legacy, "legacy-value"}, want: "legacy-value"}, {name: "primary only", args: []string{"--" + primary, "primary-value"}, want: "primary-value"}, {name: "both legacy wins", args: []string{"--" + primary, "primary-value", "--" + legacy, "legacy-value"}, want: "legacy-value"}, } } func nativePrimaryJSONField(t *testing.T, raw any, field string) string { t.Helper() encoded, ok := raw.(string) if !ok { t.Fatalf("payload = %#v, want JSON string", raw) } var payload map[string]any if err := json.Unmarshal([]byte(encoded), &payload); err != nil { t.Fatalf("decode payload %q: %v", encoded, err) } value, _ := payload[field].(string) return value } func nativePrimaryParagraphText(t *testing.T, call nativePrimaryParamCall) string { t.Helper() element, ok := call.args["element"].(map[string]any) if !ok { t.Fatalf("%s element = %#v", call.tool, call.args["element"]) } paragraph, ok := element["paragraph"].(map[string]any) if !ok { t.Fatalf("%s paragraph = %#v", call.tool, element["paragraph"]) } text, _ := paragraph["text"].(string) return text } func TestNativePrimaryParamTextPayloadCompatibility(t *testing.T) { previousDeps, previousArgs := deps, os.Args os.Args = []string{"dws", "chat"} t.Cleanup(func() { deps = previousDeps os.Args = previousArgs }) t.Run("chat message send text to content", func(t *testing.T) { for _, spelling := range nativePrimaryParamSpellings("content", "text") { t.Run(spelling.name, func(t *testing.T) { caller := &nativePrimaryParamCaller{} args := []string{"message", "send", "--group", "cid", "--title", "fixed-title"} args = append(args, spelling.args...) if err := runChatCoverageCommand(t, caller, args...); err != nil { t.Fatal(err) } call := caller.lastCall(t, "send_personal_message") if got := nativePrimaryJSONField(t, call.args["content"], "text"); got != spelling.want { t.Fatalf("content.text = %q, want %q; payload = %#v", got, spelling.want, call.args) } }) } }) t.Run("chat message send-by-webhook text to content", func(t *testing.T) { for _, spelling := range nativePrimaryParamSpellings("content", "text") { t.Run(spelling.name, func(t *testing.T) { caller := &nativePrimaryParamCaller{} args := []string{"message", "send-by-webhook", "--token", "token", "--title", "fixed-title"} args = append(args, spelling.args...) if err := runChatCoverageCommand(t, caller, args...); err != nil { t.Fatal(err) } call := caller.lastCall(t, "send_message_by_custom_robot") if got, _ := call.args["text"].(string); got != spelling.want { t.Fatalf("text = %q, want %q; payload = %#v", got, spelling.want, call.args) } }) } }) t.Run("chat message reply conversation-id to group", func(t *testing.T) { for _, spelling := range nativePrimaryParamSpellings("group", "conversation-id") { t.Run(spelling.name, func(t *testing.T) { caller := &nativePrimaryParamCaller{} args := []string{"message", "reply", "--ref-msg-id", "mid", "--ref-sender", helperCurrentDOpenID, "--content", "fixed-content"} args = append(args, spelling.args...) if err := runChatCoverageCommand(t, caller, args...); err != nil { t.Fatal(err) } call := caller.lastCall(t, "send_personal_message") if got, _ := call.args["openConversationId"].(string); got != spelling.want { t.Fatalf("openConversationId = %q, want %q; payload = %#v", got, spelling.want, call.args) } }) } }) t.Run("chat message reply text to content", func(t *testing.T) { for _, spelling := range nativePrimaryParamSpellings("content", "text") { t.Run(spelling.name, func(t *testing.T) { caller := &nativePrimaryParamCaller{} args := []string{"message", "reply", "--group", "cid", "--ref-msg-id", "mid", "--ref-sender", helperCurrentDOpenID} args = append(args, spelling.args...) if err := runChatCoverageCommand(t, caller, args...); err != nil { t.Fatal(err) } call := caller.lastCall(t, "send_personal_message") if got := nativePrimaryJSONField(t, call.args["content"], "content"); got != spelling.want { t.Fatalf("reply content = %q, want %q; payload = %#v", got, spelling.want, call.args) } }) } }) for _, command := range []struct { name string path []string tool string }{ {name: "doc block insert text to content", path: []string{"block", "insert", "--node", "node"}, tool: "insert_document_block"}, {name: "doc block update text to content", path: []string{"block", "update", "--node", "node", "--block-id", "block"}, tool: "update_document_block"}, } { command := command t.Run(command.name, func(t *testing.T) { for _, spelling := range nativePrimaryParamSpellings("content", "text") { t.Run(spelling.name, func(t *testing.T) { caller := &nativePrimaryParamCaller{} args := append(append([]string(nil), command.path...), spelling.args...) if err := runDocCoverageCommand(t, caller, args...); err != nil { t.Fatal(err) } call := caller.lastCall(t, command.tool) if got := nativePrimaryParagraphText(t, call); got != spelling.want { t.Fatalf("paragraph text = %q, want %q; payload = %#v", got, spelling.want, call.args) } }) } }) } } func TestNativePrimaryParamFilePayloadCompatibility(t *testing.T) { previousDeps, previousPut, previousArgs := deps, httpPutFile, os.Args t.Cleanup(func() { deps = previousDeps httpPutFile = previousPut os.Args = previousArgs }) httpPutFile = func(context.Context, string, map[string]string, string, int64) error { return nil } dir := t.TempDir() legacyPath := filepath.Join(dir, "legacy-file.txt") primaryPath := filepath.Join(dir, "primary-file.txt") if err := os.WriteFile(legacyPath, []byte("legacy"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(primaryPath, []byte("primary"), 0o600); err != nil { t.Fatal(err) } spellings := []nativePrimaryParamSpelling{ {name: "legacy only", args: []string{"--file-path", legacyPath}, want: filepath.Base(legacyPath)}, {name: "primary only", args: []string{"--file", primaryPath}, want: filepath.Base(primaryPath)}, {name: "both legacy wins", args: []string{"--file", primaryPath, "--file-path", legacyPath}, want: filepath.Base(legacyPath)}, } t.Run("chat message send file-path to file", func(t *testing.T) { os.Args = []string{"dws", "chat"} for _, spelling := range spellings { t.Run(spelling.name, func(t *testing.T) { caller := &nativePrimaryParamCaller{} args := []string{"message", "send", "--group", "cid", "--msg-type", "file"} args = append(args, spelling.args...) if err := runChatCoverageCommand(t, caller, args...); err != nil { t.Fatal(err) } call := caller.lastCall(t, "send_personal_message") if got := nativePrimaryJSONField(t, call.args["content"], "fileName"); got != spelling.want { t.Fatalf("fileName = %q, want %q; payload = %#v", got, spelling.want, call.args) } }) } }) t.Run("todo task add-attachment file-path to file", func(t *testing.T) { os.Args = []string{"dws", "todo"} for _, spelling := range spellings { t.Run(spelling.name, func(t *testing.T) { caller := &nativePrimaryParamCaller{} args := []string{"task", "add-attachment", "--task-id", "42"} args = append(args, spelling.args...) if err := executeNativePrimaryTodo(t, caller, args...); err != nil { t.Fatal(err) } call := caller.lastCall(t, "add_todo_attachment") request, ok := call.args["todoAttachmentAddRequest"].(map[string]any) if !ok { t.Fatalf("request = %#v", call.args) } attachments, ok := request["attachmentList"].([]any) if !ok || len(attachments) != 1 { t.Fatalf("attachments = %#v", request["attachmentList"]) } attachment, ok := attachments[0].(map[string]any) if !ok { t.Fatalf("attachment = %#v", attachments[0]) } if got, _ := attachment["fileName"].(string); got != spelling.want { t.Fatalf("fileName = %q, want %q; payload = %#v", got, spelling.want, call.args) } }) } }) } func nativePrimaryErrorMentionsFlag(message, name string) bool { return strings.Contains(message, "--"+name) || strings.Contains(message, fmt.Sprintf("%q", name)) } func assertNativePrimaryErrorHint(t *testing.T, err error, primary, legacy string) { t.Helper() if err == nil { t.Fatalf("missing --%s returned nil", primary) } message := err.Error() if !nativePrimaryErrorMentionsFlag(message, primary) { t.Fatalf("error %q does not recommend --%s", message, primary) } if nativePrimaryErrorMentionsFlag(message, legacy) { t.Fatalf("error %q still recommends legacy --%s", message, legacy) } } func TestNativePrimaryParamMissingErrorsRecommendPrimaryOnly(t *testing.T) { previousDeps, previousArgs := deps, os.Args t.Cleanup(func() { deps = previousDeps os.Args = previousArgs }) installScriptedCaller(t, &scriptedToolCaller{}) assertNativePrimaryErrorHint(t, executeFilterCoverage(t, newAisearchCommand(), "person"), "query", "keyword") os.Args = []string{"dws", "chat"} assertNativePrimaryErrorHint(t, runChatCoverageCommand(t, &nativePrimaryParamCaller{}, "message", "send", "--group", "cid"), "content", "text") assertNativePrimaryErrorHint(t, runChatCoverageCommand(t, &nativePrimaryParamCaller{}, "message", "send-by-webhook", "--token", "token", "--title", "title"), "content", "text") assertNativePrimaryErrorHint(t, runChatCoverageCommand(t, &nativePrimaryParamCaller{}, "message", "reply", "--ref-msg-id", "mid", "--ref-sender", helperCurrentDOpenID, "--content", "body"), "group", "conversation-id") assertNativePrimaryErrorHint(t, runChatCoverageCommand(t, &nativePrimaryParamCaller{}, "message", "reply", "--group", "cid", "--ref-msg-id", "mid", "--ref-sender", helperCurrentDOpenID), "content", "text") os.Args = []string{"dws", "doc"} assertNativePrimaryErrorHint(t, runDocCoverageCommand(t, &nativePrimaryParamCaller{}, "block", "insert", "--node", "node"), "content", "text") assertNativePrimaryErrorHint(t, runDocCoverageCommand(t, &nativePrimaryParamCaller{}, "block", "update", "--node", "node", "--block-id", "block"), "content", "text") os.Args = []string{"dws", "todo"} assertNativePrimaryErrorHint(t, executeTodoEdge(t, &scriptedToolCaller{}, "task", "add-attachment", "--task-id", "42"), "file", "file-path") } func TestNativePrimaryParamPromotionErrorsPropagate(t *testing.T) { chat := newChatCommand() assertPromotionError := func(t *testing.T, err error) { t.Helper() if err == nil || !strings.Contains(err.Error(), "trying to get string value of flag of type bool") { t.Fatalf("promotion error = %v", err) } } t.Run("webhook RunE", func(t *testing.T) { target := findNativePrimaryLeaf(t, chat, "message", "send-by-webhook") cmd := &cobra.Command{} cmd.Flags().String("content", "", "") cmd.Flags().Bool("text", false, "") if err := cmd.Flags().Set("text", "true"); err != nil { t.Fatal(err) } assertPromotionError(t, target.RunE(cmd, nil)) }) reply := findNativePrimaryLeaf(t, chat, "message", "reply") newInvalidGroupCommand := func(t *testing.T) *cobra.Command { t.Helper() cmd := &cobra.Command{} cmd.Flags().String("group", "", "") cmd.Flags().Bool("conversation-id", false, "") if err := cmd.Flags().Set("conversation-id", "true"); err != nil { t.Fatal(err) } return cmd } t.Run("reply PreRunE group", func(t *testing.T) { assertPromotionError(t, reply.PreRunE(newInvalidGroupCommand(t), nil)) }) t.Run("reply RunE group", func(t *testing.T) { assertPromotionError(t, reply.RunE(newInvalidGroupCommand(t), nil)) }) t.Run("reply RunE content", func(t *testing.T) { cmd := &cobra.Command{} cmd.Flags().String("group", "cid", "") cmd.Flags().String("conversation-id", "", "") cmd.Flags().String("content", "", "") cmd.Flags().Bool("text", false, "") if err := cmd.Flags().Set("text", "true"); err != nil { t.Fatal(err) } assertPromotionError(t, reply.RunE(cmd, nil)) }) }