Files
dingtalk-workspace-cli/internal/helpers/sheet_pivot_gridline_test.go
玉澜 125f0c8fe0 Merge remote-tracking branch 'upstream/main' into feat/schema-on-main
# Conflicts:
#	test/scripts/package_script_test.go
2026-07-13 21:09:27 +08:00

277 lines
9.9 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 (
"context"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
type sheetPivotGridlineCall struct {
productID string
toolName string
args map[string]any
}
type sheetPivotGridlineCaller struct {
calls []sheetPivotGridlineCall
}
func (c *sheetPivotGridlineCaller) CallTool(_ context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
c.calls = append(c.calls, sheetPivotGridlineCall{productID: productID, toolName: toolName, args: args})
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil
}
func (*sheetPivotGridlineCaller) Format() string { return "json" }
func (*sheetPivotGridlineCaller) DryRun() bool { return false }
func (*sheetPivotGridlineCaller) Fields() string { return "" }
func (*sheetPivotGridlineCaller) JQ() string { return "" }
func executeSheetPivotGridlineCommand(t *testing.T, caller *sheetPivotGridlineCaller, args ...string) error {
t.Helper()
previousDeps := deps
previousArgs := os.Args
t.Cleanup(func() {
deps = previousDeps
os.Args = previousArgs
})
InitDeps(caller)
deps.Out.w = io.Discard
os.Args = append([]string{"dws", "sheet"}, args...)
root := newSheetCommand()
root.PersistentFlags().Bool("yes", false, "skip confirmation")
root.SilenceErrors = true
root.SilenceUsage = true
root.SetArgs(args)
return root.Execute()
}
func TestSheetPivotAndGridlineCommandsRegistered(t *testing.T) {
root := newSheetCommand()
cases := []struct {
path []string
flags []string
}{
{[]string{"pivot-table", "list"}, []string{"node", "sheet-id", "pivot-table-id"}},
{[]string{"pivot-table", "create"}, []string{"node", "source", "properties", "target-sheet-id", "target-position"}},
{[]string{"pivot-table", "update"}, []string{"node", "sheet-id", "pivot-table-id", "properties"}},
{[]string{"pivot-table", "delete"}, []string{"node", "sheet-id", "pivot-table-id"}},
{[]string{"show-gridline"}, []string{"node", "sheet-id"}},
{[]string{"hide-gridline"}, []string{"node", "sheet-id"}},
}
for _, tc := range cases {
cmd, remaining, err := root.Find(tc.path)
if err != nil || len(remaining) != 0 {
t.Fatalf("dws sheet %s not registered: cmd=%v remaining=%v err=%v", strings.Join(tc.path, " "), cmd, remaining, err)
}
for _, flag := range tc.flags {
if cmd.Flags().Lookup(flag) == nil {
t.Errorf("dws sheet %s: missing flag --%s", strings.Join(tc.path, " "), flag)
}
}
}
}
func TestSheetPivotListBuildsToolArgs(t *testing.T) {
caller := &sheetPivotGridlineCaller{}
err := executeSheetPivotGridlineCommand(t, caller,
"pivot-table", "list", "--node", "node1", "--sheet-id", "sheet1", "--pivot-table-id", "pivot1")
if err != nil {
t.Fatalf("pivot-table list returned error: %v", err)
}
want := sheetPivotGridlineCall{
productID: "sheet",
toolName: "list_pivot_tables",
args: map[string]any{
"nodeId": "node1",
"sheetId": "sheet1",
"pivotTableId": "pivot1",
},
}
if len(caller.calls) != 1 || !reflect.DeepEqual(caller.calls[0], want) {
t.Fatalf("calls = %#v, want %#v", caller.calls, want)
}
}
func TestSheetPivotCreateBuildsToolArgs(t *testing.T) {
caller := &sheetPivotGridlineCaller{}
err := executeSheetPivotGridlineCommand(t, caller,
"pivot-table", "create",
"--node", "node1",
"--source", "'Data'!A1:D20",
"--properties", `{"rows":[{"field":"team"}],"values":[{"field":"amount","summarize_by":" SUM "}]}`,
"--target-sheet-id", "summary",
"--target-position", "B2")
if err != nil {
t.Fatalf("pivot-table create returned error: %v", err)
}
want := sheetPivotGridlineCall{
productID: "sheet",
toolName: "create_pivot_table",
args: map[string]any{
"nodeId": "node1",
"source": "'Data'!A1:D20",
"targetSheetId": "summary",
"targetPosition": "B2",
"properties": map[string]any{
"rows": []any{map[string]any{"field": "team"}},
"values": []any{map[string]any{"field": "amount", "summarize_by": "sum"}},
},
},
}
if len(caller.calls) != 1 || !reflect.DeepEqual(caller.calls[0], want) {
t.Fatalf("calls = %#v, want %#v", caller.calls, want)
}
}
func TestSheetPivotCreateReadsPropertiesFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "pivot.json")
if err := os.WriteFile(path, []byte(`{"values":[{"field":"amount","summarize_by":"average"}]}`), 0o600); err != nil {
t.Fatal(err)
}
caller := &sheetPivotGridlineCaller{}
if err := executeSheetPivotGridlineCommand(t, caller,
"pivot-table", "create", "--node", "node1", "--source", "'Data'!A1:B10", "--properties", "@"+path); err != nil {
t.Fatalf("pivot-table create with @file returned error: %v", err)
}
if len(caller.calls) != 1 || caller.calls[0].toolName != "create_pivot_table" {
t.Fatalf("calls = %#v, want create_pivot_table", caller.calls)
}
}
func TestSheetPivotUpdateAllowsPartialProperties(t *testing.T) {
caller := &sheetPivotGridlineCaller{}
err := executeSheetPivotGridlineCommand(t, caller,
"pivot-table", "update", "--node", "node1", "--sheet-id", "sheet1", "--pivot-table-id", "pivot1",
"--properties", `{"show_subtotals":false}`)
if err != nil {
t.Fatalf("pivot-table update returned error: %v", err)
}
wantArgs := map[string]any{
"nodeId": "node1",
"sheetId": "sheet1",
"pivotTableId": "pivot1",
"properties": map[string]any{"show_subtotals": false},
}
if len(caller.calls) != 1 || caller.calls[0].toolName != "update_pivot_table" || !reflect.DeepEqual(caller.calls[0].args, wantArgs) {
t.Fatalf("calls = %#v, want update_pivot_table %#v", caller.calls, wantArgs)
}
}
func TestSheetPivotDeleteBuildsToolArgsAfterConfirmation(t *testing.T) {
caller := &sheetPivotGridlineCaller{}
if err := executeSheetPivotGridlineCommand(t, caller,
"pivot-table", "delete", "--node", "node1", "--sheet-id", "sheet1", "--pivot-table-id", "pivot1", "--yes"); err != nil {
t.Fatalf("pivot-table delete returned error: %v", err)
}
wantArgs := map[string]any{"nodeId": "node1", "sheetId": "sheet1", "pivotTableId": "pivot1"}
if len(caller.calls) != 1 || caller.calls[0].toolName != "delete_pivot_table" || !reflect.DeepEqual(caller.calls[0].args, wantArgs) {
t.Fatalf("calls = %#v, want delete_pivot_table %#v", caller.calls, wantArgs)
}
}
func TestSheetPivotRejectsInvalidPropertiesBeforeCall(t *testing.T) {
tests := []struct {
name string
command string
props string
wantErr string
}{
{"create invalid JSON", "create", `{`, "JSON 解析失败"},
{"create missing values", "create", `{"rows":[{"field":"team"}]}`, "缺少必填字段 values"},
{"create empty values", "create", `{"values":[]}`, "至少包含一项"},
{"create invalid summarize", "create", `{"values":[{"field":"amount","summarize_by":"total"}]}`, "summarize_by 不支持"},
{"create invalid row", "create", `{"rows":[{}],"values":[{"field":"amount"}]}`, "rows[0].field"},
{"create invalid collapse", "create", `{"values":[{"field":"amount"}],"collapse":"team"}`, "collapse 必须"},
{"update empty properties", "update", `{}`, "不能为空对象"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
caller := &sheetPivotGridlineCaller{}
args := []string{"pivot-table", tt.command, "--node", "node1", "--properties", tt.props}
if tt.command == "create" {
args = append(args, "--source", "'Data'!A1:B10")
} else {
args = append(args, "--sheet-id", "sheet1", "--pivot-table-id", "pivot1")
}
err := executeSheetPivotGridlineCommand(t, caller, args...)
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("error = %v, want containing %q", err, tt.wantErr)
}
if len(caller.calls) != 0 {
t.Fatalf("remote calls = %d, want 0", len(caller.calls))
}
})
}
}
func TestSheetPivotAndGridlineRequireMandatoryFlags(t *testing.T) {
for _, args := range [][]string{
{"pivot-table", "list", "--node", "node1"},
{"pivot-table", "create", "--node", "node1", "--source", "'Data'!A1:B10"},
{"pivot-table", "update", "--node", "node1", "--sheet-id", "sheet1", "--pivot-table-id", "pivot1"},
{"pivot-table", "delete", "--node", "node1", "--sheet-id", "sheet1"},
{"show-gridline", "--node", "node1"},
{"hide-gridline", "--sheet-id", "sheet1"},
} {
caller := &sheetPivotGridlineCaller{}
if err := executeSheetPivotGridlineCommand(t, caller, args...); err == nil {
t.Fatalf("dws sheet %s returned nil error", strings.Join(args, " "))
}
if len(caller.calls) != 0 {
t.Fatalf("dws sheet %s made %d remote calls, want 0", strings.Join(args, " "), len(caller.calls))
}
}
}
func TestSheetGridlineCommandsBuildToolArgs(t *testing.T) {
for _, tt := range []struct {
command string
nodeFlag string
visibility string
}{
{"show-gridline", "node-id", "visible"},
{"hide-gridline", "file-id", "hidden"},
} {
t.Run(tt.command, func(t *testing.T) {
caller := &sheetPivotGridlineCaller{}
if err := executeSheetPivotGridlineCommand(t, caller, tt.command, "--"+tt.nodeFlag, "node1", "--sheet-id", "sheet1"); err != nil {
t.Fatalf("%s returned error: %v", tt.command, err)
}
want := sheetPivotGridlineCall{
productID: "sheet",
toolName: "set_gridline_visibility",
args: map[string]any{
"nodeId": "node1",
"sheetId": "sheet1",
"visibility": tt.visibility,
},
}
if len(caller.calls) != 1 || !reflect.DeepEqual(caller.calls[0], want) {
t.Fatalf("calls = %#v, want %#v", caller.calls, want)
}
})
}
}