diff --git a/internal/app/schema_completeness_test.go b/internal/app/schema_completeness_test.go index 29f8e985..6346b0fd 100644 --- a/internal/app/schema_completeness_test.go +++ b/internal/app/schema_completeness_test.go @@ -25,11 +25,32 @@ func TestRuntimeSchemaCompletenessCoversPublicCommandTree(t *testing.T) { if !containsSchemaPath(report.Covered, "chat category create-smart") { t.Fatal("chat category create-smart is not covered by runtime Schema") } + for _, path := range missingChatCatalogCoveragePaths() { + if !containsSchemaPath(report.Covered, path) { + t.Fatalf("%s is not covered by runtime Schema", path) + } + } if !containsSchemaPath(report.Excluded, "agoal strategy list") { t.Fatal("agoal strategy list is not recorded as a reviewed exclusion") } } +func TestRuntimeSchemaCompletenessDoesNotExcludeMissingChatCatalogPaths(t *testing.T) { + exclusions, err := cli.ReviewedRuntimeSchemaExclusions() + if err != nil { + t.Fatal(err) + } + excluded := map[string]bool{} + for _, exclusion := range exclusions { + excluded[exclusion.CLIPath] = true + } + for _, path := range missingChatCatalogCoveragePaths() { + if excluded[path] { + t.Fatalf("%s must not remain in runtime Schema exclusions", path) + } + } +} + func containsSchemaPath(paths []string, want string) bool { for _, path := range paths { if path == want { @@ -38,3 +59,38 @@ func containsSchemaPath(paths []string, want string) bool { } return false } + +func missingChatCatalogCoveragePaths() []string { + return []string{ + "chat category add-conv", + "chat category create", + "chat category delete", + "chat category remove-conv", + "chat category rename", + "chat chmod", + "chat clear-all-red-point", + "chat clear-messages", + "chat clear-red-point", + "chat data-auth cross-org", + "chat group audit-join-validation", + "chat group list-all", + "chat group list-join-validations", + "chat group members list-by-ids", + "chat group notice create", + "chat group notice edit", + "chat group notice get", + "chat group notice list", + "chat group share-invite", + "chat group update-alias", + "chat hide", + "chat list-all-conversations", + "chat mark-read", + "chat mark-unread", + "chat message list-emotion-replies", + "chat message set-top-msg", + "chat message unset-top-msg", + "chat mute-at-all", + "chat mute-red-envelope", + "chat text translate", + } +} diff --git a/internal/app/schema_shortcut_contract_test.go b/internal/app/schema_shortcut_contract_test.go index 5146dcb1..02ff8767 100644 --- a/internal/app/schema_shortcut_contract_test.go +++ b/internal/app/schema_shortcut_contract_test.go @@ -114,12 +114,14 @@ func TestDeliveryShortcutProgressiveQueriesReturnCompleteContracts(t *testing.T) product := executeShortcutSchemaQuery(t, "chat") productPayload, _ := product["product"].(map[string]any) - if got, want := int(product["count"].(float64)), 187; got != want { + if got, want := int(product["count"].(float64)), 217; got != want { t.Fatalf("schema chat count = %d, want %d", got, want) } summaries := schemaContractObjectSlice(productPayload["tools"]) shortcutCount := 0 + summaryByCLIPath := make(map[string]map[string]any, len(summaries)) for _, summary := range summaries { + summaryByCLIPath[schemaContractString(summary["cli_path"])] = summary if strings.HasPrefix(schemaContractString(summary["canonical_path"]), "chat.shortcut_") { shortcutCount++ } @@ -127,6 +129,135 @@ func TestDeliveryShortcutProgressiveQueriesReturnCompleteContracts(t *testing.T) if shortcutCount != 98 { t.Fatalf("schema chat shortcut summaries = %d, want 98", shortcutCount) } + for _, cliPath := range missingChatCatalogCoveragePaths() { + if summaryByCLIPath[cliPath] == nil { + t.Fatalf("schema chat missing expected catalog tool %q", cliPath) + } + } + assertSchemaSummarySafety(t, summaryByCLIPath, "chat clear-messages", "destructive", "high", "user_required") + assertSchemaSummarySafety(t, summaryByCLIPath, "chat data-auth cross-org", "write", "high", "user_required") + assertSchemaSummarySafety(t, summaryByCLIPath, "chat group share-invite", "write", "medium", "user_required") + assertChatCatalogCompleteLeafContracts(t) +} + +func assertSchemaSummarySafety( + t testing.TB, + summaries map[string]map[string]any, + cliPath string, + effect string, + risk string, + confirmation string, +) { + t.Helper() + summary := summaries[cliPath] + if summary == nil { + t.Fatalf("schema chat missing expected catalog tool %q", cliPath) + } + if got := schemaContractString(summary["effect"]); got != effect { + t.Fatalf("%s effect = %q, want %q", cliPath, got, effect) + } + if got := schemaContractString(summary["risk"]); got != risk { + t.Fatalf("%s risk = %q, want %q", cliPath, got, risk) + } + if got := schemaContractString(summary["confirmation"]); got != confirmation { + t.Fatalf("%s confirmation = %q, want %q", cliPath, got, confirmation) + } +} + +func assertChatCatalogCompleteLeafContracts(t testing.TB) { + t.Helper() + for _, cliPath := range []string{ + "chat clear-messages", + "chat clear-red-point", + "chat hide", + "chat mark-read", + "chat mark-unread", + "chat mute-at-all", + "chat mute-red-envelope", + } { + leaf := executeShortcutSchemaQuery(t, "--cli-path", cliPath) + assertSchemaLeafParameterRequired(t, leaf, cliPath, "conversation-id", false) + assertSchemaLeafConstraints(t, leaf, cliPath, map[string]any{ + "require_one_of": [][]string{{"conversation-id", "id", "chat"}}, + "mutually_exclusive": [][]string{{"conversation-id", "id", "chat"}}, + }) + } + + markRead := executeShortcutSchemaQuery(t, "--cli-path", "chat mark-read") + assertSchemaLeafParameterRequired(t, markRead, "chat mark-read", "message-id", true) + + chmod := executeShortcutSchemaQuery(t, "--cli-path", "chat chmod") + assertSchemaLeafConstraints(t, chmod, "chat chmod", map[string]any{ + "require_one_of": [][]string{{"conversation-id", "open-dingtalk-id", "user", "permParam"}}, + "mutually_exclusive": [][]string{{"conversation-id", "open-dingtalk-id", "user"}}, + }) + assertChatGrantParameterFacts(t, chmod, "chat chmod") + + crossOrg := executeShortcutSchemaQuery(t, "--cli-path", "chat data-auth cross-org") + assertSchemaLeafConstraints(t, crossOrg, "chat data-auth cross-org", map[string]any{ + "require_one_of": [][]string{{"target-org-id", "all"}}, + "mutually_exclusive": [][]string{{"target-org-id", "all"}}, + }) + assertChatGrantParameterFacts(t, crossOrg, "chat data-auth cross-org") + + shareInvite := executeShortcutSchemaQuery(t, "--cli-path", "chat group share-invite") + assertSchemaLeafConstraints(t, shareInvite, "chat group share-invite", map[string]any{ + "require_one_of": [][]string{{"target", "receiver"}}, + "mutually_exclusive": [][]string{{"target", "receiver"}}, + }) + + auditJoin := executeShortcutSchemaQuery(t, "--cli-path", "chat group audit-join-validation") + assertSchemaLeafParameterEnum(t, auditJoin, "chat group audit-join-validation", "status", []string{"AuditApprove", "AuditDelete"}) +} + +func assertSchemaLeafParameterRequired(t testing.TB, leaf map[string]any, cliPath, name string, want bool) { + t.Helper() + parameters := schemaContractMap(leaf["parameters"]) + parameter := parameters[name] + if parameter == nil { + t.Fatalf("%s missing --%s parameter: %#v", cliPath, name, parameters) + } + if got, _ := parameter["required"].(bool); got != want { + t.Fatalf("%s --%s required = %#v, want %v", cliPath, name, parameter["required"], want) + } +} + +func assertSchemaLeafParameterEnum(t testing.TB, leaf map[string]any, cliPath, name string, want []string) { + t.Helper() + parameters := schemaContractMap(leaf["parameters"]) + parameter := parameters[name] + if parameter == nil { + t.Fatalf("%s missing --%s parameter: %#v", cliPath, name, parameters) + } + if got := schemaContractStringSlice(parameter["enum"]); !schemaContractJSONEqual(got, want) { + t.Fatalf("%s --%s enum = %#v, want %#v", cliPath, name, got, want) + } +} + +func assertSchemaLeafConstraints(t testing.TB, leaf map[string]any, cliPath string, want map[string]any) { + t.Helper() + if got := leaf["constraints"]; !schemaContractJSONEqual(got, want) { + t.Fatalf("%s constraints = %#v, want %#v", cliPath, got, want) + } +} + +func assertChatGrantParameterFacts(t testing.TB, leaf map[string]any, cliPath string) { + t.Helper() + parameters := schemaContractMap(leaf["parameters"]) + grantType := parameters["grant-type"] + if grantType == nil { + t.Fatalf("%s missing --grant-type parameter: %#v", cliPath, parameters) + } + wantEnum := []string{"once", "session", "timed", "permanent"} + if got := schemaContractStringSlice(grantType["enum"]); !schemaContractJSONEqual(got, wantEnum) { + t.Fatalf("%s --grant-type enum = %#v, want %#v", cliPath, got, wantEnum) + } + if got := schemaContractString(parameters["session-id"]["required_when"]); got != "grant-type is session" { + t.Fatalf("%s --session-id required_when = %q, want grant-type is session", cliPath, got) + } + if got := schemaContractString(parameters["ttl"]["required_when"]); got != "grant-type is timed" { + t.Fatalf("%s --ttl required_when = %q, want grant-type is timed", cliPath, got) + } } func TestDeliveryDocUpdateShortcutPublishesCompleteConditionalContract(t *testing.T) { diff --git a/internal/cli/schema_command_exclusions.go b/internal/cli/schema_command_exclusions.go index 6ef8e2a6..328c1f42 100644 --- a/internal/cli/schema_command_exclusions.go +++ b/internal/cli/schema_command_exclusions.go @@ -91,36 +91,6 @@ var reviewedRuntimeSchemaExclusionGroups = []runtimeSchemaExclusionGroup{ "calendar acl add", "calendar acl delete", "calendar book update", - "chat category add-conv", - "chat category create", - "chat category delete", - "chat category remove-conv", - "chat category rename", - "chat chmod", - "chat clear-all-red-point", - "chat clear-messages", - "chat clear-red-point", - "chat data-auth cross-org", - "chat group audit-join-validation", - "chat group list-all", - "chat group list-join-validations", - "chat group members list-by-ids", - "chat group notice create", - "chat group notice edit", - "chat group notice get", - "chat group notice list", - "chat group share-invite", - "chat group update-alias", - "chat hide", - "chat list-all-conversations", - "chat mark-read", - "chat mark-unread", - "chat message list-emotion-replies", - "chat message set-top-msg", - "chat message unset-top-msg", - "chat mute-at-all", - "chat mute-red-envelope", - "chat text translate", "contact label get", "contact label list", "contact label list-members", diff --git a/internal/cli/schema_constraints_runtime.go b/internal/cli/schema_constraints_runtime.go index 686e7b76..ecc7ce71 100644 --- a/internal/cli/schema_constraints_runtime.go +++ b/internal/cli/schema_constraints_runtime.go @@ -38,20 +38,33 @@ func init() { registerRequireTogether("calendar.update_calendar_event", "recurrence-type", "recurrence-interval", "recurrence-range-type") registerExclusiveOneOf("chat.search_messages_by_sender", "sender-user-id", "sender-open-dingtalk-id") registerExclusiveOneOf("chat.create_and_send_card", "group", "receiver") + RegisterRuntimeSchemaConstraints("chat.chat_permission_grant", RuntimeSchemaConstraints{ + MutuallyExclusive: [][]string{{"conversation-id", "open-dingtalk-id", "user"}}, + RequireOneOf: [][]string{{"conversation-id", "open-dingtalk-id", "user", "permParam"}}, + }) + registerExclusiveOneOf("chat.chat_permission_grant_cross_org_data", "target-org-id", "all") registerRequireOneOf("chat.add_emoji_reaction", "conversation-id", "group", "id", "chat") registerRequireOneOf("chat.add_text_emotion", "conversation-id", "group", "id", "chat") + registerExclusiveOneOf("chat.clear_conversation_messages", "conversation-id", "id", "chat") + registerExclusiveOneOf("chat.clear_conversation_red_point", "conversation-id", "id", "chat") registerRequireOneOf("chat.update_text_emotion", "conversation-id", "group", "id", "chat") registerExclusiveOneOf("chat.get_conversation_info", "group", "user", "open-dingtalk-id") + registerExclusiveOneOf("chat.hide_conversation", "conversation-id", "id", "chat") registerExclusiveOneOf("chat.list_conversation_message_v2", "group", "user", "open-dingtalk-id") registerExclusiveOneOf("chat.list_individual_chat_message", "user", "open-dingtalk-id") + registerExclusiveOneOf("chat.mark_conversation_unread", "conversation-id", "id", "chat") + registerExclusiveOneOf("chat.mark_message_read", "conversation-id", "id", "chat") registerRequireOneOf("chat.remove_emoji_reaction", "conversation-id", "group", "id", "chat") registerRequireOneOf("chat.remove_text_emotion", "conversation-id", "group", "id", "chat") registerRequireOneOf("chat.send_personal_message", "text", "content", "msg-type") registerExclusiveOneOf("chat.send_robot_message", "group", "users") registerRequireOneOf("chat.set_group_member_mute_list", "users", "user") + registerExclusiveOneOf("chat.share_group_invite_url", "target", "receiver") registerExclusiveOneOf("chat.transfer_group_owner", "new-owner", "user") registerRequireOneOf("chat.update_conv_member_roles", "users", "user") + registerExclusiveOneOf("chat.update_at_all_notification_off", "conversation-id", "id", "chat") registerRequireOneOf("chat.update_notification_off", "conversation-id", "id", "chat") + registerExclusiveOneOf("chat.update_red_env_notification_off", "conversation-id", "id", "chat") registerRequireTogether("contact.query_dismission_employee_list", "start", "end") registerRequireOneOf("dev.connect_status", "robot-client-id", "unified-app-id") registerRequireOneOf("dev.connect_stop", "robot-client-id", "unified-app-id") diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 3f84eb0d..be227784 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -1492,6 +1492,36 @@ func buildChatCrossOrgDataAuthArgs(cmd *cobra.Command) (map[string]any, error) { return toolArgs, nil } +func buildChatGroupShareInviteArgs(cmd *cobra.Command) (map[string]any, error) { + if err := validateRequiredFlags(cmd, "source"); err != nil { + return nil, err + } + target, _ := cmd.Flags().GetString("target") + receiver, _ := cmd.Flags().GetString("receiver") + if target == "" && receiver == "" { + return nil, fmt.Errorf("--target or --receiver is required") + } + if target != "" && receiver != "" { + return nil, fmt.Errorf("--target and --receiver are mutually exclusive") + } + toolArgs := map[string]any{ + "sourceOpenConversationId": mustGetFlag(cmd, "source"), + } + if target != "" { + toolArgs["targetOpenConversationId"] = target + } + if receiver != "" { + toolArgs["receiverOpenDingTalkId"] = receiver + } + if v, _ := cmd.Flags().GetInt64("expires-seconds"); v > 0 || cmd.Flags().Changed("expires-seconds") { + toolArgs["expiresSeconds"] = v + } + if v, _ := cmd.Flags().GetString("uuid"); v != "" { + toolArgs["uuid"] = v + } + return toolArgs, nil +} + func appendChatChmodParams(cmd *cobra.Command, toolArgs map[string]any) error { conversationID, _ := cmd.Flags().GetString("conversation-id") openDingTalkID, _ := cmd.Flags().GetString("open-dingtalk-id") @@ -1974,11 +2004,11 @@ func newChatCommand() *cobra.Command { Parameters: []contract.ParamDecl{ {Name: "agentCode", Property: "agentCode", Required: boolPtr(false)}, {Name: "conversation-id", Property: "grantParams.openCid", Required: boolPtr(false)}, - {Name: "grant-type", Property: "grantType", Required: boolPtr(false)}, + {Name: "grant-type", Property: "grantType", Required: boolPtr(false), Enum: []string{"once", "session", "timed", "permanent"}}, {Name: "open-dingtalk-id", Property: "grantParams.openDingTalkId", Required: boolPtr(false)}, {Name: "permParam", Property: "grantParams", Required: boolPtr(false)}, - {Name: "session-id", Property: "sessionId", Required: boolPtr(false)}, - {Name: "ttl", Property: "ttl", Required: boolPtr(false)}, + {Name: "session-id", Property: "sessionId", Required: boolPtr(false), RequiredWhen: "grant-type is session"}, + {Name: "ttl", Property: "ttl", Required: boolPtr(false), RequiredWhen: "grant-type is timed"}, {Name: "user", Property: "grantParams.userId", Required: boolPtr(false)}, }, }, @@ -2017,10 +2047,15 @@ func newChatCommand() *cobra.Command { chatDataAuthCrossOrgCmd.Flags().String("grant-type", "timed", "授权策略: once|session|timed|permanent") chatDataAuthCrossOrgCmd.Flags().String("ttl", "24h", "timed 授权有效期,如 1h/4h/24h/7d") chatDataAuthCrossOrgCmd.Flags().String("session-id", "", "session 授权的会话标识") + chatDataAuthCrossOrgCmd.Flags().BoolP("yes", "y", false, "确认执行跨组织 chat 数据授权") DeclareLeafMetadata(chatDataAuthCrossOrgCmd, LeafSpec{ Safety: contract.SafetySpec{ - Effect: "write", Risk: "medium", - Confirmation: "not_required", Idempotency: "unknown", + Effect: "write", Risk: "high", + Confirmation: "user_required", Idempotency: "unknown", + }, + Validate: func(cmd *cobra.Command, args []string) error { + _, err := buildChatCrossOrgDataAuthArgs(cmd) + return err }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ @@ -2045,10 +2080,10 @@ func newChatCommand() *cobra.Command { Parameters: []contract.ParamDecl{ {Name: "agentCode", Property: "agentCode", Required: boolPtr(false)}, {Name: "all", Property: "grantParams.targetOrgId", Required: boolPtr(false)}, - {Name: "grant-type", Property: "grantType", Required: boolPtr(false)}, - {Name: "session-id", Property: "sessionId", Required: boolPtr(false)}, + {Name: "grant-type", Property: "grantType", Required: boolPtr(false), Enum: []string{"once", "session", "timed", "permanent"}}, + {Name: "session-id", Property: "sessionId", Required: boolPtr(false), RequiredWhen: "grant-type is session"}, {Name: "target-org-id", Property: "grantParams.targetOrgId", Required: boolPtr(false)}, - {Name: "ttl", Property: "ttl", Required: boolPtr(false)}, + {Name: "ttl", Property: "ttl", Required: boolPtr(false), RequiredWhen: "grant-type is timed"}, }, }, }) @@ -8227,12 +8262,16 @@ status 可选值: if err != nil { return fmt.Errorf("--record-id must be a valid integer: %w", err) } + status := mustGetFlag(cmd, "status") + if status != "AuditApprove" && status != "AuditDelete" { + return fmt.Errorf("unsupported audit status %q, must be one of: AuditApprove, AuditDelete", status) + } toolArgs := map[string]any{ "openConversationId": mustGetFlag(cmd, "group"), "applyRecordId": recordID, "applicantUid": mustGetFlag(cmd, "applicant"), "inviterUid": mustGetFlag(cmd, "inviter"), - "status": mustGetFlag(cmd, "status"), + "status": status, } if v, _ := cmd.Flags().GetString("description"); v != "" { toolArgs["auditDescription"] = v @@ -8282,7 +8321,7 @@ status 可选值: {Name: "group", Property: "openConversationId", Required: boolPtr(true)}, {Name: "inviter", Property: "inviterUid", Required: boolPtr(true)}, {Name: "record-id", Property: "applyRecordId", Required: boolPtr(true), InterfaceType: "integer"}, - {Name: "status", Property: "status", Required: boolPtr(true), Enum: []string{"AuditApprove", "AuditDelete", "AuditIgnore", "AuditRefuse", "AuditBlock"}}, + {Name: "status", Property: "status", Required: boolPtr(true), Enum: []string{"AuditApprove", "AuditDelete"}}, }, }, }) @@ -9386,32 +9425,10 @@ status 可选值: dws chat group share-invite --source --target --expires-seconds 86400 # 查询群 ID: dws chat search --query "群名"`, RunE: func(cmd *cobra.Command, args []string) error { - if err := validateRequiredFlags(cmd, "source"); err != nil { + toolArgs, err := buildChatGroupShareInviteArgs(cmd) + if err != nil { return err } - target, _ := cmd.Flags().GetString("target") - receiver, _ := cmd.Flags().GetString("receiver") - if target == "" && receiver == "" { - return fmt.Errorf("--target or --receiver is required") - } - if target != "" && receiver != "" { - return fmt.Errorf("--target and --receiver are mutually exclusive") - } - toolArgs := map[string]any{ - "sourceOpenConversationId": mustGetFlag(cmd, "source"), - } - if target != "" { - toolArgs["targetOpenConversationId"] = target - } - if receiver != "" { - toolArgs["receiverOpenDingTalkId"] = receiver - } - if v, _ := cmd.Flags().GetInt64("expires-seconds"); v > 0 || cmd.Flags().Changed("expires-seconds") { - toolArgs["expiresSeconds"] = v - } - if v, _ := cmd.Flags().GetString("uuid"); v != "" { - toolArgs["uuid"] = v - } return callMCPToolOnServer("im", "share_group_invite_url", toolArgs) }, } @@ -9421,10 +9438,11 @@ status 可选值: chatGroupShareInviteCmd.Flags().String("receiver", "", "接收分享消息的单聊用户 openDingTalkId(与 --target 二选一)") chatGroupShareInviteCmd.Flags().Int64("expires-seconds", 0, "链接有效期(秒),0 表示永久有效,不传使用服务端默认值") chatGroupShareInviteCmd.Flags().String("uuid", "", "消息幂等键(可选)") + chatGroupShareInviteCmd.Flags().BoolP("yes", "y", false, "确认分享群邀请链接") DeclareLeafMetadata(chatGroupShareInviteCmd, LeafSpec{ Safety: contract.SafetySpec{ Effect: "write", Risk: "medium", - Confirmation: "not_required", Idempotency: "unknown", + Confirmation: "user_required", Idempotency: "unknown", }, Contract: LeafContract{ Identity: contract.ToolIdentitySpec{ diff --git a/internal/helpers/chat_message_search_test.go b/internal/helpers/chat_message_search_test.go index a41711e2..623bd662 100644 --- a/internal/helpers/chat_message_search_test.go +++ b/internal/helpers/chat_message_search_test.go @@ -746,6 +746,22 @@ func TestCrossPlatformCoverageChatAuditUsesUserIDs(t *testing.T) { } } +func TestCrossPlatformCoverageChatAuditRejectsUnsupportedStatus(t *testing.T) { + caller := &chatChangedContractCaller{} + err := executeChatChangedContract(t, caller, + "group", "audit-join-validation", + "--group", "cid-1", "--record-id", "123", "--applicant", "user-a", "--inviter", "user-b", "--status", "AuditRefuse") + if err == nil { + t.Fatal("expected unsupported audit status error") + } + if !strings.Contains(err.Error(), `unsupported audit status "AuditRefuse"`) { + t.Fatalf("error = %v, want unsupported status", err) + } + if len(caller.calls) != 0 { + t.Fatalf("unsupported status must not call MCP: %#v", caller.calls) + } +} + func TestCrossPlatformCoverageChatSendResolvesUserBeforeDispatch(t *testing.T) { caller := &chatChangedContractCaller{resolveUsers: true} err := executeChatChangedContract(t, caller, "message", "send", "--user", "123", "--text", "hello") diff --git a/internal/helpers/wukong_weekly_sync_chat_todo_test.go b/internal/helpers/wukong_weekly_sync_chat_todo_test.go index 11fd362e..a20a664f 100644 --- a/internal/helpers/wukong_weekly_sync_chat_todo_test.go +++ b/internal/helpers/wukong_weekly_sync_chat_todo_test.go @@ -168,6 +168,77 @@ func TestCrossPlatformCoverageWukongWeeklyChatCategoryQueries(t *testing.T) { }) } +func TestCrossPlatformCoverageChatDataAuthCrossOrgRequiresConfirmation(t *testing.T) { + caller := &wukongWeeklySyncCaller{} + _, _, err := executeWukongWeeklySyncCommand(t, "chat", caller, newChatCommand, + "data-auth", "cross-org", "--target-org-id", "439446171") + requireWukongWeeklySyncConfirmation(t, err) + requireWukongWeeklySyncNoCalls(t, caller) + + caller = &wukongWeeklySyncCaller{} + _, _, err = executeWukongWeeklySyncCommand(t, "chat", caller, newChatCommand, + "data-auth", "cross-org", "--target-org-id", "439446171", "--yes") + if err != nil { + t.Fatalf("confirmed cross-org data auth returned error: %v", err) + } + requireWukongWeeklySyncCall(t, caller, wukongWeeklySyncCall{ + server: "im", + tool: "chat_permission_grant", + args: map[string]any{ + "agentCode": "wukong", + "grantCategory": "data", + "grantParams": `{"targetOrgId":"439446171"}`, + "grantType": "timed", + "scope": "chat.data:cross-org", + "ttl": "24h", + }, + }) + if _, ok := caller.calls[0].args["yes"]; ok { + t.Fatalf("confirmed payload leaked yes flag: %#v", caller.calls[0].args) + } +} + +func TestCrossPlatformCoverageChatGroupShareInviteRequiresConfirmation(t *testing.T) { + caller := &wukongWeeklySyncCaller{} + _, _, err := executeWukongWeeklySyncCommand(t, "chat", caller, newChatCommand, + "group", "share-invite", "--target", "cid-target") + if err == nil || !strings.Contains(err.Error(), "source") { + t.Fatalf("missing source error = %v, want source", err) + } + requireWukongWeeklySyncNoCalls(t, caller) + + caller = &wukongWeeklySyncCaller{} + _, _, err = executeWukongWeeklySyncCommand(t, "chat", caller, newChatCommand, + "group", "share-invite", "--source", "cid-source", "--target", "cid-target") + requireWukongWeeklySyncConfirmation(t, err) + requireWukongWeeklySyncNoCalls(t, caller) + + caller = &wukongWeeklySyncCaller{} + _, _, err = executeWukongWeeklySyncCommand(t, "chat", caller, newChatCommand, + "group", "share-invite", + "--source", "cid-source", + "--target", "cid-target", + "--expires-seconds", "86400", + "--uuid", "invite-uuid", + "--yes") + if err != nil { + t.Fatalf("confirmed share-invite returned error: %v", err) + } + requireWukongWeeklySyncCall(t, caller, wukongWeeklySyncCall{ + server: "im", + tool: "share_group_invite_url", + args: map[string]any{ + "expiresSeconds": int64(86400), + "sourceOpenConversationId": "cid-source", + "targetOpenConversationId": "cid-target", + "uuid": "invite-uuid", + }, + }) + if _, ok := caller.calls[0].args["yes"]; ok { + t.Fatalf("confirmed payload leaked yes flag: %#v", caller.calls[0].args) + } +} + func TestCrossPlatformCoverageWukongWeeklyChatMessageEditValidation(t *testing.T) { root := newChatCommand() edit, _, findErr := root.Find([]string{"message", "edit"})