Files
dingtalk-workspace-cli/internal/shortcut/aitable/datasource_coverage_test.go
陌渊 c7510cd1a1 fix(datasource): trim whitespace from batch IDs and fix result/processCode docs
- Add trimNonEmpty for --table-ids in +datasource-sync and --task-ids in
  +datasource-sync-status, matching the existing field-ids pattern
- Add 4 test cases: whitespace-only rejection and trim-through for both
- Fix usage guide: typical workflow and notes no longer equate result
  with processCode; correctly describe result as JSON to parse for
  approvals[].processCode/name/iconUrl/url
2026-08-21 16:19:43 +08:00

368 lines
16 KiB
Go

// Copyright 2026 Alibaba Group
// SPDX-License-Identifier: Apache-2.0
package aitable
import (
"context"
"errors"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
type datasourceCoverageCaller struct {
err error
resp string
argLog []map[string]any
}
func (c *datasourceCoverageCaller) CallTool(_ context.Context, _, _ string, args map[string]any) (*edition.ToolResult, error) {
c.argLog = append(c.argLog, args)
if c.err != nil {
return nil, c.err
}
text := c.resp
if text == "" {
text = `{"status":"success","data":{}}`
}
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: text}}}, nil
}
func (c *datasourceCoverageCaller) Format() string { return "json" }
func (c *datasourceCoverageCaller) DryRun() bool { return false }
func (c *datasourceCoverageCaller) Fields() string { return "" }
func (c *datasourceCoverageCaller) JQ() string { return "" }
func runDatasourceShortcutCLI(t *testing.T, caller *datasourceCoverageCaller, args ...string) error {
t.Helper()
helpers.InitDepsForTest(t, caller)
root := newPlatformCoverageRoot()
root.SetArgs(append([]string{"aitable"}, args...))
return root.Execute()
}
// ── DatasourceCreate error paths ─────────────────────────────────────────────
func TestCrossPlatformCoverageDatasourceCreateRejectsInvalidSourceConfig(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{},
"+datasource-create", "--base-id", "BASE1", "--datasource-type", "OA",
"--source-config", "not-json")
if err == nil || !strings.Contains(err.Error(), "source-config") {
t.Fatalf("error = %v, want source-config parse error", err)
}
}
func TestCrossPlatformCoverageDatasourceCreateRejectsInvalidAutoSyncSetting(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{},
"+datasource-create", "--base-id", "BASE1", "--datasource-type", "OA",
"--source-config", `{"processCode":"P","name":"N","dataType":"recent_time","iconUrl":"u","url":"v"}`,
"--auto-sync-setting", "not-json")
if err == nil || !strings.Contains(err.Error(), "auto-sync-setting") {
t.Fatalf("error = %v, want auto-sync-setting parse error", err)
}
}
func TestCrossPlatformCoverageDatasourceCreatePropagatesCallMCPError(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{err: errors.New("mcp-error")},
"+datasource-create", "--base-id", "BASE1", "--datasource-type", "OA",
"--source-config", `{"processCode":"P","name":"N","dataType":"recent_time","iconUrl":"u","url":"v"}`)
if err == nil || !strings.Contains(err.Error(), "mcp-error") {
t.Fatalf("error = %v, want mcp-error", err)
}
}
func TestCrossPlatformCoverageDatasourceCreateRejectsEmptyFieldIDs(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-create", "--base-id", "BASE1", "--datasource-type", "OA",
"--source-config", `{"processCode":"P","name":"N","dataType":"recent_time","iconUrl":"u","url":"v"}`,
"--field-ids", "")
if err == nil || !strings.Contains(err.Error(), "field-ids") {
t.Fatalf("error = %v, want field-ids empty error", err)
}
if len(caller.argLog) != 0 {
t.Fatalf("MCP should not be called when empty field-ids is rejected, got %d calls", len(caller.argLog))
}
}
func TestCrossPlatformCoverageDatasourceCreateRejectsWhitespaceOnlyFieldIDs(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-create", "--base-id", "BASE1", "--datasource-type", "OA",
"--source-config", `{"processCode":"P","name":"N","dataType":"recent_time","iconUrl":"u","url":"v"}`,
"--field-ids", " , , ")
if err == nil || !strings.Contains(err.Error(), "field-ids") {
t.Fatalf("error = %v, want field-ids empty error", err)
}
if len(caller.argLog) != 0 {
t.Fatalf("MCP should not be called when whitespace-only field-ids is rejected, got %d calls", len(caller.argLog))
}
}
func TestCrossPlatformCoverageDatasourceCreateRejectsEmptyAutoSyncSetting(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-create", "--base-id", "BASE1", "--datasource-type", "OA",
"--source-config", `{"processCode":"P","name":"N","dataType":"recent_time","iconUrl":"u","url":"v"}`,
"--auto-sync-setting", "")
if err == nil || !strings.Contains(err.Error(), "auto-sync-setting") {
t.Fatalf("error = %v, want auto-sync-setting empty error", err)
}
if len(caller.argLog) != 0 {
t.Fatalf("MCP should not be called when empty auto-sync-setting is rejected, got %d calls", len(caller.argLog))
}
}
func TestCrossPlatformCoverageDatasourceCreatePassesFieldIDsThrough(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-create", "--base-id", "BASE1", "--datasource-type", "OA",
"--source-config", `{"processCode":"P","name":"N","dataType":"recent_time","iconUrl":"u","url":"v"}`,
"--field-ids", "fld1,fld2")
if err != nil {
t.Fatalf("create with valid --field-ids should succeed: %v", err)
}
if len(caller.argLog) != 1 {
t.Fatalf("expected exactly 1 MCP call, got %d", len(caller.argLog))
}
got, _ := caller.argLog[0]["fieldIds"].([]string)
if len(got) != 2 || got[0] != "fld1" || got[1] != "fld2" {
t.Fatalf("fieldIds = %#v, want [fld1 fld2]", got)
}
}
// ── DatasourceUpdate error paths ─────────────────────────────────────────────
func TestCrossPlatformCoverageDatasourceUpdateRejectsInvalidSourceConfig(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{},
"+datasource-update", "--base-id", "BASE1", "--table-id", "TBL1",
"--source-config", "not-json")
if err == nil || !strings.Contains(err.Error(), "source-config") {
t.Fatalf("error = %v, want source-config parse error", err)
}
}
func TestCrossPlatformCoverageDatasourceUpdateRejectsInvalidAutoSyncSetting(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{},
"+datasource-update", "--base-id", "BASE1", "--table-id", "TBL1",
"--auto-sync-setting", "not-json")
if err == nil || !strings.Contains(err.Error(), "auto-sync-setting") {
t.Fatalf("error = %v, want auto-sync-setting parse error", err)
}
}
func TestCrossPlatformCoverageDatasourceUpdateRejectsNoChanges(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{},
"+datasource-update", "--base-id", "BASE1", "--table-id", "TBL1")
if err == nil || !strings.Contains(err.Error(), "至少需要一个配置变更") {
t.Fatalf("error = %v, want no-changes error", err)
}
}
func TestCrossPlatformCoverageDatasourceUpdatePropagatesCallMCPError(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{err: errors.New("mcp-error")},
"+datasource-update", "--base-id", "BASE1", "--table-id", "TBL1", "--auto")
if err == nil || !strings.Contains(err.Error(), "mcp-error") {
t.Fatalf("error = %v, want mcp-error", err)
}
}
func TestCrossPlatformCoverageDatasourceUpdateRejectsEmptyFieldIDs(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-update", "--base-id", "BASE1", "--table-id", "TBL1",
"--field-ids", "")
if err == nil || !strings.Contains(err.Error(), "field-ids") {
t.Fatalf("error = %v, want field-ids empty error", err)
}
if len(caller.argLog) != 0 {
t.Fatalf("MCP should not be called when empty field-ids is rejected, got %d calls", len(caller.argLog))
}
}
func TestCrossPlatformCoverageDatasourceUpdateRejectsWhitespaceOnlyFieldIDs(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-update", "--base-id", "BASE1", "--table-id", "TBL1",
"--field-ids", " , ")
if err == nil || !strings.Contains(err.Error(), "field-ids") {
t.Fatalf("error = %v, want field-ids empty error", err)
}
if len(caller.argLog) != 0 {
t.Fatalf("MCP should not be called when whitespace-only field-ids is rejected, got %d calls", len(caller.argLog))
}
}
func TestCrossPlatformCoverageDatasourceUpdateRejectsEmptyAutoSyncSetting(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-update", "--base-id", "BASE1", "--table-id", "TBL1",
"--auto-sync-setting", "")
if err == nil || !strings.Contains(err.Error(), "auto-sync-setting") {
t.Fatalf("error = %v, want auto-sync-setting empty error", err)
}
if len(caller.argLog) != 0 {
t.Fatalf("MCP should not be called when empty auto-sync-setting is rejected, got %d calls", len(caller.argLog))
}
}
func TestCrossPlatformCoverageDatasourceUpdatePassesFieldIDsThrough(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-update", "--base-id", "BASE1", "--table-id", "TBL1",
"--field-ids", "fldA,fldB,fldC")
if err != nil {
t.Fatalf("update with valid --field-ids should succeed: %v", err)
}
if len(caller.argLog) != 1 {
t.Fatalf("expected exactly 1 MCP call, got %d", len(caller.argLog))
}
got, _ := caller.argLog[0]["fieldIds"].([]string)
if len(got) != 3 || got[0] != "fldA" || got[1] != "fldB" || got[2] != "fldC" {
t.Fatalf("fieldIds = %#v, want [fldA fldB fldC]", got)
}
}
// ── DatasourceSync error paths ────────────────────────────────────────────────
func TestCrossPlatformCoverageDatasourceSyncShortcutRejectsTooManyTableIDs(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{},
"+datasource-sync", "--base-id", "BASE1", "--table-ids", "T1,T2,T3,T4,T5,T6")
if err == nil || !strings.Contains(err.Error(), "1-5") {
t.Fatalf("error = %v, want 1-5 limit", err)
}
}
func TestCrossPlatformCoverageDatasourceSyncShortcutPropagatesCallMCPError(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{err: errors.New("mcp-error")},
"+datasource-sync", "--base-id", "BASE1", "--table-ids", "TBL1")
if err == nil || !strings.Contains(err.Error(), "mcp-error") {
t.Fatalf("error = %v, want mcp-error", err)
}
}
// ── DatasourceSyncStatus error paths ─────────────────────────────────────────
func TestCrossPlatformCoverageDatasourceSyncStatusShortcutRejectsTooManyTaskIDs(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{},
"+datasource-sync-status", "--base-id", "BASE1", "--table-id", "TBL1",
"--task-ids", "T1,T2,T3,T4,T5,T6")
if err == nil || !strings.Contains(err.Error(), "1-5") {
t.Fatalf("error = %v, want 1-5 limit", err)
}
}
func TestCrossPlatformCoverageDatasourceSyncStatusShortcutPropagatesCallMCPError(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{err: errors.New("mcp-error")},
"+datasource-sync-status", "--base-id", "BASE1", "--table-id", "TBL1",
"--task-ids", "TASK1")
if err == nil || !strings.Contains(err.Error(), "mcp-error") {
t.Fatalf("error = %v, want mcp-error", err)
}
}
// ── DatasourceSync whitespace trimming ──────────────────────────────────────
func TestCrossPlatformCoverageDatasourceSyncRejectsWhitespaceOnlyTableIDs(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-sync", "--base-id", "BASE1", "--table-ids", " , ")
if err == nil || (!strings.Contains(err.Error(), "table-ids") && !strings.Contains(err.Error(), "1-5")) {
t.Fatalf("error = %v, want table-ids rejection", err)
}
if len(caller.argLog) != 0 {
t.Fatalf("MCP should not be called when whitespace-only table-ids is rejected, got %d calls", len(caller.argLog))
}
}
func TestCrossPlatformCoverageDatasourceSyncTrimsWhitespaceFromTableIDs(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-sync", "--base-id", "BASE1", "--table-ids", " TBL1 , TBL2 ")
if err != nil {
t.Fatalf("sync with whitespace-padded --table-ids should succeed: %v", err)
}
if len(caller.argLog) != 1 {
t.Fatalf("expected exactly 1 MCP call, got %d", len(caller.argLog))
}
got, _ := caller.argLog[0]["tableIds"].([]string)
if len(got) != 2 || got[0] != "TBL1" || got[1] != "TBL2" {
t.Fatalf("tableIds = %#v, want [TBL1 TBL2]", got)
}
}
// ── DatasourceSyncStatus whitespace trimming ───────────────────────────────
func TestCrossPlatformCoverageDatasourceSyncStatusRejectsWhitespaceOnlyTaskIDs(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-sync-status", "--base-id", "BASE1", "--table-id", "TBL1",
"--task-ids", " , ")
if err == nil || (!strings.Contains(err.Error(), "task-ids") && !strings.Contains(err.Error(), "1-5")) {
t.Fatalf("error = %v, want task-ids rejection", err)
}
if len(caller.argLog) != 0 {
t.Fatalf("MCP should not be called when whitespace-only task-ids is rejected, got %d calls", len(caller.argLog))
}
}
func TestCrossPlatformCoverageDatasourceSyncStatusTrimsWhitespaceFromTaskIDs(t *testing.T) {
caller := &datasourceCoverageCaller{}
err := runDatasourceShortcutCLI(t, caller,
"+datasource-sync-status", "--base-id", "BASE1", "--table-id", "TBL1",
"--task-ids", " TASK1 , TASK2 ")
if err != nil {
t.Fatalf("sync-status with whitespace-padded --task-ids should succeed: %v", err)
}
if len(caller.argLog) != 1 {
t.Fatalf("expected exactly 1 MCP call, got %d", len(caller.argLog))
}
got, _ := caller.argLog[0]["taskIds"].([]string)
if len(got) != 2 || got[0] != "TASK1" || got[1] != "TASK2" {
t.Fatalf("taskIds = %#v, want [TASK1 TASK2]", got)
}
}
// ── DatasourceGetConfig error path ───────────────────────────────────────────
func TestCrossPlatformCoverageDatasourceGetConfigShortcutPropagatesCallMCPError(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{err: errors.New("mcp-error")},
"+datasource-get-config", "--base-id", "BASE1", "--table-id", "TBL1")
if err == nil || !strings.Contains(err.Error(), "mcp-error") {
t.Fatalf("error = %v, want mcp-error", err)
}
}
// ── DatasourceListSources error path ─────────────────────────────────────────
func TestCrossPlatformCoverageDatasourceListSourcesShortcutPropagatesCallMCPError(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{err: errors.New("mcp-error")},
"+datasource-list-sources", "--base-id", "BASE1", "--datasource-type", "OA")
if err == nil || !strings.Contains(err.Error(), "mcp-error") {
t.Fatalf("error = %v, want mcp-error", err)
}
}
// ── DatasourceGetFields error paths ──────────────────────────────────────────
func TestCrossPlatformCoverageDatasourceGetFieldsShortcutRejectsInvalidSourceConfig(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{},
"+datasource-get-fields", "--base-id", "BASE1", "--datasource-type", "OA",
"--source-config", "not-json")
if err == nil || !strings.Contains(err.Error(), "source-config") {
t.Fatalf("error = %v, want source-config parse error", err)
}
}
func TestCrossPlatformCoverageDatasourceGetFieldsShortcutPropagatesCallMCPError(t *testing.T) {
err := runDatasourceShortcutCLI(t, &datasourceCoverageCaller{err: errors.New("mcp-error")},
"+datasource-get-fields", "--base-id", "BASE1", "--datasource-type", "OA",
"--source-config", `{"processCode":"P","name":"N","dataType":"recent_time","iconUrl":"u","url":"v"}`)
if err == nil || !strings.Contains(err.Error(), "mcp-error") {
t.Fatalf("error = %v, want mcp-error", err)
}
}