From c0b562cc072ac6fe185c05dc4653c8da1f1165bb Mon Sep 17 00:00:00 2001 From: xlb1130 Date: Thu, 6 Aug 2026 11:20:52 +0800 Subject: [PATCH] fix(chat): add package-level MCP call seam for toolbar remove-custom - introduce removeChatToolbarCustomShortcutFn in toolbar_remove_custom.go as a package-level injection seam; default impl routes through callMCPToolOnServer against the im server so production behavior is unchanged - update RunE to dispatch via the seam instead of calling callMCPToolOnServer inline - add two TestCrossPlatformCoverage* tests that swap the seam via testseam.Swap and verify: (1) without --yes the seam is never called and a typed confirmation_required error is returned, (2) with --yes the seam is called exactly once with openCid and shortcutId. The stub forwards to deps.Caller.CallTool so the user_required contract gate (leaf.go) still sees the CallTool channel. --- internal/helpers/toolbar_helpers_test.go | 61 +++++++++++++++++++++++ internal/helpers/toolbar_remove_custom.go | 18 +++++-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/internal/helpers/toolbar_helpers_test.go b/internal/helpers/toolbar_helpers_test.go index d7a12382..54911369 100644 --- a/internal/helpers/toolbar_helpers_test.go +++ b/internal/helpers/toolbar_helpers_test.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" ) @@ -349,3 +350,63 @@ func TestCrossPlatformCoverageToolbarRemoveCustomCallsMCPWithExactArgsWhenYes(t t.Fatalf("tool args = %#v, want %#v", call.args, wantArgs) } } + +type removeCustomSeamCall struct { + openCid string + shortcutId int64 +} + +type removeCustomSeamStub struct { + calls []removeCustomSeamCall + err error +} + +// invoke records the seam call and forwards to deps.Caller.CallTool so the +// user_required contract gate (leaf.go) is satisfied. A pure short-circuit +// would skip the CallTool channel and trigger +// "user_required confirmation was never obtained via CallTool". +func (s *removeCustomSeamStub) invoke(openCid string, shortcutId int64) error { + s.calls = append(s.calls, removeCustomSeamCall{openCid: openCid, shortcutId: shortcutId}) + caller := GetCaller() + if caller == nil { + return s.err + } + _, err := caller.CallTool(context.Background(), "im", "remove_chat_toolbar_custom_shortcut", map[string]any{ + "openCid": openCid, + "shortcutId": shortcutId, + }) + return err +} + +func TestCrossPlatformCoverageToolbarRemoveCustomSeamRejectsWithoutYes(t *testing.T) { + stub := &removeCustomSeamStub{} + testseam.Swap(t, &removeChatToolbarCustomShortcutFn, stub.invoke) + + err := executeToolbarCommand(t, newToolbarRemoveCustomCommand(), &toolbarTestCaller{}, + "--conversation-id", "cidX", "--shortcut-id", "42") + requireTypedConfirmationError(t, err) + if len(stub.calls) != 0 { + t.Fatalf("expected 0 seam calls before --yes, got %d: %+v", len(stub.calls), stub.calls) + } +} + +func TestCrossPlatformCoverageToolbarRemoveCustomSeamCallsWithExactArgsWhenYes(t *testing.T) { + stub := &removeCustomSeamStub{} + testseam.Swap(t, &removeChatToolbarCustomShortcutFn, stub.invoke) + + err := executeToolbarCommand(t, newToolbarRemoveCustomCommand(), &toolbarTestCaller{}, + "--conversation-id", "cidX", "--shortcut-id", "42", "--yes") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(stub.calls) != 1 { + t.Fatalf("expected exactly 1 seam call, got %d: %+v", len(stub.calls), stub.calls) + } + call := stub.calls[0] + if call.openCid != "cidX" { + t.Fatalf("seam openCid = %q, want cidX", call.openCid) + } + if call.shortcutId != int64(42) { + t.Fatalf("seam shortcutId = %d, want 42", call.shortcutId) + } +} diff --git a/internal/helpers/toolbar_remove_custom.go b/internal/helpers/toolbar_remove_custom.go index d0d822f4..62a721e1 100644 --- a/internal/helpers/toolbar_remove_custom.go +++ b/internal/helpers/toolbar_remove_custom.go @@ -20,6 +20,19 @@ import ( apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" ) +// removeChatToolbarCustomShortcutFn is the package-level injection seam for +// the remove-custom command's remote call. The default implementation routes +// through callMCPToolOnServer against the im server, so production behavior is +// unchanged. Tests swap it with testseam.Swap(t, &removeChatToolbarCustomShortcutFn, stub) +// to capture the call without hitting a real remote. Like every package-var +// seam in this repo it is not safe for t.Parallel tests. +var removeChatToolbarCustomShortcutFn = func(openCid string, shortcutId int64) error { + return callMCPToolOnServer("im", "remove_chat_toolbar_custom_shortcut", map[string]any{ + "openCid": openCid, + "shortcutId": shortcutId, + }) +} + func newToolbarRemoveCustomCommand() *cobra.Command { cmd := &cobra.Command{ Use: "remove-custom", @@ -40,10 +53,7 @@ func newToolbarRemoveCustomCommand() *cobra.Command { apperrors.WithActions("确认目标自定义入口", "获得用户确认后使用 --yes 执行"), ) } - err = callMCPToolOnServer("im", "remove_chat_toolbar_custom_shortcut", map[string]any{ - "openCid": cid, - "shortcutId": shortcutId, - }) + err = removeChatToolbarCustomShortcutFn(cid, shortcutId) if isSystemBusy(err) { return toolbarNewSystemBusyError() }