Files
dingtalk-workspace-cli/internal/helpers/helpers_core_test.go
T

409 lines
15 KiB
Go

package helpers
import (
"bytes"
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/spf13/cobra"
)
type helpersCoreCaller struct {
result *edition.ToolResult
err error
format string
dry bool
calls int
}
func (c *helpersCoreCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
c.calls++
return c.result, c.err
}
func (c *helpersCoreCaller) Format() string { return c.format }
func (c *helpersCoreCaller) DryRun() bool { return c.dry }
func (*helpersCoreCaller) Fields() string { return "" }
func (*helpersCoreCaller) JQ() string { return "" }
type helpersReadCaller struct {
*helpersCoreCaller
readResult *edition.ToolResult
readErr error
readCalls int
}
func (c *helpersReadCaller) CallReadTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
c.readCalls++
return c.readResult, c.readErr
}
func textToolResult(text string) *edition.ToolResult {
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: text}}}
}
func TestCrossPlatformCoverageRawToolAuditLineIsSingleLineJSON(t *testing.T) {
if got, want := formatRawDumpLine("chat", "list_messages", "{\n \"items\": []\n}"),
`DWSRAW chat list_messages {"items":[]}`; got != want {
t.Fatalf("JSON raw dump = %q, want %q", got, want)
}
if got, want := formatRawDumpLine("chat", "plain", "line one\nline two"),
`DWSRAW chat plain "line one\nline two"`; got != want {
t.Fatalf("text raw dump = %q, want %q", got, want)
}
}
func TestCrossPlatformCoverageRawToolAuditEnabled(t *testing.T) {
t.Setenv("DWS_DUMP_RAW", "1")
dumpRawToolResponse("im", "search_groups", `{"result":[]}`)
}
func installHelpersCoreDeps(t *testing.T, caller edition.ToolCaller) (*bytes.Buffer, *bytes.Buffer) {
t.Helper()
old := deps
t.Cleanup(func() { deps = old })
InitDeps(caller)
out := &bytes.Buffer{}
errOut := &bytes.Buffer{}
deps.Out.w = out
deps.Out.errW = errOut
return out, errOut
}
func TestCrossPlatformCoverageDryRunReadLookupUsesExplicitCapability(t *testing.T) {
caller := &helpersReadCaller{
helpersCoreCaller: &helpersCoreCaller{
format: "json",
dry: true,
result: textToolResult(
`{"dry_run":true,"request":{"name":"search_groups"}}`,
),
},
readResult: textToolResult(`{"success":true,"result":{"groups":[{"id":"g1"}]}}`),
}
installHelpersCoreDeps(t, caller)
got, err := CallMCPReadToolTextOnServer("im", "search_groups", map[string]any{"keyword": "x"})
if err != nil {
t.Fatalf("CallMCPReadToolTextOnServer() error = %v", err)
}
if !strings.Contains(got, `"groups"`) || caller.readCalls != 1 || caller.calls != 0 {
t.Fatalf("read result/calls = %q, read=%d regular=%d", got, caller.readCalls, caller.calls)
}
failClosed := &helpersCoreCaller{format: "json", dry: true}
installHelpersCoreDeps(t, failClosed)
if _, err := CallMCPReadToolTextOnServer("im", "search_groups", nil); err == nil {
t.Fatal("dry-run read lookup without explicit capability was accepted")
}
if failClosed.calls != 0 {
t.Fatalf("fail-closed lookup made %d regular calls", failClosed.calls)
}
}
func TestCrossPlatformCoverageReadLookupInitializationAndRegularExecution(t *testing.T) {
old := deps
t.Cleanup(func() { deps = old })
deps = nil
if _, err := CallMCPReadToolTextOnServer("im", "search_groups", nil); err == nil {
t.Fatal("uninitialized read lookup unexpectedly succeeded")
}
caller := &helpersCoreCaller{
format: "json",
result: textToolResult(`{"success":true,"result":{"groups":[]}}`),
}
installHelpersCoreDeps(t, caller)
got, err := CallMCPReadToolTextOnServer("im", "search_groups", nil)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, `"groups"`) || caller.calls != 1 {
t.Fatalf("regular read result/calls = %q, %d", got, caller.calls)
}
}
func TestCrossPlatformCoverageSharedDependenciesRoutingAndWrappers(t *testing.T) {
oldDeps := deps
deps = nil
if GetCaller() != nil || GetFormatter() == nil {
t.Fatal("nil dependency accessors returned unexpected values")
}
deps = oldDeps
caller := &helpersCoreCaller{format: "json", result: textToolResult(`{"ok":true}`)}
installHelpersCoreDeps(t, caller)
if GetCaller() != caller || GetFormatter() != deps.Out {
t.Fatal("initialized dependency accessors returned unexpected values")
}
oldArgs := os.Args
t.Cleanup(func() { os.Args = oldArgs })
os.Args = []string{"dws", "--format", "json", "doc", "get"}
if got := resolveProductID(); got != "doc" {
t.Fatalf("resolveProductID() = %q", got)
}
os.Args = []string{"dws", "unknown"}
if got := resolveProductID(); got != "" {
t.Fatalf("unknown resolveProductID() = %q", got)
}
if _, err := callMCPToolReturnText(context.Background(), "tool", nil); err == nil {
t.Fatal("unroutable return-text call should fail")
}
os.Args = []string{"dws", "doc"}
if got, err := callMCPToolReturnText(context.Background(), "tool", nil); err != nil || got != `{"ok":true}` {
t.Fatalf("callMCPToolReturnText() = %q, %v", got, err)
}
if err := CallMCPToolOnServer("doc", "tool", nil); err != nil {
t.Fatalf("CallMCPToolOnServer(): %v", err)
}
root := &cobra.Command{Use: "root"}
root.PersistentFlags().String("inherited", "fallback", "")
child := &cobra.Command{Use: "child"}
root.AddCommand(child)
if got := MustGetStringFlag(child, "inherited"); got != "fallback" {
t.Fatalf("MustGetStringFlag() = %q", got)
}
_ = GroupRunE(root, nil)
}
func TestCrossPlatformCoverageMCPReturnTextClassification(t *testing.T) {
caller := &helpersCoreCaller{}
installHelpersCoreDeps(t, caller)
cases := []struct {
name string
text string
}{
{"gateway", `{"errorCode":"DWS_SERVICE_UNAUTHORIZED"}`},
{"not-logged-in", `{"error":"Missing service_id or access_key"}`},
{"pat", `{"code":"PAT_NO_PERMISSION"}`},
{"business-bool", `{"success":false,"message":"failed"}`},
{"business-string", `{"success":"false","errorMsg":"failed"}`},
{"business-error", `{"error":"failed"}`},
{"business-webhook-errcode", `{"errcode":300005,"errmsg":"token is not exist"}`},
}
for _, tc := range cases {
caller.result = textToolResult(tc.text)
if _, err := callMCPToolReturnTextOnServer(context.Background(), "server", "tool", nil); err == nil {
t.Errorf("%s response should be classified", tc.name)
}
}
caller.result = &edition.ToolResult{Content: []edition.ContentBlock{{Type: "image", Text: "ignored"}, {Type: "text"}}}
if got, err := callMCPToolReturnTextOnServer(context.Background(), "server", "tool", nil); err != nil || got != "" {
t.Fatalf("low-level empty text representation = %q, %v", got, err)
}
caller.result = nil
if _, err := callMCPToolReturnTextOnServer(context.Background(), "server", "tool", nil); err == nil {
t.Fatal("nil result must fail")
}
caller.result = textToolResult("shortcut result")
if got, err := CallMCPToolTextOnServer("server", "tool", nil); err != nil || got != "shortcut result" {
t.Fatalf("exported text result = %q, %v", got, err)
}
caller.err = errors.New("request failed with PAT_HIGH_RISK_NO_PERMISSION")
if _, err := callMCPToolReturnTextOnServer(context.Background(), "server", "tool", nil); err == nil {
t.Fatal("PAT transport error should be classified")
}
caller.err = errors.New("ordinary transport failure")
if _, err := callMCPToolReturnTextOnServer(context.Background(), "server", "tool", nil); err == nil {
t.Fatal("ordinary transport error should be wrapped")
}
}
func TestCrossPlatformCoverageMCPOutputModesAndDevdocFormatting(t *testing.T) {
caller := &helpersCoreCaller{format: "json", result: textToolResult(`{"url":"https://example.test/?a=1&b=2"}`)}
out, _ := installHelpersCoreDeps(t, caller)
if err := callMCPToolInternalOpts("server", "tool", map[string]any{"x": 1}, true); err != nil {
t.Fatalf("unescaped JSON output: %v", err)
}
if !strings.Contains(out.String(), "&") {
t.Fatalf("unescaped output = %q", out.String())
}
out.Reset()
caller.format = "raw"
caller.result = textToolResult("plain text")
if err := callMCPToolInternalOpts("server", "tool", nil, false); err != nil || !strings.Contains(out.String(), "plain text") {
t.Fatalf("raw output = %q, %v", out.String(), err)
}
out.Reset()
caller.format = "table"
caller.result = textToolResult(`{"Result":{"Items":[{"Title":"<em>Match</em>","URL":"https://example.test"}],"currentPage":1,"totalCount":2,"hasMore":true}}`)
if err := callMCPToolInternalOpts("server", "search_open_platform_docs", nil, false); err != nil {
t.Fatalf("devdoc table output: %v", err)
}
if !strings.Contains(out.String(), "Match") || strings.Contains(out.String(), "<em>") {
t.Fatalf("devdoc table = %q", out.String())
}
out.Reset()
if !formatDevdocSearchTable(`{"Result":{"Items":[]}}`) || !strings.Contains(out.String(), "no matching") {
t.Fatalf("empty devdoc table = %q", out.String())
}
if formatDevdocSearchTable("{") {
t.Fatal("invalid devdoc JSON should not format")
}
out.Reset()
caller.result = &edition.ToolResult{Content: []edition.ContentBlock{{Type: "image", Text: "image"}}}
if err := callMCPToolInternalOpts("server", "tool", nil, false); err != nil || out.Len() == 0 {
t.Fatalf("non-text result output = %q, %v", out.String(), err)
}
caller.dry = true
before := caller.calls
if err := callMCPToolInternalOpts("server", "tool", map[string]any{"x": 1}, false); err != nil || caller.calls != before {
t.Fatalf("dry run called tool: err=%v calls=%d/%d", err, caller.calls, before)
}
}
func TestCrossPlatformCoverageMCPDryRunJSONOutputIsSingleDocument(t *testing.T) {
caller := &helpersCoreCaller{format: "json", dry: true}
out, _ := installHelpersCoreDeps(t, caller)
args := map[string]any{"name": "weekly", "enabled": true}
if err := callMCPToolInternalOpts("server", "create_workflow", args, false); err != nil {
t.Fatalf("dry run returned error: %v", err)
}
if caller.calls != 0 {
t.Fatalf("dry run called tool %d times, want 0", caller.calls)
}
var payload map[string]any
if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
t.Fatalf("dry-run stdout must be one JSON document: %v\n%s", err, out.String())
}
if payload["dry_run"] != true || payload["executed"] != false || payload["tool"] != "create_workflow" {
t.Fatalf("unexpected dry-run payload: %#v", payload)
}
if !reflect.DeepEqual(payload["arguments"], map[string]any{"name": "weekly", "enabled": true}) {
t.Fatalf("arguments = %#v", payload["arguments"])
}
}
func TestCrossPlatformCoverageCurrentUserResponseShapes(t *testing.T) {
caller := &helpersCoreCaller{}
installHelpersCoreDeps(t, caller)
for _, response := range []string{
`{"result":[{"orgEmployeeModel":{"userId":"array-user"}}]}`,
`{"result":{"userId":"object-user"}}`,
} {
caller.result = textToolResult(response)
if got, err := getCurrentUserID(context.Background()); err != nil || got == "" {
t.Errorf("getCurrentUserID(%s) = %q, %v", response, got, err)
}
}
caller.result = &edition.ToolResult{Content: []edition.ContentBlock{{Type: "image"}, {Type: "text", Text: "{"}}}
if _, err := getCurrentUserID(context.Background()); err == nil {
t.Fatal("unparseable current user should fail")
}
caller.err = errors.New("offline")
if _, err := getCurrentUserID(context.Background()); err == nil {
t.Fatal("current-user transport failure should fail")
}
}
func TestCrossPlatformCoverageCoreClassificationSuggestionsAndConfirmation(t *testing.T) {
if classifyPATError(map[string]any{"errorCode": "PAT_LOW_RISK_NO_PERMISSION"}) == nil ||
classifyPATError(map[string]any{"code": "other"}) != nil {
t.Fatal("PAT classification mismatch")
}
pat := &PATError{RawJSON: "{}"}
if reclassifyPATFromError(pat) != pat || reclassifyPATFromError(errors.New("plain")) != nil {
t.Fatal("PAT reclassification mismatch")
}
if !strings.Contains(buildMinimalPATJSON("PAT_NO_PERMISSION"), "PAT_NO_PERMISSION") {
t.Fatal("minimal PAT JSON omitted code")
}
if isBusinessError(map[string]any{"success": true}) || isBusinessError(map[string]any{}) {
t.Fatal("successful response classified as business error")
}
for _, body := range []map[string]any{{"errorMsg": "one"}, {"message": "two"}, {"error": "three"}, {}} {
_ = suggestForBusinessError(body)
}
previousEdition := edition.Get()
t.Cleanup(func() { edition.Override(previousEdition) })
edition.Override(&edition.Hooks{IsEmbedded: true})
if notLoggedInSuggestion() != "请先登录" || !strings.Contains(authExpiredSuggestion(), "re-run") {
t.Fatal("embedded auth suggestions changed")
}
edition.Override(&edition.Hooks{})
if !strings.Contains(notLoggedInSuggestion(), "auth login") || !strings.Contains(authExpiredSuggestion(), "auth login") {
t.Fatal("standalone auth suggestions changed")
}
caller := &helpersCoreCaller{}
installHelpersCoreDeps(t, caller)
oldArgs, oldStdin := os.Args, os.Stdin
t.Cleanup(func() { os.Args, os.Stdin = oldArgs, oldStdin })
os.Args = []string{"dws", "--yes"}
if !confirmDelete("doc", "id") {
t.Fatal("--yes should confirm")
}
for _, tc := range []struct {
answer string
want bool
}{{"yes\n", true}, {"Y\n", true}, {"no\n", false}} {
path := filepath.Join(t.TempDir(), "answer")
if err := os.WriteFile(path, []byte(tc.answer), 0o600); err != nil {
t.Fatalf("write confirmation: %v", err)
}
file, err := os.Open(path)
if err != nil {
t.Fatalf("open confirmation: %v", err)
}
os.Stdin = file
os.Args = []string{"dws"}
if got := confirmDelete("doc", "id"); got != tc.want {
t.Errorf("confirmDelete(%q) = %v", tc.answer, got)
}
_ = file.Close()
}
}
func TestCrossPlatformCoverageCamelCaseAliasesAndFlagCopying(t *testing.T) {
for input, want := range map[string]string{"base-id": "baseId", "plain": "plain", "a--b": "aB"} {
if got := toCamelCase(input); got != want {
t.Errorf("toCamelCase(%q) = %q, want %q", input, got, want)
}
}
root := &cobra.Command{Use: "root"}
root.Flags().Int("int-value", 0, "")
root.Flags().Int64("long-value", 0, "")
root.Flags().Float64("float-value", 0, "")
root.Flags().Bool("bool-value", false, "")
root.Flags().StringSlice("slice-value", nil, "")
root.Flags().String("text-value", "", "")
root.Flags().String("textValue", "existing", "")
child := &cobra.Command{Use: "child"}
child.Flags().String("child-value", "", "")
root.AddCommand(child)
RegisterCamelCaseAliases(root)
for _, name := range []string{"intValue", "longValue", "floatValue", "boolValue", "sliceValue"} {
flag := root.Flags().Lookup(name)
if flag == nil || !flag.Hidden {
t.Errorf("camel alias --%s missing or visible", name)
}
}
if child.Flags().Lookup("childValue") == nil {
t.Fatal("child camel alias missing")
}
if flag := root.Flags().Lookup("textValue"); flag == nil || flag.Hidden || flag.DefValue != "existing" {
t.Fatal("existing camel-case flag should be preserved")
}
src, dst := &cobra.Command{Use: "src"}, &cobra.Command{Use: "dst"}
src.Flags().String("copied", "value", "")
copyFlags(src, dst, "missing", "copied")
if flag := dst.Flags().Lookup("copied"); flag == nil || flag.DefValue != "value" {
t.Fatal("copyFlags() did not copy the requested flag")
}
}