Files
dingtalk-workspace-cli/internal/helpers/sheet_dropdown_source_range_test.go

249 lines
10 KiB
Go

package helpers
import (
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate"
)
func TestCrossPlatformCoverageSetDropdownSourceRangeCommand(t *testing.T) {
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"success":true}`}}}
installScriptedCaller(t, caller)
err := executeDimensionCoverage(t, "set-dropdown",
"--node", "node-1",
"--sheet-id", "target-sheet",
"--range", "B2:B100",
"--source-sheet-id", "source-sheet",
"--source-range", "$t$1:$t$3",
"--multi-select",
)
if err != nil {
t.Fatalf("set SourceRange dropdown: %v", err)
}
if caller.tool != "set_dropdown_lists" {
t.Fatalf("tool = %q, want set_dropdown_lists", caller.tool)
}
if _, exists := caller.args["options"]; exists {
t.Fatalf("SourceRange request must omit options: %#v", caller.args)
}
source, ok := caller.args["sourceRange"].(map[string]any)
if !ok {
t.Fatalf("sourceRange = %#v", caller.args["sourceRange"])
}
if source["sheetId"] != "source-sheet" || source["a1Notation"] != "$t$1:$t$3" {
t.Fatalf("sourceRange = %#v", source)
}
if caller.args["enableMultiSelect"] != true {
t.Fatalf("enableMultiSelect = %#v", caller.args["enableMultiSelect"])
}
}
func TestCrossPlatformCoverageSetDropdownInlineCommandStillOmitsSourceRange(t *testing.T) {
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"success":true}`}}}
installScriptedCaller(t, caller)
err := executeDimensionCoverage(t, "set-dropdown",
"--node", "node-1",
"--sheet-id", "sheet-1",
"--range", "A1:A3",
"--options", `[{"value":"one","color":"#ff0000"}]`,
)
if err != nil {
t.Fatalf("set inline dropdown: %v", err)
}
if _, exists := caller.args["sourceRange"]; exists {
t.Fatalf("inline request must omit sourceRange: %#v", caller.args)
}
options, ok := caller.args["options"].([]map[string]any)
if !ok || len(options) != 1 || options[0]["value"] != "one" {
t.Fatalf("options = %#v", caller.args["options"])
}
}
func TestCrossPlatformCoverageSetDropdownModeConstraints(t *testing.T) {
base := []string{"--node", "node", "--sheet-id", "sheet", "--range", "A1"}
tests := []struct {
name string
args []string
want string
}{
{name: "neither", want: "one of the flags"},
{name: "both", args: []string{"--options", `[{"value":"one"}]`, "--source-sheet-id", "source", "--source-range", "A1:A3"}, want: "none of the others"},
{name: "source sheet without range", args: []string{"--options", `[{"value":"one"}]`, "--source-sheet-id", "source"}, want: "must all be set"},
{name: "range without source sheet", args: []string{"--source-range", "A1:A3"}, want: "must all be set"},
{name: "sheet prefix", args: []string{"--source-sheet-id", "source", "--source-range", "Sheet2!A1:A3"}, want: "不能包含工作表前缀"},
{name: "formula", args: []string{"--source-sheet-id", "source", "--source-range", "=A1:A3"}, want: "必须是单一连续区域"},
{name: "multi region", args: []string{"--source-sheet-id", "source", "--source-range", "A1:A3,C1:C3"}, want: "必须是单一连续区域"},
{name: "blank source range", args: []string{"--source-sheet-id", "source", "--source-range", " \t "}, want: "--source-range 不能为空"},
{name: "blank source sheet", args: []string{"--source-sheet-id", " \t ", "--source-range", "A1:A3"}, want: "--source-sheet-id 不能为空"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
caller := &scriptedToolCaller{}
installScriptedCaller(t, caller)
args := append(append([]string{}, base...), tc.args...)
err := executeDimensionCoverage(t, "set-dropdown", args...)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %v, want contains %q", err, tc.want)
}
if caller.calls != 0 {
t.Fatalf("calls = %d, validation must fail before request", caller.calls)
}
})
}
}
func TestCrossPlatformCoverageSetDropdownRunEValidation(t *testing.T) {
tests := []struct {
name string
options string
source string
rangeID string
want string
}{
{name: "neither mode", want: "必须且只能指定一个"},
{name: "source range without sheet", rangeID: "A1:A3", want: "必须同时指定 --source-sheet-id"},
{name: "source sheet without range", options: `[{"value":"one"}]`, source: "source", want: "必须同时指定 --source-range"},
{name: "invalid options JSON", options: "{", want: "JSON 解析失败"},
{name: "missing option value", options: `[{}]`, want: "value 为空"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cmd := dimensionCoverageCommand(t, "set-dropdown")
for name, value := range map[string]string{
"node": "node", "sheet-id": "sheet", "range": "A1",
"options": tc.options, "source-sheet-id": tc.source, "source-range": tc.rangeID,
} {
if err := cmd.Flags().Set(name, value); err != nil {
t.Fatalf("set --%s: %v", name, err)
}
}
err := cmd.RunE(cmd, nil)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %v, want contains %q", err, tc.want)
}
})
}
}
func TestCrossPlatformCoverageSetDropdownPreservesSchemaConstraints(t *testing.T) {
cmd := dimensionCoverageCommand(t, "set-dropdown")
if constraints := runtimeannotate.CommandConstraints(cmd); !runtimeannotate.ConstraintsEmpty(constraints) {
t.Fatalf("constraints = %#v, want no new schema-level constraints", constraints)
}
}
func TestCrossPlatformCoverageBatchSetDropdownSourceRange(t *testing.T) {
got, err := translateBatchOp(map[string]any{
"toolName": "set-dropdown",
"input": map[string]any{
"sheet-id": "target-sheet",
"range": "B2:B100",
"source-sheet-id": "source-sheet",
"source-range": "T:T",
"multi-select": true,
},
})
if err != nil {
t.Fatalf("translate SourceRange batch op: %v", err)
}
input := got["input"].(map[string]any)
if _, exists := input["options"]; exists {
t.Fatalf("SourceRange batch input must omit options: %#v", input)
}
source := input["sourceRange"].(map[string]any)
if source["sheetId"] != "source-sheet" || source["a1Notation"] != "T:T" {
t.Fatalf("sourceRange = %#v", source)
}
invalid := []map[string]any{
{},
{"options": []any{map[string]any{"value": "one"}}, "source-sheet-id": "source", "source-range": "A1:A3"},
{"source-range": "A1:A3"},
{"source-sheet-id": "source", "source-range": "A1:A3", "colors": []any{"#fff"}},
{"source-sheet-id": "source", "source-range": "A1:A3", "source-colors": []any{"#fff"}},
{"source-sheet-id": "source", "source-range": "Sheet2!A1:A3"},
}
for _, value := range invalid {
if _, err := translateBatchOp(map[string]any{"toolName": "set-dropdown", "input": value}); err == nil {
t.Errorf("invalid batch SourceRange input %#v returned nil", value)
}
}
}
func TestCrossPlatformCoverageBatchSetDropdownValidationGuidance(t *testing.T) {
tests := []struct {
name string
input map[string]any
want string
}{
{
name: "inline top-level colors",
input: map[string]any{"options": []any{map[string]any{"value": "one"}}, "colors": []any{"#fff"}},
want: "Inline 颜色请写入 options[].color",
},
{
name: "source top-level colors",
input: map[string]any{"source-sheet-id": "source", "source-range": "A1:A3", "source-colors": []any{"#fff"}},
want: "SourceRange 颜色写入暂不支持",
},
{
name: "blank source range",
input: map[string]any{"source-sheet-id": "source", "source-range": " \t "},
want: "--source-range 不能为空",
},
{
name: "blank source sheet",
input: map[string]any{"source-sheet-id": " \t ", "source-range": "A1:A3"},
want: "--source-sheet-id 不能为空",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := translateBatchOp(map[string]any{"toolName": "set-dropdown", "input": tc.input})
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %v, want contains %q", err, tc.want)
}
})
}
}
func TestCrossPlatformCoverageDropdownHelpDescribesDynamicAndInvalidContracts(t *testing.T) {
setDropdown := dimensionCoverageCommand(t, "set-dropdown")
if !strings.Contains(setDropdown.Long, "会自动调整引用并保持 valid") ||
!strings.Contains(setDropdown.Long, "只有 invalid 时才重新选择来源并写入") {
t.Fatalf("set-dropdown help does not describe verified structural behavior:\n%s", setDropdown.Long)
}
getDropdown := dimensionCoverageCommand(t, "get-dropdown")
if !strings.Contains(getDropdown.Long, `仅在 sourceRangeStatus="valid" 时返回`) ||
!strings.Contains(getDropdown.Long, "invalid 结果仍保留配置组,但省略 sourceRange") {
t.Fatalf("get-dropdown help does not describe conditional sourceRange readback:\n%s", getDropdown.Long)
}
}
func TestCrossPlatformCoverageValidateSourceRangeDataValidation(t *testing.T) {
valid := []any{
map[string]any{"type": "dropdown", "sourceRange": map[string]any{"sheetId": "source", "a1Notation": "T1:T3"}},
map[string]any{"type": "dropdown", "sourceRange": map[string]any{"sheetId": "missing-sheet", "a1Notation": "not-validated-locally"}, "enableMultiSelect": true},
}
for _, value := range valid {
if err := validateDataValidation(value, "dv"); err != nil {
t.Errorf("valid SourceRange data validation %#v: %v", value, err)
}
}
invalid := []any{
map[string]any{"type": "dropdown", "options": []any{map[string]any{"value": "one"}}, "sourceRange": map[string]any{"sheetId": "source", "a1Notation": "T1:T3"}},
map[string]any{"type": "dropdown", "sourceRange": "T1:T3"},
map[string]any{"type": "dropdown", "sourceRange": map[string]any{"a1Notation": "T1:T3"}},
map[string]any{"type": "dropdown", "sourceRange": map[string]any{"sheetId": "source"}},
map[string]any{"type": "dropdown", "sourceRange": map[string]any{"sheetId": "source", "a1Notation": "T1:T3", "colors": []any{"#fff"}}},
map[string]any{"type": "dropdown", "sourceRange": map[string]any{"sheetId": "source", "a1Notation": "T1:T3"}, "enableMultiSelect": "yes"},
}
for _, value := range invalid {
if err := validateDataValidation(value, "dv"); err == nil {
t.Errorf("invalid SourceRange data validation %#v returned nil", value)
}
}
}