// 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)) } }