Files
dingtalk-workspace-cli/internal/shortcut/usage/usage_test.go

251 lines
8.8 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 usage
import (
"bytes"
"encoding/json"
"os"
"reflect"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func TestCrossPlatformCoverageShortcutListDeclaresRuntimeSchemaDelivery(t *testing.T) {
cmd := newListCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetArgs(nil)
if err := cmd.Execute(); err != nil {
t.Fatalf("execute shortcut list: %v", err)
}
var payload struct {
RuntimeSchema bool `json:"runtime_schema"`
}
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
t.Fatalf("decode shortcut list: %v", err)
}
if !payload.RuntimeSchema {
t.Fatal("shortcut list must advertise delivery through Runtime Schema")
}
}
func TestCrossPlatformCoverageShortcutListFiltersHiddenAndService(t *testing.T) {
shortcut.Register(shortcut.Shortcut{
Service: "coverage-usage",
Command: "+hidden",
})
shortcut.Register(shortcut.Shortcut{
Service: "coverage-usage",
Command: "+compatibility-visible",
CompatibilityVisible: true,
})
execute := func(args ...string) map[string]any {
t.Helper()
cmd := newListCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetArgs(args)
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
var payload map[string]any
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
t.Fatal(err)
}
return payload
}
publicRows := execute("--service", "coverage-usage")
allRows := execute("--service", "coverage-usage", "--all")
if publicRows["count"].(float64) != 0 || allRows["count"].(float64) != 2 {
t.Fatalf("hidden shortcuts were not filtered: public=%v all=%v", publicRows["count"], allRows["count"])
}
rows := allRows["shortcuts"].([]any)
foundCompatibilityVisible := false
for _, value := range rows {
row := value.(map[string]any)
if row["command"] == "+compatibility-visible" {
foundCompatibilityVisible = row["compatibility_visible"] == true && row["public"] == false
}
}
if !foundCompatibilityVisible {
t.Fatalf("compatibility-visible shortcut row lost its non-public marker: %#v", rows)
}
missing := execute("--service", "__missing__")
if missing["count"].(float64) != 0 {
t.Fatalf("missing service returned shortcuts: %#v", missing)
}
}
func TestCrossPlatformCoverageShortcutListRowPublishesCompleteContract(t *testing.T) {
row := newShortcutListRow(shortcut.Shortcut{
Service: "chat",
Command: "+messages",
Flags: []shortcut.Flag{
{Name: "group", Required: true},
{Name: "internal", Hidden: true},
},
Constraints: []shortcut.Constraint{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "user"}},
},
Tips: []string{"dws chat +messages --group cid"},
})
if row.CLIPath != "chat +messages" || row.Product != "chat" || row.Risk != "read" || row.Confirmation != "not_required" {
t.Fatalf("unexpected normalized identity: %#v", row)
}
if len(row.Flags) != 1 || row.Flags[0].Name != "group" || row.Flags[0].Type != shortcut.FlagString {
t.Fatalf("public normalized flags = %#v", row.Flags)
}
if row.Flags[0].Enum == nil {
t.Fatal("empty enum must publish as [] rather than null")
}
if !reflect.DeepEqual(row.Examples, []string{"dws chat +messages --group cid"}) {
t.Fatalf("examples = %#v", row.Examples)
}
if len(row.Constraints) != 1 || row.Constraints[0].Kind != shortcut.ConstraintExactlyOne {
t.Fatalf("constraints = %#v", row.Constraints)
}
}
func TestCrossPlatformCoverageShortcutListRowRequiresConfirmationForWrites(t *testing.T) {
row := newShortcutListRow(shortcut.Shortcut{Service: "chat", Command: "+send", Risk: shortcut.RiskWrite})
if row.Risk != "write" || row.Confirmation != "user_required" {
t.Fatalf("write shortcut safety = risk %q confirmation %q", row.Risk, row.Confirmation)
}
}
func TestCrossPlatformCoverageEnabledToggle(t *testing.T) {
for _, v := range []string{"", "0", "false", "off", "NO", "garbage"} {
t.Setenv("DWS_USAGE_TRACKING", v)
if Enabled() {
t.Errorf("DWS_USAGE_TRACKING=%q should keep tracking OFF (opt-in default)", v)
}
}
for _, v := range []string{"1", "true", "on", "YES"} {
t.Setenv("DWS_USAGE_TRACKING", v)
if !Enabled() {
t.Errorf("DWS_USAGE_TRACKING=%q should enable tracking", v)
}
}
}
func TestCrossPlatformCoverageSampleArgsRedaction(t *testing.T) {
args := map[string]any{
"open_conversation_id": "cid_abc123", // ID-like → kept
"page": 20, // number → kept
"has_read": true, // bool → kept
"text": "hi there 你好", // sensitive key → dropped
"keyword": "cid_abc123", // sensitive key → dropped even if ID-like
"note": "a long free text with spaces", // whitespace → dropped
"name": "Alice", // short content → dropped
"fileName": "roadmap.md", // short content → dropped
"originalText": "Q2", // short content → dropped
"replacedText": "第二季度", // short content → dropped
"clientId": "oauth-client", // credential metadata → dropped
"authCode": "one-time-code", // credential → dropped
"amount": 1000, // unknown numeric user data → dropped
"tags": []string{"a", "b"}, // composite → dropped
}
got := sampleArgs(args)
wantKept := map[string]string{"open_conversation_id": "cid_abc123", "page": "20", "has_read": "true"}
for k, v := range wantKept {
if got[k] != v {
t.Errorf("expected %s=%q kept, got %q", k, v, got[k])
}
}
for _, k := range []string{
"text", "keyword", "note", "name", "fileName", "originalText",
"replacedText", "clientId", "authCode", "amount", "tags",
} {
if _, ok := got[k]; ok {
t.Errorf("expected %s to be redacted/dropped, but it was recorded", k)
}
}
}
func TestCrossPlatformCoverageAggregateRequiresSampleOnEveryOccurrenceForFixedArg(t *testing.T) {
recs := []Record{
{Product: "chat", Tool: "send", ArgKeys: []string{"conversationId"}, SampleArgs: map[string]string{"conversationId": "cid_x"}},
{Product: "chat", Tool: "send", ArgKeys: []string{"conversationId"}},
}
groups := Aggregate(recs)
if len(groups) != 1 {
t.Fatalf("groups = %d, want 1", len(groups))
}
if _, fixed := groups[0].FixedArgs["conversationId"]; fixed {
t.Fatalf("partially sampled value must not become fixed: %#v", groups[0].FixedArgs)
}
}
func TestCrossPlatformCoverageAppendAndAggregate(t *testing.T) {
dir := t.TempDir()
t.Setenv("DWS_CONFIG_DIR", dir)
t.Setenv("DWS_USAGE_TRACKING", "1")
// Same shape, same fixed conversation id → should group with a fixed arg.
for i := 0; i < 3; i++ {
Append("chat", "send_message", map[string]any{
"open_conversation_id": "cid_x", "text": "msg" + string(rune('0'+i)),
}, true, false)
}
// Different tool.
Append("todo", "get_user_todos_in_current_org", map[string]any{"pageNum": "1"}, true, false)
// Dry-run must be skipped.
Append("chat", "send_message", map[string]any{"open_conversation_id": "cid_x"}, true, true)
recs, err := Read()
if err != nil {
t.Fatal(err)
}
if len(recs) != 4 {
t.Fatalf("expected 4 records (dry-run skipped), got %d", len(recs))
}
groups := Aggregate(recs)
if len(groups) != 2 {
t.Fatalf("expected 2 groups, got %d", len(groups))
}
top := groups[0]
if top.Tool != "send_message" || top.Count != 3 {
t.Fatalf("top group = %s x%d, want send_message x3", top.Tool, top.Count)
}
if top.FixedArgs["open_conversation_id"] != "cid_x" {
t.Errorf("expected fixed open_conversation_id=cid_x, got %v", top.FixedArgs)
}
if _, leaked := top.FixedArgs["text"]; leaked {
t.Error("free-text 'text' must never appear in fixed args")
}
if err := Purge(); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(LogPath()); !os.IsNotExist(err) {
t.Error("Purge should remove the log")
}
}
func TestCrossPlatformCoverageDisabledSkipsWrite(t *testing.T) {
dir := t.TempDir()
t.Setenv("DWS_CONFIG_DIR", dir)
t.Setenv("DWS_USAGE_TRACKING", "0")
Append("chat", "send_message", map[string]any{"x": "y"}, true, false)
if recs, _ := Read(); len(recs) != 0 {
t.Errorf("disabled tracking must not write, got %d records", len(recs))
}
}