Move cmdcore to corecmd, declare attendance ContractFinal in helpers, keep Sheet destructive commands --yes-only, and align catalog/policy provenance. Co-authored-by: Cursor <cursoragent@cursor.com>
302 lines
11 KiB
Go
302 lines
11 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 helpers
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"io"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||
)
|
||
|
||
// leafDispatchCaller 是 leaf 默认派发路径的 fake ToolCaller。
|
||
type leafDispatchCaller struct {
|
||
dryRun bool
|
||
productID string
|
||
toolName string
|
||
}
|
||
|
||
func (c *leafDispatchCaller) CallTool(_ context.Context, productID, toolName string, _ map[string]any) (*edition.ToolResult, error) {
|
||
c.productID = productID
|
||
c.toolName = toolName
|
||
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil
|
||
}
|
||
|
||
func (c *leafDispatchCaller) Format() string { return "json" }
|
||
func (c *leafDispatchCaller) DryRun() bool { return c.dryRun }
|
||
func (c *leafDispatchCaller) Fields() string { return "" }
|
||
func (c *leafDispatchCaller) JQ() string { return "" }
|
||
|
||
func withLeafDispatchCaller(t *testing.T, dryRun bool) *leafDispatchCaller {
|
||
t.Helper()
|
||
previousDeps := deps
|
||
t.Cleanup(func() { deps = previousDeps })
|
||
caller := &leafDispatchCaller{dryRun: dryRun}
|
||
InitDeps(caller)
|
||
deps.Out.w = io.Discard
|
||
return caller
|
||
}
|
||
|
||
// TestLeafCommandDefaultDispatch 覆盖默认 callMCPTool 派发(无 Call/Server)。
|
||
// DryRun caller 在路由前早返回,验证 RunE 走到默认派发语句且无错误。
|
||
func TestLeafCommandDefaultDispatch(t *testing.T) {
|
||
withLeafDispatchCaller(t, true)
|
||
cmd := NewLeafCommand(LeafSpec{
|
||
Use: "get", Tool: "get_thing",
|
||
Flags: []LeafFlag{{Name: "id", Usage: "ID", Bind: "id"}},
|
||
})
|
||
if err := cmd.Flags().Set("id", "x"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := cmd.RunE(cmd, nil); err != nil {
|
||
t.Fatalf("RunE() = %v, want nil via dry-run dispatch", err)
|
||
}
|
||
}
|
||
|
||
// TestLeafCommandServerDispatch 覆盖显式 Server 路由分支。
|
||
func TestLeafCommandServerDispatch(t *testing.T) {
|
||
caller := withLeafDispatchCaller(t, false)
|
||
cmd := NewLeafCommand(LeafSpec{
|
||
Use: "get", Server: "doc", Tool: "get_document",
|
||
Flags: []LeafFlag{{Name: "id", Usage: "ID", Bind: "docId"}},
|
||
})
|
||
if err := cmd.Flags().Set("id", "d1"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := cmd.RunE(cmd, nil); err != nil {
|
||
t.Fatalf("RunE() = %v", err)
|
||
}
|
||
if caller.productID != "doc" || caller.toolName != "get_document" {
|
||
t.Fatalf("dispatched to %s/%s, want doc/get_document", caller.productID, caller.toolName)
|
||
}
|
||
}
|
||
|
||
// TestLeafCommandTransformErrorAborts 覆盖 RunE 中 leafArgs 错误传播。
|
||
func TestLeafCommandTransformErrorAborts(t *testing.T) {
|
||
withLeafDispatchCaller(t, true)
|
||
cmd := NewLeafCommand(LeafSpec{
|
||
Use: "get", Tool: "get_thing",
|
||
Flags: []LeafFlag{{
|
||
Name: "when", Usage: "时间", Bind: "when",
|
||
Transform: func(string) (any, error) { return nil, errors.New("bad time format") },
|
||
}},
|
||
})
|
||
if err := cmd.Flags().Set("when", "not-a-time"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := cmd.RunE(cmd, nil); err == nil || !strings.Contains(err.Error(), "bad time format") {
|
||
t.Fatalf("RunE() = %v, want transform error", err)
|
||
}
|
||
}
|
||
|
||
// TestLeafValidateRequiredEnvDefaultHint 覆盖 EnvVar Required 未配置
|
||
// RequiredHint 时的默认报错文案。
|
||
func TestLeafValidateRequiredEnvDefaultHint(t *testing.T) {
|
||
spec := LeafSpec{
|
||
Use: "send", Tool: "send_thing",
|
||
Flags: []LeafFlag{{Name: "token", Usage: "令牌", Required: true, EnvVar: "DWS_LEAF_TEST_HINTLESS"}},
|
||
}
|
||
cmd := NewLeafCommand(spec)
|
||
err := corecmd.ValidateRequired(cmd, spec.Flags)
|
||
if err == nil || err.Error() != "flag --token is required" {
|
||
t.Fatalf("leafValidateRequired() = %v, want default hint", err)
|
||
}
|
||
}
|
||
|
||
// ── CR 探针回归:A1/A2/A3 ──────────────────────────────────────────────
|
||
|
||
// TestLeafIntRequiredAcceptsExplicitValue(A1 探针):LeafInt + Required
|
||
// 传了值必须通过校验,不得报「missing required flag(s)」。
|
||
func TestLeafIntRequiredAcceptsExplicitValue(t *testing.T) {
|
||
spec := LeafSpec{
|
||
Use: "list", Tool: "list_thing",
|
||
Flags: []LeafFlag{{Name: "n", Usage: "数量", Kind: LeafInt, Required: true, Bind: "count"}},
|
||
}
|
||
cmd := NewLeafCommand(spec)
|
||
if err := cmd.Flags().Set("n", "5"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := corecmd.ValidateRequired(cmd, spec.Flags); err != nil {
|
||
t.Fatalf("leafValidateRequired() = %v, want nil for --n 5", err)
|
||
}
|
||
args, err := corecmd.BuildArgs(cmd, spec.Flags)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := args["count"]; got != 5 {
|
||
t.Fatalf("count = %v (%T), want int 5", got, got)
|
||
}
|
||
// 未提供值时仍按主 flag 名报缺失。
|
||
missing := NewLeafCommand(spec)
|
||
if err := corecmd.ValidateRequired(missing, spec.Flags); err == nil || !strings.Contains(err.Error(), "missing required flag(s): --n") {
|
||
t.Fatalf("leafValidateRequired() = %v, want missing --n", err)
|
||
}
|
||
}
|
||
|
||
// TestLeafIntAliasAndEnvFallback(A2 探针):整型 flag 的别名与 env 回退
|
||
// 必须生效,不得静默丢值。
|
||
func TestLeafIntAliasAndEnvFallback(t *testing.T) {
|
||
spec := LeafSpec{
|
||
Use: "list", Tool: "list_thing",
|
||
Flags: []LeafFlag{{Name: "page-size", Usage: "分页大小", Kind: LeafInt, Aliases: []string{"limit"}, EnvVar: "DWS_LEAF_TEST_PAGE_SIZE", Bind: "pageSize"}},
|
||
}
|
||
// 别名提供值。
|
||
cmd := NewLeafCommand(spec)
|
||
if err := cmd.Flags().Set("limit", "7"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
args, err := corecmd.BuildArgs(cmd, spec.Flags)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := args["pageSize"]; got != 7 {
|
||
t.Fatalf("pageSize = %v (%T), want int 7 from alias", got, got)
|
||
}
|
||
// env 提供值。
|
||
t.Setenv("DWS_LEAF_TEST_PAGE_SIZE", "9")
|
||
cmd = NewLeafCommand(spec)
|
||
args, err = corecmd.BuildArgs(cmd, spec.Flags)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := args["pageSize"]; got != 9 {
|
||
t.Fatalf("pageSize = %v (%T), want int 9 from env", got, got)
|
||
}
|
||
// env 值不可解析必须报错而非静默丢弃。
|
||
t.Setenv("DWS_LEAF_TEST_PAGE_SIZE", "not-a-number")
|
||
cmd = NewLeafCommand(spec)
|
||
if _, err := corecmd.BuildArgs(cmd, spec.Flags); err == nil || !strings.Contains(err.Error(), "invalid integer value") {
|
||
t.Fatalf("leafArgs() = %v, want parse error for garbage env", err)
|
||
}
|
||
}
|
||
|
||
// TestLeafIntAliasRegisteredTyped:整型 flag 的别名按 Kind 注册且回退生效。
|
||
func TestLeafIntAliasRegisteredTyped(t *testing.T) {
|
||
spec := LeafSpec{
|
||
Use: "list", Tool: "list_thing",
|
||
Flags: []LeafFlag{{Name: "cursor", Usage: "游标", Kind: LeafInt, Aliases: []string{"offset"}, Bind: "cursor"}},
|
||
}
|
||
cmd := NewLeafCommand(spec)
|
||
if f := cmd.Flags().Lookup("offset"); f == nil || f.Value.Type() != "int" {
|
||
t.Fatalf("alias offset = %+v, want registered as int", f)
|
||
}
|
||
if err := cmd.Flags().Set("offset", "11"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
args, err := corecmd.BuildArgs(cmd, spec.Flags)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := args["cursor"]; got != 11 {
|
||
t.Fatalf("cursor = %v (%T), want int 11 from alias", got, got)
|
||
}
|
||
}
|
||
|
||
// TestLeafDefaultDoesNotShadowFallback(A3 探针):注册默认值不得遮蔽
|
||
// 别名与环境变量,只能作为链尾兜底。
|
||
func TestLeafDefaultDoesNotShadowFallback(t *testing.T) {
|
||
spec := LeafSpec{
|
||
Use: "list", Tool: "list_thing",
|
||
Flags: []LeafFlag{{Name: "type", Usage: "类型", Default: "ALL", Aliases: []string{"kind"}, EnvVar: "DWS_LEAF_TEST_TYPE", Bind: "type"}},
|
||
}
|
||
// env 覆盖注册默认值。
|
||
t.Setenv("DWS_LEAF_TEST_TYPE", "from-env")
|
||
cmd := NewLeafCommand(spec)
|
||
if got := corecmd.EffectiveValue(cmd, spec.Flags[0]); got != "from-env" {
|
||
t.Fatalf("effective = %q, want env to beat registered default", got)
|
||
}
|
||
// 别名覆盖 env 与默认值。
|
||
if err := cmd.Flags().Set("kind", "from-alias"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := corecmd.EffectiveValue(cmd, spec.Flags[0]); got != "from-alias" {
|
||
t.Fatalf("effective = %q, want alias to beat env", got)
|
||
}
|
||
// 显式主 flag 最高优先。
|
||
if err := cmd.Flags().Set("type", "explicit"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := corecmd.EffectiveValue(cmd, spec.Flags[0]); got != "explicit" {
|
||
t.Fatalf("effective = %q, want explicit primary to win", got)
|
||
}
|
||
// 全部缺席时回落注册默认值。
|
||
bare := NewLeafCommand(spec)
|
||
t.Setenv("DWS_LEAF_TEST_TYPE", "")
|
||
if got := corecmd.EffectiveValue(bare, spec.Flags[0]); got != "ALL" {
|
||
t.Fatalf("effective = %q, want registered default as tail fallback", got)
|
||
}
|
||
}
|
||
|
||
// TestLeafIntRequiredExplicitZeroReportsMissing:Required 判定与 leafArgs 入参
|
||
// 判定必须一致——LeafInt 显式 0 不会入参,因此 Required 校验也须报缺失,
|
||
// 不允许「校验通过但入参缺席」的裂缝。
|
||
func TestLeafIntRequiredExplicitZeroReportsMissing(t *testing.T) {
|
||
spec := LeafSpec{
|
||
Use: "list", Tool: "list_thing",
|
||
Flags: []LeafFlag{{Name: "n", Usage: "数量", Kind: LeafInt, Required: true, Bind: "count"}},
|
||
}
|
||
cmd := NewLeafCommand(spec)
|
||
if err := cmd.Flags().Set("n", "0"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := corecmd.ValidateRequired(cmd, spec.Flags); err == nil || !strings.Contains(err.Error(), "missing required flag(s): --n") {
|
||
t.Fatalf("leafValidateRequired() = %v, want missing --n for explicit 0", err)
|
||
}
|
||
|
||
// 整型解析失败视为「已提供」:Required 校验放行,让 leafArgs 报出更
|
||
// 精确的 invalid integer 错误。
|
||
specEnv := LeafSpec{
|
||
Use: "list", Tool: "list_thing",
|
||
Flags: []LeafFlag{{Name: "n", Usage: "数量", Kind: LeafInt, Required: true, EnvVar: "DWS_LEAF_TEST_REQ_GARBAGE"}},
|
||
}
|
||
t.Setenv("DWS_LEAF_TEST_REQ_GARBAGE", "not-a-number")
|
||
cmdEnv := NewLeafCommand(specEnv)
|
||
if err := corecmd.ValidateRequired(cmdEnv, specEnv.Flags); err != nil {
|
||
t.Fatalf("leafValidateRequired() = %v, want nil for unparsable env (leafArgs reports it)", err)
|
||
}
|
||
if _, err := corecmd.BuildArgs(cmdEnv, specEnv.Flags); err == nil || !strings.Contains(err.Error(), "invalid integer value") {
|
||
t.Fatalf("leafArgs() = %v, want invalid integer error", err)
|
||
}
|
||
}
|
||
|
||
// TestLeafTrimWhitespaceFallsThroughChain:Trim 开启时纯空白候选值与空串
|
||
// 同样落入下一级回退,不得在链中「命中后被 trim 成空」。
|
||
func TestLeafTrimWhitespaceFallsThroughChain(t *testing.T) {
|
||
spec := LeafSpec{
|
||
Use: "list", Tool: "list_thing",
|
||
Flags: []LeafFlag{{Name: "type", Usage: "类型", Trim: true, Default: "ALL", Aliases: []string{"kind"}, EnvVar: "DWS_LEAF_TEST_TRIM_TYPE"}},
|
||
}
|
||
// 主 flag 纯空白 → 回退 env。
|
||
t.Setenv("DWS_LEAF_TEST_TRIM_TYPE", "from-env")
|
||
cmd := NewLeafCommand(spec)
|
||
if err := cmd.Flags().Set("type", " "); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := corecmd.EffectiveValue(cmd, spec.Flags[0]); got != "from-env" {
|
||
t.Fatalf("effective = %q, want whitespace primary to fall through to env", got)
|
||
}
|
||
// 别名纯空白也回退;env 纯空白最终落到注册默认值。
|
||
t.Setenv("DWS_LEAF_TEST_TRIM_TYPE", " ")
|
||
if err := cmd.Flags().Set("kind", "\t"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := corecmd.EffectiveValue(cmd, spec.Flags[0]); got != "ALL" {
|
||
t.Fatalf("effective = %q, want whitespace chain to land on default", got)
|
||
}
|
||
}
|