Files
dingtalk-workspace-cli/internal/helpers/sheet_table_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

263 lines
8.7 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 sheetTableCall struct {
productID string
toolName string
args map[string]any
}
type sheetTableCaller struct {
calls []sheetTableCall
dryRun bool
}
func (c *sheetTableCaller) CallTool(_ context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
c.calls = append(c.calls, sheetTableCall{productID: productID, toolName: toolName, args: args})
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil
}
func (*sheetTableCaller) Format() string { return "json" }
func (c *sheetTableCaller) DryRun() bool { return c.dryRun }
func (*sheetTableCaller) Fields() string { return "" }
func (*sheetTableCaller) JQ() string { return "" }
func executeSheetTableCommand(t *testing.T, caller *sheetTableCaller, stdin io.Reader, 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.SilenceErrors = true
root.SilenceUsage = true
if stdin != nil {
root.SetIn(stdin)
}
root.SetArgs(args)
return root.Execute()
}
func TestSheetTableCommandsRegistered(t *testing.T) {
root := newSheetCommand()
for _, path := range []string{"table-get", "table-put", "table-read", "table-write"} {
cmd, remaining, err := root.Find([]string{path})
if err != nil || len(remaining) != 0 {
t.Fatalf("dws sheet %s not registered: cmd=%v remaining=%v err=%v", path, cmd, remaining, err)
}
}
if cmd, remaining, _ := root.Find([]string{"table"}); cmd != nil && len(remaining) == 0 {
t.Fatal("dws sheet table parent should not be registered")
}
getCmd, _, _ := root.Find([]string{"table-get"})
for _, flag := range []string{"node", "sheet-id", "range", "no-header"} {
if getCmd.Flags().Lookup(flag) == nil {
t.Errorf("table-get: missing flag --%s", flag)
}
}
putCmd, _, _ := root.Find([]string{"table-put"})
for _, flag := range []string{"node", "sheets"} {
if putCmd.Flags().Lookup(flag) == nil {
t.Errorf("table-put: missing flag --%s", flag)
}
}
}
func TestSheetTableGetBuildsToolArgs(t *testing.T) {
caller := &sheetTableCaller{}
err := executeSheetTableCommand(t, caller, nil,
"table-get", "--node", "node1", "--sheet-id", "sheet1", "--range", "A1:B2", "--no-header")
if err != nil {
t.Fatalf("table-get returned error: %v", err)
}
want := sheetTableCall{
productID: "sheet",
toolName: "table_get",
args: map[string]any{
"nodeId": "node1",
"sheetId": "sheet1",
"range": "A1:B2",
"noHeader": true,
},
}
if len(caller.calls) != 1 || !reflect.DeepEqual(caller.calls[0], want) {
t.Fatalf("calls = %#v, want %#v", caller.calls, want)
}
}
func TestSheetTableCommandsConsumeNodeAliases(t *testing.T) {
tests := []struct {
name string
args []string
}{
{name: "get", args: []string{"table-get", "--file-id", "node1"}},
{name: "put", args: []string{"table-put", "--doc-id", "node1", "--sheets", `[{"name":"Sheet1","columns":["name"],"data":[["Alice"]]}]`}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
caller := &sheetTableCaller{}
if err := executeSheetTableCommand(t, caller, nil, tt.args...); err != nil {
t.Fatalf("%s returned error: %v", strings.Join(tt.args, " "), err)
}
if len(caller.calls) != 1 || caller.calls[0].args["nodeId"] != "node1" {
t.Fatalf("calls = %#v, want nodeId=node1", caller.calls)
}
})
}
}
func TestSheetTableGetOmitsUnsetOptionalArgs(t *testing.T) {
caller := &sheetTableCaller{}
if err := executeSheetTableCommand(t, caller, nil, "table-get", "--node", "node1"); err != nil {
t.Fatalf("table-get returned error: %v", err)
}
want := map[string]any{"nodeId": "node1"}
if len(caller.calls) != 1 || !reflect.DeepEqual(caller.calls[0].args, want) {
t.Fatalf("calls = %#v, want args %#v", caller.calls, want)
}
}
func TestSheetTableGetPreservesExplicitFalseNoHeader(t *testing.T) {
caller := &sheetTableCaller{}
if err := executeSheetTableCommand(t, caller, nil, "table-get", "--node", "node1", "--no-header=false"); err != nil {
t.Fatalf("table-get returned error: %v", err)
}
if got, ok := caller.calls[0].args["noHeader"]; !ok || got != false {
t.Fatalf("noHeader = %#v, present=%v; want explicit false", got, ok)
}
}
func TestSheetTableGetRequiresNodeBeforeCall(t *testing.T) {
caller := &sheetTableCaller{}
if err := executeSheetTableCommand(t, caller, nil, "table-get"); err == nil {
t.Fatal("table-get without --node returned nil error")
}
if len(caller.calls) != 0 {
t.Fatalf("remote calls = %d, want 0", len(caller.calls))
}
}
func TestSheetTablePutNormalizesAcceptedInputs(t *testing.T) {
tests := []struct {
name string
input string
}{
{name: "array", input: `[ {"name":"Sheet1","columns":["name"],"data":[["Alice"]]} ]`},
{name: "wrapped", input: `{"sheets":[{"name":"Sheet1","columns":["name"],"data":[["Alice"]]}]}`},
{name: "single", input: `{"name":"Sheet1","columns":["name"],"data":[["Alice"]]}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
caller := &sheetTableCaller{}
if err := executeSheetTableCommand(t, caller, nil, "table-put", "--node", "node1", "--sheets", tt.input); err != nil {
t.Fatalf("table-put returned error: %v", err)
}
if len(caller.calls) != 1 {
t.Fatalf("remote calls = %d, want 1", len(caller.calls))
}
call := caller.calls[0]
if call.productID != "sheet" || call.toolName != "table_put" || call.args["nodeId"] != "node1" {
t.Fatalf("call = %#v", call)
}
sheets, ok := call.args["sheets"].([]any)
if !ok || len(sheets) != 1 {
t.Fatalf("sheets = %#v, want one-element array", call.args["sheets"])
}
})
}
}
func TestSheetTablePutReadsStdinAndFile(t *testing.T) {
payload := `[{"name":"Sheet1","columns":["name"],"data":[["Alice"]]}]`
t.Run("stdin", func(t *testing.T) {
caller := &sheetTableCaller{}
if err := executeSheetTableCommand(t, caller, strings.NewReader(payload), "table-put", "--node", "node1", "--sheets", "-"); err != nil {
t.Fatalf("table-put stdin returned error: %v", err)
}
if len(caller.calls) != 1 {
t.Fatalf("remote calls = %d, want 1", len(caller.calls))
}
})
t.Run("file", func(t *testing.T) {
path := filepath.Join(t.TempDir(), "table.json")
if err := os.WriteFile(path, []byte(payload), 0o600); err != nil {
t.Fatalf("write table JSON: %v", err)
}
caller := &sheetTableCaller{}
if err := executeSheetTableCommand(t, caller, nil, "table-put", "--node", "node1", "--sheets", "@"+path); err != nil {
t.Fatalf("table-put file returned error: %v", err)
}
if len(caller.calls) != 1 {
t.Fatalf("remote calls = %d, want 1", len(caller.calls))
}
})
}
func TestSheetTablePutRejectsInvalidInputBeforeCall(t *testing.T) {
for _, input := range []string{`not-json`, `123`, `[]`, `{"sheets":{}}`, `{"sheets":[]}`} {
caller := &sheetTableCaller{}
if err := executeSheetTableCommand(t, caller, nil, "table-put", "--node", "node1", "--sheets", input); err == nil {
t.Errorf("input %q returned nil error", input)
}
if len(caller.calls) != 0 {
t.Errorf("input %q made %d remote calls, want 0", input, len(caller.calls))
}
}
}
func TestSheetTablePutDryRunSkipsRemoteCall(t *testing.T) {
caller := &sheetTableCaller{dryRun: true}
input := `[{"name":"Sheet1","columns":["name"],"data":[["Alice"]]}]`
if err := executeSheetTableCommand(t, caller, nil, "table-put", "--node", "node1", "--sheets", input); err != nil {
t.Fatalf("table-put dry-run returned error: %v", err)
}
if len(caller.calls) != 0 {
t.Fatalf("remote calls = %d, want 0", len(caller.calls))
}
}
func TestSheetBatchUpdateRejectsTableCommands(t *testing.T) {
for _, toolName := range []string{"table-get", "table-put"} {
_, err := translateBatchOp(map[string]any{
"toolName": toolName,
"input": map[string]any{"sheet-id": "sheet1"},
})
if err == nil || !strings.Contains(err.Error(), toolName) {
t.Errorf("translateBatchOp(%q) error = %v, want rejection naming command", toolName, err)
}
}
}