Files
dingtalk-workspace-cli/internal/compat/todo_hooks_test.go
539d10f80f feat: 合并 pre-mcp-discovery 分支能力到 main(服务发现保持生产) (#391)
将 test/pre-mcp-discovery 分支上多位同学的能力改动整体合入 main,
服务发现端点保持生产 https://mcp.dingtalk.com,未携带分支上的预发改动。

主要内容:
- 修雨域(chat / report / todo / contact / mail)对齐 wukong 基线 (#355)
- DWS 内容生产能力增强 (#387)、文档导出与 OA 对齐 (#362 #388 #390)
- envelope 注册 cli.Aliases 为 cobra aliases、命令结构与 JSON 解析优化
- skills 多包同步 wukong 对齐内容(attendance/calendar/minutes/oa/sheet 等)
- product/oa 进一步对齐 wukong

排除项:未携带 566b4e1(test: point service discovery to pre-mcp.dingtalk.com);
以下 4 个常量保持生产:skill_command.go / auth/endpoints.go / cli/loader.go / market/registry.go。

Co-authored-by: john <32427341+wqyenjoy@users.noreply.github.com>
Co-authored-by: wxianfeng <wang.fl1429@gmail.com>
2026-06-02 21:04:12 +08:00

218 lines
7.5 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 compat
import (
"errors"
"strings"
"testing"
"github.com/spf13/cobra"
)
// newTodoCreateSubStub mirrors the leaf command shape emitted by
// BuildDynamicCommands for `todo task create-sub` (envelope:
// create_personal_sub_todo). Only the flags the hook touches are
// registered; the others are irrelevant to the validation.
func newTodoCreateSubStub() *cobra.Command {
cmd := &cobra.Command{Use: "create-sub"}
cmd.Flags().String("parent-id", "", "parent todo id")
cmd.Flags().String("title", "", "title")
cmd.Flags().String("executors", "", "executors")
return cmd
}
func TestValidateTodoParentIdNumeric_AcceptsPureDigits(t *testing.T) {
cmd := newTodoCreateSubStub()
if err := cmd.Flags().Set("parent-id", "53340859882"); err != nil {
t.Fatalf("set flag: %v", err)
}
if err := validateTodoParentIdNumeric(cmd); err != nil {
t.Fatalf("expected nil for numeric parent-id, got %v", err)
}
}
func TestValidateTodoParentIdNumeric_RejectsAlphanumeric(t *testing.T) {
cmd := newTodoCreateSubStub()
if err := cmd.Flags().Set("parent-id", "INVALID_PARENT_ID_99999"); err != nil {
t.Fatalf("set flag: %v", err)
}
err := validateTodoParentIdNumeric(cmd)
if err == nil {
t.Fatal("expected validation error for non-numeric parent-id")
}
if !strings.Contains(err.Error(), "纯数字") {
t.Fatalf("expected '纯数字' in error, got %v", err)
}
if !strings.Contains(err.Error(), "INVALID_PARENT_ID_99999") {
t.Fatalf("expected offending value in error, got %v", err)
}
}
func TestValidateTodoParentIdNumeric_RejectsLeadingZeroPaddedHex(t *testing.T) {
// "0xdeadbeef" should fail strconv.ParseInt base 10, ensuring we are
// not silently accepting hex-shaped IDs.
cmd := newTodoCreateSubStub()
if err := cmd.Flags().Set("parent-id", "0xdeadbeef"); err != nil {
t.Fatalf("set flag: %v", err)
}
if err := validateTodoParentIdNumeric(cmd); err == nil {
t.Fatal("expected validation error for hex-shaped parent-id")
}
}
func TestValidateTodoParentIdNumeric_RejectsWhitespacePadded(t *testing.T) {
// Trimmed value is "abc" — must still reject; equally guards against
// " 123 " false-positive once trimmed (which we DO accept as 123).
cmd := newTodoCreateSubStub()
if err := cmd.Flags().Set("parent-id", " abc "); err != nil {
t.Fatalf("set flag: %v", err)
}
if err := validateTodoParentIdNumeric(cmd); err == nil {
t.Fatal("expected validation error for non-numeric (whitespace-padded) parent-id")
}
}
func TestValidateTodoParentIdNumeric_AcceptsWhitespacePaddedDigits(t *testing.T) {
cmd := newTodoCreateSubStub()
if err := cmd.Flags().Set("parent-id", " 53340859882 "); err != nil {
t.Fatalf("set flag: %v", err)
}
if err := validateTodoParentIdNumeric(cmd); err != nil {
t.Fatalf("expected whitespace-padded digits to pass after trim, got %v", err)
}
}
func TestValidateTodoParentIdNumeric_EmptyPassesThrough(t *testing.T) {
// Envelope marks parent-id required, so cobra produces the missing-flag
// error itself. We must not preempt that with a confusing message.
cmd := newTodoCreateSubStub()
if err := validateTodoParentIdNumeric(cmd); err != nil {
t.Fatalf("expected nil for empty parent-id (cobra owns required-check), got %v", err)
}
}
func TestValidateTodoParentIdNumeric_NoFlagRegistered(t *testing.T) {
// Defensive: a command without the flag must not panic / error.
cmd := &cobra.Command{Use: "noop"}
if err := validateTodoParentIdNumeric(cmd); err != nil {
t.Fatalf("expected nil when --parent-id absent, got %v", err)
}
}
// ── installTodoHook composition ────────────────────────────────
func TestInstallTodoHook_NoOpForOtherProduct(t *testing.T) {
cmd := newTodoCreateSubStub()
originalCalled := false
cmd.PreRunE = func(*cobra.Command, []string) error { originalCalled = true; return nil }
installTodoHook(cmd, "chat", "create_personal_sub_todo")
if err := cmd.PreRunE(cmd, nil); err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !originalCalled {
t.Fatal("original PreRunE should still run when hook skips")
}
// Bad parent-id must NOT fail since hook is no-op for non-todo product.
if err := cmd.Flags().Set("parent-id", "INVALID"); err != nil {
t.Fatal(err)
}
if err := cmd.PreRunE(cmd, nil); err != nil {
t.Fatalf("non-todo product must not validate parent-id: %v", err)
}
}
func TestInstallTodoHook_NoOpForOtherTodoTool(t *testing.T) {
// e.g. `todo task get` reuses parent-id-less plumbing — make sure we do
// not blanket-validate every todo leaf.
cmd := newTodoCreateSubStub()
installTodoHook(cmd, "todo", "get_personal_todo_detail")
if err := cmd.Flags().Set("parent-id", "INVALID"); err != nil {
t.Fatal(err)
}
if cmd.PreRunE != nil {
if err := cmd.PreRunE(cmd, nil); err != nil {
t.Fatalf("non-target todo tool must not validate parent-id: %v", err)
}
}
}
func TestInstallTodoHook_TargetToolRejectsInvalid(t *testing.T) {
cmd := newTodoCreateSubStub()
installTodoHook(cmd, "todo", "create_personal_sub_todo")
if cmd.PreRunE == nil {
t.Fatal("installTodoHook should install a PreRunE for the target tool")
}
if err := cmd.Flags().Set("parent-id", "INVALID_PARENT_ID_99999"); err != nil {
t.Fatal(err)
}
err := cmd.PreRunE(cmd, nil)
if err == nil {
t.Fatal("expected hook to reject non-numeric parent-id")
}
if !strings.Contains(err.Error(), "纯数字") {
t.Fatalf("unexpected error message: %v", err)
}
}
func TestInstallTodoHook_TargetToolAcceptsValid(t *testing.T) {
cmd := newTodoCreateSubStub()
installTodoHook(cmd, "todo", "create_personal_sub_todo")
if err := cmd.Flags().Set("parent-id", "53340859882"); err != nil {
t.Fatal(err)
}
if err := cmd.PreRunE(cmd, nil); err != nil {
t.Fatalf("numeric parent-id must pass: %v", err)
}
}
func TestInstallTodoHook_ChainsExistingPreRunE(t *testing.T) {
cmd := newTodoCreateSubStub()
originalCalled := false
cmd.PreRunE = func(*cobra.Command, []string) error {
originalCalled = true
return nil
}
installTodoHook(cmd, "todo", "create_personal_sub_todo")
if err := cmd.Flags().Set("parent-id", "1"); err != nil {
t.Fatal(err)
}
if err := cmd.PreRunE(cmd, nil); err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !originalCalled {
t.Fatal("original PreRunE was dropped")
}
}
func TestInstallTodoHook_BailsIfChainedPreRunEFails(t *testing.T) {
cmd := newTodoCreateSubStub()
cmd.PreRunE = func(*cobra.Command, []string) error { return errors.New("original boom") }
installTodoHook(cmd, "todo", "create_personal_sub_todo")
// Even with a VALID parent-id, the chained original error must bubble up
// before our validation runs.
if err := cmd.Flags().Set("parent-id", "1"); err != nil {
t.Fatal(err)
}
err := cmd.PreRunE(cmd, nil)
if err == nil || !strings.Contains(err.Error(), "original boom") {
t.Fatalf("expected original PreRunE error to bubble, got %v", err)
}
}
func TestInstallTodoHook_NilCmdSafe(t *testing.T) {
// Defensive: should not panic.
installTodoHook(nil, "todo", "create_personal_sub_todo")
}