Files
dingtalk-workspace-cli/internal/shortcut/smart/doc_access_test.go
dxy704330469 eb0bd69b82 feat(doc): add reviewed document shortcuts
- add 45 public document shortcuts and 2 reviewed expert-only paths
- preserve six historical command and Schema identities alongside canonical leaves
- add safe local download primitives and document access/share orchestration
- keep comment create/reply confirmation backward-compatible
- ensure grant-and-share upgrades insufficient roles before messaging
- return non-zero partial/failure message ledgers and structured partial-write recovery metadata
- enumerate every selection candidate and use rune-safe Unicode keyword contexts
- assert zero-call confirmation boundaries for destructive shortcuts

Validation:
- full Go test suite and repository policy
- real DingTalk E2E for 34 canonical shortcuts, all 8 compatibility-affected entries, READER-to-EDITOR grant-and-share upgrade, and same-block selection ambiguity with zero comment writes
- command compatibility across 1,221 historical nodes and complete Schema compatibility
- 1,152 Agent examples including 62 real Cobra dry-runs
- 100% changed-code coverage across 1,260 executable statements
2026-08-06 09:56:58 +08:00

397 lines
21 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0
package smart
import (
"bytes"
"encoding/json"
"errors"
"io"
"reflect"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
"github.com/spf13/cobra"
)
func docAccessCoverageResponses(permission string) map[string][]string {
return map[string][]string{
"contact/search_contact_by_key_word": {`{"result":[{"userId":"u1","name":"张三","openDingTalkId":"open1"}]}`},
"doc/list_permission": {permission},
"doc/add_permission": {`{"result":{"ok":true}}`},
"doc/update_permission": {`{"result":{"ok":true}}`},
"doc/remove_permission": {`{"result":{"ok":true}}`},
"chat/send_personal_message": {`{"result":{"messageId":"m1"}}`},
}
}
type docAccessErrorWriter struct{}
func (docAccessErrorWriter) Write([]byte) (int, error) { return 0, errors.New("output failure") }
func runDocAccessCoverage(t *testing.T, caller *smartCoverageCaller, args ...string) error {
t.Helper()
helpers.InitDeps(caller)
root := newPlatformCoverageRoot()
root.SetIn(bytes.NewReader(nil))
root.SetArgs(args)
return root.Execute()
}
func runDocAccessCoverageOutput(t *testing.T, caller *smartCoverageCaller, args ...string) (map[string]any, error) {
t.Helper()
helpers.InitDeps(caller)
root := newPlatformCoverageRoot()
var stdout bytes.Buffer
root.SetOut(&stdout)
root.SetErr(io.Discard)
root.SetIn(bytes.NewReader(nil))
root.SetArgs(args)
err := root.Execute()
if stdout.Len() == 0 {
return nil, err
}
var payload map[string]any
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
t.Fatalf("decode output %q: %v", stdout.String(), err)
}
return payload, err
}
func runDocAccessDeclaration(t *testing.T, declaration shortcut.Shortcut, caller *smartCoverageCaller, args ...string) error {
t.Helper()
helpers.InitDeps(caller)
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
root.SetOut(io.Discard)
root.SetErr(io.Discard)
root.PersistentFlags().Bool("yes", false, "")
root.PersistentFlags().Bool("dry-run", false, "")
root.PersistentFlags().String("format", "json", "")
service := &cobra.Command{Use: "doc"}
service.AddCommand(corecmd.New(shortcut.FromShortcut(declaration)))
root.AddCommand(service)
root.SetArgs(append([]string{"doc", declaration.Command}, args...))
return root.Execute()
}
func TestCrossPlatformCoverageDocAccessSuccessDryRunAndPreflight(t *testing.T) {
if docSmartContract("+test", "d", "i", nil, true).DryRun == nil {
t.Fatal("explicit doc smart dry-run declaration missing")
}
present := `{"result":[{"userId":"u1","roleId":"READER"}]}`
empty := `{"result":[]}`
cases := []struct {
name string
permission string
args []string
wantErr bool
}{
{"grant", present, []string{"doc", "+access-grant", "--node", "n", "--to", "张三", "--role", "READER", "--workspace", "w", "--yes"}, false},
{"grant dry", present, []string{"doc", "+access-grant", "--node", "n", "--to", "张三", "--dry-run", "--yes"}, false},
{"change", present, []string{"doc", "+access-change", "--node", "n", "--to", "张三", "--role", "EDITOR", "--yes"}, false},
{"change dry", present, []string{"doc", "+access-change", "--node", "n", "--to", "张三", "--dry-run", "--yes"}, false},
{"change missing", empty, []string{"doc", "+access-change", "--node", "n", "--to", "张三", "--yes"}, true},
{"revoke", present, []string{"doc", "+access-revoke", "--node", "n", "--to", "张三", "--workspace", "w", "--yes"}, false},
{"revoke dry", present, []string{"doc", "+access-revoke", "--node", "n", "--to", "张三", "--dry-run", "--yes"}, false},
{"revoke missing", empty, []string{"doc", "+access-revoke", "--node", "n", "--to", "张三", "--yes"}, true},
{"share", present, []string{"doc", "+share", "--to", "张三", "--url", "https://example.com/doc", "--note", "看一下", "--yes"}, false},
{"share dry", present, []string{"doc", "+share", "--to", "张三", "--url", "https://example.com/doc", "--dry-run", "--yes"}, false},
{"grant share existing", present, []string{"doc", "+grant-and-share", "--node", "n", "--to", "张三", "--url", "https://example.com/doc", "--yes"}, false},
{"grant share missing", empty, []string{"doc", "+grant-and-share", "--node", "n", "--to", "张三", "--url", "https://example.com/doc", "--yes"}, false},
{"grant share dry", empty, []string{"doc", "+grant-and-share", "--node", "n", "--to", "张三", "--url", "https://example.com/doc", "--dry-run", "--yes"}, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
caller := &smartCoverageCaller{responses: docAccessCoverageResponses(tc.permission), failAt: map[string]int{}}
err := runDocAccessCoverage(t, caller, tc.args...)
if (err != nil) != tc.wantErr {
t.Fatalf("error = %v, wantErr %v", err, tc.wantErr)
}
})
}
}
func TestCrossPlatformCoverageDocGrantAndShareProjectionMatchesWrite(t *testing.T) {
permission := func(payload map[string]any) string {
t.Helper()
data, _ := payload["data"].(map[string]any)
recipients, _ := data["recipients"].([]any)
if len(recipients) != 1 {
t.Fatalf("recipients = %#v", data["recipients"])
}
row, _ := recipients[0].(map[string]any)
value, _ := row["permission"].(string)
return value
}
missingCaller := &smartCoverageCaller{responses: docAccessCoverageResponses(`{"result":[]}`), failAt: map[string]int{}}
missing, err := runDocAccessCoverageOutput(t, missingCaller, "doc", "+grant-and-share", "--node", "n", "--to", "张三", "--url", "https://example.com/doc", "--yes")
if err != nil {
t.Fatal(err)
}
if missing["operation"] != "doc.grant_and_share" || permission(missing) != "granted" {
t.Fatalf("missing projection = %#v", missing)
}
if missingCaller.counts["doc/add_permission"] != 1 {
t.Fatalf("missing add calls = %d", missingCaller.counts["doc/add_permission"])
}
presentCaller := &smartCoverageCaller{responses: docAccessCoverageResponses(`{"result":[{"id":"u1","roleId":"READER"}]}`), failAt: map[string]int{}}
present, err := runDocAccessCoverageOutput(t, presentCaller, "doc", "+grant-and-share", "--node", "n", "--to", "张三", "--url", "https://example.com/doc", "--yes")
if err != nil {
t.Fatal(err)
}
if present["operation"] != "doc.grant_and_share" || permission(present) != "unchanged" {
t.Fatalf("present projection = %#v", present)
}
if presentCaller.counts["doc/add_permission"] != 0 || presentCaller.counts["doc/update_permission"] != 0 {
t.Fatalf("present writes = %#v", presentCaller.counts)
}
upgradeCaller := &smartCoverageCaller{responses: docAccessCoverageResponses(`{"result":[{"userId":"u1","roleId":"READER"}]}`), failAt: map[string]int{}}
upgraded, err := runDocAccessCoverageOutput(t, upgradeCaller, "doc", "+grant-and-share", "--node", "n", "--to", "张三", "--url", "https://example.com/doc", "--role", "EDITOR", "--yes")
if err != nil {
t.Fatal(err)
}
if permission(upgraded) != "upgraded" || upgradeCaller.counts["doc/add_permission"] != 0 || upgradeCaller.counts["doc/update_permission"] != 1 || upgradeCaller.counts["chat/send_personal_message"] != 1 {
t.Fatalf("upgrade projection=%#v calls=%#v", upgraded, upgradeCaller.counts)
}
wantUpgrade := map[string]any{"nodeId": "n", "roleId": "EDITOR", "userIds": []string{"u1"}}
if got := upgradeCaller.arguments["doc/update_permission"]; len(got) != 1 || !reflect.DeepEqual(got[0], wantUpgrade) {
t.Fatalf("upgrade params = %#v, want %#v", got, wantUpgrade)
}
unknownCaller := &smartCoverageCaller{responses: docAccessCoverageResponses(`{"result":[{"userId":"u1"}]}`), failAt: map[string]int{}}
if _, err := runDocAccessCoverageOutput(t, unknownCaller, "doc", "+grant-and-share", "--node", "n", "--to", "张三", "--url", "https://example.com/doc", "--yes"); err == nil {
t.Fatal("unknown existing role must stop before permission writes and messaging")
}
if unknownCaller.counts["doc/add_permission"] != 0 || unknownCaller.counts["doc/update_permission"] != 0 || unknownCaller.counts["chat/send_personal_message"] != 0 {
t.Fatalf("unknown role continued: %#v", unknownCaller.counts)
}
shareCaller := &smartCoverageCaller{responses: docAccessCoverageResponses(`{"result":[]}`), failAt: map[string]int{}}
shared, err := runDocAccessCoverageOutput(t, shareCaller, "doc", "+share", "--to", "张三", "--url", "https://example.com/doc", "--yes")
if err != nil {
t.Fatal(err)
}
if shared["operation"] != "doc.share" || permission(shared) != "unchanged" {
t.Fatalf("share projection = %#v", shared)
}
if missing := usersMissingPermission(map[string]any{"result": []any{map[string]any{"userId": "u10"}}}, []contactUser{{name: "张三", userID: "u1"}}); len(missing) != 1 {
t.Fatalf("substring permission match must not pass: %#v", missing)
}
roles := map[string]int{"READER": 1, "DOWNLOADER": 2, "EDITOR": 3, "MANAGER": 4, "OWNER": 5, "unknown": 0}
for role, want := range roles {
if got := permissionRoleRank(role); got != want {
t.Fatalf("permissionRoleRank(%q) = %d, want %d", role, got, want)
}
}
collected := map[string]string{}
collectPermissionRoles(map[string]any{"result": []any{
map[string]any{"userID": "u2", "permissionRole": "READER"},
map[string]any{"memberId": "u2", "permissionType": "EDITOR"},
map[string]any{"targetId": "u3", "roleType": "DOWNLOADER"},
map[string]any{"uid": "u4", "roleID": "MANAGER"},
map[string]any{"id": "u5", "role": "OWNER"},
}}, collected)
if !reflect.DeepEqual(collected, map[string]string{"u2": "EDITOR", "u3": "DOWNLOADER", "u4": "MANAGER", "u5": "OWNER"}) {
t.Fatalf("collected roles = %#v", collected)
}
}
func TestCrossPlatformCoverageDocAccessRevokeConfirmationBoundary(t *testing.T) {
present := `{"result":[{"userId":"u1","roleId":"READER"}]}`
unconfirmed := &smartCoverageCaller{responses: docAccessCoverageResponses(present), failAt: map[string]int{}}
if err := runDocAccessCoverage(t, unconfirmed, "doc", "+access-revoke", "--node", "n", "--to", "张三"); err == nil {
t.Fatal("access revoke without --yes must reject")
}
if len(unconfirmed.counts) != 0 {
t.Fatalf("unconfirmed access revoke called MCP: %#v", unconfirmed.counts)
}
confirmed := &smartCoverageCaller{responses: docAccessCoverageResponses(present), failAt: map[string]int{}}
if err := runDocAccessCoverage(t, confirmed, "doc", "+access-revoke", "--node", "n", "--to", "张三", "--yes"); err != nil {
t.Fatal(err)
}
wantCounts := map[string]int{
"contact/search_contact_by_key_word": 1,
"doc/list_permission": 1,
"doc/remove_permission": 1,
}
if !reflect.DeepEqual(confirmed.counts, wantCounts) {
t.Fatalf("confirmed calls = %#v, want %#v", confirmed.counts, wantCounts)
}
wantRemove := map[string]any{"nodeId": "n", "userIds": []string{"u1"}}
if got := confirmed.arguments["doc/remove_permission"]; len(got) != 1 || !reflect.DeepEqual(got[0], wantRemove) {
t.Fatalf("remove params = %#v, want %#v", got, wantRemove)
}
}
func TestCrossPlatformCoverageDocShareKeepsLegacyStringFlagAndCSV(t *testing.T) {
testseam.Swap(t, &resolveDocShareUser, func(_ *shortcut.RuntimeContext, name string) (contactUser, error) {
return contactUser{name: name, userID: "user-" + name, openDingTalkID: "open-" + name}, nil
})
responses := docAccessCoverageResponses(`{"result":[]}`)
responses["chat/send_personal_message"] = []string{
`{"result":{"messageId":"m1"}}`,
`{"result":{"messageId":"m2"}}`,
}
caller := &smartCoverageCaller{responses: responses, failAt: map[string]int{}}
payload, err := runDocAccessCoverageOutput(t, caller, "doc", "+share-doc", "--to", "alice,bob", "--url", "https://example.com/doc", "--yes")
if err != nil {
t.Fatal(err)
}
data, _ := payload["data"].(map[string]any)
recipients, _ := data["recipients"].([]any)
if len(recipients) != 2 || caller.counts["chat/send_personal_message"] != 2 {
t.Fatalf("recipients=%#v send calls=%d", recipients, caller.counts["chat/send_personal_message"])
}
}
func TestCrossPlatformCoverageDocShareFailureExitContracts(t *testing.T) {
resolve := func(_ *shortcut.RuntimeContext, name string) (contactUser, error) {
return contactUser{name: name, userID: "user-" + name, openDingTalkID: "open-" + name}, nil
}
testseam.Swap(t, &resolveDocShareUser, resolve)
testseam.Swap(t, &resolveDocPermissionUser, resolve)
assertFailure := func(t *testing.T, payload map[string]any, err error, wantStatus string, wantSucceeded, wantFailed int) {
t.Helper()
if err == nil {
t.Fatal("message failure must return a non-zero exit error")
}
var typed *apperrors.Error
if !errors.As(err, &typed) || typed.Reason != "doc_share_message_failed" || typed.ExitCode() != 1 {
t.Fatalf("message error = %#v", err)
}
if payload["ok"] != false || payload["status"] != wantStatus {
t.Fatalf("failure payload = %#v", payload)
}
data, _ := payload["data"].(map[string]any)
if data["succeededCount"] != float64(wantSucceeded) || data["failedCount"] != float64(wantFailed) {
t.Fatalf("failure counts = %#v", data)
}
}
allShareCaller := &smartCoverageCaller{responses: docAccessCoverageResponses(`{"result":[]}`), failAt: map[string]int{"chat/send_personal_message": -1}}
allShare, err := runDocAccessCoverageOutput(t, allShareCaller, "doc", "+share", "--to", "alice", "--url", "https://example.com/doc", "--yes")
assertFailure(t, allShare, err, "failed", 0, 1)
permissions := `{"result":[{"userId":"user-alice","roleId":"READER"},{"userId":"user-bob","roleId":"READER"}]}`
allGrantCaller := &smartCoverageCaller{responses: docAccessCoverageResponses(permissions), failAt: map[string]int{"chat/send_personal_message": -1}}
allGrant, err := runDocAccessCoverageOutput(t, allGrantCaller, "doc", "+grant-and-share", "--node", "n", "--to", "alice,bob", "--url", "https://example.com/doc", "--yes")
assertFailure(t, allGrant, err, "failed", 0, 2)
partialCaller := &smartCoverageCaller{responses: docAccessCoverageResponses(permissions), failAt: map[string]int{"chat/send_personal_message": 2}}
partial, err := runDocAccessCoverageOutput(t, partialCaller, "doc", "+grant-and-share", "--node", "n", "--to", "alice,bob", "--url", "https://example.com/doc", "--yes")
assertFailure(t, partial, err, "partial_success", 1, 1)
helpers.InitDeps(&smartCoverageCaller{responses: docAccessCoverageResponses(`{"result":[]}`), failAt: map[string]int{}})
root := newPlatformCoverageRoot()
root.SetOut(docAccessErrorWriter{})
root.SetErr(io.Discard)
root.SetIn(bytes.NewReader(nil))
root.SetArgs([]string{"doc", "+share", "--to", "alice", "--url", "https://example.com/doc", "--yes"})
if err := root.Execute(); err == nil || err.Error() != "output failure" {
t.Fatalf("output failure = %v", err)
}
}
func TestCrossPlatformCoverageDocGrantAndSharePartialPermissionFailure(t *testing.T) {
resolve := func(_ *shortcut.RuntimeContext, name string) (contactUser, error) {
return contactUser{name: name, userID: "user-" + name, openDingTalkID: "open-" + name}, nil
}
testseam.Swap(t, &resolveDocPermissionUser, resolve)
responses := docAccessCoverageResponses(`{"result":[{"userId":"user-bob","roleId":"READER"}]}`)
caller := &smartCoverageCaller{responses: responses, failAt: map[string]int{"doc/update_permission": 1}}
err := runDocAccessCoverage(t, caller, "doc", "+grant-and-share", "--node", "n", "--to", "alice,bob", "--url", "https://example.com/doc", "--role", "EDITOR", "--yes")
if err == nil {
t.Fatal("partial permission write unexpectedly succeeded")
}
var typed *apperrors.Error
if !errors.As(err, &typed) || typed.Reason != "doc_grant_permission_partial_failure" || typed.FailureStage != "update_permission" || typed.ExecutionStarted == nil || !*typed.ExecutionStarted || !typed.RetryableSet || typed.Retryable {
t.Fatalf("partial permission error = %#v", err)
}
if typed.Details["status"] != "partial_success" {
t.Fatalf("partial permission details = %#v", typed.Details)
}
steps, _ := typed.Details["steps"].([]map[string]any)
if len(steps) != 3 || steps[0]["status"] != "success" || steps[1]["status"] != "failed" || steps[2]["status"] != "not_started" {
t.Fatalf("partial permission steps = %#v", steps)
}
if caller.counts["doc/add_permission"] != 1 || caller.counts["doc/update_permission"] != 1 || caller.counts["chat/send_personal_message"] != 0 {
t.Fatalf("partial permission calls = %#v", caller.counts)
}
}
func TestCrossPlatformCoverageDocAccessFailureBoundaries(t *testing.T) {
present := `{"result":[{"userId":"u1","roleId":"READER"}]}`
commands := []struct {
args []string
fails []string
}{
{[]string{"doc", "+access-grant", "--node", "n", "--to", "张三", "--yes"}, []string{"contact/search_contact_by_key_word", "doc/add_permission"}},
{[]string{"doc", "+access-change", "--node", "n", "--to", "张三", "--yes"}, []string{"contact/search_contact_by_key_word", "doc/list_permission", "doc/update_permission"}},
{[]string{"doc", "+access-revoke", "--node", "n", "--to", "张三", "--yes"}, []string{"contact/search_contact_by_key_word", "doc/list_permission", "doc/remove_permission"}},
{[]string{"doc", "+share", "--to", "张三", "--url", "https://example.com/doc", "--yes"}, []string{"contact/search_contact_by_key_word", "chat/send_personal_message"}},
{[]string{"doc", "+grant-and-share", "--node", "n", "--to", "张三", "--url", "https://example.com/doc", "--yes"}, []string{"contact/search_contact_by_key_word", "doc/list_permission", "chat/send_personal_message"}},
}
for _, command := range commands {
for _, fail := range command.fails {
responses := docAccessCoverageResponses(present)
if fail == "doc/add_permission" {
responses["doc/list_permission"] = []string{`{"result":[]}`}
}
caller := &smartCoverageCaller{responses: responses, failAt: map[string]int{fail: 1}}
_ = runDocAccessCoverage(t, caller, command.args...)
}
}
external := &smartCoverageCaller{responses: docAccessCoverageResponses(present), failAt: map[string]int{}}
external.responses["contact/search_contact_by_key_word"] = []string{`{"result":[{"name":"外部","openDingTalkId":"open-ext"}]}`}
_ = runDocAccessCoverage(t, external, "doc", "+access-grant", "--node", "n", "--to", "外部", "--yes")
noOpenID := &smartCoverageCaller{responses: docAccessCoverageResponses(present), failAt: map[string]int{}}
noOpenID.responses["contact/search_contact_by_key_word"] = []string{`{"result":[{"name":"张三","userId":"u1"}]}`}
_ = runDocAccessCoverage(t, noOpenID, "doc", "+grant-and-share", "--node", "n", "--to", "张三", "--url", "https://example.com/doc", "--yes")
emptyName := &smartCoverageCaller{responses: docAccessCoverageResponses(present), failAt: map[string]int{}}
_ = runDocAccessCoverage(t, emptyName, "doc", "+access-grant", "--node", "n", "--to", " ", "--yes")
duplicate := &smartCoverageCaller{responses: docAccessCoverageResponses(present), failAt: map[string]int{}}
_ = runDocAccessCoverage(t, duplicate, "doc", "+access-grant", "--node", "n", "--to", "张三,张三", "--yes")
_ = usersMissingPermission(map[string]any{}, []contactUser{{name: "empty"}})
optionalTo := AccessGrant
optionalTo.Flags = append([]shortcut.Flag(nil), AccessGrant.Flags...)
optionalTo.Flags[1].Required = false
_ = runDocAccessDeclaration(t, optionalTo, &smartCoverageCaller{responses: docAccessCoverageResponses(present), failAt: map[string]int{}}, "--node", "n", "--to", " ", "--yes")
_ = runDocAccessDeclaration(t, optionalTo, &smartCoverageCaller{responses: docAccessCoverageResponses(present), failAt: map[string]int{}}, "--node", "n", "--yes")
t.Run("permission resolver defensive identity check", func(t *testing.T) {
testseam.Swap(t, &resolveDocPermissionUser, func(*shortcut.RuntimeContext, string) (contactUser, error) {
return contactUser{name: "external", openDingTalkID: "open"}, nil
})
_ = runDocAccessCoverage(t, &smartCoverageCaller{responses: docAccessCoverageResponses(present), failAt: map[string]int{}}, "doc", "+access-grant", "--node", "n", "--to", "external", "--yes")
})
t.Run("permission resolver error", func(t *testing.T) {
testseam.Swap(t, &resolveDocPermissionUser, func(*shortcut.RuntimeContext, string) (contactUser, error) {
return contactUser{}, errors.New("resolve")
})
_ = runDocAccessCoverage(t, &smartCoverageCaller{responses: docAccessCoverageResponses(present), failAt: map[string]int{}}, "doc", "+access-grant", "--node", "n", "--to", "x", "--yes")
})
grantShareAddFailure := &smartCoverageCaller{responses: docAccessCoverageResponses(`{"result":[]}`), failAt: map[string]int{"doc/add_permission": 1}}
_ = runDocAccessCoverage(t, grantShareAddFailure, "doc", "+grant-and-share", "--node", "n", "--to", "张三", "--url", "https://example.com/doc", "--yes")
grantShareUpdateFailure := &smartCoverageCaller{responses: docAccessCoverageResponses(`{"result":[{"userId":"u1","roleId":"READER"}]}`), failAt: map[string]int{"doc/update_permission": 1}}
_ = runDocAccessCoverage(t, grantShareUpdateFailure, "doc", "+grant-and-share", "--node", "n", "--to", "张三", "--url", "https://example.com/doc", "--role", "EDITOR", "--yes")
if grantShareUpdateFailure.counts["chat/send_personal_message"] != 0 {
t.Fatalf("message sent after permission upgrade failure: %#v", grantShareUpdateFailure.counts)
}
}