202 lines
6.2 KiB
Go
202 lines
6.2 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 app
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/usage"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
|
)
|
|
|
|
type crossPlatformCoverageCaller struct {
|
|
args map[string]any
|
|
token string
|
|
dryRun bool
|
|
reads int
|
|
}
|
|
|
|
func (c *crossPlatformCoverageCaller) CallTool(_ context.Context, _, _ string, args map[string]any) (*edition.ToolResult, error) {
|
|
c.args = args
|
|
return &edition.ToolResult{}, nil
|
|
}
|
|
|
|
func (c *crossPlatformCoverageCaller) CallToolWithToken(
|
|
_ context.Context,
|
|
token, _, _ string,
|
|
args map[string]any,
|
|
) (*edition.ToolResult, error) {
|
|
c.token = token
|
|
c.args = args
|
|
return &edition.ToolResult{}, nil
|
|
}
|
|
|
|
func (c *crossPlatformCoverageCaller) CallReadTool(
|
|
_ context.Context,
|
|
_, _ string,
|
|
args map[string]any,
|
|
) (*edition.ToolResult, error) {
|
|
c.reads++
|
|
c.args = args
|
|
return &edition.ToolResult{}, nil
|
|
}
|
|
|
|
func (*crossPlatformCoverageCaller) Format() string { return "json" }
|
|
func (c *crossPlatformCoverageCaller) DryRun() bool { return c.dryRun }
|
|
func (*crossPlatformCoverageCaller) Fields() string { return "id,name" }
|
|
func (*crossPlatformCoverageCaller) JQ() string { return ".result" }
|
|
|
|
func TestCrossPlatformCoverageCloneToolArgsDefensiveCopy(t *testing.T) {
|
|
args := map[string]any{"page": 1, "query": "keep"}
|
|
cloned := cloneToolArgs(args)
|
|
args["page"] = 2
|
|
args["extra"] = true
|
|
|
|
if got := cloned["page"]; got != 1 {
|
|
t.Fatalf("cloned page = %#v, want 1", got)
|
|
}
|
|
if _, ok := cloned["extra"]; ok {
|
|
t.Fatal("clone changed after source map mutation")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCloneToolArgsEmpty(t *testing.T) {
|
|
if got := cloneToolArgs(nil); got != nil {
|
|
t.Fatalf("nil clone = %#v, want nil", got)
|
|
}
|
|
if got := cloneToolArgs(map[string]any{}); got != nil {
|
|
t.Fatalf("empty clone = %#v, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageRecordingToolCaller(t *testing.T) {
|
|
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
|
t.Setenv("DWS_USAGE_TRACKING", "1")
|
|
|
|
inner := &crossPlatformCoverageCaller{}
|
|
caller := newRecordingToolCaller(inner)
|
|
args := map[string]any{"open_conversation_id": "cid_x", "text": "private"}
|
|
if _, err := caller.CallTool(context.Background(), "chat", "send_message", args); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inner.args["open_conversation_id"] != "cid_x" {
|
|
t.Fatalf("forwarded args = %#v", inner.args)
|
|
}
|
|
if caller.Format() != "json" || caller.Fields() != "id,name" || caller.JQ() != ".result" || caller.DryRun() {
|
|
t.Fatal("recording caller did not delegate output settings")
|
|
}
|
|
records, err := usage.Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(records) != 1 || records[0].Product != "chat" || records[0].Tool != "send_message" {
|
|
t.Fatalf("usage records = %#v", records)
|
|
}
|
|
if _, leaked := records[0].SampleArgs["text"]; leaked {
|
|
t.Fatal("sensitive text must not be recorded")
|
|
}
|
|
|
|
tokenCaller, ok := caller.(tokenOverrideToolCaller)
|
|
if !ok {
|
|
t.Fatal("recording caller dropped token override support")
|
|
}
|
|
if _, err := tokenCaller.CallToolWithToken(
|
|
context.Background(),
|
|
"temporary-token",
|
|
"contact",
|
|
"get_current_user_profile",
|
|
nil,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inner.token != "temporary-token" || len(inner.args) != 0 {
|
|
t.Fatalf("token override forwarding = token %q args %#v", inner.token, inner.args)
|
|
}
|
|
|
|
inner.dryRun = true
|
|
if _, err := caller.CallTool(context.Background(), "chat", "send_message", args); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
records, err = usage.Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(records) != 2 {
|
|
t.Fatalf("dry-run call must not be recorded: %#v", records)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageRecordingReadToolCaller(t *testing.T) {
|
|
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
|
t.Setenv("DWS_USAGE_TRACKING", "1")
|
|
|
|
inner := &crossPlatformCoverageCaller{dryRun: true}
|
|
caller := newRecordingToolCaller(inner)
|
|
readCaller, ok := caller.(edition.ReadToolCaller)
|
|
if !ok {
|
|
t.Fatal("recording caller dropped read-only capability")
|
|
}
|
|
if _, err := readCaller.CallReadTool(
|
|
context.Background(),
|
|
"im",
|
|
"search_groups",
|
|
map[string]any{"open_conversation_id": "cid_x"},
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if inner.reads != 1 || inner.args["open_conversation_id"] != "cid_x" {
|
|
t.Fatalf("read forwarding = reads %d args %#v", inner.reads, inner.args)
|
|
}
|
|
records, err := usage.Read()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(records) != 1 || !records[0].OK {
|
|
t.Fatalf("real read must be recorded as successful: %#v", records)
|
|
}
|
|
|
|
withoutRead := recordingToolCaller{inner: nonReadToolCaller{}}
|
|
if _, err := withoutRead.CallReadTool(context.Background(), "im", "search_groups", nil); err == nil {
|
|
t.Fatal("recording caller accepted an inner caller without read support")
|
|
}
|
|
}
|
|
|
|
type nonReadToolCaller struct{}
|
|
|
|
func (nonReadToolCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
|
|
return &edition.ToolResult{}, nil
|
|
}
|
|
func (nonReadToolCaller) Format() string { return "json" }
|
|
func (nonReadToolCaller) DryRun() bool { return false }
|
|
func (nonReadToolCaller) Fields() string { return "" }
|
|
func (nonReadToolCaller) JQ() string { return "" }
|
|
|
|
func TestCrossPlatformCoverageRootPublishesShortcutCommands(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
t.Setenv("DWS_CONFIG_DIR", configDir)
|
|
root := NewRootCommand(context.Background())
|
|
for _, path := range [][]string{{"shortcut", "list"}, {"calendar", "+today"}} {
|
|
cmd, remaining, err := root.Find(path)
|
|
if err != nil || len(remaining) != 0 || cmd == nil {
|
|
t.Fatalf("root.Find(%v) = cmd=%v remaining=%v err=%v", path, cmd, remaining, err)
|
|
}
|
|
}
|
|
if _, err := os.Stat(filepath.Join(configDir, "audit", ".audit.lock")); !os.IsNotExist(err) {
|
|
t.Fatalf("constructing the root command opened an audit lock: %v", err)
|
|
}
|
|
}
|