- Add testseam.Protect for save-and-restore seams with no up-front stub value (e.g. os.Args mutated by the code under test). - Migrate all 35 remaining manual prev/assign/t.Cleanup-restore trios to testseam.Swap/Protect across app, auth, cli, event, helpers, output, pipeline, and shortcut tests; restores can no longer be forgotten. - check-schema-catalog.sh: fail closed when a manual seam restore reappears in any *_test.go (internal/testseam exempt). Seam injection is now a mechanism, not a convention.
654 lines
23 KiB
Go
654 lines
23 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 (
|
||
"errors"
|
||
"io"
|
||
"os"
|
||
"reflect"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/spf13/cobra"
|
||
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
|
||
)
|
||
|
||
func leafConstraintTestSpec(captured *map[string]any) LeafSpec {
|
||
return LeafSpec{
|
||
Use: "route",
|
||
Short: "路由",
|
||
Tool: "route_thing",
|
||
Flags: []LeafFlag{
|
||
{Name: "group", Usage: "群 ID", Aliases: []string{"chat"}},
|
||
{Name: "user", Usage: "用户 ID", EnvVar: "DWS_LEAF_CONSTRAINT_TEST_USER"},
|
||
{Name: "channel", Usage: "频道", Default: "main"},
|
||
{Name: "verbose", Usage: "详细输出", Kind: LeafBool, Bind: "verboseOutput"},
|
||
{Name: "tags", Usage: "标签列表", Kind: LeafStringSlice, Aliases: []string{"labels"}, Bind: "tagList"},
|
||
},
|
||
Constraints: []LeafConstraint{
|
||
{Kind: LeafExactlyOne, Flags: []string{"group", "user"}},
|
||
{Kind: LeafMutuallyExclusive, Flags: []string{"verbose", "tags"}},
|
||
},
|
||
Call: func(_ *cobra.Command, _ string, args map[string]any) error {
|
||
*captured = args
|
||
return nil
|
||
},
|
||
}
|
||
}
|
||
|
||
func leafConstraintExecute(t *testing.T, args ...string) (map[string]any, error) {
|
||
t.Helper()
|
||
var captured map[string]any
|
||
cmd := NewLeafCommand(leafConstraintTestSpec(&captured))
|
||
cmd.SilenceErrors = true
|
||
cmd.SilenceUsage = true
|
||
cmd.SetArgs(args)
|
||
err := cmd.Execute()
|
||
return captured, err
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafConstraintExactlyOne(t *testing.T) {
|
||
if _, err := leafConstraintExecute(t); err == nil ||
|
||
!strings.Contains(err.Error(), "请指定 --group、--user 之一") {
|
||
t.Fatalf("zero provided err = %v", err)
|
||
}
|
||
if _, err := leafConstraintExecute(t, "--group", "g1", "--user", "u1"); err == nil ||
|
||
!strings.Contains(err.Error(), "参数 --group、--user 只能指定其一(当前指定了 --group、--user)") {
|
||
t.Fatalf("both provided err = %v", err)
|
||
}
|
||
if _, err := leafConstraintExecute(t, "--group", "g1"); err != nil {
|
||
t.Fatalf("exactly one err = %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafConstraintAliasAndEnvCountAsProvided(t *testing.T) {
|
||
// 隐藏别名 --chat 命中 group:满足 exactly_one。
|
||
if _, err := leafConstraintExecute(t, "--chat", "g1"); err != nil {
|
||
t.Fatalf("alias provided err = %v", err)
|
||
}
|
||
// 环境变量命中 user:同样满足。
|
||
t.Setenv("DWS_LEAF_CONSTRAINT_TEST_USER", "u-env")
|
||
if _, err := leafConstraintExecute(t); err != nil {
|
||
t.Fatalf("env provided err = %v", err)
|
||
}
|
||
// env 提供 user + 显式 group:应触发 exactly_one 冲突。
|
||
if _, err := leafConstraintExecute(t, "--group", "g1"); err == nil ||
|
||
!strings.Contains(err.Error(), "只能指定其一") {
|
||
t.Fatalf("env + explicit err = %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafConstraintDefaultNotCounted(t *testing.T) {
|
||
// channel 有注册默认值 "main",但未显式提供时不得参与约束判定:
|
||
// 用仅含 at_least_one(channel, group) 的 spec 验证默认值不算提供。
|
||
var captured map[string]any
|
||
spec := LeafSpec{
|
||
Use: "pick",
|
||
Tool: "pick_thing",
|
||
Flags: []LeafFlag{
|
||
{Name: "channel", Usage: "频道", Default: "main"},
|
||
{Name: "group", Usage: "群 ID"},
|
||
},
|
||
Constraints: []LeafConstraint{
|
||
{Kind: LeafAtLeastOne, Flags: []string{"channel", "group"}},
|
||
},
|
||
Call: func(_ *cobra.Command, _ string, args map[string]any) error {
|
||
captured = args
|
||
return nil
|
||
},
|
||
}
|
||
cmd := NewLeafCommand(spec)
|
||
cmd.SilenceErrors = true
|
||
cmd.SilenceUsage = true
|
||
cmd.SetArgs(nil)
|
||
err := cmd.Execute()
|
||
if err == nil || !strings.Contains(err.Error(), "请至少指定 --channel、--group 之一") {
|
||
t.Fatalf("default counted as provided: err = %v, captured = %#v", err, captured)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafConstraintMutuallyExclusive(t *testing.T) {
|
||
if _, err := leafConstraintExecute(t, "--group", "g1", "--verbose", "--tags", "a,b"); err == nil ||
|
||
!strings.Contains(err.Error(), "参数 --verbose、--tags 互斥,只能指定其一(当前指定了 --verbose、--tags)") {
|
||
t.Fatalf("mutually exclusive err = %v", err)
|
||
}
|
||
// 各自单独提供均合法。
|
||
if _, err := leafConstraintExecute(t, "--group", "g1", "--verbose"); err != nil {
|
||
t.Fatalf("verbose alone err = %v", err)
|
||
}
|
||
if _, err := leafConstraintExecute(t, "--group", "g1", "--tags", "a"); err != nil {
|
||
t.Fatalf("tags alone err = %v", err)
|
||
}
|
||
// 空白元素的列表视为未提供,不触发互斥。
|
||
if _, err := leafConstraintExecute(t, "--group", "g1", "--verbose", "--tags", " "); err != nil {
|
||
t.Fatalf("blank slice counted as provided: err = %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafBoolAndSliceArgs(t *testing.T) {
|
||
captured, err := leafConstraintExecute(t, "--group", "g1", "--verbose=false")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if captured["verboseOutput"] != false {
|
||
t.Fatalf("explicit false not delivered: %#v", captured)
|
||
}
|
||
|
||
captured, err = leafConstraintExecute(t, "--group", "g1", "--tags", "a, ,b")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !reflect.DeepEqual(captured["tagList"], []string{"a", "b"}) {
|
||
t.Fatalf("tagList = %#v, want trimmed non-empty elements", captured["tagList"])
|
||
}
|
||
if _, ok := captured["verboseOutput"]; ok {
|
||
t.Fatalf("unchanged bool leaked into args: %#v", captured)
|
||
}
|
||
|
||
// 别名 --labels 提供列表同样入参。
|
||
captured, err = leafConstraintExecute(t, "--group", "g1", "--labels", "x")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !reflect.DeepEqual(captured["tagList"], []string{"x"}) {
|
||
t.Fatalf("alias slice = %#v", captured["tagList"])
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafBoolSliceRequired(t *testing.T) {
|
||
var captured map[string]any
|
||
spec := LeafSpec{
|
||
Use: "need",
|
||
Tool: "need_thing",
|
||
Flags: []LeafFlag{
|
||
{Name: "confirm", Usage: "确认", Kind: LeafBool, Required: true},
|
||
{Name: "ids", Usage: "ID 列表", Kind: LeafStringSlice, Required: true},
|
||
},
|
||
Call: func(_ *cobra.Command, _ string, args map[string]any) error {
|
||
captured = args
|
||
return nil
|
||
},
|
||
}
|
||
run := func(args ...string) error {
|
||
cmd := NewLeafCommand(spec)
|
||
cmd.SilenceErrors = true
|
||
cmd.SilenceUsage = true
|
||
cmd.SetArgs(args)
|
||
return cmd.Execute()
|
||
}
|
||
if err := run("--ids", "a"); err == nil || !strings.Contains(err.Error(), "confirm") {
|
||
t.Fatalf("missing bool err = %v", err)
|
||
}
|
||
if err := run("--confirm", "--ids", " "); err == nil || !strings.Contains(err.Error(), "ids") {
|
||
t.Fatalf("blank slice err = %v", err)
|
||
}
|
||
if err := run("--confirm=false", "--ids", "a"); err != nil {
|
||
t.Fatalf("explicit false should satisfy required: %v", err)
|
||
}
|
||
if captured["confirm"] != false || !reflect.DeepEqual(captured["ids"], []string{"a"}) {
|
||
t.Fatalf("captured = %#v", captured)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafConstraintSchemaAndHelp(t *testing.T) {
|
||
var captured map[string]any
|
||
cmd := NewLeafCommand(leafConstraintTestSpec(&captured))
|
||
encoded := cmd.Annotations["dws.schema.constraints"]
|
||
if !strings.Contains(encoded, `"require_one_of":[["group","user"]]`) {
|
||
t.Fatalf("schema constraints missing require_one_of: %s", encoded)
|
||
}
|
||
if !strings.Contains(encoded, `["group","user"]`) || !strings.Contains(encoded, `["verbose","tags"]`) {
|
||
t.Fatalf("schema constraints incomplete: %s", encoded)
|
||
}
|
||
if !strings.Contains(cmd.Long, "参数约束:") ||
|
||
!strings.Contains(cmd.Long, "--group、--user 必须且只能指定一个") ||
|
||
!strings.Contains(cmd.Long, "--verbose、--tags 互斥,最多指定一个") {
|
||
t.Fatalf("constraint help missing: %q", cmd.Long)
|
||
}
|
||
|
||
withDesc := LeafSpec{
|
||
Use: "desc",
|
||
Tool: "desc_thing",
|
||
Flags: []LeafFlag{
|
||
{Name: "a", Usage: "A"},
|
||
{Name: "b", Usage: "B"},
|
||
},
|
||
Constraints: []LeafConstraint{
|
||
{Kind: LeafAtLeastOne, Flags: []string{"a", "b"}, Description: "自定义文案"},
|
||
},
|
||
}
|
||
if long := NewLeafCommand(withDesc).Long; !strings.Contains(long, " - 自定义文案") {
|
||
t.Fatalf("custom description missing: %q", long)
|
||
}
|
||
atLeast := LeafSpec{
|
||
Use: "least",
|
||
Tool: "least_thing",
|
||
Flags: []LeafFlag{
|
||
{Name: "a", Usage: "A"},
|
||
{Name: "b", Usage: "B"},
|
||
},
|
||
Constraints: []LeafConstraint{{Kind: LeafAtLeastOne, Flags: []string{"a", "b"}}},
|
||
}
|
||
if long := NewLeafCommand(atLeast).Long; !strings.Contains(long, "--a、--b 至少指定一个") {
|
||
t.Fatalf("at_least_one help missing: %q", long)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafConstraintDeclPanics(t *testing.T) {
|
||
mustPanic := func(name string, spec LeafSpec, needle string) {
|
||
t.Helper()
|
||
defer func() {
|
||
r := recover()
|
||
if r == nil {
|
||
t.Fatalf("%s: expected panic", name)
|
||
}
|
||
if msg, _ := r.(string); !strings.Contains(msg, needle) {
|
||
t.Fatalf("%s: panic = %v, want %q", name, r, needle)
|
||
}
|
||
}()
|
||
NewLeafCommand(spec)
|
||
}
|
||
base := []LeafFlag{{Name: "a", Usage: "A"}, {Name: "b", Usage: "B"}}
|
||
mustPanic("unknown kind", LeafSpec{
|
||
Use: "x", Tool: "t", Flags: base,
|
||
Constraints: []LeafConstraint{{Kind: "bogus", Flags: []string{"a", "b"}}},
|
||
}, "unknown constraint kind")
|
||
mustPanic("too few flags", LeafSpec{
|
||
Use: "x", Tool: "t", Flags: base,
|
||
Constraints: []LeafConstraint{{Kind: LeafExactlyOne, Flags: []string{"a"}}},
|
||
}, "needs at least two flags")
|
||
mustPanic("undeclared flag", LeafSpec{
|
||
Use: "x", Tool: "t", Flags: base,
|
||
Constraints: []LeafConstraint{{Kind: LeafExactlyOne, Flags: []string{"a", "missing"}}},
|
||
}, "references undeclared flag")
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafConstraintRunsBeforeValidateHook(t *testing.T) {
|
||
hookRan := false
|
||
spec := LeafSpec{
|
||
Use: "order",
|
||
Tool: "order_thing",
|
||
Flags: []LeafFlag{
|
||
{Name: "a", Usage: "A"},
|
||
{Name: "b", Usage: "B"},
|
||
},
|
||
Constraints: []LeafConstraint{{Kind: LeafExactlyOne, Flags: []string{"a", "b"}}},
|
||
Validate: func(*cobra.Command, []string) error {
|
||
hookRan = true
|
||
return nil
|
||
},
|
||
Call: func(*cobra.Command, string, map[string]any) error { return nil },
|
||
}
|
||
cmd := NewLeafCommand(spec)
|
||
cmd.SilenceErrors = true
|
||
cmd.SilenceUsage = true
|
||
cmd.SetArgs(nil)
|
||
if err := cmd.Execute(); err == nil {
|
||
t.Fatal("constraint should fail before Validate")
|
||
}
|
||
if hookRan {
|
||
t.Fatal("Validate hook ran despite constraint failure")
|
||
}
|
||
}
|
||
|
||
// TestCrossPlatformCoverageLeafSpecCorePaths exercises the LeafSpec paths that
|
||
// the framework shares between constrained and unconstrained leaves
|
||
// (MarkRequired, RunE escape hatch, Server routing, LeafInt assembly, Transform
|
||
// and EnvVar-backed required hints). leaf_test.go asserts the same behaviour but
|
||
// its names sit outside the platform coverage runner's filter.
|
||
func TestCrossPlatformCoverageLeafSpecCorePaths(t *testing.T) {
|
||
markRequired := NewLeafCommand(LeafSpec{
|
||
Use: "mark",
|
||
Tool: "mark_thing",
|
||
Flags: []LeafFlag{{Name: "id", Usage: "ID", MarkRequired: true}},
|
||
})
|
||
if ann := markRequired.Flags().Lookup("id").Annotations[cobra.BashCompOneRequiredFlag]; len(ann) == 0 {
|
||
t.Fatal("MarkRequired did not reach cobra")
|
||
}
|
||
|
||
escaped := false
|
||
escape := NewLeafCommand(LeafSpec{
|
||
Use: "escape",
|
||
Tool: "escape_thing",
|
||
RunE: func(*cobra.Command, []string) error {
|
||
escaped = true
|
||
return nil
|
||
},
|
||
})
|
||
escape.SetArgs(nil)
|
||
if err := escape.Execute(); err != nil || !escaped {
|
||
t.Fatalf("RunE escape hatch not used: err = %v", err)
|
||
}
|
||
|
||
caller := &guardedMutationCaller{}
|
||
serverRouted := func() error {
|
||
testseam.Protect(t, &deps)
|
||
InitDeps(caller)
|
||
deps.Out.w = io.Discard
|
||
cmd := NewLeafCommand(LeafSpec{
|
||
Use: "route-server",
|
||
Tool: "route_tool",
|
||
Server: "im",
|
||
Flags: []LeafFlag{
|
||
{Name: "count", Usage: "数量", Kind: LeafInt, Bind: "count"},
|
||
{Name: "scope", Usage: "范围", ArgDefault: "ALL", Bind: "scope"},
|
||
{Name: "note", Usage: "备注", OmitEmpty: true},
|
||
},
|
||
})
|
||
cmd.SilenceErrors = true
|
||
cmd.SilenceUsage = true
|
||
cmd.SetArgs([]string{"--count", "3"})
|
||
return cmd.Execute()
|
||
}
|
||
if err := serverRouted(); err != nil {
|
||
t.Fatalf("server routing err = %v", err)
|
||
}
|
||
if len(caller.calls) != 1 || caller.calls[0].productID != "im" || caller.calls[0].toolName != "route_tool" {
|
||
t.Fatalf("server routing call = %#v", caller.calls)
|
||
}
|
||
if caller.calls[0].args["count"] != 3 || caller.calls[0].args["scope"] != "ALL" {
|
||
t.Fatalf("server routing args = %#v", caller.calls[0].args)
|
||
}
|
||
if _, ok := caller.calls[0].args["note"]; ok {
|
||
t.Fatalf("OmitEmpty flag leaked: %#v", caller.calls[0].args)
|
||
}
|
||
|
||
intSpec := LeafSpec{
|
||
Use: "count",
|
||
Tool: "count_thing",
|
||
Flags: []LeafFlag{{Name: "n", Usage: "N", Kind: LeafInt, EnvVar: "DWS_LEAF_CORE_TEST_N"}},
|
||
Call: func(*cobra.Command, string, map[string]any) error { return nil },
|
||
}
|
||
t.Setenv("DWS_LEAF_CORE_TEST_N", "not-a-number")
|
||
badInt := NewLeafCommand(intSpec)
|
||
badInt.SilenceErrors = true
|
||
badInt.SilenceUsage = true
|
||
badInt.SetArgs(nil)
|
||
if err := badInt.Execute(); err == nil || !strings.Contains(err.Error(), "invalid integer value") {
|
||
t.Fatalf("invalid env integer err = %v", err)
|
||
}
|
||
|
||
transformFail := NewLeafCommand(LeafSpec{
|
||
Use: "transform",
|
||
Tool: "transform_thing",
|
||
Flags: []LeafFlag{{Name: "raw", Usage: "RAW", Transform: func(string) (any, error) {
|
||
return nil, errTransformTest
|
||
}}},
|
||
Call: func(*cobra.Command, string, map[string]any) error { return nil },
|
||
})
|
||
transformFail.SilenceErrors = true
|
||
transformFail.SilenceUsage = true
|
||
transformFail.SetArgs([]string{"--raw", "x"})
|
||
if err := transformFail.Execute(); err == nil || !strings.Contains(err.Error(), "transform failed") {
|
||
t.Fatalf("transform error not surfaced: %v", err)
|
||
}
|
||
|
||
hinted := NewLeafCommand(LeafSpec{
|
||
Use: "hint",
|
||
Tool: "hint_thing",
|
||
Flags: []LeafFlag{{
|
||
Name: "token", Usage: "令牌", Required: true,
|
||
EnvVar: "DWS_LEAF_CORE_TEST_TOKEN", RequiredHint: "flag --token is required (or set DWS_LEAF_CORE_TEST_TOKEN)",
|
||
}},
|
||
Call: func(*cobra.Command, string, map[string]any) error { return nil },
|
||
})
|
||
hinted.SilenceErrors = true
|
||
hinted.SilenceUsage = true
|
||
hinted.SetArgs(nil)
|
||
if err := hinted.Execute(); err == nil || !strings.Contains(err.Error(), "DWS_LEAF_CORE_TEST_TOKEN") {
|
||
t.Fatalf("required hint err = %v", err)
|
||
}
|
||
|
||
// 默认 product 路由(无 Server)+ LeafInt Required(走 leafHasEffectiveValue
|
||
// 的 LeafInt 分支)+ Transform 返回 (nil,nil) 跳过键。
|
||
defaultRouted := func() error {
|
||
testseam.Protect(t, &deps)
|
||
oldArgs := os.Args
|
||
os.Args = []string{"dws", "chat"}
|
||
t.Cleanup(func() { os.Args = oldArgs })
|
||
InitDeps(caller)
|
||
deps.Out.w = io.Discard
|
||
caller.calls = nil
|
||
cmd := NewLeafCommand(LeafSpec{
|
||
Use: "default-route",
|
||
Tool: "im_default_tool",
|
||
Flags: []LeafFlag{
|
||
{Name: "n", Usage: "N", Kind: LeafInt, Required: true, Bind: "n"},
|
||
{Name: "opt", Usage: "OPT", Transform: func(string) (any, error) { return nil, nil }},
|
||
},
|
||
})
|
||
cmd.SilenceErrors = true
|
||
cmd.SilenceUsage = true
|
||
cmd.SetArgs([]string{"--n", "5", "--opt", "ignored"})
|
||
return cmd.Execute()
|
||
}
|
||
if err := defaultRouted(); err != nil {
|
||
t.Fatalf("default route err = %v", err)
|
||
}
|
||
if len(caller.calls) != 1 {
|
||
t.Fatalf("default route call = %#v", caller.calls)
|
||
}
|
||
if caller.calls[0].args["n"] != 5 {
|
||
t.Fatalf("int required arg = %#v", caller.calls[0].args)
|
||
}
|
||
if _, ok := caller.calls[0].args["opt"]; ok {
|
||
t.Fatalf("Transform nil should skip key: %#v", caller.calls[0].args)
|
||
}
|
||
// LeafInt Required 缺失(值为 0)应报缺参。
|
||
missingInt := NewLeafCommand(LeafSpec{
|
||
Use: "need-int",
|
||
Tool: "need_int_tool",
|
||
Flags: []LeafFlag{{Name: "n", Usage: "N", Kind: LeafInt, Required: true}},
|
||
Call: func(*cobra.Command, string, map[string]any) error { return nil },
|
||
})
|
||
missingInt.SilenceErrors = true
|
||
missingInt.SilenceUsage = true
|
||
missingInt.SetArgs(nil)
|
||
if err := missingInt.Execute(); err == nil || !strings.Contains(err.Error(), "n") {
|
||
t.Fatalf("missing int required err = %v", err)
|
||
}
|
||
// 带 EnvVar 但 RequiredHint 为空:走默认 'flag --x is required' 文案。
|
||
envDefaultHint := NewLeafCommand(LeafSpec{
|
||
Use: "env-default-hint",
|
||
Tool: "env_hint_tool",
|
||
Flags: []LeafFlag{{Name: "key", Usage: "KEY", Required: true, EnvVar: "DWS_LEAF_CORE_TEST_MISSING"}},
|
||
Call: func(*cobra.Command, string, map[string]any) error { return nil },
|
||
})
|
||
envDefaultHint.SilenceErrors = true
|
||
envDefaultHint.SilenceUsage = true
|
||
envDefaultHint.SetArgs(nil)
|
||
if err := envDefaultHint.Execute(); err == nil || !strings.Contains(err.Error(), "flag --key is required") {
|
||
t.Fatalf("env default hint err = %v", err)
|
||
}
|
||
}
|
||
|
||
var errTransformTest = errors.New("transform failed")
|
||
|
||
func leafTestReadSafety() contract.SafetySpec {
|
||
return contract.SafetySpec{
|
||
Effect: "read", Risk: "low",
|
||
Confirmation: "not_required", Idempotency: "idempotent",
|
||
}
|
||
}
|
||
|
||
func leafTestWriteSafety() contract.SafetySpec {
|
||
return contract.SafetySpec{
|
||
Effect: "write", Risk: "medium",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
}
|
||
}
|
||
|
||
func leafTestDestructiveSafety() contract.SafetySpec {
|
||
return contract.SafetySpec{
|
||
Effect: "destructive", Risk: "high",
|
||
Confirmation: "user_required", Idempotency: "unknown",
|
||
}
|
||
}
|
||
|
||
func leafSafetySpec(safety contract.SafetySpec, called *bool) LeafSpec {
|
||
return LeafSpec{
|
||
Use: "danger",
|
||
Short: "危险",
|
||
Tool: "danger_thing",
|
||
Safety: safety,
|
||
Flags: []LeafFlag{{Name: "id", Usage: "ID"}},
|
||
Call: func(*cobra.Command, string, map[string]any) error {
|
||
*called = true
|
||
return nil
|
||
},
|
||
}
|
||
}
|
||
|
||
func leafSafetyRun(t *testing.T, safety contract.SafetySpec, stdin string, args ...string) (bool, error) {
|
||
t.Helper()
|
||
called := false
|
||
cmd := NewLeafCommand(leafSafetySpec(safety, &called))
|
||
// 注入根级 --yes/--dry-run 持久 flag,模拟真实根命令。
|
||
cmd.PersistentFlags().Bool("yes", false, "")
|
||
cmd.PersistentFlags().Bool("dry-run", false, "")
|
||
cmd.SilenceErrors = true
|
||
cmd.SilenceUsage = true
|
||
if stdin != "" {
|
||
cmd.SetIn(strings.NewReader(stdin))
|
||
}
|
||
cmd.SetArgs(args)
|
||
return called, cmd.Execute()
|
||
}
|
||
|
||
// TestCrossPlatformCoverageLeafSafetyUnderRootGlobalFlags mirrors the production
|
||
// shape: the ROOT command owns the global --yes/--dry-run persistent flags and
|
||
// the leaf is a child, so the inherited / root-persistent lookup path is what
|
||
// resolves them (leafSafetyRun registers them on the leaf itself, where
|
||
// cmd.Root() == cmd).
|
||
func TestCrossPlatformCoverageLeafSafetyUnderRootGlobalFlags(t *testing.T) {
|
||
run := func(globalFlag string, stdin string) (bool, error) {
|
||
called := false
|
||
leaf := NewLeafCommand(leafSafetySpec(leafTestDestructiveSafety(), &called))
|
||
root := &cobra.Command{Use: "dws"}
|
||
root.PersistentFlags().Bool("yes", false, "")
|
||
root.PersistentFlags().Bool("dry-run", false, "")
|
||
root.AddCommand(leaf)
|
||
root.SilenceErrors = true
|
||
root.SilenceUsage = true
|
||
if stdin != "" {
|
||
root.SetIn(strings.NewReader(stdin))
|
||
}
|
||
args := []string{"danger", "--id", "x"}
|
||
if globalFlag != "" {
|
||
args = append(args, globalFlag)
|
||
}
|
||
root.SetArgs(args)
|
||
return called, root.Execute()
|
||
}
|
||
|
||
// root-level --yes bypasses the prompt (no stdin available).
|
||
if called, err := run("--yes", ""); err != nil || !called {
|
||
t.Fatalf("root --yes err = %v called = %v", err, called)
|
||
}
|
||
// root-level --dry-run bypasses the prompt.
|
||
if called, err := run("--dry-run", ""); err != nil || !called {
|
||
t.Fatalf("root --dry-run err = %v called = %v", err, called)
|
||
}
|
||
// without either, the leaf prompts and honors a decline.
|
||
called, err := run("", "no\n")
|
||
if called {
|
||
t.Fatal("declined write must not dispatch")
|
||
}
|
||
if err == nil || !strings.Contains(err.Error(), "用户取消了操作") {
|
||
t.Fatalf("declined err = %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafSafetyReadNeverPrompts(t *testing.T) {
|
||
// 只读:无 stdin 也直接派发。
|
||
if called, err := leafSafetyRun(t, leafTestReadSafety(), "", "--id", "x"); err != nil || !called {
|
||
t.Fatalf("read safety err = %v called = %v", err, called)
|
||
}
|
||
// 空 Safety 保留只读默认。
|
||
if called, err := leafSafetyRun(t, contract.SafetySpec{}, "", "--id", "x"); err != nil || !called {
|
||
t.Fatalf("empty safety err = %v called = %v", err, called)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafSafetyWriteConfirmation(t *testing.T) {
|
||
// 拒绝:不派发,返回取消错误。
|
||
called, err := leafSafetyRun(t, leafTestWriteSafety(), "no\n", "--id", "x")
|
||
if called {
|
||
t.Fatal("declined write should not dispatch")
|
||
}
|
||
if err == nil || !strings.Contains(err.Error(), "用户取消了操作") {
|
||
t.Fatalf("declined write err = %v", err)
|
||
}
|
||
// 同意:派发。
|
||
if called, err := leafSafetyRun(t, leafTestWriteSafety(), "yes\n", "--id", "x"); err != nil || !called {
|
||
t.Fatalf("confirmed write err = %v called = %v", err, called)
|
||
}
|
||
// 高危同样走确认链。
|
||
if called, err := leafSafetyRun(t, leafTestDestructiveSafety(), "y\n", "--id", "x"); err != nil || !called {
|
||
t.Fatalf("confirmed high-write err = %v called = %v", err, called)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafSafetyYesAndDryRunBypass(t *testing.T) {
|
||
// --yes 跳过提示直接派发(无 stdin)。
|
||
if called, err := leafSafetyRun(t, leafTestDestructiveSafety(), "", "--id", "x", "--yes"); err != nil || !called {
|
||
t.Fatalf("--yes bypass err = %v called = %v", err, called)
|
||
}
|
||
// --dry-run 跳过提示直接派发(无 stdin)。
|
||
if called, err := leafSafetyRun(t, leafTestWriteSafety(), "", "--id", "x", "--dry-run"); err != nil || !called {
|
||
t.Fatalf("--dry-run bypass err = %v called = %v", err, called)
|
||
}
|
||
}
|
||
|
||
func TestCrossPlatformCoverageLeafYesFlagAndIntEdges(t *testing.T) {
|
||
if corecmd.BoolFlag(nil, "yes") {
|
||
t.Fatal("nil cmd should not report --yes")
|
||
}
|
||
|
||
// Required LeafInt 的 env 值不可解析:leafHasEffectiveValue 视为已提供
|
||
// (err→true 分支),required 通过后由 leafArgs 报 invalid integer。
|
||
t.Setenv("DWS_LEAF_INT_EDGE", "not-an-int")
|
||
badInt := NewLeafCommand(LeafSpec{
|
||
Use: "int-edge",
|
||
Tool: "int_edge_tool",
|
||
Flags: []LeafFlag{{Name: "n", Usage: "N", Kind: LeafInt, Required: true, EnvVar: "DWS_LEAF_INT_EDGE"}},
|
||
Call: func(*cobra.Command, string, map[string]any) error { return nil },
|
||
})
|
||
badInt.SilenceErrors = true
|
||
badInt.SilenceUsage = true
|
||
badInt.SetArgs(nil)
|
||
if err := badInt.Execute(); err == nil || !strings.Contains(err.Error(), "invalid integer value") {
|
||
t.Fatalf("unparseable int env err = %v", err)
|
||
}
|
||
|
||
// 写风险但全局无 --yes flag 注册:leafYesFlag 各 getter 均报错走兜底 false,
|
||
// 于是进入提示;stdin "no" 取消。
|
||
called := false
|
||
noYes := NewLeafCommand(leafSafetySpec(leafTestWriteSafety(), &called))
|
||
noYes.SilenceErrors = true
|
||
noYes.SilenceUsage = true
|
||
noYes.SetIn(strings.NewReader("no\n"))
|
||
noYes.SetArgs([]string{"--id", "x"})
|
||
if err := noYes.Execute(); err == nil || !strings.Contains(err.Error(), "用户取消了操作") {
|
||
t.Fatalf("no-yes-flag cancel err = %v", err)
|
||
}
|
||
if called {
|
||
t.Fatal("declined write dispatched despite no --yes flag")
|
||
}
|
||
}
|